network #28

Merged
AmineB merged 28 commits from davidoskky/ReaderForSelfoss-multiplatform:network into master 2022-08-22 19:01:16 +00: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")
//Dependency Injection
implementation("org.kodein.di:kodein-di:7.12.0")
implementation("org.kodein.di:kodein-di-framework-android-x:7.12.0")
implementation("org.kodein.di:kodein-di:7.14.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
implementation("com.russhwolf:multiplatform-settings-no-arg:0.9")
@ -193,13 +194,18 @@ dependencies {
//PhotoView
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-common-java8:2.4.0")
implementation("androidx.lifecycle:lifecycle-livedata-ktx:2.5.1")
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")
kapt("androidx.room:room-compiler:2.4.0-beta01")
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.removeBadge
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.toView
import bou.amine.apps.readerforselfossv2.android.viewmodel.AppViewModel
import bou.amine.apps.readerforselfossv2.repository.Repository
import bou.amine.apps.readerforselfossv2.rest.SelfossModel
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 fromTabShortcut: Boolean = false
private var offlineShortcut: Boolean = false
private lateinit var tagsBadge: Map<Long, Int>
@ -153,7 +152,7 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
val view = binding.root
fromTabShortcut = intent.getIntExtra("shortcutTab", -1) != -1
offlineShortcut = intent.getBooleanExtra("startOffline", false)
repository.offlineOverride = intent.getBooleanExtra("startOffline", false)
if (fromTabShortcut) {
elementsShown = ItemType.fromInt(intent.getIntExtra("shortcutTab", ItemType.UNREAD.position))
@ -175,7 +174,6 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
AppDatabase::class.java, "selfoss-database"
).addMigrations(MIGRATION_1_2).addMigrations(MIGRATION_2_3).addMigrations(MIGRATION_3_4).build()
customTabActivityHelper = CustomTabActivityHelper()
dataBase = AndroidDeviceDatabase(applicationContext)
@ -197,7 +195,7 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
R.color.refresh_progress_3
)
binding.swipeRefreshLayout.setOnRefreshListener {
offlineShortcut = false
repository.offlineOverride = false
lastFetchDone = false
handleDrawerItems()
CoroutineScope(Dispatchers.Main).launch {
@ -689,30 +687,26 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
var sources: List<SelfossModel.Source>?
fun sourcesApiCall() {
if (this@HomeActivity.isNetworkAvailable(null, offlineShortcut) && updateSources) {
CoroutineScope(Dispatchers.Main).launch {
val response = repository.getSources()
if (response != null) {
sources = response
val apiDrawerData = DrawerData(tags, sources)
if ((maybeDrawerData != null && maybeDrawerData != apiDrawerData) || maybeDrawerData == null) {
handleDrawerData(apiDrawerData)
}
} else {
val apiDrawerData = DrawerData(tags, null)
if ((maybeDrawerData != null && maybeDrawerData != apiDrawerData) || maybeDrawerData == null) {
handleDrawerData(apiDrawerData)
}
CoroutineScope(Dispatchers.Main).launch {
val response = repository.getSources()
if (response != null) {
sources = response
val apiDrawerData = DrawerData(tags, sources)
if ((maybeDrawerData != null && maybeDrawerData != apiDrawerData) || maybeDrawerData == null) {
handleDrawerData(apiDrawerData)
}
} else {
val apiDrawerData = DrawerData(tags, null)
if ((maybeDrawerData != null && maybeDrawerData != apiDrawerData) || maybeDrawerData == null) {
handleDrawerData(apiDrawerData)
}
}
}
}
if (this@HomeActivity.isNetworkAvailable(null, offlineShortcut) && updateSources) {
CoroutineScope(Dispatchers.IO).launch {
tags = repository.getTags()
sourcesApiCall()
}
CoroutineScope(Dispatchers.IO).launch {
tags = repository.getTags()
sourcesApiCall()
}
}
@ -944,10 +938,8 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
private fun reloadBadges() {
if (displayUnreadCount || displayAllCount) {
CoroutineScope(Dispatchers.Main).launch {
if (applicationContext.isNetworkAvailable()) {
repository.reloadBadges()
reloadBadgeContent()
}
repository.reloadBadges()
reloadBadgeContent()
}
}
}
@ -1021,61 +1013,56 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
R.id.refresh -> {
if (this@HomeActivity.isNetworkAvailable(null, offlineShortcut)) {
needsConfirmation(R.string.menu_home_refresh, R.string.refresh_dialog_message) {
Toast.makeText(this, R.string.refresh_in_progress, Toast.LENGTH_SHORT).show()
// TODO: Use Dispatchers.IO
CoroutineScope(Dispatchers.Main).launch {
val updatedRemote = repository.updateRemote()
if (updatedRemote) {
Toast.makeText(
this@HomeActivity,
R.string.refresh_success_response, Toast.LENGTH_LONG
)
.show()
} else {
Toast.makeText(
this@HomeActivity,
R.string.refresh_failer_message,
Toast.LENGTH_SHORT
).show()
}
needsConfirmation(R.string.menu_home_refresh, R.string.refresh_dialog_message) {
Toast.makeText(this, R.string.refresh_in_progress, Toast.LENGTH_SHORT).show()
// TODO: Use Dispatchers.IO
CoroutineScope(Dispatchers.Main).launch {
val updatedRemote = repository.updateRemote()
AmineB marked this conversation as resolved Outdated

repository.offlineOverride should only refreshed on app reload or swipe to refresh.

It shouldn't be overridden here.

repository.offlineOverride should only refreshed on app reload or swipe to refresh. It shouldn't be overridden here.
if (updatedRemote) {
// TODO: Send toast messages from the repository
Toast.makeText(
this@HomeActivity,
R.string.refresh_success_response, Toast.LENGTH_LONG
)
.show()
} else {
Toast.makeText(
this@HomeActivity,
R.string.refresh_failer_message,
Toast.LENGTH_SHORT
).show()
}
}
return true
} else {
return false
}
return true
}
R.id.readAll -> {
if (elementsShown == ItemType.UNREAD) {
needsConfirmation(R.string.readAll, R.string.markall_dialog_message) {
binding.swipeRefreshLayout.isRefreshing = true
if (this@HomeActivity.isNetworkAvailable(null, offlineShortcut)) {
CoroutineScope(Dispatchers.Main).launch {
val success = repository.markAllAsRead(items)
if (success) {
Toast.makeText(
this@HomeActivity,
R.string.all_posts_read,
Toast.LENGTH_SHORT
).show()
tabNewBadge.removeBadge()
CoroutineScope(Dispatchers.Main).launch {
val success = repository.markAllAsRead(items)
if (success) {
Toast.makeText(
AmineB marked this conversation as resolved Outdated

repository.offlineOverride should only refreshed on app reload or swipe to refresh.

It shouldn't be overridden here.

`repository.offlineOverride` should only refreshed on app reload or swipe to refresh. It shouldn't be overridden here.
this@HomeActivity,
R.string.all_posts_read,
Toast.LENGTH_SHORT
).show()
tabNewBadge.removeBadge()
handleDrawerItems()
handleDrawerItems()
getElementsAccordingToTab()
} else {
Toast.makeText(
this@HomeActivity,
R.string.all_posts_not_read,
Toast.LENGTH_SHORT
).show()
}
handleListResult()
binding.swipeRefreshLayout.isRefreshing = false
getElementsAccordingToTab()
} else {
Toast.makeText(
this@HomeActivity,
R.string.all_posts_not_read,
Toast.LENGTH_SHORT
).show()
}
handleListResult()
binding.swipeRefreshLayout.isRefreshing = false
}
}
}
@ -1127,17 +1114,15 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
}
}
if (this@HomeActivity.isNetworkAvailable(null, offlineShortcut)) {
CoroutineScope(Dispatchers.Main).launch {
val actions = db.actionsDao().actions()
CoroutineScope(Dispatchers.Main).launch {
val actions = db.actionsDao().actions()
actions.forEach { action ->
when {
action.read -> doAndReportOnFail(repository.markAsReadById(action.articleId.toInt()), action)
action.unread -> doAndReportOnFail(repository.unmarkAsReadById(action.articleId.toInt()), action)
action.starred -> doAndReportOnFail(repository.starrById(action.articleId.toInt()), action)
action.unstarred -> doAndReportOnFail(repository.unstarrById(action.articleId.toInt()), action)
}
actions.forEach { action ->
when {
action.read -> doAndReportOnFail(repository.markAsReadById(action.articleId.toInt()), action)
action.unread -> doAndReportOnFail(repository.unmarkAsReadById(action.articleId.toInt()), action)
action.starred -> doAndReportOnFail(repository.starrById(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.themes.AppColors
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 com.mikepenz.aboutlibraries.LibsBuilder
import com.russhwolf.settings.Settings
@ -202,15 +201,13 @@ class LoginActivity() : AppCompatActivity(), DIAware {
repository.refreshLoginInformation(url, login, password, httpLogin, httpPassword, isWithSelfSignedCert)
if (this@LoginActivity.isNetworkAvailable(this@LoginActivity.findViewById(R.id.loginForm))) {
CoroutineScope(Dispatchers.IO).launch {
val result = repository.login()
if (result) {
goToMain()
} else {
CoroutineScope(Dispatchers.Main).launch {
preferenceError(Exception("Not success"))
}
CoroutineScope(Dispatchers.IO).launch {
val result = repository.login()
if (result) {
goToMain()
} else {
CoroutineScope(Dispatchers.Main).launch {
preferenceError(Exception("Not success"))
}
}
}

View File

@ -7,18 +7,28 @@ import android.graphics.drawable.Drawable
import android.net.Uri
import android.os.Build
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.preference.PreferenceManager
import bou.amine.apps.readerforselfossv2.DI.networkModule
import bou.amine.apps.readerforselfossv2.android.utils.Config
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 com.bumptech.glide.Glide
import com.bumptech.glide.request.RequestOptions
import com.ftinc.scoop.Scoop
import com.github.ln_12.library.ConnectivityStatus
import com.mikepenz.materialdrawer.util.AbstractDrawerImageLoader
import com.mikepenz.materialdrawer.util.DrawerImageLoader
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.Napier
import org.kodein.di.*
@ -27,9 +37,14 @@ class MyApp : MultiDexApplication(), DIAware {
override val di by DI.lazy {
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 settings : Settings
@ -46,6 +61,18 @@ class MyApp : MultiDexApplication(), DIAware {
tryToHandleBug()
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() {
@ -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.themes.AppColors
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.rest.SelfossModel
import com.ftinc.scoop.Scoop
@ -64,30 +63,28 @@ class SourcesActivity : AppCompatActivity(), DIAware {
binding.recyclerView.setHasFixedSize(true)
binding.recyclerView.layoutManager = mLayoutManager
if (this@SourcesActivity.isNetworkAvailable(binding.recyclerView)) {
CoroutineScope(Dispatchers.Main).launch {
val response = repository.getSources()
if (response != null) {
items = response
val mAdapter = SourcesListAdapter(
this@SourcesActivity, items
)
binding.recyclerView.adapter = mAdapter
mAdapter.notifyDataSetChanged()
if (items.isEmpty()) {
Toast.makeText(
this@SourcesActivity,
R.string.nothing_here,
Toast.LENGTH_SHORT
).show()
}
} else {
CoroutineScope(Dispatchers.Main).launch {
val response = repository.getSources()
if (response != null) {
items = response
val mAdapter = SourcesListAdapter(
this@SourcesActivity, items
)
binding.recyclerView.adapter = mAdapter
mAdapter.notifyDataSetChanged()
if (items.isEmpty()) {
Toast.makeText(
this@SourcesActivity,
R.string.cant_get_sources,
R.string.nothing_here,
Toast.LENGTH_SHORT
).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.glide.bitmapCenterCrop
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.rest.SelfossModel
import com.amulyakhare.textdrawable.TextDrawable
@ -110,22 +109,20 @@ class ItemCardAdapter(
binding.favButton.setOnClickListener {
val item = items[bindingAdapterPosition]
if (c.isNetworkAvailable()) {
if (item.starred) {
CoroutineScope(Dispatchers.IO).launch {
repository.unstarr(item)
// 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
if (item.starred) {
CoroutineScope(Dispatchers.IO).launch {
repository.unstarr(item)
// 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
}
}

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.utils.Config
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.repository.Repository
import bou.amine.apps.readerforselfossv2.rest.SelfossModel
@ -78,21 +77,19 @@ class SourcesListAdapter(
val deleteBtn: Button = mView.findViewById(R.id.deleteBtn)
deleteBtn.setOnClickListener {
if (c.isNetworkAvailable(null)) {
val (id) = items[adapterPosition]
CoroutineScope(Dispatchers.IO).launch {
val successfullyDeletedSource = repository.deleteSource(id)
if (successfullyDeletedSource) {
items.removeAt(adapterPosition)
notifyItemRemoved(adapterPosition)
notifyItemRangeChanged(adapterPosition, itemCount)
} else {
Toast.makeText(
app,
R.string.can_delete_source,
Toast.LENGTH_SHORT
).show()
}
val (id) = items[adapterPosition]
CoroutineScope(Dispatchers.IO).launch {
val successfullyDeletedSource = repository.deleteSource(id)
if (successfullyDeletedSource) {
items.removeAt(adapterPosition)
notifyItemRemoved(adapterPosition)
notifyItemRangeChanged(adapterPosition, itemCount)
} else {
Toast.makeText(
app,
R.string.can_delete_source,
Toast.LENGTH_SHORT
).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_3_4
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.rest.SelfossModel
import bou.amine.apps.readerforselfossv2.utils.ItemType
@ -44,67 +44,62 @@ class LoadingWorker(val context: Context, params: WorkerParameters) : Worker(con
override fun doWork(): Result {
val settings = Settings()
val periodicRefresh = settings.getBoolean("periodic_refresh", false)
AmineB marked this conversation as resolved Outdated

This should be reverted to the old network status check, because the network availability watcher should be stopped when the app is in the background.

This should be reverted to the old network status check, because the network availability watcher should be stopped when the app is in the background.
if (periodicRefresh) {
if (periodicRefresh && isNetworkAccessible(context)) {
if (context.isNetworkAvailable()) {
AmineB marked this conversation as resolved Outdated

I think that this should be the only place where the connectivity check should stay like this.

If there is no network connectivity, you don't do anything in the background.

I think that this should be the only place where the connectivity check should stay like this. If there is no network connectivity, you don't do anything in the background.
CoroutineScope(Dispatchers.IO).launch {
val notificationManager =
applicationContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
CoroutineScope(Dispatchers.IO).launch {
val notificationManager =
applicationContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
val notification =
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)
val notification =
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())
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(
applicationContext,
AppDatabase::class.java, "selfoss-database"
).addMigrations(MIGRATION_1_2).addMigrations(MIGRATION_2_3)
.addMigrations(MIGRATION_3_4).build()
val actions = db.actionsDao().actions()
val actions = db.actionsDao().actions()
actions.forEach { action ->
when {
action.read -> doAndReportOnFail(
repository.markAsReadById(action.articleId.toInt()),
action
)
action.unread -> doAndReportOnFail(
repository.unmarkAsReadById(action.articleId.toInt()),
action
)
action.starred -> doAndReportOnFail(
repository.starrById(action.articleId.toInt()),
action
)
action.unstarred -> doAndReportOnFail(
repository.unstarrById(action.articleId.toInt()),
action
)
}
actions.forEach { action ->
when {
action.read -> doAndReportOnFail(
repository.markAsReadById(action.articleId.toInt()),
action
)
action.unread -> doAndReportOnFail(
repository.unmarkAsReadById(action.articleId.toInt()),
action
)
action.starred -> doAndReportOnFail(
repository.starrById(action.articleId.toInt()),
action
)
action.unstarred -> doAndReportOnFail(
repository.unstarrById(action.articleId.toInt()),
action
)
}
}
if (context.isNetworkAvailable()) {
AmineB marked this conversation as resolved Outdated

Same here.

Same here.
launch {
try {
val newItems = repository.allItems(ItemType.UNREAD)
handleNewItemsNotification(newItems, notifyNewItems, notificationManager)
val readItems = repository.allItems(ItemType.ALL)
val starredItems = repository.allItems(ItemType.STARRED)
// TODO: save all to DB
} catch (e: Throwable) {}
}
}
launch {
try {
val newItems = repository.allItems(ItemType.UNREAD)
handleNewItemsNotification(newItems, notifyNewItems, notificationManager)
val readItems = repository.allItems(ItemType.ALL)
val starredItems = repository.allItems(ItemType.STARRED)
// TODO: save all to DB
} 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.glide.getBitmapInputStream
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.rest.SelfossModel
import bou.amine.apps.readerforselfossv2.utils.isEmptyOrNullOrNullString
@ -276,9 +275,9 @@ class ArticleFragment : Fragment(), DIAware {
}
private fun getContentFromMercury(customTabsIntent: CustomTabsIntent) {
if ((context != null && requireContext().isNetworkAvailable(null)) || context == null) {
AmineB marked this conversation as resolved Outdated

Same here. We are fetching data from an api, and we there is no need to do try anything if there is no network available.

Same here. We are fetching data from an api, and we there is no need to do try anything if there is no network available.

Right!
By the way, what should this Mercury be?
I never saw this work and each time I got the article open in the browser instead.

Right! By the way, what should this Mercury be? I never saw this work and each time I got the article open in the browser instead.

This is mercury. It parses the page and fetch it´s content.

I'll look into why it does not work.

[This is mercury](https://mercury.postlight.com/web-parser/). It parses the page and fetch it´s content. I'll look into why it does not work.
binding.progressBar.visibility = View.VISIBLE
val parser = MercuryApi()
if (repository.isNetworkAvailable()) {
AmineB marked this conversation as resolved Outdated

This should only be done if the network is available as it was before.

This should only be done if the network is available as it was before.
binding.progressBar.visibility = View.VISIBLE
val parser = MercuryApi()
parser.parseUrl(url).enqueue(
object : Callback<ParsedContent> {
@ -317,7 +316,10 @@ class ArticleFragment : Fragment(), DIAware {
Glide
.with(requireContext())
.asBitmap()
.loadMaybeBasicAuth(config, response.body()!!.lead_image_url.orEmpty())
.loadMaybeBasicAuth(
config,
response.body()!!.lead_image_url.orEmpty()
)
.apply(RequestOptions.fitCenterTransform())
.into(binding.imageView)
} catch (e: Exception) {

View File

@ -1,52 +1,14 @@
package bou.amine.apps.readerforselfossv2.android.utils.network
import android.content.Context
import android.graphics.Color
import android.net.ConnectivityManager
import android.net.NetworkCapabilities
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
var snackBarShown = false
var view: View? = null
lateinit var s: Snackbar
fun Context.isNetworkAvailable(
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 {
fun isNetworkAccessible(context: Context): Boolean {
val connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
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) {
AmineB marked this conversation as resolved Outdated

It would be better to emit an enum. We'll handle the translation on the android/ios side.

It would be better to emit an enum. We'll handle the translation on the android/ios side.

What do you mean? This view model is already in the android side; these strings will have to be localized. I can do it now

What do you mean? This view model is already in the android side; these strings will have to be localized. I can do it now

I didn´t see that it was in the android side.

Yes, please use localized stringd.

I didn´t see that it was in the android side. Yes, please use localized stringd.
_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_summary">Disable this if your server is receiving excessive amounts of database queries.</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_off">Articles will not be synced in the background</string>
<string name="pref_switch_periodic_refresh_on">Articles will periodically be synced</string>

View File

@ -36,6 +36,9 @@ kotlin {
//Logging
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 {

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.utils.DateUtils
import bou.amine.apps.readerforselfossv2.utils.ItemType
import com.github.ln_12.library.ConnectivityStatus
import com.russhwolf.settings.Settings
import io.github.aakira.napier.Napier
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
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()
var items = ArrayList<SelfossModel.Item>()
val isConnectionAvailable = connectivityStatus.isNetworkConnected
var connectionMonitored = false
var baseUrl = apiDetails.getBaseUrl()
lateinit var dateUtils: DateUtils
@ -26,6 +29,7 @@ class Repository(private val api: SelfossApi, private val apiDetails: ApiDetails
var searchFilter: String? = null
var itemsCaching = settings.getBoolean("items_caching", false)
var offlineOverride = false
var apiMajorVersion = 0
var badgeUnread = 0
@ -45,14 +49,21 @@ class Repository(private val api: SelfossApi, private val apiDetails: ApiDetails
}
suspend fun getNewerItems(): ArrayList<SelfossModel.Item> {
// TODO: Check connectivity, use the updatedSince parameter
val fetchedItems = api.getItems(displayedItems.type,
settings.getString("prefer_api_items_number", "200").toInt(),
offset = 0,
tagFilter?.tag,
sourceFilter?.id?.toLong(),
searchFilter,
null)
// TODO: Use the updatedSince parameter
var fetchedItems: List<SelfossModel.Item>? = null
AmineB marked this conversation as resolved Outdated

isConnectionAvailable.value && !offlineOverride should be refactored inside a method named isNetworkAvailable()

`isConnectionAvailable.value && !offlineOverride` should be refactored inside a method named `isNetworkAvailable()`
if (isNetworkAvailable()) {
fetchedItems = api.getItems(
displayedItems.type,
AmineB marked this conversation as resolved Outdated

There should be a message emited on network available too.

There should be a message emited on network available too.
settings.getString("prefer_api_items_number", "200").toInt(),
offset = 0,
tagFilter?.tag,
sourceFilter?.id?.toLong(),
searchFilter,
null
)
AmineB marked this conversation as resolved Outdated

This block should be after the else, since the db will work as a fallback and we'll get the items from there.

Same for the other block later.

This block should be after the else, since the db will work as a fallback and we'll get the items from there. Same for the other block later.

I'm not sure how the database will handle this.
This is correct as of now, moving this block after the else will generate errors if the else block is encountered since the fetchedItems variables is only defined within the if block.

I'm not sure how the database will handle this. This is correct as of now, moving this block after the else will generate errors if the else block is encountered since the fetchedItems variables is only defined within the if block.

The else block would have the BD fetching that'll assign the DB items to fetchedItems.

The `else` block would have the BD fetching that'll assign the DB items to `fetchedItems`.
} else {
AmineB marked this conversation as resolved Outdated

This should be moved after the else block

This should be moved after the `else` block
// TODO: Get items from the database
}
AmineB marked this conversation as resolved Outdated

The error message should be displayed when the network status change, not after every api call.

Same for every comment like this one.

The error message should be displayed when the network status change, not after every api call. Same for every comment like this one.
if (fetchedItems != null) {
items = ArrayList(fetchedItems)
@ -61,15 +72,21 @@ class Repository(private val api: SelfossApi, private val apiDetails: ApiDetails
}
suspend fun getOlderItems(): ArrayList<SelfossModel.Item> {
// TODO: Check connectivity
val offset = items.size
val fetchedItems = api.getItems(displayedItems.type,
settings.getString("prefer_api_items_number", "200").toInt(),
offset,
tagFilter?.tag,
sourceFilter?.id?.toLong(),
searchFilter,
null)
var fetchedItems: List<SelfossModel.Item>? = null
if (isNetworkAvailable()) {
val offset = items.size
fetchedItems = api.getItems(
displayedItems.type,
settings.getString("prefer_api_items_number", "200").toInt(),
offset,
tagFilter?.tag,
sourceFilter?.id?.toLong(),
searchFilter,
null
)
} else {
AmineB marked this conversation as resolved Outdated

This should be moved after the else block

This should be moved after the `else` block
// TODO: Get items from the database
}
if (fetchedItems != null) {
appendItems(fetchedItems)
@ -77,8 +94,22 @@ class Repository(private val api: SelfossApi, private val apiDetails: ApiDetails
return items
}
suspend fun allItems(itemType: ItemType): List<SelfossModel.Item>? =
api.getItems(itemType.type, 200, 0, tagFilter?.tag, sourceFilter?.id?.toLong(), searchFilter, null)
suspend fun allItems(itemType: ItemType): List<SelfossModel.Item>? {
return if (isNetworkAvailable()) {
api.getItems(
itemType.type,
200,
0,
tagFilter?.tag,
sourceFilter?.id?.toLong(),
searchFilter,
null
)
AmineB marked this conversation as resolved Outdated

This should be a todo

This should be a todo
} else {
// TODO: Provide an error message
null
}
}
private fun appendItems(fetchedItems: List<SelfossModel.Item>) {
// TODO: Store in DB if enabled by user
@ -94,35 +125,52 @@ class Repository(private val api: SelfossApi, private val apiDetails: ApiDetails
}
suspend fun reloadBadges(): Boolean {
// TODO: Check connectivity, calculate from DB
var success = false
val response = api.stats()
if (response != null) {
badgeUnread = response.unread
badgeAll = response.total
badgeStarred = response.starred
success = true
if (isNetworkAvailable()) {
val response = api.stats()
if (response != null) {
badgeUnread = response.unread
badgeAll = response.total
badgeStarred = response.starred
success = true
}
} else {
// TODO: Compute badges from database
}
return success
}
suspend fun getTags(): List<SelfossModel.Tag>? {
// TODO: Check success, store in DB
return api.tags()
// TODO: Store in DB
return if (isNetworkAvailable()) {
api.tags()
} else {
// TODO: Compute from database
null
}
}
suspend fun getSpouts(): Map<String, SelfossModel.Spout>? {
// TODO: Check success, store in DB
return api.spouts()
// TODO: Store in DB
return if (isNetworkAvailable()) {
api.spouts()
} else {
// TODO: Compute from database
null
}
}
suspend fun getSources(): ArrayList<SelfossModel.Source>? {
// TODO: Check success
return api.sources()
// TODO: Store in DB
return if (isNetworkAvailable()) {
api.sources()
} else {
// TODO: Compute from database
null
}
}
suspend fun markAsRead(item: SelfossModel.Item): Boolean {
// TODO: Check internet connection
val success = markAsReadById(item.id)
if (success) {
@ -131,10 +179,9 @@ class Repository(private val api: SelfossApi, private val apiDetails: ApiDetails
return success
}
suspend fun markAsReadById(id: Int): Boolean {
// TODO: Check internet connection
return api.markAsRead(id.toString())?.isSuccess == true
}
suspend fun markAsReadById(id: Int): Boolean =
AmineB marked this conversation as resolved Outdated

Every block that does something like

        var success = false
	
        if (isNetworkAvailable()) {
            success = DOSOMETHING()
        }
        return success

can be refactored to something like

return isNetworkAvailable() && DOSOMETHING()

Every block that does something like ``` var success = false if (isNetworkAvailable()) { success = DOSOMETHING() } return success ``` can be refactored to something like ``` return isNetworkAvailable() && DOSOMETHING() ```
isNetworkAvailable() && api.markAsRead(id.toString())?.isSuccess == true
suspend fun unmarkAsRead(item: SelfossModel.Item): Boolean {
val success = unmarkAsReadById(item.id)
@ -145,10 +192,8 @@ class Repository(private val api: SelfossApi, private val apiDetails: ApiDetails
return success
}
suspend fun unmarkAsReadById(id: Int): Boolean {
// TODO: Check internet connection
return api.unmarkAsRead(id.toString())?.isSuccess == true
}
suspend fun unmarkAsReadById(id: Int): Boolean =
isNetworkAvailable() && api.unmarkAsRead(id.toString())?.isSuccess == true
suspend fun starr(item: SelfossModel.Item): Boolean {
val success = starrById(item.id)
@ -159,10 +204,8 @@ class Repository(private val api: SelfossApi, private val apiDetails: ApiDetails
return success
}
suspend fun starrById(id: Int): Boolean {
// TODO: Check success, store in DB
return api.starr(id.toString())?.isSuccess == true
}
suspend fun starrById(id: Int): Boolean =
isNetworkAvailable() && api.starr(id.toString())?.isSuccess == true
suspend fun unstarr(item: SelfossModel.Item): Boolean {
val success = unstarrById(item.id)
@ -173,17 +216,15 @@ class Repository(private val api: SelfossApi, private val apiDetails: ApiDetails
return success
}
AmineB marked this conversation as resolved Outdated

Please refactor this

Please refactor this
suspend fun unstarrById(id: Int): Boolean {
// TODO: Check internet connection
return api.unstarr(id.toString())?.isSuccess == true
}
suspend fun unstarrById(id: Int): Boolean =
isNetworkAvailable() && api.unstarr(id.toString())?.isSuccess == true
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 (success) {
if (isNetworkAvailable() && api.markAllAsRead(items.map { it.id.toString() })?.isSuccess == true) {
success = true
for (item in items) {
markAsReadLocally(item)
}
@ -230,45 +271,46 @@ class Repository(private val api: SelfossApi, private val apiDetails: ApiDetails
tags: String,
filter: String
): Boolean {
// TODO: Check connectivity
val response = api.createSourceForVersion(
title,
url,
spout,
tags,
filter,
apiMajorVersion
)
var response = false
if (isNetworkAvailable()) {
response = api.createSourceForVersion(
title,
url,
spout,
tags,
filter,
apiMajorVersion
)?.isSuccess == true
}
return response != null
return response
}
suspend fun deleteSource(id: Int): Boolean {
// TODO: Check connectivity, store in DB
// TODO: Store in DB
var success = false
val response = api.deleteSource(id)
if (response != null) {
success = response.isSuccess
if (isNetworkAvailable()) {
val response = api.deleteSource(id)
if (response != null) {
success = response.isSuccess
}
}
return success
}
suspend fun updateRemote(): Boolean {
// TODO: Handle connectivity issues
val response = api.update()
return response?.isSuccess ?: false
}
suspend fun updateRemote(): Boolean =
isNetworkAvailable() && api.update()?.isSuccess == true
suspend fun login(): Boolean {
var result = false
try {
val response = api.login()
if (response != null && response.isSuccess) {
result = true
if (isNetworkAvailable()) {
try {
val response = api.login()
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
}
@ -287,15 +329,18 @@ class Repository(private val api: SelfossApi, private val apiDetails: ApiDetails
}
private suspend fun updateApiVersion() {
// TODO: Handle connectivity issues
val fetchedVersion = api.version()
if (fetchedVersion != null) {
apiMajorVersion = fetchedVersion.getApiMajorVersion()
settings.putInt("apiVersionMajor", apiMajorVersion)
} else {
apiMajorVersion = settings.getInt("apiVersionMajor", 0)
apiMajorVersion = settings.getInt("apiVersionMajor", 0)
if (isNetworkAvailable()) {
val fetchedVersion = api.version()
if (fetchedVersion != null) {
apiMajorVersion = fetchedVersion.getApiMajorVersion()
settings.putInt("apiVersionMajor", apiMajorVersion)
}
}
}
fun isNetworkAvailable() = isConnectionAvailable.value && !offlineOverride
// TODO: Handle offline actions
}