Compare commits

..

13 Commits

Author SHA1 Message Date
ed05dc7b57 ci: Instrumentation tests coverage in ci.
Some checks failed
Check PR code / BuildAndTestAndCoverage (pull_request) Failing after 28m26s
2025-03-23 17:10:01 +01:00
02d503e03a chore: comment robolectric tests for now. 2025-03-16 15:44:02 +01:00
24b9320d6d fix: Fixed source deletion test. 2025-03-16 14:27:30 +01:00
ceba58e98f Merge pull request 'Fix alignment changes resetting reader article position' (#190) from davidoskky/ReaderForSelfoss-multiplatform:alignment into master
All checks were successful
Check master code / build (push) Successful in 8m27s
Reviewed-on: #190
2025-03-16 13:14:44 +00:00
c3ee07dd85 Refactor star icon handling
All checks were successful
Check PR code / translations (pull_request) Successful in 1m4s
Check PR code / Lint (pull_request) Successful in 1m25s
Check PR code / build (pull_request) Successful in 11m52s
Extracted all favorite handling to two functions. Makes it a little bit more readable.
2025-03-12 16:07:49 +01:00
93d99192b3 Don't restart activity changing alignment
When changing alignment in the reader we were restarting the reader activity to reload. Doing this led to reloading the article which was initially opened every time you changed alignment. Now, when changing the alignment we retain all existing fragments but command all of them to update their alignment setting.
2025-03-12 16:07:49 +01:00
359dec2ca0 Changelog for v125030711 2025-03-12 11:39:57 +00:00
62354ec70a Merge pull request 'fix: initial status loading issues.' (#192) from connectivity into master
All checks were successful
Check master code / build (push) Successful in 18m28s
Create tag / build (push) Successful in 18m21s
Create tag / createTagAndChangelog (push) Successful in 53s
Create tag / release (push) Successful in 4m19s
Reviewed-on: #192
2025-03-12 11:20:14 +00:00
18a17251ac chore: check changes for translations and android.
All checks were successful
Check PR code / translations (pull_request) Successful in 2m30s
Check PR code / Lint (pull_request) Successful in 2m36s
Check PR code / build (pull_request) Successful in 16m14s
2025-03-11 22:20:07 +01:00
5e91724ee2 fix: initial status loading issues.
Some checks failed
Check PR code / Lint (pull_request) Successful in 3m56s
Check PR code / translations (pull_request) Successful in 1m32s
Check PR code / build (pull_request) Has been cancelled
2025-03-11 22:04:42 +01:00
212d259a33 Merge pull request 'chore: new connectivity dep. Closes #84.' (#189) from connectivity into master
All checks were successful
Check master code / build (push) Successful in 14m19s
Reviewed-on: #189
2025-03-11 13:48:57 +00:00
3bf60f1146 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
2025-03-10 21:11:39 +01:00
ef13e300f0 Changelog for v125030681 2025-03-09 17:41:13 +00:00
28 changed files with 864 additions and 382 deletions

View File

@ -10,30 +10,44 @@ jobs:
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: "Check android app changes"
id: check-android-changes
uses: tj-actions/changed-files@v45
with:
files: |
androidApp/src/**
- name: Fetch tags
if: steps.check-android-changes.outputs.any_modified == 'true'
run: git fetch --tags -p
- uses: actions/setup-java@v4
if: steps.check-android-changes.outputs.any_modified == 'true'
with:
distribution: 'temurin'
java-version: '17'
cache: gradle
- uses: gradle/actions/setup-gradle@v3
if: steps.check-android-changes.outputs.any_modified == 'true'
- uses: android-actions/setup-android@v3
if: steps.check-android-changes.outputs.any_modified == 'true'
- name: Configure gradle...
if: steps.check-android-changes.outputs.any_modified == 'true'
run: mkdir -p ~/.gradle && echo "org.gradle.daemon=false\nignoreGitVersion=true" >> ~/.gradle/gradle.properties
- name: Build and test
if: steps.check-android-changes.outputs.any_modified == 'true'
run: ./gradlew build -x testReleaseUnitTest -x testDebugUnitTest -x testGithubConfigReleaseUnitTest -x testGithubConfigDebugUnitTest # These tests will be done
- uses: KengoTODA/actions-setup-docker-compose@v1
with:
version: "2.23.3"
# TESTS ARE RUN LOCALLY
# TESTS ARE RUN LOCALLY
# - uses: KengoTODA/actions-setup-docker-compose@v1
# with:
# version: "2.23.3"
# - name: run selfoss
# run: |
# docker compose -f .gitea/workflows/assets/docker-compose.yml up -d
- name: coverage
if: steps.check-android-changes.outputs.any_modified == 'true'
run: |
./gradlew :koverHtmlReport
- uses: actions/upload-artifact@v3
if: steps.check-android-changes.outputs.any_modified == 'true'
with:
name: coverage
path: build/reports/kover/html

View File

@ -0,0 +1,65 @@
name: Coverage
on:
workflow_call:
jobs:
BuildAndTestAndCoverage:
runs-on: ubuntu-latest
steps:
- name: Check out repository code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Fetch tags
run: git fetch --tags -p
- uses: actions/setup-java@v4
with:
distribution: 'temurin'
java-version: '17'
cache: gradle
- uses: gradle/actions/setup-gradle@v3
- uses: android-actions/setup-android@v3
- name: Configure gradle...
run: mkdir -p ~/.gradle && echo "ignoreGitVersion=true" >> ~/.gradle/gradle.properties
- uses: KengoTODA/actions-setup-docker-compose@v1
with:
version: "2.23.3"
- name: run selfoss
run: |
docker compose -f .gitea/workflows/assets/docker-compose.yml up -d
- name: Set env url
run: |
export SELFOSS_URL=172.17.0.1:8888
# https://github.com/ReactiveCircus/android-emulator-runner/issues/385
- name: Kill crashpad_handler processes
if: always()
run: |
pkill -SIGTERM crashpad_handler || true
sleep 5
pkill -SIGKILL crashpad_handler || true
- name: Tests
uses: reactivecircus/android-emulator-runner@v2
with:
api-level: 29
script: |
./gradlew androidApp:connectedAndroidTest
killall -INT crashpad_handler || true
- uses: actions/upload-artifact@v3
if: failure()
with:
name: failure-espresso
path: build/reports/androidTests/connected/screenshots
retention-days: 2
overwrite: true
include-hidden-files: true
- uses: actions/upload-artifact@v3
with:
name: coverage-espresso
path: build/reports/coverage/androidTest/githubConfig/debug/connected
retention-days: 1
overwrite: true
include-hidden-files: true
- name: Clean
if: always()
run: |
docker compose -f .gitea/workflows/assets/docker-compose.yml stop

View File

@ -3,77 +3,151 @@ on:
pull_request:
branches:
- master
- chore-crowdin-ci
jobs:
Lint:
BuildAndTestAndCoverage:
runs-on: ubuntu-latest
steps:
- name: Check out repository code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Fetch tags
run: git fetch --tags -p
- uses: actions/setup-java@v4
with:
distribution: 'temurin'
java-version: '17'
cache: gradle
- name: Install klint
run: curl -sSLO https://github.com/pinterest/ktlint/releases/download/1.5.0/ktlint && chmod a+x ktlint && mv ktlint /usr/local/bin/
- name: Install detekt
run: curl -sSLO https://github.com/detekt/detekt/releases/download/v1.23.7/detekt-cli-1.23.7.zip && unzip detekt-cli-1.23.7.zip
- name: Linting...
run: ktlint 'shared/**/*.kt' 'androidApp/**/*.kt' '!shared/build'
- name: Detecting...
run: ./detekt-cli-1.23.7/bin/detekt-cli -c detekt.yml --excludes '**/shared/build/**/*.kt'
translations:
runs-on: ubuntu-latest
steps:
- name: Check out repository code
uses: actions/checkout@v4
- name: upload translation sources
uses: crowdin/github-action@v2
- uses: gradle/actions/setup-gradle@v3
- uses: android-actions/setup-android@v3
- name: Configure gradle...
run: mkdir -p ~/.gradle && echo "ignoreGitVersion=true" >> ~/.gradle/gradle.properties
- uses: KengoTODA/actions-setup-docker-compose@v1
with:
config: './.gitea/workflows/assets/crowdin.yml'
upload_sources: true
upload_translations: false
download_translations: false
create_pull_request: false
push_translations: false
env:
CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }}
CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }}
- name: wait
run: sleep 10s
- name: download translations
uses: crowdin/github-action@v2
with:
config: './.gitea/workflows/assets/crowdin.yml'
upload_sources: false
upload_translations: false
download_translations: true
create_pull_request: false
push_translations: false
env:
CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }}
CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }}
- name: Check for uncommitted changes
id: check-changes
uses: mskri/check-uncommitted-changes-action@v1.0.1
- name: Commit Changes
if: steps.check-changes.outputs.changes != ''
version: "2.23.3"
- name: run selfoss
run: |
git config --global user.email aminecmi+giteadrone@pm.me
git config --global user.name giteadrone
git add ./androidApp/src/main/res/*
git commit -m "translation: translation files"
- name: Push changes
if: steps.check-changes.outputs.changes != ''
uses: appleboy/git-push-action@v1.0.0
docker compose -f .gitea/workflows/assets/docker-compose.yml up -d
- name: Set env url
run: |
export SELFOSS_URL=172.17.0.1:8888
# https://github.com/ReactiveCircus/android-emulator-runner/issues/385
- name: Kill crashpad_handler processes
if: always()
run: |
pkill -SIGTERM crashpad_handler || true
sleep 5
pkill -SIGKILL crashpad_handler || true
- name: Tests
uses: reactivecircus/android-emulator-runner@v2
with:
author_name: giteadrone
author_email: aminecmi+giteadrone@pm.me
remote: ${{ secrets.REMOTE_URL }}
ssh_key: ${{ secrets.PRIVATE_KEY }}
branch: ${{ github.head_ref || github.ref_name }}
build:
needs: Lint
uses: ./.gitea/workflows/common_build.yml
api-level: 29
script: |
./gradlew androidApp:clearScreenshotsTask || true
./gradlew androidApp:createScreenshotDirectory
./gradlew JacocoDebugCodeCoverage || true
adb logcat *:S TestRunner:V -d > ./androidApp/build/reports/androidTests/connected/screenshots/logs.txt
./gradlew androidApp:fetchScreenshots
- uses: actions/upload-artifact@v3
if: always()
with:
name: failure-espresso
path: androidApp/build/reports/androidTests/connected/screenshots
retention-days: 2
overwrite: true
include-hidden-files: true
- uses: actions/upload-artifact@v3
with:
name: coverage-espresso
path: androidApp/build/reports/jacoco/JacocoDebugCodeCoverage
retention-days: 1
overwrite: true
include-hidden-files: true
- name: Clean
if: always()
run: |
docker compose -f .gitea/workflows/assets/docker-compose.yml stop
# Lint:
# runs-on: ubuntu-latest
# steps:
# - name: Check out repository code
# uses: actions/checkout@v4
# - uses: actions/setup-java@v4
# with:
# distribution: 'temurin'
# java-version: '17'
# cache: gradle
# - name: Install klint
# run: curl -sSLO https://github.com/pinterest/ktlint/releases/download/1.5.0/ktlint && chmod a+x ktlint && mv ktlint /usr/local/bin/
# - name: Install detekt
# run: curl -sSLO https://github.com/detekt/detekt/releases/download/v1.23.7/detekt-cli-1.23.7.zip && unzip detekt-cli-1.23.7.zip
# - name: Linting...
# run: ktlint 'shared/**/*.kt' 'androidApp/**/*.kt' '!shared/build'
# - name: Detecting...
# run: ./detekt-cli-1.23.7/bin/detekt-cli -c detekt.yml --excludes '**/shared/build/**/*.kt'
# translations:
# runs-on: ubuntu-latest
# steps:
# - name: Check out repository code
# uses: actions/checkout@v4
# with:
# fetch-depth: 0
# - name: "Check translations changes"
# id: check-translations-changes
# uses: tj-actions/changed-files@v45
# with:
# files: |
# androidApp/src/main/res/values/strings.xml
# - name: upload translation sources
# if: steps.check-api-changes.outputs.any_modified == 'true'
# uses: crowdin/github-action@v2
# with:
# config: './.gitea/workflows/assets/crowdin.yml'
# upload_sources: true
# upload_translations: false
# download_translations: false
# create_pull_request: false
# push_translations: false
# env:
# CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }}
# CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }}
# - name: wait
# if: steps.check-api-changes.outputs.any_modified == 'true'
# run: sleep 10s
# - name: download translations
# if: steps.check-api-changes.outputs.any_modified == 'true'
# uses: crowdin/github-action@v2
# with:
# config: './.gitea/workflows/assets/crowdin.yml'
# upload_sources: false
# upload_translations: false
# download_translations: true
# create_pull_request: false
# push_translations: false
# env:
# CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }}
# CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }}
# - name: Check for uncommitted changes
# if: steps.check-api-changes.outputs.any_modified == 'true'
# id: check-changes
# uses: mskri/check-uncommitted-changes-action@v1.0.1
# - name: Commit Changes
# if: steps.check-api-changes.outputs.any_modified == 'true' && steps.check-changes.outputs.changes != ''
# run: |
# git config --global user.email aminecmi+giteadrone@pm.me
# git config --global user.name giteadrone
# git add ./androidApp/src/main/res/*
# git commit -m "translation: translation files"
# - name: Push changes
# if: steps.check-api-changes.outputs.any_modified == 'true' && steps.check-changes.outputs.changes != ''
# uses: appleboy/git-push-action@v1.0.0
# with:
# author_name: giteadrone
# author_email: aminecmi+giteadrone@pm.me
# remote: ${{ secrets.REMOTE_URL }}
# ssh_key: ${{ secrets.PRIVATE_KEY }}
# branch: ${{ github.head_ref || github.ref_name }}
# build:
# needs: Lint
# uses: ./.gitea/workflows/common_build.yml

View File

@ -1,3 +1,29 @@
**v125030711
- Merge pull request 'fix: initial status loading issues.' (#192) from connectivity into master
- chore: check changes for translations and android.
- fix: initial status loading issues.
- Merge pull request 'chore: new connectivity dep. Closes #84.' (#189) from connectivity into master
- chore: new connectivity dep. Closes #84.
- Changelog for v125030681
--------------------------------------------------------------------
**v125030681
- chore: do not send reports on simulators.
- Merge pull request 'chore: do not send reports on simulators.' (#188) from chore-acra-simulator into master
- chore: do not send reports on simulators.
- Merge pull request 'fix: Url validation was not failing login. Added tests.' (#186) from fix-invalid-url into master
- Merge pull request 'chore: crowding ci integration.' (#187) from chore-crowdin-ci into master
- chore: we don't need to check if the url is valid in upsert screen.
- fix: Url validation was not failing login. Added tests.
- chore: crowding ci integration.
- Show a confirmation dialog before deleting sources (#185)
- Changelog for v125020581
--------------------------------------------------------------------
**v125020581
- fix: url can be empty ?

View File

@ -10,30 +10,41 @@ plugins {
id("com.mikepenz.aboutlibraries.plugin")
id("org.jetbrains.kotlinx.kover")
id("app.cash.sqldelight") version "2.0.2"
jacoco
}
fun Project.execWithOutput(cmd: String, ignore: Boolean = false): String {
val result: String = ByteArrayOutputStream().use { outputStream ->
project.exec {
commandLine = cmd.split(" ")
standardOutput = outputStream
isIgnoreExitValue = ignore
fun Project.execWithOutput(
cmd: String,
ignore: Boolean = false,
): String {
val result: String =
ByteArrayOutputStream().use { outputStream ->
project.exec {
commandLine = cmd.split(" ")
standardOutput = outputStream
isIgnoreExitValue = ignore
}
outputStream.toString()
}
outputStream.toString()
}
return result
}
fun gitVersion(): String {
val maybeTagOfCurrentCommit = execWithOutput("git -C ../ describe --contains HEAD", true)
val process = if (maybeTagOfCurrentCommit.isEmpty()) {
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")
} else {
println("Tag found on current commit")
execWithOutput("git -C ../ describe --contains HEAD")
}
return process.replace("^0", "").replace("'", "").substring(1).replace("\\.", "").trim()
val process =
if (maybeTagOfCurrentCommit.isEmpty()) {
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")
} else {
println("Tag found on current commit")
execWithOutput("git -C ../ describe --contains HEAD")
}
return process
.replace("^0", "")
.replace("'", "")
.substring(1)
.replace("\\.", "")
.trim()
}
fun versionCodeFromGit(): Int {
@ -54,6 +65,15 @@ fun versionNameFromGit(): String {
return gitVersion()
}
val exclusions =
listOf(
"**/R.class",
"**/R\$*.class",
"**/BuildConfig.*",
"**/Manifest*.*",
"**/*Test*.*",
)
android {
compileOptions {
isCoreLibraryDesugaringEnabled = true
@ -85,7 +105,7 @@ android {
// tests
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
testInstrumentationRunnerArguments["clearPackageData"] = "true"
testInstrumentationRunnerArguments["useTestStorageService"] = "true"
}
packaging {
resources {
@ -99,6 +119,44 @@ android {
proguardFiles(getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro")
}
getByName("debug") {
isTestCoverageEnabled = true
enableAndroidTestCoverage = true
installation {
installOptions("-g", "-r")
}
val androidTests = "connectedAndroidTest"
tasks.register<JacocoReport>("JacocoDebugCodeCoverage") {
// Depend on unit tests and Android tests tasks
dependsOn(listOf(androidTests))
// Set task grouping and description
group = "Reporting"
description = "Execute UI and unit tests, generate and combine Jacoco coverage report"
// Configure reports to generate both XML and HTML formats
reports {
xml.required.set(true)
html.required.set(true)
}
// Set source directories to the main source directory
sourceDirectories.setFrom(layout.projectDirectory.dir("src/main"))
// Set class directories to compiled Java and Kotlin classes, excluding specified exclusions
classDirectories.setFrom(
files(
fileTree(layout.buildDirectory.dir("intermediates/javac/")) {
exclude(exclusions)
},
fileTree(layout.buildDirectory.dir("tmp/kotlin-classes/")) {
exclude(exclusions)
},
),
)
// Collect execution data from .exec and .ec files generated during test execution
executionData.setFrom(
files(
fileTree(layout.buildDirectory) { include(listOf("**/*.exec", "**/*.ec")) },
),
)
}
}
}
flavorDimensions.add("build")
@ -111,12 +169,10 @@ android {
namespace = "bou.amine.apps.readerforselfossv2.android"
testOptions {
animationsDisabled = true
execution = "ANDROIDX_TEST_ORCHESTRATOR"
unitTests {
isIncludeAndroidResources = true
}
}
}
dependencies {
@ -141,12 +197,12 @@ dependencies {
implementation("androidx.constraintlayout:constraintlayout:2.2.0")
implementation("org.jsoup:jsoup:1.18.3")
//multidex
// multidex
implementation("androidx.multidex:multidex:2.0.1")
// About
implementation("com.mikepenz:aboutlibraries-core:10.5.1")
implementation("com.mikepenz:aboutlibraries:10.5.1")
implementation("com.mikepenz:aboutlibraries-core:11.6.3")
implementation("com.mikepenz:aboutlibraries:11.6.3")
// Material-ish things
implementation("com.ashokvarma.android:bottom-navigation-bar:2.2.0")
@ -162,43 +218,41 @@ dependencies {
implementation("me.relex:circleindicator:2.1.6")
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-framework-android-x: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")
//Logging
// Logging
implementation("io.github.aakira:napier:2.7.1")
//PhotoView
// PhotoView
implementation("com.github.chrisbanes:PhotoView:2.3.0")
implementation("androidx.core:core-ktx:1.15.0")
implementation("androidx.lifecycle:lifecycle-extensions:2.2.0")
// Network information
implementation("com.github.ln-12:multiplatform-connectivity-status:1.3.0")
// SQLDELIGHT
implementation("app.cash.sqldelight:android-driver:2.0.2")
//test
// test
testImplementation("junit:junit:4.13.2")
testImplementation("io.mockk:mockk:1.13.14")
testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.1")
implementation("org.jetbrains.kotlinx:kotlinx-datetime:0.6.1")
androidTestImplementation("androidx.test:runner:1.6.2")
androidTestImplementation("androidx.test:rules:1.6.1")
androidTestImplementation("androidx.test:runner:1.7.0-alpha01")
androidTestImplementation("androidx.test:rules:1.7.0-alpha01")
androidTestImplementation("androidx.test.espresso:espresso-core:3.6.1")
implementation("androidx.test.espresso:espresso-idling-resource:3.6.1")
androidTestImplementation("androidx.test.ext:junit-ktx:1.2.1")
androidTestUtil("androidx.test:orchestrator:1.5.1")
androidTestUtil("androidx.test.services:test-services:1.6.0-alpha02")
testImplementation("org.robolectric:robolectric:4.14.1")
testImplementation("androidx.test:core-ktx:1.6.1")
testImplementation("androidx.test:core-ktx:1.7.0-alpha01")
androidTestImplementation("androidx.test.uiautomator:uiautomator:2.3.0")
implementation("ch.acra:acra-http:$acraVersion")
implementation("ch.acra:acra-toast:$acraVersion")
@ -210,16 +264,18 @@ tasks.withType<Test> {
useJUnit()
testLogging {
exceptionFormat = org.gradle.api.tasks.testing.logging.TestExceptionFormat.FULL
events = setOf(
org.gradle.api.tasks.testing.logging.TestLogEvent.PASSED,
org.gradle.api.tasks.testing.logging.TestLogEvent.FAILED,
org.gradle.api.tasks.testing.logging.TestLogEvent.STANDARD_ERROR
)
events =
setOf(
org.gradle.api.tasks.testing.logging.TestLogEvent.PASSED,
org.gradle.api.tasks.testing.logging.TestLogEvent.FAILED,
org.gradle.api.tasks.testing.logging.TestLogEvent.STANDARD_ERROR,
)
showStandardStreams = true
}
}
aboutLibraries {
excludeFields = arrayOf("generated")
offlineMode = true
fetchRemoteLicense = false
fetchRemoteFunding = false
@ -227,4 +283,40 @@ aboutLibraries {
strictMode = com.mikepenz.aboutlibraries.plugin.StrictMode.FAIL
duplicationMode = com.mikepenz.aboutlibraries.plugin.DuplicateMode.MERGE
duplicationRule = com.mikepenz.aboutlibraries.plugin.DuplicateRule.GROUP
}
}
val clearScreenshotsTask =
tasks.register<Exec>("clearScreenshots") {
println("AMINE : clear")
commandLine = listOf("adb", "shell", "rm", "-r", "/storage/emulated/0/Pictures/selfoss_tests/screenshots/*")
}
val createScreenshotDirectoryTask =
tasks.register<Exec>("createScreenshotDirectory") {
println("AMINE : create directory")
group = "reporting"
commandLine = listOf("adb", "shell", "mkdir", "-p", "/storage/emulated/0/Pictures/selfoss_tests/screenshots")
}
tasks.register<Exec>("fetchScreenshots") {
val reportsDirectory = file("$buildDir/reports/androidTests/connected")
println("AMINE : fetch")
group = "reporting"
executable(android.adbExecutable.toString())
commandLine = listOf("adb", "pull", "/storage/emulated/0/Pictures/selfoss_tests/screenshots", reportsDirectory.toString())
finalizedBy(clearScreenshotsTask)
doFirst {
reportsDirectory.mkdirs()
}
}
tasks.withType(Test::class) {
if (this.name == "connectedAndroidTest") {
configure<JacocoTaskExtension> {
isIncludeNoLocationClasses = true
excludes = listOf("jdk.internal.*")
}
}
}

View File

@ -15,13 +15,17 @@ import androidx.test.filters.LargeTest
import bou.amine.apps.readerforselfossv2.android.testing.CountingIdlingResourceSingleton
import org.junit.After
import org.junit.Before
import org.junit.FixMethodOrder
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.MethodSorters
@Suppress("ktlint:standard:class-naming", "detekt:ClassNaming")
@RunWith(AndroidJUnit4::class)
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
@LargeTest
class LoginActivityTest {
class `1-LoginActivityTest` : WithANRException() {
@get:Rule
val activityRule = ActivityScenarioRule(LoginActivity::class.java)
@ -40,7 +44,7 @@ class LoginActivityTest {
}
@Test
fun viewIsInitialized() {
fun `1-viewIsInitialized`() {
onView(withId(R.id.urlView)).check(matches(isDisplayed()))
onView(withId(R.id.selfSigned))
.check(matches(isDisplayed()))
@ -57,28 +61,28 @@ class LoginActivityTest {
}
@Test
fun urlError() {
fun `2-urlError`() {
performLogin("10.0.2.2:8888")
onView(withId(R.id.urlView)).perform(click())
onView(withId(R.id.urlView)).check(matches(withError(R.string.login_url_problem)))
}
@Test
fun connectError() {
performLogin("http://10.0.2.2:8889")
onView(withId(R.id.urlView)).perform(click())
onView(withId(R.id.urlView)).check(matches(withError(R.string.wrong_infos)))
}
@Test
fun urlSlashError() {
fun `3-urlSlashError`() {
performLogin("https://google.fr/toto")
onView(withId(R.id.urlView)).perform(click())
onView(withId(R.id.urlView)).check(matches(withError(R.string.login_url_problem)))
}
@Test
fun multiError() {
fun `4-connectError`() {
performLogin("http://10.0.2.2:8889")
onView(withId(R.id.urlView)).perform(click())
onView(withId(R.id.urlView)).check(matches(withError(R.string.wrong_infos)))
}
@Test
fun `5-multiError`() {
onView(withId(R.id.signInButton)).perform(click())
onView(withId(R.id.signInButton)).perform(click())
onView(withId(R.id.signInButton)).perform(click())
@ -86,8 +90,9 @@ class LoginActivityTest {
}
@Test
fun connect() {
fun `6-connect`() {
performLogin()
onView(withText(R.string.gdpr_dialog_title)).check(matches(isDisplayed()))
onView(withText("OK")).perform(click())
}
}

View File

@ -15,21 +15,19 @@ import androidx.test.ext.junit.rules.ActivityScenarioRule
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.LargeTest
import org.hamcrest.CoreMatchers.not
import org.junit.Before
import org.junit.FixMethodOrder
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.MethodSorters
@Suppress("ktlint:standard:class-naming", "detekt:ClassNaming")
@RunWith(AndroidJUnit4::class)
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
@LargeTest
class HomeActivityTest {
class `2-HomeActivityTest` : WithANRException() {
@get:Rule
val activityRule = ActivityScenarioRule(LoginActivity::class.java)
@Before
fun init() {
loginAndInitHome()
}
val activityRule = ActivityScenarioRule(HomeActivity::class.java)
@Test
fun testMenu() {

View File

@ -19,9 +19,11 @@ import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
@LargeTest
class SettingsActivityTest {
@Suppress("ktlint:standard:class-naming", "detekt:ClassNaming")
class `3-SettingsActivityTest` : WithANRException() {
@get:Rule
val activityRule = ActivityScenarioRule(LoginActivity::class.java)
val activityRule = ActivityScenarioRule(HomeActivity::class.java)
lateinit var context: Context
@Before
@ -29,7 +31,6 @@ class SettingsActivityTest {
activityRule.scenario.onActivity { activity ->
context = activity.window.context
}
loginAndInitHome()
openMenu()
onView(withText(R.string.title_activity_settings)).perform(click())
}
@ -68,6 +69,9 @@ class SettingsActivityTest {
changeAndSaveSetting("", "10") {
onView(withText(R.string.pref_api_timeout)).perform(click())
}
changeAndSaveSetting("", "60") {
onView(withText(R.string.pref_api_timeout)).perform(click())
}
}
@Test

View File

@ -22,19 +22,22 @@ import androidx.test.filters.LargeTest
import org.hamcrest.CoreMatchers.allOf
import org.hamcrest.CoreMatchers.not
import org.junit.Before
import org.junit.FixMethodOrder
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.MethodSorters
@Suppress("ktlint:standard:class-naming", "detekt:ClassNaming")
@RunWith(AndroidJUnit4::class)
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
@LargeTest
class SettingsActivityGeneralTest {
class `4-SettingsActivityGeneralTest` : WithANRException() {
@get:Rule
val activityRule = ActivityScenarioRule(LoginActivity::class.java)
val activityRule = ActivityScenarioRule(HomeActivity::class.java)
@Before
fun init() {
loginAndInitHome()
openActionBarOverflowOrOptionsMenu(
ApplicationProvider.getApplicationContext(),
)

View File

@ -1,29 +1,32 @@
package bou.amine.apps.readerforselfossv2.android
import android.content.Context
import androidx.test.core.app.ApplicationProvider
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.Espresso.openActionBarOverflowOrOptionsMenu
import androidx.test.espresso.action.ViewActions
import androidx.test.espresso.action.ViewActions.click
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.ViewMatchers.isChecked
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
import androidx.test.espresso.matcher.ViewMatchers.isRoot
import androidx.test.espresso.matcher.ViewMatchers.withText
import androidx.test.ext.junit.rules.ActivityScenarioRule
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.LargeTest
import bou.amine.apps.readerforselfossv2.android.settings.SettingsActivity
import org.hamcrest.CoreMatchers.allOf
import org.hamcrest.CoreMatchers.not
import org.junit.After
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@Suppress("ktlint:standard:class-naming", "detekt:ClassNaming")
@RunWith(AndroidJUnit4::class)
@LargeTest
class SettingsActivityReaderTest {
class `5-SettingsActivityReaderTest` : WithANRException() {
@get:Rule
val activityRule = ActivityScenarioRule(LoginActivity::class.java)
val activityRule = ActivityScenarioRule(SettingsActivity::class.java)
lateinit var context: Context
@ -32,14 +35,14 @@ class SettingsActivityReaderTest {
activityRule.scenario.onActivity { activity ->
context = activity.window.context
}
loginAndInitHome()
openActionBarOverflowOrOptionsMenu(
ApplicationProvider.getApplicationContext(),
)
onView(withText(R.string.title_activity_settings)).perform(click())
onView(withText(R.string.pref_header_viewer)).perform(click())
}
@After
fun back() {
onView(isRoot()).perform(ViewActions.pressBack())
}
@Test
fun testReader() {
onView(withSettingsCheckboxFrame(R.string.pref_switch_actions_pager_scroll)).check(

View File

@ -1,31 +1,34 @@
package bou.amine.apps.readerforselfossv2.android
import android.content.Context
import androidx.test.core.app.ApplicationProvider
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.Espresso.openActionBarOverflowOrOptionsMenu
import androidx.test.espresso.action.ViewActions
import androidx.test.espresso.action.ViewActions.click
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.ViewMatchers.isChecked
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
import androidx.test.espresso.matcher.ViewMatchers.isEnabled
import androidx.test.espresso.matcher.ViewMatchers.isNotEnabled
import androidx.test.espresso.matcher.ViewMatchers.isRoot
import androidx.test.espresso.matcher.ViewMatchers.withText
import androidx.test.ext.junit.rules.ActivityScenarioRule
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.LargeTest
import bou.amine.apps.readerforselfossv2.android.settings.SettingsActivity
import org.hamcrest.CoreMatchers.allOf
import org.hamcrest.CoreMatchers.not
import org.junit.After
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@Suppress("ktlint:standard:class-naming", "detekt:ClassNaming")
@RunWith(AndroidJUnit4::class)
@LargeTest
class SettingsActivityOfflineTest {
class `6-SettingsActivityOfflineTest` : WithANRException() {
@get:Rule
val activityRule = ActivityScenarioRule(LoginActivity::class.java)
val activityRule = ActivityScenarioRule(SettingsActivity::class.java)
lateinit var context: Context
@ -34,14 +37,14 @@ class SettingsActivityOfflineTest {
activityRule.scenario.onActivity { activity ->
context = activity.window.context
}
loginAndInitHome()
openActionBarOverflowOrOptionsMenu(
ApplicationProvider.getApplicationContext(),
)
onView(withText(R.string.title_activity_settings)).perform(click())
onView(withText(R.string.pref_header_offline)).perform(click())
}
@After
fun back() {
onView(isRoot()).perform(ViewActions.pressBack())
}
@Suppress("detekt:LongMethod")
@Test
fun testOffline() {

View File

@ -21,11 +21,12 @@ import org.junit.Test
import org.junit.runner.RunWith
import java.util.UUID
@Suppress("ktlint:standard:class-naming", "detekt:ClassNaming")
@RunWith(AndroidJUnit4::class)
@LargeTest
class SourcesActivityTest {
class `7-SourcesActivityTest` : WithANRException() {
@get:Rule
val activityRule = ActivityScenarioRule(LoginActivity::class.java)
val activityRule = ActivityScenarioRule(HomeActivity::class.java)
lateinit var sourceName: String
@ -33,7 +34,6 @@ class SourcesActivityTest {
fun init() {
sourceName = UUID.randomUUID().toString().substring(0, 15)
loginAndInitHome()
goToSources()
}
@ -71,12 +71,8 @@ class SourcesActivityTest {
fun deleteTheCreatedSource() {
onView(withText(sourceName)).check(matches(isDisplayed()))
onView(withId(R.id.deleteBtn)).perform(click())
onView(withText(R.string.confirm_delete_title)).check(matches(isDisplayed()))
onView(withId(android.R.id.button1)).perform(click())
onView(withText(sourceName)).check(doesNotExist())
}
private fun goToSources() {
openMenu()
onView(withText(R.string.menu_home_sources))
.perform(click())
}
}

View File

@ -1,7 +1,12 @@
package bou.amine.apps.readerforselfossv2.android
import android.content.Context
import android.graphics.Bitmap
import android.os.Environment.DIRECTORY_PICTURES
import android.os.Environment.getExternalStoragePublicDirectory
import android.util.Log
import androidx.annotation.ArrayRes
import androidx.test.espresso.Espresso
import androidx.test.espresso.Espresso.onData
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.action.ViewActions.click
@ -9,29 +14,36 @@ import androidx.test.espresso.action.ViewActions.replaceText
import androidx.test.espresso.action.ViewActions.typeTextIntoFocusedView
import androidx.test.espresso.assertion.ViewAssertions.doesNotExist
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.base.DefaultFailureHandler
import androidx.test.espresso.matcher.ViewMatchers.isChecked
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
import androidx.test.espresso.matcher.ViewMatchers.isNotChecked
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.espresso.matcher.ViewMatchers.withText
import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
import androidx.test.uiautomator.UiDevice
import androidx.test.uiautomator.UiSelector
import org.hamcrest.CoreMatchers.allOf
import org.hamcrest.Matchers.hasToString
import org.junit.BeforeClass
import java.io.BufferedOutputStream
import java.io.File
import java.io.FileOutputStream
import java.io.IOException
import java.util.Locale
val defaultUrl = (System.getenv("SELFOSS_URL") ?: "").ifEmpty { "http://10.0.2.2:8888" }
fun performLogin(someUrl: String? = null) {
Log.i("AUTOMATION", "The url used will be ${if (!someUrl.isNullOrEmpty()) someUrl else defaultUrl}")
onView(withId(R.id.urlView)).perform(click()).perform(
typeTextIntoFocusedView(
if (!someUrl.isNullOrEmpty()) someUrl else "http://10.0.2.2:8888",
if (!someUrl.isNullOrEmpty()) someUrl else defaultUrl,
),
)
onView(withId(R.id.signInButton)).perform(click())
}
fun loginAndInitHome() {
performLogin()
onView(withText(R.string.gdpr_dialog_title)).check(matches(isDisplayed()))
onView(withText("OK")).perform(click())
}
fun changeAndCancelSetting(
oldValue: String,
newValue: String,
@ -97,6 +109,12 @@ fun testPreferencesFromArray(
}
}
fun goToSources() {
openMenu()
onView(withText(R.string.menu_home_sources))
.perform(click())
}
fun testAddSourceWithUrl(
url: String,
sourceName: String,
@ -119,3 +137,88 @@ fun testAddSourceWithUrl(
.perform(click())
onView(withText(sourceName)).check(matches(isDisplayed()))
}
@Suppress("detekt:UtilityClassWithPublicConstructor")
open class WithANRException {
companion object {
// Running count of the number of Android Not Responding dialogues to prevent endless dismissal.
private var anrCount = 0
// `RootViewWithoutFocusException` class is private, need to match the message (instead of using type matching).
private val rootViewWithoutFocusExceptionMsg =
java.lang.String.format(
Locale.ROOT,
"Waited for the root of the view hierarchy to have " +
"window focus and not request layout for 10 seconds. If you specified a non " +
"default root matcher, it may be picking a root that never takes focus. " +
"Root:",
)
private val otherException = "System Ul isn't responding"
private fun handleAnrDialogue() {
val device = UiDevice.getInstance(getInstrumentation())
// If running the device in English Locale
val waitButton = device.findObject(UiSelector().textContains("wait"))
if (waitButton.exists()) waitButton.click()
}
@JvmStatic
@BeforeClass
fun setUpHandler() {
Espresso.setFailureHandler { error, viewMatcher ->
takeScreenshot()
if (error.message!!.contains(otherException)) {
handleAnrDialogue()
} else if (error.message!!.contains(rootViewWithoutFocusExceptionMsg) &&
anrCount < 20
) {
anrCount++
handleAnrDialogue()
} else { // chain all failures down to the default espresso handler
Log.e("AMINE", "AMINE : ${error.message}")
println("AMINE : ${error.message}")
DefaultFailureHandler(getInstrumentation().targetContext).handle(error, viewMatcher)
}
}
}
}
}
fun takeScreenshot() {
try {
val bitmap = getInstrumentation().uiAutomation.takeScreenshot()
val folder =
File(
File(
getExternalStoragePublicDirectory(DIRECTORY_PICTURES),
"selfoss_tests",
).absolutePath,
"screenshots",
)
if (!folder.exists()) {
folder.mkdirs()
}
var out: BufferedOutputStream? = null
val size = folder.list().size + 1
try {
out = BufferedOutputStream(FileOutputStream(folder.path + "/" + size + ".png"))
bitmap.compress(Bitmap.CompressFormat.PNG, 100, out)
Log.d("Screenshots", "Screenshot taken")
} catch (e: IOException) {
Log.e("Screenshots", "Could not save the screenshot", e)
} finally {
if (out != null) {
try {
out.close()
} catch (e: IOException) {
Log.e("Screenshots", "Could not save the screenshot", e)
}
}
}
} catch (ex: IOException) {
Log.e("Screenshots", "Could not take the screenshot", ex)
}
}

View File

@ -0,0 +1,87 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<application
android:name=".MyApp"
android:allowBackup="false"
android:configChanges="uiMode"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="false"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:networkSecurityConfig="@xml/network_security_config"
android:requestLegacyExternalStorage="true"
android:supportsRtl="true"
android:theme="@style/NoBar"
tools:replace="android:allowBackup">
<activity
android:name=".MainActivity"
android:exported="true"
android:theme="@style/SplashTheme">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<meta-data
android:name="android.app.shortcuts"
android:resource="@xml/shortcuts" />
</activity>
<activity
android:name=".LoginActivity"
android:label="@string/title_activity_login"></activity>
<activity android:name=".HomeActivity"></activity>
<activity
android:name=".settings.SettingsActivity"
android:label="@string/title_activity_settings"
android:parentActivityName=".HomeActivity">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value=".HomeActivity" />
</activity>
<activity
android:name=".SourcesActivity"
android:parentActivityName=".HomeActivity">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value=".HomeActivity" />
</activity>
<activity
android:name=".UpsertSourceActivity"
android:exported="true"
android:parentActivityName=".SourcesActivity">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value=".SourcesActivity" />
<intent-filter>
<action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="text/plain" />
</intent-filter>
</activity>
<activity android:name=".ReaderActivity"></activity>
<activity
android:name=".ImageActivity"
android:theme="@style/Theme.AppCompat.ImageActivity"></activity>
<meta-data
android:name="android.webkit.WebView.MetricsOptOut"
android:value="true" />
<meta-data
android:name="android.webkit.WebView.EnableSafeBrowsing"
android:value="true" />
<meta-data
android:name="android.max_aspect"
android:value="2.1" />
</application>
</manifest>

View File

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

View File

@ -37,22 +37,6 @@ class ReaderActivity :
private val repository: Repository by instance()
private val appSettingsService: AppSettingsService by instance()
private fun showMenuItem(willAddToFavorite: Boolean) {
if (willAddToFavorite) {
toolbarMenu.findItem(R.id.star).icon?.setTint(Color.WHITE)
} else {
toolbarMenu.findItem(R.id.star).icon?.setTint(Color.RED)
}
}
private fun canFavorite() {
showMenuItem(true)
}
private fun canRemoveFromFavorite() {
showMenuItem(false)
}
@Suppress("detekt:SwallowedException")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@ -73,14 +57,21 @@ class ReaderActivity :
finish()
}
try {
readItem(allItems[currentItem])
} catch (e: IndexOutOfBoundsException) {
finish()
}
readItem()
binding.pager.adapter = ScreenSlidePagerAdapter(this)
binding.pager.setCurrentItem(currentItem, false)
binding.pager.registerOnPageChangeCallback(
object : ViewPager2.OnPageChangeCallback() {
override fun onPageSelected(position: Int) {
super.onPageSelected(position)
currentItem = position
updateStarIcon()
readItem()
}
},
)
}
override fun onResume() {
@ -89,14 +80,20 @@ class ReaderActivity :
binding.indicator.setViewPager(binding.pager)
}
private fun readItem(item: SelfossModel.Item) {
if (appSettingsService.isMarkOnScrollEnabled() && !appSettingsService.getPublicAccess()) {
private fun readItem() {
val item = allItems.getOrNull(currentItem)
if (appSettingsService.isMarkOnScrollEnabled() && !appSettingsService.getPublicAccess() && item != null) {
CoroutineScope(Dispatchers.IO).launch {
repository.markAsRead(item)
}
}
}
private fun updateStarIcon() {
val isStarred = allItems.getOrNull(currentItem)?.starred ?: false
toolbarMenu.findItem(R.id.star)?.icon?.setTint(if (isStarred) Color.RED else Color.WHITE)
}
override fun onSaveInstanceState(oldInstanceState: Bundle) {
super.onSaveInstanceState(oldInstanceState)
oldInstanceState.clear()
@ -141,8 +138,7 @@ class ReaderActivity :
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
val inflater = menuInflater
inflater.inflate(R.menu.reader_menu, menu)
menuInflater.inflate(R.menu.reader_menu, menu)
toolbarMenu = menu
alignmentMenu()
@ -150,87 +146,50 @@ class ReaderActivity :
if (appSettingsService.getPublicAccess()) {
menu.removeItem(R.id.star)
} else {
if (allItems.isNotEmpty() && allItems[currentItem].starred) {
canRemoveFromFavorite()
} else {
canFavorite()
}
binding.pager.registerOnPageChangeCallback(
object : ViewPager2.OnPageChangeCallback() {
override fun onPageSelected(position: Int) {
super.onPageSelected(position)
if (!allItems.isNullOrEmpty() && allItems.size >= position) {
if (allItems[position].starred) {
canRemoveFromFavorite()
} else {
canFavorite()
}
readItem(allItems[position])
}
}
},
)
updateStarIcon()
}
return true
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
fun afterSave() {
allItems[binding.pager.currentItem] =
allItems[binding.pager.currentItem].toggleStar()
canRemoveFromFavorite()
}
fun afterUnsave() {
allItems[binding.pager.currentItem] = allItems[binding.pager.currentItem].toggleStar()
canFavorite()
}
when (item.itemId) {
android.R.id.home -> {
onBackPressedDispatcher.onBackPressed()
return true
}
R.id.star -> {
if (allItems[binding.pager.currentItem].starred) {
CoroutineScope(Dispatchers.IO).launch {
repository.unstarr(allItems[binding.pager.currentItem])
}
afterUnsave()
} else {
CoroutineScope(Dispatchers.IO).launch {
repository.starr(allItems[binding.pager.currentItem])
}
afterSave()
}
}
R.id.align_left -> {
switchAlignmentSetting(AppSettingsService.ALIGN_LEFT)
refreshFragment()
}
R.id.align_justify -> {
switchAlignmentSetting(AppSettingsService.JUSTIFY)
refreshFragment()
}
android.R.id.home -> onBackPressedDispatcher.onBackPressed()
R.id.star -> toggleFavorite()
R.id.align_left -> switchAlignmentSetting(AppSettingsService.ALIGN_LEFT)
R.id.align_justify -> switchAlignmentSetting(AppSettingsService.JUSTIFY)
}
return super.onOptionsItemSelected(item)
}
private fun switchAlignmentSetting(allignment: Int) {
appSettingsService.changeAllignment(allignment)
alignmentMenu()
private fun toggleFavorite() {
val item = allItems.getOrNull(currentItem) ?: return
val starred = item.starred
CoroutineScope(Dispatchers.IO).launch {
if (starred) {
repository.unstarr(item)
} else {
repository.starr(item)
}
}
item.toggleStar()
updateStarIcon()
}
private fun refreshFragment() {
finish()
overridePendingTransition(0, 0)
startActivity(intent)
overridePendingTransition(0, 0)
private fun switchAlignmentSetting(alignment: Int) {
appSettingsService.changeAllignment(alignment)
alignmentMenu()
val fragmentManager = supportFragmentManager
val fragments = fragmentManager.fragments
for (fragment in fragments) {
if (fragment is ArticleFragment) {
fragment.refreshAlignment()
}
}
}
}

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.rest.MercuryApi
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.getImages
import bou.amine.apps.readerforselfossv2.utils.getThumbnail
@ -88,6 +89,7 @@ class ArticleFragment :
override val di: DI by closestDI()
private val repository: Repository by instance()
private val appSettingsService: AppSettingsService by instance()
private val connectivityService: ConnectivityService by instance()
private var typeface: Typeface? = null
private var resId: Int = 0
@ -168,7 +170,7 @@ class ArticleFragment :
private fun handleContent() {
if (contentText.isEmptyOrNullOrNullString()) {
if (repository.isNetworkAvailable() && url.isUrlValid()) {
if (connectivityService.isNetworkAvailable() && url.isUrlValid()) {
getContentFromMercury(url!!)
}
} else {
@ -261,13 +263,15 @@ class ArticleFragment :
)
}
private fun refreshAlignment() {
fun refreshAlignment() {
textAlignment =
when (appSettingsService.getActiveAllignment()) {
1 -> "justify"
2 -> "left"
else -> "justify"
}
htmlToWebview()
}
@Suppress("detekt:SwallowedException")

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

@ -1,3 +1,5 @@
@file:Suppress("ktlint")
/*
package bou.amine.apps.readerforselfossv2.android.tests.robolectric
import android.view.Menu
@ -25,3 +27,4 @@ fun Menu.assertVisible(
val item = this.findItem(id)
assertTrue(item.isVisible)
}
*/

View File

@ -1,3 +1,5 @@
@file:Suppress("ktlint")
/*
package bou.amine.apps.readerforselfossv2.android.tests.robolectric
import android.widget.Button
@ -57,7 +59,8 @@ class LoginActivityTest {
}
}
/* @Test
*/
/* @Test
fun connect() {
Robolectric.buildActivity(LoginActivity::class.java).use { controller ->
controller.setup() // Moves the Activity to the RESUMED state
@ -72,4 +75,7 @@ class LoginActivityTest {
assertEquals(expectedIntent.component, actual.component)
}
}*/
/*
}
*/

View File

@ -1,3 +1,5 @@
@file:Suppress("ktlint")
/*
package bou.amine.apps.readerforselfossv2.android.tests.robolectric
import org.robolectric.RobolectricTestRunner
@ -8,3 +10,4 @@ class RobotElectriqueRunner(
) : RobolectricTestRunner(testClass) {
override fun buildGlobalConfig(): Config = Config.Builder().setSdk(25, 30, 33).build()
}
*/

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

View File

@ -0,0 +1,12 @@
**v125030681**
- chore: do not send reports on simulators.
- Merge pull request 'chore: do not send reports on simulators.' (#188) from chore-acra-simulator into master
- chore: do not send reports on simulators.
- Merge pull request 'fix: Url validation was not failing login. Added tests.' (#186) from fix-invalid-url into master
- Merge pull request 'chore: crowding ci integration.' (#187) from chore-crowdin-ci into master
- chore: we don't need to check if the url is valid in upsert screen.
- fix: Url validation was not failing login. Added tests.
- chore: crowding ci integration.
- Show a confirmation dialog before deleting sources (#185)
- Changelog for v125020581

View File

@ -0,0 +1,8 @@
**v125030711**
- Merge pull request 'fix: initial status loading issues.' (#192) from connectivity into master
- chore: check changes for translations and android.
- fix: initial status loading issues.
- Merge pull request 'chore: new connectivity dep. Closes #84.' (#189) from connectivity into master
- chore: new connectivity dep. Closes #84.
- Changelog for v125030681

View File

@ -27,3 +27,4 @@ org.gradle.caching=true
ignoreGitVersion=false
kotlin.native.cacheKind.iosX64=none
org.gradle.configureondemand=true
kotlin.jvm.target.validation.mode=IGNORE

View File

@ -4,7 +4,6 @@ object SqlDelight {
const val runtime = "app.cash.sqldelight:runtime: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"
}
plugins {
@ -41,13 +40,13 @@ kotlin {
implementation("org.jsoup:jsoup:1.15.4")
//Dependency Injection
// Dependency Injection
implementation("org.kodein.di:kodein-di:7.14.0")
//Settings
// Settings
implementation("com.russhwolf:multiplatform-settings-no-arg:1.0.0-RC")
//Logging
// Logging
implementation("io.github.aakira:napier:2.6.1")
// Sql
@ -55,6 +54,10 @@ kotlin {
// Sql
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 {
@ -114,4 +117,4 @@ sqldelight {
packageName.set("bou.amine.apps.readerforselfossv2.dao")
}
}
}
}

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

View File

@ -0,0 +1,46 @@
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 = true
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 -> {
if (!currentStatus) {
currentStatus = true
_networkAvailableProvider.emit(true)
}
}
is Connectivity.Status.Disconnected -> {
if (currentStatus) {
currentStatus = false
_networkAvailableProvider.emit(false)
}
}
}
}
}
}
fun isNetworkAvailable(): Boolean = currentStatus
fun stop() {
currentStatus = true
connectivity.stop()
}
}