diff --git a/androidApp/build.gradle.kts b/androidApp/build.gradle.kts index 72c3fa7..b548d33 100644 --- a/androidApp/build.gradle.kts +++ b/androidApp/build.gradle.kts @@ -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") } \ No newline at end of file diff --git a/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/HomeActivity.kt b/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/HomeActivity.kt index c77bbfe..4934d5f 100644 --- a/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/HomeActivity.kt +++ b/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/HomeActivity.kt @@ -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 @@ -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? 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() + 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( + 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) } } } diff --git a/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/LoginActivity.kt b/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/LoginActivity.kt index 3ef0544..544325f 100644 --- a/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/LoginActivity.kt +++ b/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/LoginActivity.kt @@ -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")) } } } diff --git a/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/MyApp.kt b/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/MyApp.kt index a59b0d4..be77f62 100644 --- a/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/MyApp.kt +++ b/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/MyApp.kt @@ -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() with singleton { Repository(instance(), instance()) } + bind() with singleton { Repository(instance(), instance(), connectivityStatus) } + bind() with singleton { ConnectivityStatus(applicationContext) } + bind() 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) + } + } } \ No newline at end of file diff --git a/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/SourcesActivity.kt b/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/SourcesActivity.kt index fe94cf3..10988e9 100644 --- a/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/SourcesActivity.kt +++ b/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/SourcesActivity.kt @@ -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() } } diff --git a/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/adapters/ItemCardAdapter.kt b/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/adapters/ItemCardAdapter.kt index c4a32fe..aa87278 100644 --- a/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/adapters/ItemCardAdapter.kt +++ b/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/adapters/ItemCardAdapter.kt @@ -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 } } diff --git a/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/adapters/SourcesListAdapter.kt b/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/adapters/SourcesListAdapter.kt index 30c9260..dd7d9e0 100644 --- a/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/adapters/SourcesListAdapter.kt +++ b/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/adapters/SourcesListAdapter.kt @@ -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() } } } diff --git a/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/background/background.kt b/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/background/background.kt index ddbf1d8..03a0097 100644 --- a/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/background/background.kt +++ b/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/background/background.kt @@ -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) - 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 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()) { - 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) {} } } } diff --git a/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/fragments/ArticleFragment.kt b/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/fragments/ArticleFragment.kt index 2b9d6b2..abc8f2c 100644 --- a/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/fragments/ArticleFragment.kt +++ b/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/fragments/ArticleFragment.kt @@ -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) { - binding.progressBar.visibility = View.VISIBLE - val parser = MercuryApi() + if (repository.isNetworkAvailable()) { + binding.progressBar.visibility = View.VISIBLE + val parser = MercuryApi() parser.parseUrl(url).enqueue( object : Callback { @@ -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) { diff --git a/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/utils/network/NetworkUtils.kt b/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/utils/network/NetworkUtils.kt index 2edfc1c..3ed5c53 100644 --- a/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/utils/network/NetworkUtils.kt +++ b/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/utils/network/NetworkUtils.kt @@ -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) { diff --git a/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/viewmodel/AppViewModel.kt b/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/viewmodel/AppViewModel.kt new file mode 100644 index 0000000..9320762 --- /dev/null +++ b/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/viewmodel/AppViewModel.kt @@ -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() + 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 + } + } + } + } +} \ No newline at end of file diff --git a/androidApp/src/main/res/values/strings.xml b/androidApp/src/main/res/values/strings.xml index fc20983..a83ab53 100644 --- a/androidApp/src/main/res/values/strings.xml +++ b/androidApp/src/main/res/values/strings.xml @@ -143,6 +143,8 @@ Check for new sources and tags Disable this if your server is receiving excessive amounts of database queries. Not connected ! + "Network connection lost" + "Network connection is now available" Sync articles Articles will not be synced in the background Articles will periodically be synced diff --git a/shared/build.gradle.kts b/shared/build.gradle.kts index ec4c4b3..f8242cd 100644 --- a/shared/build.gradle.kts +++ b/shared/build.gradle.kts @@ -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 { diff --git a/shared/src/commonMain/kotlin/bou/amine/apps/readerforselfossv2/repository/RepositoryImpl.kt b/shared/src/commonMain/kotlin/bou/amine/apps/readerforselfossv2/repository/RepositoryImpl.kt index 2dcaa46..287ef1c 100644 --- a/shared/src/commonMain/kotlin/bou/amine/apps/readerforselfossv2/repository/RepositoryImpl.kt +++ b/shared/src/commonMain/kotlin/bou/amine/apps/readerforselfossv2/repository/RepositoryImpl.kt @@ -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() + 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 { - // 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? = null + if (isNetworkAvailable()) { + fetchedItems = api.getItems( + displayedItems.type, + settings.getString("prefer_api_items_number", "200").toInt(), + offset = 0, + tagFilter?.tag, + sourceFilter?.id?.toLong(), + searchFilter, + null + ) + } else { + // TODO: Get items from the database + } if (fetchedItems != null) { items = ArrayList(fetchedItems) @@ -61,15 +72,21 @@ class Repository(private val api: SelfossApi, private val apiDetails: ApiDetails } suspend fun getOlderItems(): ArrayList { - // 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? = 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 { + // 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? = - api.getItems(itemType.type, 200, 0, tagFilter?.tag, sourceFilter?.id?.toLong(), searchFilter, null) + suspend fun allItems(itemType: ItemType): List? { + 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) { // 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? { - // 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? { - // 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? { - // 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 = + 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 } - 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): 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 } \ No newline at end of file