chore: new connectivity dep. Closes #84.
All checks were successful
Check PR code / Lint (pull_request) Successful in 1m2s
Check PR code / translations (pull_request) Successful in 1m24s
Check PR code / build (pull_request) Successful in 14m10s

This commit is contained in:
Amine Bouabdallaoui 2025-03-10 21:11:39 +01:00
parent ef13e300f0
commit 3bf60f1146
8 changed files with 152 additions and 144 deletions

View File

@ -12,8 +12,12 @@ plugins {
id("app.cash.sqldelight") version "2.0.2" id("app.cash.sqldelight") version "2.0.2"
} }
fun Project.execWithOutput(cmd: String, ignore: Boolean = false): String { fun Project.execWithOutput(
val result: String = ByteArrayOutputStream().use { outputStream -> cmd: String,
ignore: Boolean = false,
): String {
val result: String =
ByteArrayOutputStream().use { outputStream ->
project.exec { project.exec {
commandLine = cmd.split(" ") commandLine = cmd.split(" ")
standardOutput = outputStream standardOutput = outputStream
@ -26,14 +30,20 @@ fun Project.execWithOutput(cmd: String, ignore: Boolean = false): String {
fun gitVersion(): String { fun gitVersion(): String {
val maybeTagOfCurrentCommit = execWithOutput("git -C ../ describe --contains HEAD", true) val maybeTagOfCurrentCommit = execWithOutput("git -C ../ describe --contains HEAD", true)
val process = if (maybeTagOfCurrentCommit.isEmpty()) { val process =
if (maybeTagOfCurrentCommit.isEmpty()) {
println("No tag on current commit. Will take the latest one.") println("No tag on current commit. Will take the latest one.")
execWithOutput("git -C ../ for-each-ref refs/tags --sort=-refname --format='%(refname:short)' --count=1") execWithOutput("git -C ../ for-each-ref refs/tags --sort=-refname --format='%(refname:short)' --count=1")
} else { } else {
println("Tag found on current commit") println("Tag found on current commit")
execWithOutput("git -C ../ describe --contains HEAD") execWithOutput("git -C ../ describe --contains HEAD")
} }
return process.replace("^0", "").replace("'", "").substring(1).replace("\\.", "").trim() return process
.replace("^0", "")
.replace("'", "")
.substring(1)
.replace("\\.", "")
.trim()
} }
fun versionCodeFromGit(): Int { fun versionCodeFromGit(): Int {
@ -116,7 +126,6 @@ android {
isIncludeAndroidResources = true isIncludeAndroidResources = true
} }
} }
} }
dependencies { dependencies {
@ -141,7 +150,7 @@ dependencies {
implementation("androidx.constraintlayout:constraintlayout:2.2.0") implementation("androidx.constraintlayout:constraintlayout:2.2.0")
implementation("org.jsoup:jsoup:1.18.3") implementation("org.jsoup:jsoup:1.18.3")
//multidex // multidex
implementation("androidx.multidex:multidex:2.0.1") implementation("androidx.multidex:multidex:2.0.1")
// About // About
@ -162,31 +171,28 @@ dependencies {
implementation("me.relex:circleindicator:2.1.6") implementation("me.relex:circleindicator:2.1.6")
implementation("androidx.viewpager2:viewpager2:1.1.0") implementation("androidx.viewpager2:viewpager2:1.1.0")
//Dependency Injection // Dependency Injection
implementation("org.kodein.di:kodein-di:7.23.1") implementation("org.kodein.di:kodein-di:7.23.1")
implementation("org.kodein.di:kodein-di-framework-android-x:7.23.1") implementation("org.kodein.di:kodein-di-framework-android-x:7.23.1")
implementation("org.kodein.di:kodein-di-framework-android-x-viewmodel:7.23.1") implementation("org.kodein.di:kodein-di-framework-android-x-viewmodel:7.23.1")
//Settings // Settings
implementation("com.russhwolf:multiplatform-settings-no-arg:1.3.0") implementation("com.russhwolf:multiplatform-settings-no-arg:1.3.0")
//Logging // Logging
implementation("io.github.aakira:napier:2.7.1") implementation("io.github.aakira:napier:2.7.1")
//PhotoView // PhotoView
implementation("com.github.chrisbanes:PhotoView:2.3.0") implementation("com.github.chrisbanes:PhotoView:2.3.0")
implementation("androidx.core:core-ktx:1.15.0") implementation("androidx.core:core-ktx:1.15.0")
implementation("androidx.lifecycle:lifecycle-extensions:2.2.0") implementation("androidx.lifecycle:lifecycle-extensions:2.2.0")
// Network information
implementation("com.github.ln-12:multiplatform-connectivity-status:1.3.0")
// SQLDELIGHT // SQLDELIGHT
implementation("app.cash.sqldelight:android-driver:2.0.2") implementation("app.cash.sqldelight:android-driver:2.0.2")
//test // test
testImplementation("junit:junit:4.13.2") testImplementation("junit:junit:4.13.2")
testImplementation("io.mockk:mockk:1.13.14") testImplementation("io.mockk:mockk:1.13.14")
testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.1") testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.1")
@ -210,10 +216,11 @@ tasks.withType<Test> {
useJUnit() useJUnit()
testLogging { testLogging {
exceptionFormat = org.gradle.api.tasks.testing.logging.TestExceptionFormat.FULL exceptionFormat = org.gradle.api.tasks.testing.logging.TestExceptionFormat.FULL
events = setOf( events =
setOf(
org.gradle.api.tasks.testing.logging.TestLogEvent.PASSED, org.gradle.api.tasks.testing.logging.TestLogEvent.PASSED,
org.gradle.api.tasks.testing.logging.TestLogEvent.FAILED, org.gradle.api.tasks.testing.logging.TestLogEvent.FAILED,
org.gradle.api.tasks.testing.logging.TestLogEvent.STANDARD_ERROR org.gradle.api.tasks.testing.logging.TestLogEvent.STANDARD_ERROR,
) )
showStandardStreams = true showStandardStreams = true
} }

View File

@ -10,18 +10,16 @@ import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.ProcessLifecycleOwner import androidx.lifecycle.ProcessLifecycleOwner
import androidx.multidex.MultiDexApplication import androidx.multidex.MultiDexApplication
import bou.amine.apps.readerforselfossv2.android.testing.TestingHelper import bou.amine.apps.readerforselfossv2.android.testing.TestingHelper
import bou.amine.apps.readerforselfossv2.android.viewmodel.AppViewModel
import bou.amine.apps.readerforselfossv2.dao.DriverFactory import bou.amine.apps.readerforselfossv2.dao.DriverFactory
import bou.amine.apps.readerforselfossv2.dao.ReaderForSelfossDB import bou.amine.apps.readerforselfossv2.dao.ReaderForSelfossDB
import bou.amine.apps.readerforselfossv2.di.networkModule import bou.amine.apps.readerforselfossv2.di.networkModule
import bou.amine.apps.readerforselfossv2.repository.Repository import bou.amine.apps.readerforselfossv2.repository.Repository
import bou.amine.apps.readerforselfossv2.service.AppSettingsService import bou.amine.apps.readerforselfossv2.service.AppSettingsService
import com.github.ln_12.library.ConnectivityStatus import bou.amine.apps.readerforselfossv2.service.ConnectivityService
import io.github.aakira.napier.DebugAntilog import io.github.aakira.napier.DebugAntilog
import io.github.aakira.napier.Napier import io.github.aakira.napier.Napier
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.acra.ACRA import org.acra.ACRA
import org.acra.ReportField import org.acra.ReportField
@ -44,27 +42,21 @@ class MyApp :
import(networkModule) import(networkModule)
bind<DriverFactory>() with singleton { DriverFactory(applicationContext) } bind<DriverFactory>() with singleton { DriverFactory(applicationContext) }
bind<ReaderForSelfossDB>() with singleton { ReaderForSelfossDB(driverFactory.createDriver()) } bind<ReaderForSelfossDB>() with singleton { ReaderForSelfossDB(driverFactory.createDriver()) }
bind<ConnectivityService>() with singleton { ConnectivityService() }
bind<Repository>() with bind<Repository>() with
singleton { singleton {
Repository( Repository(
instance(), instance(),
instance(), instance(),
isConnectionAvailable, instance(),
instance(), instance(),
) )
} }
bind<ConnectivityStatus>() with singleton { ConnectivityStatus(applicationContext) }
bind<AppViewModel>() with singleton { AppViewModel(repository = instance()) }
} }
private val repository: Repository by instance() private val repository: Repository by instance()
private val viewModel: AppViewModel by instance()
private val connectivityStatus: ConnectivityStatus by instance()
private val driverFactory: DriverFactory by instance() private val driverFactory: DriverFactory by instance()
private val connectivityService: ConnectivityService by instance()
@Suppress("detekt:ForbiddenComment")
// TODO: handle with the "previous" way
private val isConnectionAvailable: MutableStateFlow<Boolean> = MutableStateFlow(true)
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
@ -77,13 +69,12 @@ class MyApp :
ProcessLifecycleOwner.get().lifecycle.addObserver( ProcessLifecycleOwner.get().lifecycle.addObserver(
AppLifeCycleObserver( AppLifeCycleObserver(
connectivityStatus, connectivityService,
repository,
), ),
) )
CoroutineScope(Dispatchers.Main).launch { CoroutineScope(Dispatchers.Main).launch {
viewModel.networkAvailableProvider.collect { networkAvailable -> connectivityService.networkAvailableProvider.collect { networkAvailable ->
val toastMessage = val toastMessage =
if (networkAvailable) { if (networkAvailable) {
repository.handleDBActions() repository.handleDBActions()
@ -189,18 +180,15 @@ class MyApp :
} }
class AppLifeCycleObserver( class AppLifeCycleObserver(
val connectivityStatus: ConnectivityStatus, val connectivityService: ConnectivityService,
val repository: Repository,
) : DefaultLifecycleObserver { ) : DefaultLifecycleObserver {
override fun onResume(owner: LifecycleOwner) { override fun onResume(owner: LifecycleOwner) {
super.onResume(owner) super.onResume(owner)
repository.connectionMonitored = true connectivityService.start()
connectivityStatus.start()
} }
override fun onPause(owner: LifecycleOwner) { override fun onPause(owner: LifecycleOwner) {
repository.connectionMonitored = false connectivityService.stop()
connectivityStatus.stop()
super.onPause(owner) super.onPause(owner)
} }
} }

View File

@ -42,6 +42,7 @@ import bou.amine.apps.readerforselfossv2.model.SelfossModel
import bou.amine.apps.readerforselfossv2.repository.Repository import bou.amine.apps.readerforselfossv2.repository.Repository
import bou.amine.apps.readerforselfossv2.rest.MercuryApi import bou.amine.apps.readerforselfossv2.rest.MercuryApi
import bou.amine.apps.readerforselfossv2.service.AppSettingsService import bou.amine.apps.readerforselfossv2.service.AppSettingsService
import bou.amine.apps.readerforselfossv2.service.ConnectivityService
import bou.amine.apps.readerforselfossv2.utils.getHtmlDecoded import bou.amine.apps.readerforselfossv2.utils.getHtmlDecoded
import bou.amine.apps.readerforselfossv2.utils.getImages import bou.amine.apps.readerforselfossv2.utils.getImages
import bou.amine.apps.readerforselfossv2.utils.getThumbnail import bou.amine.apps.readerforselfossv2.utils.getThumbnail
@ -88,6 +89,7 @@ class ArticleFragment :
override val di: DI by closestDI() override val di: DI by closestDI()
private val repository: Repository by instance() private val repository: Repository by instance()
private val appSettingsService: AppSettingsService by instance() private val appSettingsService: AppSettingsService by instance()
private val connectivityService: ConnectivityService by instance()
private var typeface: Typeface? = null private var typeface: Typeface? = null
private var resId: Int = 0 private var resId: Int = 0
@ -168,7 +170,7 @@ class ArticleFragment :
private fun handleContent() { private fun handleContent() {
if (contentText.isEmptyOrNullOrNullString()) { if (contentText.isEmptyOrNullOrNullString()) {
if (repository.isNetworkAvailable() && url.isUrlValid()) { if (connectivityService.isNetworkAvailable() == true && url.isUrlValid()) {
getContentFromMercury(url!!) getContentFromMercury(url!!)
} }
} else { } else {

View File

@ -1,32 +0,0 @@
package bou.amine.apps.readerforselfossv2.android.viewmodel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
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 _networkAvailableProvider = MutableSharedFlow<Boolean>()
val networkAvailableProvider = _networkAvailableProvider.asSharedFlow()
private var wasConnected = true
init {
viewModelScope.launch {
repository.isConnectionAvailable.collect { isConnected ->
if (repository.connectionMonitored) {
if (isConnected && !wasConnected && repository.connectionMonitored) {
_networkAvailableProvider.emit(true)
wasConnected = true
} else if (!isConnected && wasConnected && repository.connectionMonitored) {
_networkAvailableProvider.emit(false)
wasConnected = false
}
}
}
}
}
}

View File

@ -11,6 +11,7 @@ import bou.amine.apps.readerforselfossv2.model.SuccessResponse
import bou.amine.apps.readerforselfossv2.repository.Repository import bou.amine.apps.readerforselfossv2.repository.Repository
import bou.amine.apps.readerforselfossv2.rest.SelfossApi import bou.amine.apps.readerforselfossv2.rest.SelfossApi
import bou.amine.apps.readerforselfossv2.service.AppSettingsService import bou.amine.apps.readerforselfossv2.service.AppSettingsService
import bou.amine.apps.readerforselfossv2.service.ConnectivityService
import bou.amine.apps.readerforselfossv2.utils.ItemType import bou.amine.apps.readerforselfossv2.utils.ItemType
import bou.amine.apps.readerforselfossv2.utils.toView import bou.amine.apps.readerforselfossv2.utils.toView
import io.mockk.clearAllMocks import io.mockk.clearAllMocks
@ -24,7 +25,6 @@ import junit.framework.TestCase.assertFalse
import junit.framework.TestCase.assertNotSame import junit.framework.TestCase.assertNotSame
import junit.framework.TestCase.assertSame import junit.framework.TestCase.assertSame
import junit.framework.TestCase.assertTrue import junit.framework.TestCase.assertTrue
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertNotEquals import org.junit.Assert.assertNotEquals
import org.junit.Before import org.junit.Before
@ -52,15 +52,12 @@ class RepositoryTest {
private val db = mockk<ReaderForSelfossDB>(relaxed = true) private val db = mockk<ReaderForSelfossDB>(relaxed = true)
private val appSettingsService = mockk<AppSettingsService>() private val appSettingsService = mockk<AppSettingsService>()
private val api = mockk<SelfossApi>() private val api = mockk<SelfossApi>()
private val connectivityService = mockk<ConnectivityService>()
private lateinit var repository: Repository private lateinit var repository: Repository
private fun initializeRepository( private fun initializeRepository(isNetworkAvailable: Boolean? = true) {
isConnectionAvailable: MutableStateFlow<Boolean> = every { connectivityService.isNetworkAvailable() } returns isNetworkAvailable
MutableStateFlow( repository = Repository(api, appSettingsService, connectivityService, db)
true,
),
) {
repository = Repository(api, appSettingsService, isConnectionAvailable, db)
runBlocking { runBlocking {
repository.updateApiInformation() repository.updateApiInformation()
@ -110,7 +107,7 @@ class RepositoryTest {
fun instantiate_repository_without_api_version() { fun instantiate_repository_without_api_version() {
every { appSettingsService.getApiVersion() } returns -1 every { appSettingsService.getApiVersion() } returns -1
initializeRepository(MutableStateFlow(false)) initializeRepository(false)
coVerify(exactly = 0) { api.apiInformation() } coVerify(exactly = 0) { api.apiInformation() }
coVerify(exactly = 0) { api.stats() } coVerify(exactly = 0) { api.stats() }
@ -287,7 +284,7 @@ class RepositoryTest {
fun get_newer_items_without_connectivity() { fun get_newer_items_without_connectivity() {
every { appSettingsService.isItemCachingEnabled() } returns true every { appSettingsService.isItemCachingEnabled() } returns true
initializeRepository(MutableStateFlow(false)) initializeRepository(false)
runBlocking { runBlocking {
repository.getNewerItems() repository.getNewerItems()
} }
@ -314,7 +311,7 @@ class RepositoryTest {
every { appSettingsService.isItemCachingEnabled() } returns true every { appSettingsService.isItemCachingEnabled() } returns true
initializeRepository(MutableStateFlow(false)) initializeRepository(false)
repository.setTagFilter(SelfossModel.Tag("Test", "red", 3)) repository.setTagFilter(SelfossModel.Tag("Test", "red", 3))
runBlocking { runBlocking {
repository.getNewerItems() repository.getNewerItems()
@ -342,7 +339,7 @@ class RepositoryTest {
every { appSettingsService.isItemCachingEnabled() } returns true every { appSettingsService.isItemCachingEnabled() } returns true
initializeRepository(MutableStateFlow(false)) initializeRepository(false)
repository.setSourceFilter( repository.setSourceFilter(
SelfossModel.SourceDetail( SelfossModel.SourceDetail(
1, 1,
@ -457,7 +454,7 @@ class RepositoryTest {
var success: Boolean var success: Boolean
initializeRepository(MutableStateFlow(false)) initializeRepository(false)
runBlocking { runBlocking {
success = repository.reloadBadges() success = repository.reloadBadges()
} }
@ -477,7 +474,7 @@ class RepositoryTest {
var success: Boolean var success: Boolean
initializeRepository(MutableStateFlow(false)) initializeRepository(false)
runBlocking { runBlocking {
success = repository.reloadBadges() success = repository.reloadBadges()
} }
@ -572,7 +569,7 @@ class RepositoryTest {
every { appSettingsService.isUpdateSourcesEnabled() } returns true every { appSettingsService.isUpdateSourcesEnabled() } returns true
every { appSettingsService.isItemCachingEnabled() } returns true every { appSettingsService.isItemCachingEnabled() } returns true
initializeRepository(MutableStateFlow(false)) initializeRepository(false)
var testTags: List<SelfossModel.Tag> var testTags: List<SelfossModel.Tag>
runBlocking { runBlocking {
testTags = repository.getTags() testTags = repository.getTags()
@ -590,7 +587,7 @@ class RepositoryTest {
every { appSettingsService.isItemCachingEnabled() } returns false every { appSettingsService.isItemCachingEnabled() } returns false
every { appSettingsService.isUpdateSourcesEnabled() } returns true every { appSettingsService.isUpdateSourcesEnabled() } returns true
initializeRepository(MutableStateFlow(false)) initializeRepository(false)
var testTags: List<SelfossModel.Tag> var testTags: List<SelfossModel.Tag>
runBlocking { runBlocking {
testTags = repository.getTags() testTags = repository.getTags()
@ -607,7 +604,7 @@ class RepositoryTest {
every { appSettingsService.isUpdateSourcesEnabled() } returns false every { appSettingsService.isUpdateSourcesEnabled() } returns false
every { appSettingsService.isItemCachingEnabled() } returns true every { appSettingsService.isItemCachingEnabled() } returns true
initializeRepository(MutableStateFlow(false)) initializeRepository(false)
var testTags: List<SelfossModel.Tag> var testTags: List<SelfossModel.Tag>
runBlocking { runBlocking {
testTags = repository.getTags() testTags = repository.getTags()
@ -625,7 +622,7 @@ class RepositoryTest {
every { appSettingsService.isUpdateSourcesEnabled() } returns false every { appSettingsService.isUpdateSourcesEnabled() } returns false
every { appSettingsService.isItemCachingEnabled() } returns false every { appSettingsService.isItemCachingEnabled() } returns false
initializeRepository(MutableStateFlow(false)) initializeRepository(false)
var testTags: List<SelfossModel.Tag> var testTags: List<SelfossModel.Tag>
runBlocking { runBlocking {
testTags = repository.getTags() testTags = repository.getTags()
@ -775,7 +772,7 @@ class RepositoryTest {
@Test @Test
fun get_sources_without_connection() { fun get_sources_without_connection() {
val (_, sourcesDB) = prepareSources() val (_, sourcesDB) = prepareSources()
initializeRepository(MutableStateFlow(false)) initializeRepository(false)
var testSources: List<SelfossModel.Source> var testSources: List<SelfossModel.Source>
runBlocking { runBlocking {
testSources = repository.getSourcesDetails() testSources = repository.getSourcesDetails()
@ -792,7 +789,7 @@ class RepositoryTest {
every { appSettingsService.isItemCachingEnabled() } returns false every { appSettingsService.isItemCachingEnabled() } returns false
every { appSettingsService.isUpdateSourcesEnabled() } returns true every { appSettingsService.isUpdateSourcesEnabled() } returns true
initializeRepository(MutableStateFlow(false)) initializeRepository(false)
var testSources: List<SelfossModel.Source> var testSources: List<SelfossModel.Source>
runBlocking { runBlocking {
testSources = repository.getSourcesDetails() testSources = repository.getSourcesDetails()
@ -809,7 +806,7 @@ class RepositoryTest {
every { appSettingsService.isItemCachingEnabled() } returns true every { appSettingsService.isItemCachingEnabled() } returns true
every { appSettingsService.isUpdateSourcesEnabled() } returns false every { appSettingsService.isUpdateSourcesEnabled() } returns false
initializeRepository(MutableStateFlow(false)) initializeRepository(false)
var testSources: List<SelfossModel.Source> var testSources: List<SelfossModel.Source>
runBlocking { runBlocking {
testSources = repository.getSourcesDetails() testSources = repository.getSourcesDetails()
@ -826,7 +823,7 @@ class RepositoryTest {
every { appSettingsService.isItemCachingEnabled() } returns false every { appSettingsService.isItemCachingEnabled() } returns false
every { appSettingsService.isUpdateSourcesEnabled() } returns false every { appSettingsService.isUpdateSourcesEnabled() } returns false
initializeRepository(MutableStateFlow(false)) initializeRepository(false)
var testSources: List<SelfossModel.Source> var testSources: List<SelfossModel.Source>
runBlocking { runBlocking {
testSources = repository.getSourcesDetails() testSources = repository.getSourcesDetails()
@ -898,7 +895,7 @@ class RepositoryTest {
coEvery { api.createSourceForVersion(any(), any(), any(), any()) } returns coEvery { api.createSourceForVersion(any(), any(), any(), any()) } returns
SuccessResponse(true) SuccessResponse(true)
initializeRepository(MutableStateFlow(false)) initializeRepository(false)
var response: Boolean var response: Boolean
runBlocking { runBlocking {
response = response =
@ -955,7 +952,7 @@ class RepositoryTest {
fun delete_source_without_connection() { fun delete_source_without_connection() {
coEvery { api.deleteSource(any()) } returns SuccessResponse(false) coEvery { api.deleteSource(any()) } returns SuccessResponse(false)
initializeRepository(MutableStateFlow(false)) initializeRepository(false)
var response: Boolean var response: Boolean
runBlocking { runBlocking {
response = repository.deleteSource(5, "src") response = repository.deleteSource(5, "src")
@ -1028,7 +1025,7 @@ class RepositoryTest {
data = "undocumented...", data = "undocumented...",
) )
initializeRepository(MutableStateFlow(false)) initializeRepository(false)
var response: Boolean var response: Boolean
runBlocking { runBlocking {
response = repository.updateRemote() response = repository.updateRemote()
@ -1070,7 +1067,7 @@ class RepositoryTest {
fun login_but_without_connection() { fun login_but_without_connection() {
coEvery { api.login() } returns SuccessResponse(success = true) coEvery { api.login() } returns SuccessResponse(success = true)
initializeRepository(MutableStateFlow(false)) initializeRepository(false)
var response: Boolean var response: Boolean
runBlocking { runBlocking {
response = repository.login() response = repository.login()
@ -1150,7 +1147,7 @@ class RepositoryTest {
coEvery { api.getItems(any(), any(), any(), any(), any(), any(), any()) } returns coEvery { api.getItems(any(), any(), any(), any(), any(), any(), any()) } returns
StatusAndData(success = false, data = generateTestApiItem()) StatusAndData(success = false, data = generateTestApiItem())
initializeRepository(MutableStateFlow(false)) initializeRepository(false)
prepareSearch() prepareSearch()
runBlocking { runBlocking {
repository.tryToCacheItemsAndGetNewOnes() repository.tryToCacheItemsAndGetNewOnes()

View File

@ -4,7 +4,6 @@ object SqlDelight {
const val runtime = "app.cash.sqldelight:runtime:2.0.2" const val runtime = "app.cash.sqldelight:runtime:2.0.2"
const val android = "app.cash.sqldelight:android-driver:2.0.2" const val android = "app.cash.sqldelight:android-driver:2.0.2"
const val native = "app.cash.sqldelight:native-driver:2.0.2" const val native = "app.cash.sqldelight:native-driver:2.0.2"
} }
plugins { plugins {
@ -41,13 +40,13 @@ kotlin {
implementation("org.jsoup:jsoup:1.15.4") implementation("org.jsoup:jsoup:1.15.4")
//Dependency Injection // Dependency Injection
implementation("org.kodein.di:kodein-di:7.14.0") implementation("org.kodein.di:kodein-di:7.14.0")
//Settings // Settings
implementation("com.russhwolf:multiplatform-settings-no-arg:1.0.0-RC") implementation("com.russhwolf:multiplatform-settings-no-arg:1.0.0-RC")
//Logging // Logging
implementation("io.github.aakira:napier:2.6.1") implementation("io.github.aakira:napier:2.6.1")
// Sql // Sql
@ -55,6 +54,10 @@ kotlin {
// Sql // Sql
implementation("org.jetbrains.kotlinx:kotlinx-datetime:0.6.1") implementation("org.jetbrains.kotlinx:kotlinx-datetime:0.6.1")
// Connectivity
implementation("dev.jordond.connectivity:connectivity-core:1.2.0")
implementation("dev.jordond.connectivity:connectivity-device:1.2.0")
} }
} }
val commonTest by getting { val commonTest by getting {

View File

@ -13,6 +13,7 @@ import bou.amine.apps.readerforselfossv2.model.SelfossModel
import bou.amine.apps.readerforselfossv2.model.StatusAndData import bou.amine.apps.readerforselfossv2.model.StatusAndData
import bou.amine.apps.readerforselfossv2.rest.SelfossApi import bou.amine.apps.readerforselfossv2.rest.SelfossApi
import bou.amine.apps.readerforselfossv2.service.AppSettingsService import bou.amine.apps.readerforselfossv2.service.AppSettingsService
import bou.amine.apps.readerforselfossv2.service.ConnectivityService
import bou.amine.apps.readerforselfossv2.utils.ItemType import bou.amine.apps.readerforselfossv2.utils.ItemType
import bou.amine.apps.readerforselfossv2.utils.getHtmlDecoded import bou.amine.apps.readerforselfossv2.utils.getHtmlDecoded
import bou.amine.apps.readerforselfossv2.utils.toEntity import bou.amine.apps.readerforselfossv2.utils.toEntity
@ -30,11 +31,10 @@ private const val MAX_ITEMS_NUMBER = 200
class Repository( class Repository(
private val api: SelfossApi, private val api: SelfossApi,
private val appSettingsService: AppSettingsService, private val appSettingsService: AppSettingsService,
val isConnectionAvailable: MutableStateFlow<Boolean>, private val connectivityService: ConnectivityService,
private val db: ReaderForSelfossDB, private val db: ReaderForSelfossDB,
) { ) {
var items = ArrayList<SelfossModel.Item>() var items = ArrayList<SelfossModel.Item>()
var connectionMonitored = false
var baseUrl = appSettingsService.getBaseUrl() var baseUrl = appSettingsService.getBaseUrl()
@ -63,7 +63,7 @@ class Repository(
suspend fun getNewerItems(): ArrayList<SelfossModel.Item> { suspend fun getNewerItems(): ArrayList<SelfossModel.Item> {
var fetchedItems: StatusAndData<List<SelfossModel.Item>> = StatusAndData.error() var fetchedItems: StatusAndData<List<SelfossModel.Item>> = StatusAndData.error()
if (isNetworkAvailable()) { if (connectivityService.isNetworkAvailable() == true) {
fetchedItems = fetchedItems =
api.getItems( api.getItems(
displayedItems.type, displayedItems.type,
@ -102,7 +102,7 @@ class Repository(
suspend fun getOlderItems(): ArrayList<SelfossModel.Item> { suspend fun getOlderItems(): ArrayList<SelfossModel.Item> {
var fetchedItems: StatusAndData<List<SelfossModel.Item>> = StatusAndData.error() var fetchedItems: StatusAndData<List<SelfossModel.Item>> = StatusAndData.error()
if (isNetworkAvailable()) { if (connectivityService.isNetworkAvailable() == true) {
val offset = items.size val offset = items.size
fetchedItems = fetchedItems =
api.getItems( api.getItems(
@ -122,7 +122,7 @@ class Repository(
} }
private suspend fun getMaxItemsForBackground(itemType: ItemType): List<SelfossModel.Item> { private suspend fun getMaxItemsForBackground(itemType: ItemType): List<SelfossModel.Item> {
return if (isNetworkAvailable()) { return if (connectivityService.isNetworkAvailable() == true) {
val items = val items =
api.getItems( api.getItems(
itemType.type, itemType.type,
@ -146,7 +146,7 @@ class Repository(
@Suppress("detekt:ForbiddenComment") @Suppress("detekt:ForbiddenComment")
suspend fun reloadBadges(): Boolean { suspend fun reloadBadges(): Boolean {
var success = false var success = false
if (isNetworkAvailable()) { if (connectivityService.isNetworkAvailable() == true) {
val response = api.stats() val response = api.stats()
if (response.success && response.data != null) { if (response.success && response.data != null) {
_badgeUnread.value = response.data.unread ?: 0 _badgeUnread.value = response.data.unread ?: 0
@ -168,7 +168,7 @@ class Repository(
suspend fun getTags(): List<SelfossModel.Tag> { suspend fun getTags(): List<SelfossModel.Tag> {
val isDatabaseEnabled = val isDatabaseEnabled =
appSettingsService.isItemCachingEnabled() || !appSettingsService.isUpdateSourcesEnabled() appSettingsService.isItemCachingEnabled() || !appSettingsService.isUpdateSourcesEnabled()
return if (isNetworkAvailable() && !fetchedTags) { return if (connectivityService.isNetworkAvailable() == true && !fetchedTags) {
val apiTags = api.tags() val apiTags = api.tags()
if (apiTags.success && apiTags.data != null && isDatabaseEnabled) { if (apiTags.success && apiTags.data != null && isDatabaseEnabled) {
resetDBTagsWithData(apiTags.data) resetDBTagsWithData(apiTags.data)
@ -185,7 +185,7 @@ class Repository(
} }
suspend fun getSpouts(): Map<String, SelfossModel.Spout> = suspend fun getSpouts(): Map<String, SelfossModel.Spout> =
if (isNetworkAvailable()) { if (connectivityService.isNetworkAvailable() == true) {
val spouts = api.spouts() val spouts = api.spouts()
if (spouts.success && spouts.data != null) { if (spouts.success && spouts.data != null) {
spouts.data spouts.data
@ -201,7 +201,7 @@ class Repository(
val isDatabaseEnabled = val isDatabaseEnabled =
appSettingsService.isItemCachingEnabled() || !appSettingsService.isUpdateSourcesEnabled() appSettingsService.isItemCachingEnabled() || !appSettingsService.isUpdateSourcesEnabled()
val shouldFetch = if (!appSettingsService.isUpdateSourcesEnabled()) !fetchedSources else true val shouldFetch = if (!appSettingsService.isUpdateSourcesEnabled()) !fetchedSources else true
if (shouldFetch && isNetworkAvailable()) { if (shouldFetch && connectivityService.isNetworkAvailable() == true) {
if (appSettingsService.getPublicAccess()) { if (appSettingsService.getPublicAccess()) {
val apiSources = api.sourcesStats() val apiSources = api.sourcesStats()
if (apiSources.success && apiSources.data != null) { if (apiSources.success && apiSources.data != null) {
@ -223,7 +223,7 @@ class Repository(
val isDatabaseEnabled = val isDatabaseEnabled =
appSettingsService.isItemCachingEnabled() || !appSettingsService.isUpdateSourcesEnabled() appSettingsService.isItemCachingEnabled() || !appSettingsService.isUpdateSourcesEnabled()
val shouldFetch = if (!appSettingsService.isUpdateSourcesEnabled()) !fetchedSources else true val shouldFetch = if (!appSettingsService.isUpdateSourcesEnabled()) !fetchedSources else true
if (shouldFetch && isNetworkAvailable()) { if (shouldFetch && connectivityService.isNetworkAvailable() == true) {
val apiSources = api.sourcesDetailed() val apiSources = api.sourcesDetailed()
if (apiSources.success && apiSources.data != null) { if (apiSources.success && apiSources.data != null) {
fetchedSources = true fetchedSources = true
@ -248,7 +248,7 @@ class Repository(
} }
private suspend fun markAsReadById(id: Int): Boolean = private suspend fun markAsReadById(id: Int): Boolean =
if (isNetworkAvailable()) { if (connectivityService.isNetworkAvailable() == true) {
api.markAsRead(id.toString()).isSuccess api.markAsRead(id.toString()).isSuccess
} else { } else {
insertDBAction(id.toString(), read = true) insertDBAction(id.toString(), read = true)
@ -265,7 +265,7 @@ class Repository(
} }
private suspend fun unmarkAsReadById(id: Int): Boolean = private suspend fun unmarkAsReadById(id: Int): Boolean =
if (isNetworkAvailable()) { if (connectivityService.isNetworkAvailable() == true) {
api.unmarkAsRead(id.toString()).isSuccess api.unmarkAsRead(id.toString()).isSuccess
} else { } else {
insertDBAction(id.toString(), unread = true) insertDBAction(id.toString(), unread = true)
@ -282,7 +282,7 @@ class Repository(
} }
private suspend fun starrById(id: Int): Boolean = private suspend fun starrById(id: Int): Boolean =
if (isNetworkAvailable()) { if (connectivityService.isNetworkAvailable() == true) {
api.starr(id.toString()).isSuccess api.starr(id.toString()).isSuccess
} else { } else {
insertDBAction(id.toString(), starred = true) insertDBAction(id.toString(), starred = true)
@ -299,7 +299,7 @@ class Repository(
} }
private suspend fun unstarrById(id: Int): Boolean = private suspend fun unstarrById(id: Int): Boolean =
if (isNetworkAvailable()) { if (connectivityService.isNetworkAvailable() == true) {
api.unstarr(id.toString()).isSuccess api.unstarr(id.toString()).isSuccess
} else { } else {
insertDBAction(id.toString(), starred = true) insertDBAction(id.toString(), starred = true)
@ -309,7 +309,10 @@ class Repository(
suspend fun markAllAsRead(items: ArrayList<SelfossModel.Item>): Boolean { suspend fun markAllAsRead(items: ArrayList<SelfossModel.Item>): Boolean {
var success = false var success = false
if (isNetworkAvailable() && api.markAllAsRead(items.map { it.id.toString() }).isSuccess) { if (connectivityService.isNetworkAvailable() != null &&
connectivityService.isNetworkAvailable()!! &&
api.markAllAsRead(items.map { it.id.toString() }).isSuccess
) {
success = true success = true
for (item in items) { for (item in items) {
markAsReadLocally(item) markAsReadLocally(item)
@ -369,7 +372,7 @@ class Repository(
tags: String, tags: String,
): Boolean { ): Boolean {
var response = false var response = false
if (isNetworkAvailable()) { if (connectivityService.isNetworkAvailable() == true) {
response = api response = api
.createSourceForVersion( .createSourceForVersion(
title, title,
@ -390,7 +393,7 @@ class Repository(
tags: String, tags: String,
): Boolean { ): Boolean {
var response = false var response = false
if (isNetworkAvailable()) { if (connectivityService.isNetworkAvailable() == true) {
response = api.updateSourceForVersion(id, title, url, spout, tags).isSuccess == true response = api.updateSourceForVersion(id, title, url, spout, tags).isSuccess == true
} }
@ -402,13 +405,13 @@ class Repository(
title: String, title: String,
): Boolean { ): Boolean {
var success = false var success = false
if (isNetworkAvailable()) { if (connectivityService.isNetworkAvailable() == true) {
val response = api.deleteSource(id) val response = api.deleteSource(id)
success = response.isSuccess success = response.isSuccess
} }
// We filter on success or if the network isn't available // We filter on success or if the network isn't available
if (success || !isNetworkAvailable()) { if (success || !(connectivityService.isNetworkAvailable() == true)) {
items = ArrayList(items.filter { it.sourcetitle != title }) items = ArrayList(items.filter { it.sourcetitle != title })
setReaderItems(items) setReaderItems(items)
db.itemsQueries.deleteItemsWhereSource(title) db.itemsQueries.deleteItemsWhereSource(title)
@ -418,7 +421,7 @@ class Repository(
} }
suspend fun updateRemote(): Boolean = suspend fun updateRemote(): Boolean =
if (isNetworkAvailable()) { if (connectivityService.isNetworkAvailable() == true) {
api.update().data.equals("finished") api.update().data.equals("finished")
} else { } else {
false false
@ -426,7 +429,7 @@ class Repository(
suspend fun login(): Boolean { suspend fun login(): Boolean {
var result = false var result = false
if (isNetworkAvailable()) { if (connectivityService.isNetworkAvailable() == true) {
try { try {
val response = api.login() val response = api.login()
result = response.isSuccess == true result = response.isSuccess == true
@ -439,7 +442,7 @@ class Repository(
suspend fun checkIfFetchFails(): Boolean { suspend fun checkIfFetchFails(): Boolean {
var fetchFailed = true var fetchFailed = true
if (isNetworkAvailable()) { if (connectivityService.isNetworkAvailable() == true) {
try { try {
// Trying to fetch one item, and check someone is trying to use the app with // Trying to fetch one item, and check someone is trying to use the app with
// a random rss feed, that would throw a NoTransformationFoundException // a random rss feed, that would throw a NoTransformationFoundException
@ -453,7 +456,7 @@ class Repository(
} }
suspend fun logout() { suspend fun logout() {
if (isNetworkAvailable()) { if (connectivityService.isNetworkAvailable() == true) {
try { try {
val response = api.logout() val response = api.logout()
if (!response.isSuccess) { if (!response.isSuccess) {
@ -481,7 +484,7 @@ class Repository(
suspend fun updateApiInformation() { suspend fun updateApiInformation() {
val apiMajorVersion = appSettingsService.getApiVersion() val apiMajorVersion = appSettingsService.getApiVersion()
if (isNetworkAvailable()) { if (connectivityService.isNetworkAvailable() == true) {
val fetchedInformation = api.apiInformation() val fetchedInformation = api.apiInformation()
if (fetchedInformation.success && fetchedInformation.data != null) { if (fetchedInformation.success && fetchedInformation.data != null) {
if (fetchedInformation.data.getApiMajorVersion() != apiMajorVersion) { if (fetchedInformation.data.getApiMajorVersion() != apiMajorVersion) {
@ -500,8 +503,6 @@ class Repository(
} }
} }
fun isNetworkAvailable() = isConnectionAvailable.value && !offlineOverride
private fun getDBActions(): List<ACTION> = db.actionsQueries.actions().executeAsList() private fun getDBActions(): List<ACTION> = db.actionsQueries.actions().executeAsList()
private fun deleteDBAction(action: ACTION) = db.actionsQueries.deleteAction(action.id) private fun deleteDBAction(action: ACTION) = db.actionsQueries.deleteAction(action.id)

View File

@ -0,0 +1,42 @@
package bou.amine.apps.readerforselfossv2.service
import dev.jordond.connectivity.Connectivity
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.launch
class ConnectivityService {
private val _networkAvailableProvider = MutableSharedFlow<Boolean>()
val networkAvailableProvider = _networkAvailableProvider.asSharedFlow()
private var currentStatus: Boolean? = null
private lateinit var connectivity: Connectivity
fun start() {
connectivity = Connectivity()
connectivity.start()
CoroutineScope(Dispatchers.Main).launch {
connectivity.statusUpdates.collect { status ->
when (status) {
is Connectivity.Status.Connected -> {
currentStatus = true
_networkAvailableProvider.emit(true)
}
is Connectivity.Status.Disconnected -> {
currentStatus = false
_networkAvailableProvider.emit(false)
}
}
}
}
}
fun isNetworkAvailable(): Boolean? = currentStatus
fun stop() {
currentStatus = null
connectivity.stop()
}
}