Compare commits

...

26 Commits

Author SHA1 Message Date
07e3710d44 Merge pull request 'acra' (#104) from acra into master
Reviewed-on: https://gitea.amine-louveau.fr/Louvorg/ReaderForSelfoss-multiplatform/pulls/104
2022-12-01 21:01:18 +00:00
e68da7764f Settings for acra and analytics. Closes #98. 2022-12-01 21:30:20 +01:00
c3ff894027 Initial integration. 2022-11-30 20:53:11 +01:00
f09f731d30 Merge pull request 'Cookies login and logout.' (#103) from login into master
Reviewed-on: https://gitea.amine-louveau.fr/Louvorg/ReaderForSelfoss-multiplatform/pulls/103
2022-11-30 10:04:13 +00:00
956c4341c7 Cookies login and logout. 2022-11-29 21:38:58 +01:00
7b68264dd7 Cleaning. 2022-11-21 20:20:27 +01:00
cfcf030bf8 Removing gradle props. 2022-11-14 13:38:10 +01:00
0e7d7a5835 Conditionnal siteId 2022-11-14 13:18:20 +01:00
0856ebb889 Removing matomo url from build config. 2022-11-14 13:10:17 +01:00
25bf68cf0c Merge pull request 'Initial Matomo integration.' (#101) from login into master
Reviewed-on: https://gitea.amine-louveau.fr/Louvorg/ReaderForSelfoss-multiplatform/pulls/101
2022-11-13 13:18:49 +00:00
afc6f392c6 Initial Matomo integration. 2022-11-13 13:13:03 +01:00
a0b5e2052b Merge pull request 'Fixed theme reload issues.' (#100) from fix-theme-reload into master
Reviewed-on: https://gitea.amine-louveau.fr/Louvorg/ReaderForSelfoss-multiplatform/pulls/100
2022-11-11 20:29:08 +00:00
87d1ef2bce Fixed theme reload issues. 2022-11-11 20:51:49 +01:00
537a6d3a0b Merge pull request 'Checkerboard background for transparency in zoomed images' (#92) from davidoskky/ReaderForSelfoss-multiplatform:checkerboard into master
Reviewed-on: https://gitea.amine-louveau.fr/Louvorg/ReaderForSelfoss-multiplatform/pulls/92
2022-11-11 19:29:40 +00:00
dbe97f564e Revert imageview changes 2022-11-11 09:40:36 +01:00
3a3bf03114 Bigger checktile. 2022-11-10 21:41:55 +01:00
c09a32e9ad Add checkerboard background to the images in the image view
A checkerboard is drawn beneath the image in the imageview to allow
a simpler viewing of images with transparency
2022-11-09 16:39:00 +01:00
b02a588dff Add a checkerboard background drawable 2022-11-09 16:34:37 +01:00
a4527940b8 Merge pull request 'Mercury issues fixing.' (#96) from mercury-common into master
Reviewed-on: https://gitea.amine-louveau.fr/Louvorg/ReaderForSelfoss-multiplatform/pulls/96
2022-11-08 21:17:23 +00:00
9e8a25ed3e Fixing tests. 2022-11-08 22:02:20 +01:00
8ea46e146b Cleaning. 2022-11-08 21:54:12 +01:00
5ecf3c3f87 Mercury api in common code. 2022-11-08 21:31:40 +01:00
325f103417 Timeout. 2022-11-08 20:49:18 +01:00
ab4b1ae644 Merge pull request 'Theme should automatically change on phone settings change.' (#95) from theme-update into master
Reviewed-on: https://gitea.amine-louveau.fr/Louvorg/ReaderForSelfoss-multiplatform/pulls/95
2022-11-08 07:38:54 +00:00
87ea44754e Font update. 2022-11-07 22:36:20 +01:00
04dec50808 Theme should automatically change on phone settings change. 2022-11-07 22:07:35 +01:00
55 changed files with 890 additions and 441 deletions

View File

@ -8,7 +8,7 @@ steps:
commands: commands:
- echo "---------------------------------------------------------" - echo "---------------------------------------------------------"
- echo "Configure gradle..." - echo "Configure gradle..."
- mkdir -p ~/.gradle && echo "org.gradle.daemon=false\nignoreGitVersion=true\nappLoginUrl=\"URL\"\nappLoginUsername=\"LOGIN\"\nappLoginPassword=\"PASS\"\npushCache=false" >> ~/.gradle/gradle.properties - mkdir -p ~/.gradle && echo "org.gradle.daemon=false\nignoreGitVersion=true\npushCache=false\nsystemProp.org.gradle.internal.http.connectionTimeout=180000\nsystemProp.org.gradle.internal.http.socketTimeout=180000" >> ~/.gradle/gradle.properties
- echo "---------------------------------------------------------" - echo "---------------------------------------------------------"
- echo "Analysing..." - echo "Analysing..."
- ./gradlew sonarqube -Dsonar.projectKey=RFS2 -Dsonar.host.url=$SONAR_HOST_URL -Dsonar.login=$SONAR_LOGIN - ./gradlew sonarqube -Dsonar.projectKey=RFS2 -Dsonar.host.url=$SONAR_HOST_URL -Dsonar.login=$SONAR_LOGIN
@ -90,7 +90,7 @@ steps:
commands: commands:
- echo "---------------------------------------------------------" - echo "---------------------------------------------------------"
- echo "Configure gradle..." - echo "Configure gradle..."
- mkdir -p ~/.gradle && echo "org.gradle.daemon=false\nignoreGitVersion=true\nappLoginUrl=\"URL\"\nappLoginUsername=\"LOGIN\"\nappLoginPassword=\"PASS\"\npushCache=false" >> ~/.gradle/gradle.properties - mkdir -p ~/.gradle && echo "org.gradle.daemon=false\nignoreGitVersion=true\npushCache=false\nsystemProp.org.gradle.internal.http.connectionTimeout=180000\nsystemProp.org.gradle.internal.http.socketTimeout=180000" >> ~/.gradle/gradle.properties
- echo "---------------------------------------------------------" - echo "---------------------------------------------------------"
- echo "Generate APK" - echo "Generate APK"
- ./gradlew :androidApp:assembleGithubConfigRelease -P pushCache=false - ./gradlew :androidApp:assembleGithubConfigRelease -P pushCache=false

View File

@ -46,28 +46,3 @@ Always check if the web version of your instance is working.
I won't provide any selfoss instance url. If you want to help, but to not have one, you'll have to install one, and use it. I won't provide any selfoss instance url. If you want to help, but to not have one, you'll have to install one, and use it.
All the details to need are [here](https://selfoss.aditu.de/). All the details to need are [here](https://selfoss.aditu.de/).
# Build the project
You can directly import this project into IntellIJ/Android Studio.
You'll have to:
- Define some parameters either in `~/.gradle/gradle.properties` or as gradle parameters (see the examples)
- appLoginUrl, appLoginUsername and appLoginPassword: url, username and password of a selfoss instance. **These are only used for tests. They can be empty if you don't test API calls.**
### Examples:
#### Inside ~/.gradle/gradle.properties
```
appLoginUrl="URL" # It can be empty.
appLoginUsername="LOGIN" # It can be empty.
appLoginPassword="PASS" # It can be empty.
```
#### As gradle parameters
```
./gradlew .... -P appLoginUrl="URL" -P appLoginUsername="LOGIN" -P appLoginPassword="PASS"
```

View File

@ -1,6 +1,7 @@
import java.io.ByteArrayOutputStream import java.io.ByteArrayOutputStream
val ignoreGitVersion: String by project val ignoreGitVersion: String by project
val acraVersion = "5.9.7"
plugins { plugins {
id("com.android.application") id("com.android.application")
@ -96,9 +97,6 @@ android {
proguardFiles(getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro") proguardFiles(getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro")
} }
getByName("debug") { getByName("debug") {
buildConfigField("String", "LOGIN_URL", properties["appLoginUrl"] as String)
buildConfigField("String", "LOGIN_PASSWORD", properties["appLoginPassword"] as String)
buildConfigField("String", "LOGIN_USERNAME", properties["appLoginUsername"] as String)
} }
} }
flavorDimensions.add("build") flavorDimensions.add("build")
@ -142,12 +140,6 @@ dependencies {
implementation("com.mikepenz:aboutlibraries-core:10.5.1") implementation("com.mikepenz:aboutlibraries-core:10.5.1")
implementation("com.mikepenz:aboutlibraries:10.5.1") implementation("com.mikepenz:aboutlibraries:10.5.1")
// Retrofit + http logging + okhttp
implementation("com.squareup.retrofit2:retrofit:2.9.0")
implementation("com.squareup.okhttp3:logging-interceptor:5.0.0-alpha.3")
implementation("com.squareup.retrofit2:converter-gson:2.9.0")
implementation("com.burgstaller:okhttp-digest:2.5")
// Material-ish things // Material-ish things
implementation("com.ashokvarma.android:bottom-navigation-bar:2.2.0") implementation("com.ashokvarma.android:bottom-navigation-bar:2.2.0")
implementation("com.amulyakhare:com.amulyakhare.textdrawable:1.0.1") implementation("com.amulyakhare:com.amulyakhare.textdrawable:1.0.1")
@ -195,6 +187,12 @@ dependencies {
testImplementation("io.mockk:mockk:1.12.0") testImplementation("io.mockk:mockk:1.12.0")
testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.0") testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.0")
implementation("org.jetbrains.kotlinx:kotlinx-datetime:0.4.0") implementation("org.jetbrains.kotlinx:kotlinx-datetime:0.4.0")
implementation("ch.acra:acra-http:$acraVersion")
implementation("ch.acra:acra-toast:$acraVersion")
// Matomo
implementation("com.github.matomo-org:matomo-sdk-android:4.1.4")
} }
tasks.withType<Test> { tasks.withType<Test> {

View File

@ -30,15 +30,8 @@
<fields>; <fields>;
} }
-dontwarn okio.**
-dontwarn retrofit2.Platform$Java8
-keep class retrofit.** { *; }
-keepclasseswithmembers class * {
@retrofit.http.* <methods>;
}
-keepattributes *Annotation*,Signature -keepattributes *Annotation*,Signature
-keepattributes Exceptions -keepattributes Exceptions
-dontwarn okio.**
-dontwarn javax.annotation.Nullable -dontwarn javax.annotation.Nullable
-dontwarn javax.annotation.ParametersAreNonnullByDefault -dontwarn javax.annotation.ParametersAreNonnullByDefault

View File

@ -15,7 +15,8 @@
android:supportsRtl="true" android:supportsRtl="true"
android:networkSecurityConfig="@xml/network_security_config" android:networkSecurityConfig="@xml/network_security_config"
android:theme="@style/NoBar" android:theme="@style/NoBar"
android:dataExtractionRules="@xml/data_extraction_rules"> android:dataExtractionRules="@xml/data_extraction_rules"
android:configChanges="uiMode">
<activity <activity
android:name=".MainActivity" android:name=".MainActivity"
android:theme="@style/SplashTheme" android:theme="@style/SplashTheme"

View File

@ -35,7 +35,10 @@ import bou.amine.apps.readerforselfossv2.android.utils.bottombar.removeBadge
import bou.amine.apps.readerforselfossv2.model.SelfossModel 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.service.AppSettingsService import bou.amine.apps.readerforselfossv2.service.AppSettingsService
import bou.amine.apps.readerforselfossv2.utils.* import bou.amine.apps.readerforselfossv2.utils.ItemType
import bou.amine.apps.readerforselfossv2.utils.getHtmlDecoded
import bou.amine.apps.readerforselfossv2.utils.getIcon
import bou.amine.apps.readerforselfossv2.utils.longHash
import com.ashokvarma.bottomnavigation.BottomNavigationBar import com.ashokvarma.bottomnavigation.BottomNavigationBar
import com.ashokvarma.bottomnavigation.BottomNavigationItem import com.ashokvarma.bottomnavigation.BottomNavigationItem
import com.ashokvarma.bottomnavigation.TextBadgeItem import com.ashokvarma.bottomnavigation.TextBadgeItem
@ -56,9 +59,15 @@ import com.mikepenz.materialdrawer.util.updateBadge
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.acra.ACRA
import org.acra.ktx.sendSilentlyWithAcra
import org.acra.ktx.sendWithAcra
import org.kodein.di.DIAware import org.kodein.di.DIAware
import org.kodein.di.android.closestDI import org.kodein.di.android.closestDI
import org.kodein.di.instance import org.kodein.di.instance
import org.matomo.sdk.Tracker
import org.matomo.sdk.extra.TrackHelper
import java.security.MessageDigest
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
@ -95,12 +104,16 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
override val di by closestDI() override val 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 tracker : Tracker by instance()
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
binding = ActivityHomeBinding.inflate(layoutInflater) binding = ActivityHomeBinding.inflate(layoutInflater)
val view = binding.root val view = binding.root
TrackHelper.track().screen("/home").with(tracker)
fromTabShortcut = intent.getIntExtra("shortcutTab", -1) != -1 fromTabShortcut = intent.getIntExtra("shortcutTab", -1) != -1
repository.offlineOverride = intent.getBooleanExtra("startOffline", false) repository.offlineOverride = intent.getBooleanExtra("startOffline", false)
@ -297,6 +310,8 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
handleBottomBarActions() handleBottomBarActions()
handleGDPRDialog(appSettingsService.settings.getBoolean("GDPR_shown", false))
handleRecurringTask() handleRecurringTask()
CoroutineScope(Dispatchers.Main).launch { CoroutineScope(Dispatchers.Main).launch {
@ -306,6 +321,26 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
getElementsAccordingToTab() getElementsAccordingToTab()
} }
private fun handleGDPRDialog(GDPRShown: Boolean) {
val messageDigest: MessageDigest = MessageDigest.getInstance("SHA-256")
messageDigest.update(appSettingsService.getBaseUrl().toByteArray())
ACRA.errorReporter.putCustomData("unique_id", String(messageDigest.digest()))
if (!GDPRShown) {
val alertDialog = AlertDialog.Builder(this).create()
alertDialog.setTitle(getString(R.string.gdpr_dialog_title))
alertDialog.setMessage(getString(R.string.gdpr_dialog_message))
alertDialog.setButton(
AlertDialog.BUTTON_NEUTRAL,
"OK"
) { dialog, _ ->
appSettingsService.settings.putBoolean("GDPR_shown", true)
dialog.dismiss()
}
alertDialog.show()
}
}
private fun initDrawer() { private fun initDrawer() {
DrawerImageLoader.init(object : AbstractDrawerImageLoader() { DrawerImageLoader.init(object : AbstractDrawerImageLoader() {
override fun set(imageView: ImageView, uri: Uri, placeholder: Drawable, tag: String?) { override fun set(imageView: ImageView, uri: Uri, placeholder: Drawable, tag: String?) {
@ -478,6 +513,7 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
val gdColor = try { val gdColor = try {
Color.parseColor(it.color) Color.parseColor(it.color)
} catch (e: IllegalArgumentException) { } catch (e: IllegalArgumentException) {
e.sendSilentlyWithAcra()
resources.getColor(R.color.colorPrimary) resources.getColor(R.color.colorPrimary)
} }
gd.setColor(gdColor) gd.setColor(gdColor)
@ -860,7 +896,9 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
return true return true
} }
R.id.action_disconnect -> { R.id.action_disconnect -> {
appSettingsService.clearAll() CoroutineScope(Dispatchers.Main).launch {
repository.logout()
}
val intent = Intent(this, LoginActivity::class.java) val intent = Intent(this, LoginActivity::class.java)
this.startActivity(intent) this.startActivity(intent)
this@HomeActivity.finish() this@HomeActivity.finish()

View File

@ -2,6 +2,7 @@ package bou.amine.apps.readerforselfossv2.android
import android.animation.Animator import android.animation.Animator
import android.animation.AnimatorListenerAdapter import android.animation.AnimatorListenerAdapter
import android.annotation.SuppressLint
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.text.TextUtils import android.text.TextUtils
@ -24,6 +25,12 @@ import kotlinx.coroutines.launch
import org.kodein.di.DIAware import org.kodein.di.DIAware
import org.kodein.di.android.closestDI import org.kodein.di.android.closestDI
import org.kodein.di.instance import org.kodein.di.instance
import org.matomo.sdk.Tracker
import org.matomo.sdk.extra.DimensionQueue
import org.matomo.sdk.extra.DownloadTracker
import org.matomo.sdk.extra.TrackHelper
import java.security.MessageDigest
class LoginActivity : AppCompatActivity(), DIAware { class LoginActivity : AppCompatActivity(), DIAware {
@ -35,10 +42,17 @@ class LoginActivity : AppCompatActivity(), DIAware {
override val di by closestDI() override val 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 tracker : Tracker by instance()
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM)
TrackHelper.track().download().identifier(DownloadTracker.Extra.ApkChecksum(applicationContext))
.with(tracker)
TrackHelper.track().screen("/login").with(tracker)
handleTheme()
binding = ActivityLoginBinding.inflate(layoutInflater) binding = ActivityLoginBinding.inflate(layoutInflater)
val view = binding.root val view = binding.root
@ -56,6 +70,11 @@ class LoginActivity : AppCompatActivity(), DIAware {
handleActions() handleActions()
} }
@SuppressLint("WrongConstant") // Constant is fetched from the settings
private fun handleTheme() {
AppCompatDelegate.setDefaultNightMode(appSettingsService.getCurrentTheme())
}
private fun handleActions() { private fun handleActions() {
binding.passwordView.setOnEditorActionListener( binding.passwordView.setOnEditorActionListener(
@ -95,6 +114,15 @@ class LoginActivity : AppCompatActivity(), DIAware {
private fun goToMain() { private fun goToMain() {
CoroutineScope(Dispatchers.Main).launch { CoroutineScope(Dispatchers.Main).launch {
repository.updateApiVersion() repository.updateApiVersion()
val messageDigest: MessageDigest = MessageDigest.getInstance("SHA-256")
messageDigest.update(appSettingsService.getBaseUrl().toByteArray())
tracker.userId = String(messageDigest.digest())
val mDimensionQueue = DimensionQueue(tracker)
mDimensionQueue.add(1, appSettingsService.getApiVersion().toString())
tracker.isOptOut = !appSettingsService.isAnalyticsEnabled()
} }
val intent = Intent(this, HomeActivity::class.java) val intent = Intent(this, HomeActivity::class.java)
startActivity(intent) startActivity(intent)

View File

@ -12,9 +12,7 @@ import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.ProcessLifecycleOwner import androidx.lifecycle.ProcessLifecycleOwner
import androidx.multidex.MultiDexApplication import androidx.multidex.MultiDexApplication
import androidx.preference.PreferenceManager
import bou.amine.apps.readerforselfossv2.DI.networkModule import bou.amine.apps.readerforselfossv2.DI.networkModule
import bou.amine.apps.readerforselfossv2.android.utils.network.isNetworkAccessible
import bou.amine.apps.readerforselfossv2.android.viewmodel.AppViewModel 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
@ -31,7 +29,17 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.acra.ACRA
import org.acra.ReportField
import org.acra.config.httpSender
import org.acra.config.toast
import org.acra.data.StringFormat
import org.acra.ktx.initAcra
import org.acra.sender.HttpSender
import org.kodein.di.* import org.kodein.di.*
import org.matomo.sdk.Matomo
import org.matomo.sdk.Tracker
import org.matomo.sdk.TrackerBuilder
class MyApp : MultiDexApplication(), DIAware { class MyApp : MultiDexApplication(), DIAware {
@ -42,6 +50,8 @@ class MyApp : MultiDexApplication(), DIAware {
bind<Repository>() with singleton { Repository(instance(), instance(), isConnectionAvailable, instance()) } bind<Repository>() with singleton { Repository(instance(), instance(), isConnectionAvailable, instance()) }
bind<ConnectivityStatus>() with singleton { ConnectivityStatus(applicationContext) } bind<ConnectivityStatus>() with singleton { ConnectivityStatus(applicationContext) }
bind<AppViewModel>() with singleton { AppViewModel(repository = instance()) } bind<AppViewModel>() with singleton { AppViewModel(repository = instance()) }
bind<Tracker>() with singleton { TrackerBuilder.createDefault("https://matomo.amine-louveau.fr/matomo.php", if (BuildConfig.DEBUG) 4 else 5).build(
Matomo.getInstance(applicationContext)) }
} }
private val repository: Repository by instance() private val repository: Repository by instance()
@ -56,6 +66,7 @@ class MyApp : MultiDexApplication(), DIAware {
super.onCreate() super.onCreate()
Napier.base(DebugAntilog()) Napier.base(DebugAntilog())
if (!ACRA.isACRASenderServiceProcess()) {
initDrawerImageLoader() initDrawerImageLoader()
tryToHandleBug() tryToHandleBug()
@ -81,6 +92,34 @@ class MyApp : MultiDexApplication(), DIAware {
} }
} }
} }
}
override fun attachBaseContext(base: Context?) {
super.attachBaseContext(base)
initAcra {
reportFormat = StringFormat.JSON
reportContent = listOf(
ReportField.REPORT_ID, ReportField.INSTALLATION_ID,
ReportField.APP_VERSION_CODE, ReportField.APP_VERSION_NAME,
ReportField.BUILD, ReportField.ANDROID_VERSION, ReportField.BRAND, ReportField.PHONE_MODEL,
ReportField.AVAILABLE_MEM_SIZE, ReportField.TOTAL_MEM_SIZE,
ReportField.STACK_TRACE, ReportField.APPLICATION_LOG, ReportField.LOGCAT,
ReportField.INITIAL_CONFIGURATION, ReportField.CRASH_CONFIGURATION, ReportField.IS_SILENT,
ReportField.USER_APP_START_DATE, ReportField.USER_COMMENT, ReportField.USER_CRASH_DATE, ReportField.USER_EMAIL, ReportField.CUSTOM_DATA)
toast {
//required
text = getString(R.string.crash_toast_text)
length = Toast.LENGTH_SHORT
}
httpSender {
uri = "https://bugs.amine-louveau.fr/report" /*best guess, you may need to adjust this*/
basicAuthLogin = "LMTlLZuazADohTCm"
basicAuthPassword = "he6ghHp83F0PYPfh"
httpMethod = HttpSender.Method.POST
}
}
}
private fun handleNotificationChannels() { private fun handleNotificationChannels() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
@ -122,10 +161,9 @@ class MyApp : MultiDexApplication(), DIAware {
val oldHandler = Thread.getDefaultUncaughtExceptionHandler() val oldHandler = Thread.getDefaultUncaughtExceptionHandler()
Thread.setDefaultUncaughtExceptionHandler { thread, e -> Thread.setDefaultUncaughtExceptionHandler { thread, e ->
if (e is java.lang.NoClassDefFoundError && e.stackTrace.asList().any { if (e is NoClassDefFoundError && e.stackTrace.asList().any {
it.toString().contains("android.view.ViewDebug") it.toString().contains("android.view.ViewDebug")
}) { }) {
Unit
} else { } else {
oldHandler.uncaughtException(thread, e) oldHandler.uncaughtException(thread, e)
} }

View File

@ -1,35 +0,0 @@
package bou.amine.apps.readerforselfossv2.android.api.mercury
import com.google.gson.GsonBuilder
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import retrofit2.Call
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
class MercuryApi() {
private val service: MercuryService
init {
val interceptor = HttpLoggingInterceptor()
interceptor.level = HttpLoggingInterceptor.Level.NONE
val client = OkHttpClient.Builder().addInterceptor(interceptor).build()
val gson = GsonBuilder()
.setLenient()
.create()
val retrofit =
Retrofit
.Builder()
.baseUrl("https://www.amine-louveau.fr")
.client(client)
.addConverterFactory(GsonConverterFactory.create(gson))
.build()
service = retrofit.create(MercuryService::class.java)
}
fun parseUrl(url: String): Call<ParsedContent> {
return service.parseUrl(url)
}
}

View File

@ -1,59 +0,0 @@
package bou.amine.apps.readerforselfossv2.android.api.mercury
import android.os.Parcel
import android.os.Parcelable
import com.google.gson.annotations.SerializedName
class ParsedContent(
@SerializedName("title") val title: String,
@SerializedName("content") val content: String?,
@SerializedName("date_published") val date_published: String,
@SerializedName("lead_image_url") val lead_image_url: String?,
@SerializedName("dek") val dek: String,
@SerializedName("url") val url: String,
@SerializedName("domain") val domain: String,
@SerializedName("excerpt") val excerpt: String,
@SerializedName("total_pages") val total_pages: Int,
@SerializedName("rendered_pages") val rendered_pages: Int,
@SerializedName("next_page_url") val next_page_url: String
) : Parcelable {
companion object {
@JvmField
val CREATOR: Parcelable.Creator<ParsedContent> =
object : Parcelable.Creator<ParsedContent> {
override fun createFromParcel(source: Parcel): ParsedContent = ParsedContent(source)
override fun newArray(size: Int): Array<ParsedContent?> = arrayOfNulls(size)
}
}
constructor(source: Parcel) : this(
title = source.readString().orEmpty(),
content = source.readString(),
date_published = source.readString().orEmpty(),
lead_image_url = source.readString(),
dek = source.readString().orEmpty(),
url = source.readString().orEmpty(),
domain = source.readString().orEmpty(),
excerpt = source.readString().orEmpty(),
total_pages = source.readInt(),
rendered_pages = source.readInt(),
next_page_url = source.readString().orEmpty()
)
override fun describeContents() = 0
override fun writeToParcel(dest: Parcel, flags: Int) {
dest.writeString(title)
dest.writeString(content)
dest.writeString(date_published)
dest.writeString(lead_image_url)
dest.writeString(dek)
dest.writeString(url)
dest.writeString(domain)
dest.writeString(excerpt)
dest.writeInt(total_pages)
dest.writeInt(rendered_pages)
dest.writeString(next_page_url)
}
}

View File

@ -1,10 +0,0 @@
package bou.amine.apps.readerforselfossv2.android.api.mercury
import retrofit2.Call
import retrofit2.http.GET
import retrofit2.http.Query
interface MercuryService {
@GET("parser.php")
fun parseUrl(@Query("link") link: String): Call<ParsedContent>
}

View File

@ -21,8 +21,6 @@ import androidx.core.widget.NestedScrollView
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import bou.amine.apps.readerforselfossv2.android.ImageActivity import bou.amine.apps.readerforselfossv2.android.ImageActivity
import bou.amine.apps.readerforselfossv2.android.R import bou.amine.apps.readerforselfossv2.android.R
import bou.amine.apps.readerforselfossv2.android.api.mercury.MercuryApi
import bou.amine.apps.readerforselfossv2.android.api.mercury.ParsedContent
import bou.amine.apps.readerforselfossv2.android.databinding.FragmentArticleBinding import bou.amine.apps.readerforselfossv2.android.databinding.FragmentArticleBinding
import bou.amine.apps.readerforselfossv2.android.model.ParecelableItem import bou.amine.apps.readerforselfossv2.android.model.ParecelableItem
import bou.amine.apps.readerforselfossv2.android.model.toModel import bou.amine.apps.readerforselfossv2.android.model.toModel
@ -32,6 +30,7 @@ import bou.amine.apps.readerforselfossv2.android.utils.openInBrowserAsNewTask
import bou.amine.apps.readerforselfossv2.android.utils.shareLink import bou.amine.apps.readerforselfossv2.android.utils.shareLink
import bou.amine.apps.readerforselfossv2.model.SelfossModel 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.service.AppSettingsService import bou.amine.apps.readerforselfossv2.service.AppSettingsService
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
@ -45,13 +44,12 @@ import com.google.android.material.floatingactionbutton.FloatingActionButton
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.acra.ktx.sendSilentlyWithAcra
import org.acra.ktx.sendWithAcra
import org.kodein.di.DI import org.kodein.di.DI
import org.kodein.di.DIAware import org.kodein.di.DIAware
import org.kodein.di.android.x.closestDI import org.kodein.di.android.x.closestDI
import org.kodein.di.instance import org.kodein.di.instance
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
import java.net.MalformedURLException import java.net.MalformedURLException
import java.net.URL import java.net.URL
import java.util.* import java.util.*
@ -81,6 +79,9 @@ class ArticleFragment : Fragment(), DIAware {
private var font = "" private var font = ""
private var staticBar = false private var staticBar = false
private val mercuryApi : MercuryApi by instance()
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@ -113,6 +114,7 @@ class ArticleFragment : Fragment(), DIAware {
typeface = try { typeface = try {
ResourcesCompat.getFont(requireContext(), resId)!! ResourcesCompat.getFont(requireContext(), resId)!!
} catch (e: java.lang.Exception) { } catch (e: java.lang.Exception) {
e.sendSilentlyWithAcra()
// Just to be sure // Just to be sure
null null
} }
@ -218,6 +220,7 @@ class ArticleFragment : Fragment(), DIAware {
) )
} catch (e: InflateException) { } catch (e: InflateException) {
e.sendSilentlyWithAcra()
AlertDialog.Builder(requireContext()) AlertDialog.Builder(requireContext())
.setMessage(requireContext().getString(R.string.webview_dialog_issue_message)) .setMessage(requireContext().getString(R.string.webview_dialog_issue_message))
.setTitle(requireContext().getString(R.string.webview_dialog_issue_title)) .setTitle(requireContext().getString(R.string.webview_dialog_issue_title))
@ -249,58 +252,56 @@ class ArticleFragment : Fragment(), DIAware {
private fun getContentFromMercury() { private fun getContentFromMercury() {
if (repository.isNetworkAvailable()) { if (repository.isNetworkAvailable()) {
binding.progressBar.visibility = View.VISIBLE binding.progressBar.visibility = View.VISIBLE
val parser = MercuryApi()
parser.parseUrl(url).enqueue( CoroutineScope(Dispatchers.Main).launch {
object : Callback<ParsedContent> { val response = mercuryApi.query(url)
override fun onResponse( if (response.success) {
call: Call<ParsedContent>,
response: Response<ParsedContent>
) {
// TODO: clean all the following after finding the mercury content issue
try { try {
if (response.body() != null && response.body()!!.content != null && !response.body()!!.content.isNullOrEmpty()) { if (response.data != null && response.data!!.content != null && !response.data!!.content.isNullOrEmpty()) {
try { try {
binding.titleView.text = response.body()!!.title binding.titleView.text = response.data!!.title
if (typeface != null) { if (typeface != null) {
binding.titleView.typeface = typeface binding.titleView.typeface = typeface
} }
try { try {
// Note: Mercury may return relative urls... If it does the url val will not be changed. // Note: Mercury may return relative urls... If it does the url val will not be changed.
URL(response.body()!!.url) URL(response.data!!.url)
url = response.body()!!.url url = response.data!!.url
} catch (e: MalformedURLException) { } catch (e: MalformedURLException) {
// Mercury returned a relative url. We do nothing. // Mercury returned a relative url
e.sendSilentlyWithAcra()
} }
} catch (e: Exception) { } catch (e: Exception) {
e.sendSilentlyWithAcra()
} }
try { try {
contentText = response.body()!!.content.orEmpty() contentText = response.data!!.content.orEmpty()
htmlToWebview() htmlToWebview()
} catch (e: Exception) { } catch (e: Exception) {
e.sendSilentlyWithAcra()
} }
try { try {
if (response.body()!!.lead_image_url != null && !response.body()!!.lead_image_url.isNullOrEmpty() && context != null) { if (response.data!!.lead_image_url != null && !response.data!!.lead_image_url.isNullOrEmpty() && context != null) {
binding.imageView.visibility = View.VISIBLE binding.imageView.visibility = View.VISIBLE
try { try {
Glide Glide
.with(requireContext()) .with(requireContext())
.asBitmap() .asBitmap()
.load( .load(
response.body()!!.lead_image_url.orEmpty() response.data!!.lead_image_url.orEmpty()
) )
.apply(RequestOptions.fitCenterTransform()) .apply(RequestOptions.fitCenterTransform())
.into(binding.imageView) .into(binding.imageView)
} catch (e: Exception) { } catch (e: Exception) {
e.sendSilentlyWithAcra()
} }
} else { } else {
binding.imageView.visibility = View.GONE binding.imageView.visibility = View.GONE
} }
} catch (e: Exception) { } catch (e: Exception) {
if (context != null) { e.sendSilentlyWithAcra()
}
} }
try { try {
@ -308,30 +309,23 @@ class ArticleFragment : Fragment(), DIAware {
binding.progressBar.visibility = View.GONE binding.progressBar.visibility = View.GONE
} catch (e: Exception) { } catch (e: Exception) {
if (context != null) { e.sendSilentlyWithAcra()
}
} }
} else { } else {
try { try {
openInBrowserAfterFailing() openInBrowserAfterFailing()
} catch (e: Exception) { } catch (e: Exception) {
if (context != null) { e.sendSilentlyWithAcra()
}
} }
} }
} catch (e: Exception) { } catch (e: Exception) {
if (context != null) { e.sendSilentlyWithAcra()
}
} else {
openInBrowserAfterFailing()
} }
} }
} }
override fun onFailure(
call: Call<ParsedContent>,
t: Throwable
) = openInBrowserAfterFailing()
}
)
}
} }
private fun htmlToWebview() { private fun htmlToWebview() {
@ -369,19 +363,25 @@ class ArticleFragment : Fragment(), DIAware {
try { try {
val image = Glide.with(view).asBitmap().apply(glideOptions).load(url).submit().get() val image = Glide.with(view).asBitmap().apply(glideOptions).load(url).submit().get()
return WebResourceResponse("image/jpg", "UTF-8", getBitmapInputStream(image, Bitmap.CompressFormat.JPEG)) return WebResourceResponse("image/jpg", "UTF-8", getBitmapInputStream(image, Bitmap.CompressFormat.JPEG))
}catch ( e : ExecutionException) {} } catch ( e : ExecutionException) {
e.sendSilentlyWithAcra()
}
} }
else if (url.lowercase(Locale.US).contains(".png")) { else if (url.lowercase(Locale.US).contains(".png")) {
try { try {
val image = Glide.with(view).asBitmap().apply(glideOptions).load(url).submit().get() val image = Glide.with(view).asBitmap().apply(glideOptions).load(url).submit().get()
return WebResourceResponse("image/jpg", "UTF-8", getBitmapInputStream(image, Bitmap.CompressFormat.PNG)) return WebResourceResponse("image/jpg", "UTF-8", getBitmapInputStream(image, Bitmap.CompressFormat.PNG))
}catch ( e : ExecutionException) {} } catch ( e : ExecutionException) {
e.sendSilentlyWithAcra()
}
} }
else if (url.lowercase(Locale.US).contains(".webp")) { else if (url.lowercase(Locale.US).contains(".webp")) {
try { try {
val image = Glide.with(view).asBitmap().apply(glideOptions).load(url).submit().get() val image = Glide.with(view).asBitmap().apply(glideOptions).load(url).submit().get()
return WebResourceResponse("image/jpg", "UTF-8", getBitmapInputStream(image, Bitmap.CompressFormat.WEBP)) return WebResourceResponse("image/jpg", "UTF-8", getBitmapInputStream(image, Bitmap.CompressFormat.WEBP))
}catch ( e : ExecutionException) {} } catch ( e : ExecutionException) {
e.sendSilentlyWithAcra()
}
} }
return super.shouldInterceptRequest(view, url) return super.shouldInterceptRequest(view, url)
@ -405,11 +405,13 @@ class ArticleFragment : Fragment(), DIAware {
val itemUrl = URL(url) val itemUrl = URL(url)
baseUrl = itemUrl.protocol + "://" + itemUrl.host baseUrl = itemUrl.protocol + "://" + itemUrl.host
} catch (e: MalformedURLException) { } catch (e: MalformedURLException) {
e.sendSilentlyWithAcra()
} }
val fontName = when (font) { val fontName = when (font) {
getString(R.string.open_sans_font_id) -> "Open Sans" getString(R.string.open_sans_font_id) -> "Open Sans"
getString(R.string.roboto_font_id) -> "Roboto" getString(R.string.roboto_font_id) -> "Roboto"
getString(R.string.source_code_pro_font_id) -> "Source Code Pro"
else -> "" else -> ""
} }

View File

@ -7,6 +7,7 @@ import bou.amine.apps.readerforselfossv2.utils.getImages
import com.bumptech.glide.Glide import com.bumptech.glide.Glide
import com.bumptech.glide.load.engine.DiskCacheStrategy import com.bumptech.glide.load.engine.DiskCacheStrategy
import com.bumptech.glide.request.RequestOptions import com.bumptech.glide.request.RequestOptions
import org.acra.ktx.sendSilentlyWithAcra
fun SelfossModel.Item.preloadImages(context: Context) : Boolean { fun SelfossModel.Item.preloadImages(context: Context) : Boolean {
val imageUrls = this.getImages() val imageUrls = this.getImages()
@ -23,6 +24,7 @@ fun SelfossModel.Item.preloadImages(context: Context) : Boolean {
} }
} }
} catch (e : Error) { } catch (e : Error) {
e.sendSilentlyWithAcra()
return false return false
} }
@ -35,7 +37,7 @@ fun String.toTextDrawableString(): String {
try { try {
textDrawable.append(s[0]) textDrawable.append(s[0])
} catch (e: StringIndexOutOfBoundsException) { } catch (e: StringIndexOutOfBoundsException) {
// We do nothing e.sendSilentlyWithAcra()
} }
} }
return textDrawable.toString() return textDrawable.toString()

View File

@ -3,7 +3,6 @@ package bou.amine.apps.readerforselfossv2.android.model
import android.os.Parcel import android.os.Parcel
import android.os.Parcelable import android.os.Parcelable
import bou.amine.apps.readerforselfossv2.model.SelfossModel import bou.amine.apps.readerforselfossv2.model.SelfossModel
import com.google.gson.annotations.SerializedName
fun SelfossModel.Item.toParcelable() : ParecelableItem = fun SelfossModel.Item.toParcelable() : ParecelableItem =
ParecelableItem( ParecelableItem(
@ -34,17 +33,17 @@ fun ParecelableItem.toModel() : SelfossModel.Item =
this.tags.split(",") this.tags.split(",")
) )
data class ParecelableItem( data class ParecelableItem(
@SerializedName("id") val id: Int, val id: Int,
@SerializedName("datetime") val datetime: String, val datetime: String,
@SerializedName("title") val title: String, val title: String,
@SerializedName("content") val content: String, val content: String,
@SerializedName("unread") var unread: Boolean, var unread: Boolean,
@SerializedName("starred") var starred: Boolean, var starred: Boolean,
@SerializedName("thumbnail") val thumbnail: String?, val thumbnail: String?,
@SerializedName("icon") val icon: String?, val icon: String?,
@SerializedName("link") val link: String, val link: String,
@SerializedName("sourcetitle") val sourcetitle: String, val sourcetitle: String,
@SerializedName("tags") val tags: String val tags: String
) : Parcelable { ) : Parcelable {
companion object { companion object {

View File

@ -17,16 +17,28 @@ import androidx.preference.PreferenceFragmentCompat
import bou.amine.apps.readerforselfossv2.android.R import bou.amine.apps.readerforselfossv2.android.R
import bou.amine.apps.readerforselfossv2.android.databinding.ActivitySettingsBinding import bou.amine.apps.readerforselfossv2.android.databinding.ActivitySettingsBinding
import bou.amine.apps.readerforselfossv2.service.AppSettingsService import bou.amine.apps.readerforselfossv2.service.AppSettingsService
import org.acra.ktx.sendSilentlyWithAcra
import org.acra.ktx.sendWithAcra
import org.kodein.di.DIAware
import org.kodein.di.android.closestDI
import org.kodein.di.instance
import org.matomo.sdk.Tracker
import org.matomo.sdk.extra.TrackHelper
private const val TITLE_TAG = "settingsActivityTitle" private const val TITLE_TAG = "settingsActivityTitle"
class SettingsActivity : AppCompatActivity(), class SettingsActivity : AppCompatActivity(),
PreferenceFragmentCompat.OnPreferenceStartFragmentCallback { PreferenceFragmentCompat.OnPreferenceStartFragmentCallback, DIAware {
override val di by closestDI()
private val tracker : Tracker by instance()
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
val binding = ActivitySettingsBinding.inflate(layoutInflater) val binding = ActivitySettingsBinding.inflate(layoutInflater)
TrackHelper.track().screen("/settings").with(tracker)
setContentView(binding.root) setContentView(binding.root)
if (savedInstanceState == null) { if (savedInstanceState == null) {
supportFragmentManager supportFragmentManager
@ -91,6 +103,11 @@ class SettingsActivity : AppCompatActivity(),
class MainPreferenceFragment : PreferenceFragmentCompat() { class MainPreferenceFragment : PreferenceFragmentCompat() {
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
setPreferencesFromResource(R.xml.pref_main, rootKey) setPreferencesFromResource(R.xml.pref_main, rootKey)
preferenceManager.findPreference<Preference>("currentMode")?.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, newValue ->
AppCompatDelegate.setDefaultNightMode(newValue.toString().toInt()) // ListPreference Only takes string-arrays ¯\_(ツ)_/¯
true
}
} }
} }
@ -107,6 +124,7 @@ class SettingsActivity : AppCompatActivity(),
val input: Int = (dest.toString() + source.toString()).toInt() val input: Int = (dest.toString() + source.toString()).toInt()
if (input in 1..200) return@InputFilter null if (input in 1..200) return@InputFilter null
} catch (nfe: NumberFormatException) { } catch (nfe: NumberFormatException) {
nfe.sendSilentlyWithAcra()
Toast.makeText(activity, R.string.items_number_should_be_number, Toast.LENGTH_LONG).show() Toast.makeText(activity, R.string.items_number_should_be_number, Toast.LENGTH_LONG).show()
} }
"" ""
@ -130,6 +148,7 @@ class SettingsActivity : AppCompatActivity(),
try { try {
editText.textSize = editable.toString().toInt().toFloat() editText.textSize = editable.toString().toInt().toFloat()
} catch (e: NumberFormatException) { } catch (e: NumberFormatException) {
e.sendSilentlyWithAcra()
} }
} }
} } } }
@ -139,6 +158,7 @@ class SettingsActivity : AppCompatActivity(),
val input = (dest.toString() + source.toString()).toInt() val input = (dest.toString() + source.toString()).toInt()
if (input > 0) return@InputFilter null if (input > 0) return@InputFilter null
} catch (nfe: NumberFormatException) { } catch (nfe: NumberFormatException) {
nfe.sendSilentlyWithAcra()
} }
"" ""
} }

View File

@ -1,13 +1,9 @@
package bou.amine.apps.readerforselfossv2.android.utils package bou.amine.apps.readerforselfossv2.android.utils
import android.app.Activity import android.app.Activity
import android.app.PendingIntent
import android.content.ActivityNotFoundException
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.graphics.BitmapFactory
import android.net.Uri import android.net.Uri
import android.os.Build
import android.text.Spannable import android.text.Spannable
import android.text.style.ClickableSpan import android.text.style.ClickableSpan
import android.util.Patterns import android.util.Patterns

View File

@ -0,0 +1,5 @@
<bitmap
xmlns:android="http://schemas.android.com/apk/res/android"
android:dither="true"
android:src="@drawable/checktile"
android:tileMode="repeat"/>

Binary file not shown.

After

Width:  |  Height:  |  Size: 235 B

View File

@ -0,0 +1,5 @@
<vector android:height="24dp" android:tint="#000000"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M20,8h-2.81c-0.45,-0.78 -1.07,-1.45 -1.82,-1.96L17,4.41 15.59,3l-2.17,2.17C12.96,5.06 12.49,5 12,5c-0.49,0 -0.96,0.06 -1.41,0.17L8.41,3 7,4.41l1.62,1.63C7.88,6.55 7.26,7.22 6.81,8L4,8v2h2.09c-0.05,0.33 -0.09,0.66 -0.09,1v1L4,12v2h2v1c0,0.34 0.04,0.67 0.09,1L4,16v2h2.81c1.04,1.79 2.97,3 5.19,3s4.15,-1.21 5.19,-3L20,18v-2h-2.09c0.05,-0.33 0.09,-0.66 0.09,-1v-1h2v-2h-2v-1c0,-0.34 -0.04,-0.67 -0.09,-1L20,10L20,8zM14,16h-4v-2h4v2zM14,12h-4v-2h4v2z"/>
</vector>

View File

@ -0,0 +1,7 @@
<vector android:height="24dp" android:tint="#000000"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M21,8c-1.45,0 -2.26,1.44 -1.93,2.51l-3.55,3.56c-0.3,-0.09 -0.74,-0.09 -1.04,0l-2.55,-2.55C12.27,10.45 11.46,9 10,9c-1.45,0 -2.27,1.44 -1.93,2.52l-4.56,4.55C2.44,15.74 1,16.55 1,18c0,1.1 0.9,2 2,2c1.45,0 2.26,-1.44 1.93,-2.51l4.55,-4.56c0.3,0.09 0.74,0.09 1.04,0l2.55,2.55C12.73,16.55 13.54,18 15,18c1.45,0 2.27,-1.44 1.93,-2.52l3.56,-3.55C21.56,12.26 23,11.45 23,10C23,8.9 22.1,8 21,8z"/>
<path android:fillColor="@android:color/white" android:pathData="M15,9l0.94,-2.07l2.06,-0.93l-2.06,-0.93l-0.94,-2.07l-0.92,2.07l-2.08,0.93l2.08,0.93z"/>
<path android:fillColor="@android:color/white" android:pathData="M3.5,11l0.5,-2l2,-0.5l-2,-0.5l-0.5,-2l-0.5,2l-2,0.5l2,0.5z"/>
</vector>

View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<font-family xmlns:app="http://schemas.android.com/apk/res-auto"
app:fontProviderAuthority="com.google.android.gms.fonts"
app:fontProviderPackage="com.google.android.gms"
app:fontProviderQuery="name=Source Code Pro&amp;weight=500"
app:fontProviderCerts="@array/com_google_android_gms_fonts_certs">
</font-family>

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android" <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent">
@ -9,8 +9,8 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:layout_centerVertical="true" android:layout_centerVertical="true"
android:layout_centerHorizontal="true" android:adjustViewBounds="true"
android:background="@android:color/black" android:background="@drawable/checkerboard"
app:srcCompat="@android:drawable/screen_background_dark" /> app:srcCompat="@android:drawable/screen_background_dark" />
</androidx.coordinatorlayout.widget.CoordinatorLayout> </RelativeLayout>

View File

@ -133,4 +133,9 @@
<string name="mode_system">Follow the system setting</string> <string name="mode_system">Follow the system setting</string>
<string name="mode_light">Light mode</string> <string name="mode_light">Light mode</string>
<string name="drawer_error_loading_sources">Error loading sources…</string> <string name="drawer_error_loading_sources">Error loading sources…</string>
<string name="pref_switch_enable_analytics">Enable analytics</string>
<string name="gdpr_dialog_title">The app does not share any personal data about you.</string>
<string name="gdpr_dialog_message"><![CDATA[Crash reports sending is now enabled. It can be disabled from the settings page. Keep in mind that crash reports are essential for the app development.]]></string>
<string name="crash_toast_text">A crash occured. Sending the details to the developper.</string>
<string name="pref_switch_disable_acra">"Disable automatic bug reporting. "</string>
</resources> </resources>

View File

@ -133,4 +133,9 @@
<string name="mode_system">Follow the system setting</string> <string name="mode_system">Follow the system setting</string>
<string name="mode_light">Light mode</string> <string name="mode_light">Light mode</string>
<string name="drawer_error_loading_sources">Error loading sources…</string> <string name="drawer_error_loading_sources">Error loading sources…</string>
<string name="pref_switch_enable_analytics">Enable analytics</string>
<string name="gdpr_dialog_title">The app does not share any personal data about you.</string>
<string name="gdpr_dialog_message"><![CDATA[Crash reports sending is now enabled. It can be disabled from the settings page. Keep in mind that crash reports are essential for the app development.]]></string>
<string name="crash_toast_text">A crash occured. Sending the details to the developper.</string>
<string name="pref_switch_disable_acra">"Disable automatic bug reporting. "</string>
</resources> </resources>

View File

@ -133,4 +133,9 @@
<string name="mode_system">Follow the system setting</string> <string name="mode_system">Follow the system setting</string>
<string name="mode_light">Light mode</string> <string name="mode_light">Light mode</string>
<string name="drawer_error_loading_sources">Error loading sources…</string> <string name="drawer_error_loading_sources">Error loading sources…</string>
<string name="pref_switch_enable_analytics">Enable analytics</string>
<string name="gdpr_dialog_title">The app does not share any personal data about you.</string>
<string name="gdpr_dialog_message"><![CDATA[Crash reports sending is now enabled. It can be disabled from the settings page. Keep in mind that crash reports are essential for the app development.]]></string>
<string name="crash_toast_text">A crash occured. Sending the details to the developper.</string>
<string name="pref_switch_disable_acra">"Disable automatic bug reporting. "</string>
</resources> </resources>

View File

@ -133,4 +133,9 @@
<string name="mode_system">Follow the system setting</string> <string name="mode_system">Follow the system setting</string>
<string name="mode_light">Light mode</string> <string name="mode_light">Light mode</string>
<string name="drawer_error_loading_sources">Error loading sources…</string> <string name="drawer_error_loading_sources">Error loading sources…</string>
<string name="pref_switch_enable_analytics">Enable analytics</string>
<string name="gdpr_dialog_title">The app does not share any personal data about you.</string>
<string name="gdpr_dialog_message"><![CDATA[Crash reports sending is now enabled. It can be disabled from the settings page. Keep in mind that crash reports are essential for the app development.]]></string>
<string name="crash_toast_text">A crash occured. Sending the details to the developper.</string>
<string name="pref_switch_disable_acra">"Disable automatic bug reporting. "</string>
</resources> </resources>

View File

@ -133,4 +133,9 @@
<string name="mode_system">Utiliser les paramètres système</string> <string name="mode_system">Utiliser les paramètres système</string>
<string name="mode_light">Thème clair</string> <string name="mode_light">Thème clair</string>
<string name="drawer_error_loading_sources">Error loading sources…</string> <string name="drawer_error_loading_sources">Error loading sources…</string>
<string name="pref_switch_enable_analytics">Enable analytics</string>
<string name="gdpr_dialog_title">The app does not share any personal data about you.</string>
<string name="gdpr_dialog_message"><![CDATA[Crash reports sending is now enabled. It can be disabled from the settings page. Keep in mind that crash reports are essential for the app development.]]></string>
<string name="crash_toast_text">A crash occured. Sending the details to the developper.</string>
<string name="pref_switch_disable_acra">"Disable automatic bug reporting. "</string>
</resources> </resources>

View File

@ -133,4 +133,9 @@
<string name="mode_system">Follow the system setting</string> <string name="mode_system">Follow the system setting</string>
<string name="mode_light">Light mode</string> <string name="mode_light">Light mode</string>
<string name="drawer_error_loading_sources">Error loading sources…</string> <string name="drawer_error_loading_sources">Error loading sources…</string>
<string name="pref_switch_enable_analytics">Enable analytics</string>
<string name="gdpr_dialog_title">The app does not share any personal data about you.</string>
<string name="gdpr_dialog_message"><![CDATA[Crash reports sending is now enabled. It can be disabled from the settings page. Keep in mind that crash reports are essential for the app development.]]></string>
<string name="crash_toast_text">A crash occured. Sending the details to the developper.</string>
<string name="pref_switch_disable_acra">"Disable automatic bug reporting. "</string>
</resources> </resources>

View File

@ -133,4 +133,9 @@
<string name="mode_system">Follow the system setting</string> <string name="mode_system">Follow the system setting</string>
<string name="mode_light">Light mode</string> <string name="mode_light">Light mode</string>
<string name="drawer_error_loading_sources">Error loading sources…</string> <string name="drawer_error_loading_sources">Error loading sources…</string>
<string name="pref_switch_enable_analytics">Enable analytics</string>
<string name="gdpr_dialog_title">The app does not share any personal data about you.</string>
<string name="gdpr_dialog_message"><![CDATA[Crash reports sending is now enabled. It can be disabled from the settings page. Keep in mind that crash reports are essential for the app development.]]></string>
<string name="crash_toast_text">A crash occured. Sending the details to the developper.</string>
<string name="pref_switch_disable_acra">"Disable automatic bug reporting. "</string>
</resources> </resources>

View File

@ -133,4 +133,9 @@
<string name="mode_system">Follow the system setting</string> <string name="mode_system">Follow the system setting</string>
<string name="mode_light">Light mode</string> <string name="mode_light">Light mode</string>
<string name="drawer_error_loading_sources">Error loading sources…</string> <string name="drawer_error_loading_sources">Error loading sources…</string>
<string name="pref_switch_enable_analytics">Enable analytics</string>
<string name="gdpr_dialog_title">The app does not share any personal data about you.</string>
<string name="gdpr_dialog_message"><![CDATA[Crash reports sending is now enabled. It can be disabled from the settings page. Keep in mind that crash reports are essential for the app development.]]></string>
<string name="crash_toast_text">A crash occured. Sending the details to the developper.</string>
<string name="pref_switch_disable_acra">"Disable automatic bug reporting. "</string>
</resources> </resources>

View File

@ -133,4 +133,9 @@
<string name="mode_system">Follow the system setting</string> <string name="mode_system">Follow the system setting</string>
<string name="mode_light">Light mode</string> <string name="mode_light">Light mode</string>
<string name="drawer_error_loading_sources">Error loading sources…</string> <string name="drawer_error_loading_sources">Error loading sources…</string>
<string name="pref_switch_enable_analytics">Enable analytics</string>
<string name="gdpr_dialog_title">The app does not share any personal data about you.</string>
<string name="gdpr_dialog_message"><![CDATA[Crash reports sending is now enabled. It can be disabled from the settings page. Keep in mind that crash reports are essential for the app development.]]></string>
<string name="crash_toast_text">A crash occured. Sending the details to the developper.</string>
<string name="pref_switch_disable_acra">"Disable automatic bug reporting. "</string>
</resources> </resources>

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="pref_switch_enable_analytics">Enable analytics</string>
<string name="gdpr_dialog_title">The app does not share any personal data about you.</string>
<string name="gdpr_dialog_message"><![CDATA[Crash reports sending is now enabled. It can be disabled from the settings page. Keep in mind that crash reports are essential for the app development.]]></string>
<string name="crash_toast_text">A crash occured. Sending the details to the developper.</string>
<string name="pref_switch_disable_acra">"Disable automatic bug reporting. "</string>
</resources>

View File

@ -133,4 +133,9 @@
<string name="mode_system">Follow the system setting</string> <string name="mode_system">Follow the system setting</string>
<string name="mode_light">Light mode</string> <string name="mode_light">Light mode</string>
<string name="drawer_error_loading_sources">Error loading sources…</string> <string name="drawer_error_loading_sources">Error loading sources…</string>
<string name="pref_switch_enable_analytics">Enable analytics</string>
<string name="gdpr_dialog_title">The app does not share any personal data about you.</string>
<string name="gdpr_dialog_message"><![CDATA[Crash reports sending is now enabled. It can be disabled from the settings page. Keep in mind that crash reports are essential for the app development.]]></string>
<string name="crash_toast_text">A crash occured. Sending the details to the developper.</string>
<string name="pref_switch_disable_acra">"Disable automatic bug reporting. "</string>
</resources> </resources>

View File

@ -133,4 +133,9 @@
<string name="mode_system">Follow the system setting</string> <string name="mode_system">Follow the system setting</string>
<string name="mode_light">Light mode</string> <string name="mode_light">Light mode</string>
<string name="drawer_error_loading_sources">Error loading sources…</string> <string name="drawer_error_loading_sources">Error loading sources…</string>
<string name="pref_switch_enable_analytics">Enable analytics</string>
<string name="gdpr_dialog_title">The app does not share any personal data about you.</string>
<string name="gdpr_dialog_message"><![CDATA[Crash reports sending is now enabled. It can be disabled from the settings page. Keep in mind that crash reports are essential for the app development.]]></string>
<string name="crash_toast_text">A crash occured. Sending the details to the developper.</string>
<string name="pref_switch_disable_acra">"Disable automatic bug reporting. "</string>
</resources> </resources>

View File

@ -133,4 +133,9 @@
<string name="mode_system">Follow the system setting</string> <string name="mode_system">Follow the system setting</string>
<string name="mode_light">Light mode</string> <string name="mode_light">Light mode</string>
<string name="drawer_error_loading_sources">Error loading sources…</string> <string name="drawer_error_loading_sources">Error loading sources…</string>
<string name="pref_switch_enable_analytics">Enable analytics</string>
<string name="gdpr_dialog_title">The app does not share any personal data about you.</string>
<string name="gdpr_dialog_message"><![CDATA[Crash reports sending is now enabled. It can be disabled from the settings page. Keep in mind that crash reports are essential for the app development.]]></string>
<string name="crash_toast_text">A crash occured. Sending the details to the developper.</string>
<string name="pref_switch_disable_acra">"Disable automatic bug reporting. "</string>
</resources> </resources>

View File

@ -133,4 +133,9 @@
<string name="mode_system">Follow the system setting</string> <string name="mode_system">Follow the system setting</string>
<string name="mode_light">Light mode</string> <string name="mode_light">Light mode</string>
<string name="drawer_error_loading_sources">Error loading sources…</string> <string name="drawer_error_loading_sources">Error loading sources…</string>
<string name="pref_switch_enable_analytics">Enable analytics</string>
<string name="gdpr_dialog_title">The app does not share any personal data about you.</string>
<string name="gdpr_dialog_message"><![CDATA[Crash reports sending is now enabled. It can be disabled from the settings page. Keep in mind that crash reports are essential for the app development.]]></string>
<string name="crash_toast_text">A crash occured. Sending the details to the developper.</string>
<string name="pref_switch_disable_acra">"Disable automatic bug reporting. "</string>
</resources> </resources>

View File

@ -133,4 +133,9 @@
<string name="mode_system">Follow the system setting</string> <string name="mode_system">Follow the system setting</string>
<string name="mode_light">Light mode</string> <string name="mode_light">Light mode</string>
<string name="drawer_error_loading_sources">Error loading sources…</string> <string name="drawer_error_loading_sources">Error loading sources…</string>
<string name="pref_switch_enable_analytics">Enable analytics</string>
<string name="gdpr_dialog_title">The app does not share any personal data about you.</string>
<string name="gdpr_dialog_message"><![CDATA[Crash reports sending is now enabled. It can be disabled from the settings page. Keep in mind that crash reports are essential for the app development.]]></string>
<string name="crash_toast_text">A crash occured. Sending the details to the developper.</string>
<string name="pref_switch_disable_acra">"Disable automatic bug reporting. "</string>
</resources> </resources>

View File

@ -133,4 +133,9 @@
<string name="mode_system">遵循系统设置</string> <string name="mode_system">遵循系统设置</string>
<string name="mode_light">浅色模式</string> <string name="mode_light">浅色模式</string>
<string name="drawer_error_loading_sources">Error loading sources…</string> <string name="drawer_error_loading_sources">Error loading sources…</string>
<string name="pref_switch_enable_analytics">Enable analytics</string>
<string name="gdpr_dialog_title">The app does not share any personal data about you.</string>
<string name="gdpr_dialog_message"><![CDATA[Crash reports sending is now enabled. It can be disabled from the settings page. Keep in mind that crash reports are essential for the app development.]]></string>
<string name="crash_toast_text">A crash occured. Sending the details to the developper.</string>
<string name="pref_switch_disable_acra">"Disable automatic bug reporting. "</string>
</resources> </resources>

View File

@ -133,4 +133,9 @@
<string name="mode_system">Follow the system setting</string> <string name="mode_system">Follow the system setting</string>
<string name="mode_light">Light mode</string> <string name="mode_light">Light mode</string>
<string name="drawer_error_loading_sources">Error loading sources…</string> <string name="drawer_error_loading_sources">Error loading sources…</string>
<string name="pref_switch_enable_analytics">Enable analytics</string>
<string name="gdpr_dialog_title">The app does not share any personal data about you.</string>
<string name="gdpr_dialog_message"><![CDATA[Crash reports sending is now enabled. It can be disabled from the settings page. Keep in mind that crash reports are essential for the app development.]]></string>
<string name="crash_toast_text">A crash occured. Sending the details to the developper.</string>
<string name="pref_switch_disable_acra">"Disable automatic bug reporting. "</string>
</resources> </resources>

View File

@ -9,7 +9,7 @@
<string-array name="ModeValues"> <string-array name="ModeValues">
<item>1</item> <!--MODE_NIGHT_NO--> <item>1</item> <!--MODE_NIGHT_NO-->
<item>2</item> <!--MODE_NIGHT_YES--> <item>2</item> <!--MODE_NIGHT_YES-->
<item>0</item> <!--MODE_NIGHT_AUTO_TIME--> <item>-1</item> <!--MODE_NIGHT_FOLLOW_SYSTEM-->
</string-array> </string-array>
<string-array name="Voice"> <string-array name="Voice">

View File

@ -3,5 +3,6 @@
<array name="preloaded_fonts" translatable="false"> <array name="preloaded_fonts" translatable="false">
<item>@font/open_sans</item> <item>@font/open_sans</item>
<item>@font/roboto</item> <item>@font/roboto</item>
<item>@font/source_code_pro_medium</item>
</array> </array>
</resources> </resources>

View File

@ -4,5 +4,6 @@
<item></item> <item></item>
<item>@string/open_sans_font_id</item> <item>@string/open_sans_font_id</item>
<item>@string/roboto_font_id</item> <item>@string/roboto_font_id</item>
<item>@string/source_code_pro_font_id</item>
</array> </array>
</resources> </resources>

View File

@ -4,5 +4,6 @@
<item>Systems</item> <item>Systems</item>
<item>Open Sans</item> <item>Open Sans</item>
<item>Roboto</item> <item>Roboto</item>
<item>Source Code Pro</item>
</array> </array>
</resources> </resources>

View File

@ -125,6 +125,7 @@
<string name="reader_text_align_left">Align left</string> <string name="reader_text_align_left">Align left</string>
<string name="reader_text_align_justify">Justify</string> <string name="reader_text_align_justify">Justify</string>
<string name="settings_reader_font">Reader font</string> <string name="settings_reader_font">Reader font</string>
<string name="source_code_pro_font_id" translatable="false">source_code_pro_medium</string>
<string name="open_sans_font_id" translatable="false">open_sans</string> <string name="open_sans_font_id" translatable="false">open_sans</string>
<string name="roboto_font_id" translatable="false">roboto</string> <string name="roboto_font_id" translatable="false">roboto</string>
<string name="reader_static_bar_title">Static bottom bar in the article viewer</string> <string name="reader_static_bar_title">Static bottom bar in the article viewer</string>
@ -135,4 +136,9 @@
<string name="mode_dark">Dark mode</string> <string name="mode_dark">Dark mode</string>
<string name="mode_system">Follow the system setting</string> <string name="mode_system">Follow the system setting</string>
<string name="mode_light">Light mode</string> <string name="mode_light">Light mode</string>
<string name="pref_switch_enable_analytics">Enable analytics</string>
<string name="gdpr_dialog_title">The app does not share any personal data about you.</string>
<string name="gdpr_dialog_message"><![CDATA[Crash reports sending is now enabled. It can be disabled from the settings page. Keep in mind that crash reports are essential for the app development.]]></string>
<string name="crash_toast_text">A crash occured. Sending the details to the developper.</string>
<string name="pref_switch_disable_acra">"Disable automatic bug reporting. "</string>
</resources> </resources>

View File

@ -1,5 +1,6 @@
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android" <PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"> xmlns:app="http://schemas.android.com/apk/res-auto">
<EditTextPreference <EditTextPreference
android:inputType="number" android:inputType="number"
android:key="api_timeout" android:key="api_timeout"

View File

@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android" <PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:title="@string/title_activity_settings"> android:title="@string/title_activity_settings">
<Preference <Preference
@ -17,9 +18,13 @@
android:title="@string/pref_header_offline" android:title="@string/pref_header_offline"
android:icon="@drawable/ic_signal_wifi_off_black_24dp" /> android:icon="@drawable/ic_signal_wifi_off_black_24dp" />
<Preference <ListPreference
android:fragment="bou.amine.apps.readerforselfossv2.android.settings.SettingsActivity$ThemePreferenceFragment" android:defaultValue="0"
android:entries="@array/ModeTitles"
android:entryValues="@array/ModeValues"
android:key="currentMode"
android:title="@string/pref_header_theme" android:title="@string/pref_header_theme"
app:useSimpleSummaryProvider="false"
android:icon="@drawable/ic_color_lens_black_24dp" /> android:icon="@drawable/ic_color_lens_black_24dp" />
<Preference <Preference
@ -32,4 +37,18 @@
android:title="@string/pref_header_experimental" android:title="@string/pref_header_experimental"
android:icon="@drawable/ic_widgets_black_24dp" /> android:icon="@drawable/ic_widgets_black_24dp" />
<SwitchPreference
android:defaultValue="false"
android:key="enable_analytics"
android:title="@string/pref_switch_enable_analytics"
android:icon="@drawable/ic_baseline_insights_24"/>
<SwitchPreference
android:defaultValue="false"
android:key="acra.disable"
android:title="@string/pref_switch_disable_acra"
android:icon="@drawable/ic_baseline_bug_report_24"/>
</PreferenceScreen> </PreferenceScreen>

View File

@ -4,6 +4,8 @@ import bou.amine.apps.readerforselfossv2.dao.ReaderForSelfossDB
import bou.amine.apps.readerforselfossv2.dao.SOURCE import bou.amine.apps.readerforselfossv2.dao.SOURCE
import bou.amine.apps.readerforselfossv2.dao.TAG import bou.amine.apps.readerforselfossv2.dao.TAG
import bou.amine.apps.readerforselfossv2.model.SelfossModel import bou.amine.apps.readerforselfossv2.model.SelfossModel
import bou.amine.apps.readerforselfossv2.model.StatusAndData
import bou.amine.apps.readerforselfossv2.model.SuccessResponse
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.utils.ItemType import bou.amine.apps.readerforselfossv2.utils.ItemType
@ -47,11 +49,11 @@ class RepositoryTest {
every { appSettingsService.isItemCachingEnabled() } returns false every { appSettingsService.isItemCachingEnabled() } returns false
every { appSettingsService.isUpdateSourcesEnabled() } returns false every { appSettingsService.isUpdateSourcesEnabled() } returns false
coEvery { api.version() } returns SelfossModel.StatusAndData( coEvery { api.version() } returns StatusAndData(
success = true, success = true,
data = SelfossModel.ApiVersion("2.19-ba1e8e3", "4.0.0") data = SelfossModel.ApiVersion("2.19-ba1e8e3", "4.0.0")
) )
coEvery { api.stats() } returns SelfossModel.StatusAndData( coEvery { api.stats() } returns StatusAndData(
success = true, success = true,
data = SelfossModel.Stats(NUMBER_ARTICLES, NUMBER_UNREAD, NUMBER_STARRED) data = SelfossModel.Stats(NUMBER_ARTICLES, NUMBER_UNREAD, NUMBER_STARRED)
) )
@ -83,7 +85,7 @@ class RepositoryTest {
fun get_api_4_date_with_api_1_version_stored() { fun get_api_4_date_with_api_1_version_stored() {
every { appSettingsService.getApiVersion() } returns 1 every { appSettingsService.getApiVersion() } returns 1
coEvery { api.getItems(any(), any(), any(), any(), any(), any(), any()) } returns coEvery { api.getItems(any(), any(), any(), any(), any(), any(), any()) } returns
SelfossModel.StatusAndData(success = true, data = generateTestApiItem()) StatusAndData(success = true, data = generateTestApiItem())
every { appSettingsService.updateApiVersion(any()) } returns Unit every { appSettingsService.updateApiVersion(any()) } returns Unit
initializeRepository() initializeRepository()
@ -98,11 +100,11 @@ class RepositoryTest {
@Test @Test
fun get_api_1_date_with_api_4_version_stored() { fun get_api_1_date_with_api_4_version_stored() {
every { appSettingsService.getApiVersion() } returns 4 every { appSettingsService.getApiVersion() } returns 4
coEvery { api.version() } returns SelfossModel.StatusAndData(success = false, null) coEvery { api.version() } returns StatusAndData(success = false, null)
val itemParameters = FakeItemParameters() val itemParameters = FakeItemParameters()
itemParameters.datetime = "2021-04-23 11:45:32" itemParameters.datetime = "2021-04-23 11:45:32"
coEvery { api.getItems(any(), any(), any(), any(), any(), any(), any()) } returns coEvery { api.getItems(any(), any(), any(), any(), any(), any(), any()) } returns
SelfossModel.StatusAndData( StatusAndData(
success = true, success = true,
data = generateTestApiItem(itemParameters) data = generateTestApiItem(itemParameters)
) )
@ -118,7 +120,7 @@ class RepositoryTest {
@Test @Test
fun get_newer_items() { fun get_newer_items() {
coEvery { api.getItems(any(), any(), any(), any(), any(), any(), any()) } returns coEvery { api.getItems(any(), any(), any(), any(), any(), any(), any()) } returns
SelfossModel.StatusAndData(success = true, data = generateTestApiItem()) StatusAndData(success = true, data = generateTestApiItem())
initializeRepository() initializeRepository()
runBlocking { runBlocking {
@ -133,7 +135,7 @@ class RepositoryTest {
@Test @Test
fun get_all_newer_items() { fun get_all_newer_items() {
coEvery { api.getItems(any(), any(), any(), any(), any(), any(), any()) } returns coEvery { api.getItems(any(), any(), any(), any(), any(), any(), any()) } returns
SelfossModel.StatusAndData(success = true, data = generateTestApiItem()) StatusAndData(success = true, data = generateTestApiItem())
initializeRepository() initializeRepository()
repository.displayedItems = ItemType.ALL repository.displayedItems = ItemType.ALL
@ -149,7 +151,7 @@ class RepositoryTest {
@Test @Test
fun get_newer_starred_items() { fun get_newer_starred_items() {
coEvery { api.getItems(any(), any(), any(), any(), any(), any(), any()) } returns coEvery { api.getItems(any(), any(), any(), any(), any(), any(), any()) } returns
SelfossModel.StatusAndData(success = true, data = generateTestApiItem()) StatusAndData(success = true, data = generateTestApiItem())
initializeRepository() initializeRepository()
repository.displayedItems = ItemType.STARRED repository.displayedItems = ItemType.STARRED
@ -242,7 +244,7 @@ class RepositoryTest {
@Test @Test
fun get_older_items() { fun get_older_items() {
coEvery { api.getItems(any(), any(), any(), any(), any(), any(), any()) } returns coEvery { api.getItems(any(), any(), any(), any(), any(), any(), any()) } returns
SelfossModel.StatusAndData(success = true, data = generateTestApiItem()) StatusAndData(success = true, data = generateTestApiItem())
initializeRepository() initializeRepository()
repository.items = ArrayList(generateTestApiItem()) repository.items = ArrayList(generateTestApiItem())
@ -258,7 +260,7 @@ class RepositoryTest {
@Test @Test
fun get_all_older_items() { fun get_all_older_items() {
coEvery { api.getItems(any(), any(), any(), any(), any(), any(), any()) } returns coEvery { api.getItems(any(), any(), any(), any(), any(), any(), any()) } returns
SelfossModel.StatusAndData(success = true, data = generateTestApiItem()) StatusAndData(success = true, data = generateTestApiItem())
initializeRepository() initializeRepository()
repository.items = ArrayList(generateTestApiItem()) repository.items = ArrayList(generateTestApiItem())
@ -275,7 +277,7 @@ class RepositoryTest {
@Test @Test
fun get_older_starred_items() { fun get_older_starred_items() {
coEvery { api.getItems(any(), any(), any(), any(), any(), any(), any()) } returns coEvery { api.getItems(any(), any(), any(), any(), any(), any(), any()) } returns
SelfossModel.StatusAndData(success = true, data = generateTestApiItem()) StatusAndData(success = true, data = generateTestApiItem())
initializeRepository() initializeRepository()
repository.displayedItems = ItemType.STARRED repository.displayedItems = ItemType.STARRED
@ -308,7 +310,7 @@ class RepositoryTest {
@Test @Test
fun reload_badges_without_response() { fun reload_badges_without_response() {
coEvery { api.stats() } returns SelfossModel.StatusAndData(success = false, data = null) coEvery { api.stats() } returns StatusAndData(success = false, data = null)
var success: Boolean var success: Boolean
@ -376,7 +378,7 @@ class RepositoryTest {
TAG("second_DB", "yellow", 0) TAG("second_DB", "yellow", 0)
) )
coEvery { api.tags() } returns SelfossModel.StatusAndData(success = true, data = tags) coEvery { api.tags() } returns StatusAndData(success = true, data = tags)
coEvery { db.tagsQueries.tags().executeAsList() } returns tagsDB coEvery { db.tagsQueries.tags().executeAsList() } returns tagsDB
every { appSettingsService.isUpdateSourcesEnabled() } returns true every { appSettingsService.isUpdateSourcesEnabled() } returns true
every { appSettingsService.isItemCachingEnabled() } returns true every { appSettingsService.isItemCachingEnabled() } returns true
@ -403,7 +405,7 @@ class RepositoryTest {
TAG("second_DB", "yellow", 0) TAG("second_DB", "yellow", 0)
) )
coEvery { api.tags() } returns SelfossModel.StatusAndData(success = true, data = tags) coEvery { api.tags() } returns StatusAndData(success = true, data = tags)
coEvery { db.tagsQueries.tags().executeAsList() } returns tagsDB coEvery { db.tagsQueries.tags().executeAsList() } returns tagsDB
every { appSettingsService.isUpdateSourcesEnabled() } returns false every { appSettingsService.isUpdateSourcesEnabled() } returns false
every { appSettingsService.isItemCachingEnabled() } returns true every { appSettingsService.isItemCachingEnabled() } returns true
@ -433,7 +435,7 @@ class RepositoryTest {
TAG("second_DB", "yellow", 0) TAG("second_DB", "yellow", 0)
) )
coEvery { api.tags() } returns SelfossModel.StatusAndData(success = true, data = tags) coEvery { api.tags() } returns StatusAndData(success = true, data = tags)
coEvery { db.tagsQueries.tags().executeAsList() } returns tagsDB coEvery { db.tagsQueries.tags().executeAsList() } returns tagsDB
every { appSettingsService.isUpdateSourcesEnabled() } returns true every { appSettingsService.isUpdateSourcesEnabled() } returns true
every { appSettingsService.isItemCachingEnabled() } returns false every { appSettingsService.isItemCachingEnabled() } returns false
@ -460,7 +462,7 @@ class RepositoryTest {
TAG("second_DB", "yellow", 0) TAG("second_DB", "yellow", 0)
) )
coEvery { api.tags() } returns SelfossModel.StatusAndData(success = true, data = tags) coEvery { api.tags() } returns StatusAndData(success = true, data = tags)
coEvery { db.tagsQueries.tags().executeAsList() } returns tagsDB coEvery { db.tagsQueries.tags().executeAsList() } returns tagsDB
every { appSettingsService.isUpdateSourcesEnabled() } returns false every { appSettingsService.isUpdateSourcesEnabled() } returns false
every { appSettingsService.isItemCachingEnabled() } returns false every { appSettingsService.isItemCachingEnabled() } returns false
@ -489,7 +491,7 @@ class RepositoryTest {
TAG("second_DB", "yellow", 0) TAG("second_DB", "yellow", 0)
) )
coEvery { api.tags() } returns SelfossModel.StatusAndData(success = true, data = tags) coEvery { api.tags() } returns StatusAndData(success = true, data = tags)
coEvery { db.tagsQueries.tags().executeAsList() } returns tagsDB coEvery { db.tagsQueries.tags().executeAsList() } returns tagsDB
every { appSettingsService.isUpdateSourcesEnabled() } returns true every { appSettingsService.isUpdateSourcesEnabled() } returns true
every { appSettingsService.isItemCachingEnabled() } returns true every { appSettingsService.isItemCachingEnabled() } returns true
@ -517,7 +519,7 @@ class RepositoryTest {
TAG("second_DB", "yellow", 0) TAG("second_DB", "yellow", 0)
) )
coEvery { api.tags() } returns SelfossModel.StatusAndData(success = true, data = tags) coEvery { api.tags() } returns StatusAndData(success = true, data = tags)
coEvery { db.tagsQueries.tags().executeAsList() } returns tagsDB coEvery { db.tagsQueries.tags().executeAsList() } returns tagsDB
every { appSettingsService.isItemCachingEnabled() } returns false every { appSettingsService.isItemCachingEnabled() } returns false
every { appSettingsService.isUpdateSourcesEnabled() } returns true every { appSettingsService.isUpdateSourcesEnabled() } returns true
@ -544,7 +546,7 @@ class RepositoryTest {
TAG("second_DB", "yellow", 0) TAG("second_DB", "yellow", 0)
) )
coEvery { api.tags() } returns SelfossModel.StatusAndData(success = true, data = tags) coEvery { api.tags() } returns StatusAndData(success = true, data = tags)
coEvery { db.tagsQueries.tags().executeAsList() } returns tagsDB coEvery { db.tagsQueries.tags().executeAsList() } returns tagsDB
every { appSettingsService.isUpdateSourcesEnabled() } returns false every { appSettingsService.isUpdateSourcesEnabled() } returns false
every { appSettingsService.isItemCachingEnabled() } returns true every { appSettingsService.isItemCachingEnabled() } returns true
@ -572,7 +574,7 @@ class RepositoryTest {
TAG("second_DB", "yellow", 0) TAG("second_DB", "yellow", 0)
) )
coEvery { api.tags() } returns SelfossModel.StatusAndData(success = true, data = tags) coEvery { api.tags() } returns StatusAndData(success = true, data = tags)
coEvery { db.tagsQueries.tags().executeAsList() } returns tagsDB coEvery { db.tagsQueries.tags().executeAsList() } returns tagsDB
every { appSettingsService.isUpdateSourcesEnabled() } returns false every { appSettingsService.isUpdateSourcesEnabled() } returns false
every { appSettingsService.isItemCachingEnabled() } returns false every { appSettingsService.isItemCachingEnabled() } returns false
@ -627,7 +629,7 @@ class RepositoryTest {
) )
) )
coEvery { api.sources() } returns SelfossModel.StatusAndData(success = true, data = sources) coEvery { api.sources() } returns StatusAndData(success = true, data = sources)
every { db.sourcesQueries.sources().executeAsList() } returns sourcesDB every { db.sourcesQueries.sources().executeAsList() } returns sourcesDB
initializeRepository() initializeRepository()
var testSources: List<SelfossModel.Source>? var testSources: List<SelfossModel.Source>?
@ -681,7 +683,7 @@ class RepositoryTest {
every { appSettingsService.isUpdateSourcesEnabled() } returns false every { appSettingsService.isUpdateSourcesEnabled() } returns false
every { appSettingsService.isItemCachingEnabled() } returns true every { appSettingsService.isItemCachingEnabled() } returns true
coEvery { api.sources() } returns SelfossModel.StatusAndData(success = true, data = sources) coEvery { api.sources() } returns StatusAndData(success = true, data = sources)
every { db.sourcesQueries.sources().executeAsList() } returns sourcesDB every { db.sourcesQueries.sources().executeAsList() } returns sourcesDB
initializeRepository() initializeRepository()
var testSources: List<SelfossModel.Source>? var testSources: List<SelfossModel.Source>?
@ -738,7 +740,7 @@ class RepositoryTest {
every { appSettingsService.isUpdateSourcesEnabled() } returns true every { appSettingsService.isUpdateSourcesEnabled() } returns true
every { appSettingsService.isItemCachingEnabled() } returns false every { appSettingsService.isItemCachingEnabled() } returns false
coEvery { api.sources() } returns SelfossModel.StatusAndData(success = true, data = sources) coEvery { api.sources() } returns StatusAndData(success = true, data = sources)
every { db.sourcesQueries.sources().executeAsList() } returns sourcesDB every { db.sourcesQueries.sources().executeAsList() } returns sourcesDB
initializeRepository() initializeRepository()
var testSources: List<SelfossModel.Source>? var testSources: List<SelfossModel.Source>?
@ -792,7 +794,7 @@ class RepositoryTest {
every { appSettingsService.isUpdateSourcesEnabled() } returns false every { appSettingsService.isUpdateSourcesEnabled() } returns false
every { appSettingsService.isItemCachingEnabled() } returns false every { appSettingsService.isItemCachingEnabled() } returns false
coEvery { api.sources() } returns SelfossModel.StatusAndData(success = true, data = sources) coEvery { api.sources() } returns StatusAndData(success = true, data = sources)
every { db.sourcesQueries.sources().executeAsList() } returns sourcesDB every { db.sourcesQueries.sources().executeAsList() } returns sourcesDB
initializeRepository() initializeRepository()
var testSources: List<SelfossModel.Source>? var testSources: List<SelfossModel.Source>?
@ -844,7 +846,7 @@ class RepositoryTest {
) )
) )
coEvery { api.sources() } returns SelfossModel.StatusAndData(success = true, data = sources) coEvery { api.sources() } returns StatusAndData(success = true, data = sources)
every { db.sourcesQueries.sources().executeAsList() } returns sourcesDB every { db.sourcesQueries.sources().executeAsList() } returns sourcesDB
initializeRepository(MutableStateFlow(false)) initializeRepository(MutableStateFlow(false))
var testSources: List<SelfossModel.Source>? var testSources: List<SelfossModel.Source>?
@ -898,7 +900,7 @@ class RepositoryTest {
every { appSettingsService.isItemCachingEnabled() } returns false every { appSettingsService.isItemCachingEnabled() } returns false
every { appSettingsService.isUpdateSourcesEnabled() } returns true every { appSettingsService.isUpdateSourcesEnabled() } returns true
coEvery { api.sources() } returns SelfossModel.StatusAndData(success = true, data = sources) coEvery { api.sources() } returns StatusAndData(success = true, data = sources)
every { db.sourcesQueries.sources().executeAsList() } returns sourcesDB every { db.sourcesQueries.sources().executeAsList() } returns sourcesDB
initializeRepository(MutableStateFlow(false)) initializeRepository(MutableStateFlow(false))
var testSources: List<SelfossModel.Source>? var testSources: List<SelfossModel.Source>?
@ -952,7 +954,7 @@ class RepositoryTest {
every { appSettingsService.isItemCachingEnabled() } returns true every { appSettingsService.isItemCachingEnabled() } returns true
every { appSettingsService.isUpdateSourcesEnabled() } returns false every { appSettingsService.isUpdateSourcesEnabled() } returns false
coEvery { api.sources() } returns SelfossModel.StatusAndData(success = true, data = sources) coEvery { api.sources() } returns StatusAndData(success = true, data = sources)
every { db.sourcesQueries.sources().executeAsList() } returns sourcesDB every { db.sourcesQueries.sources().executeAsList() } returns sourcesDB
initializeRepository(MutableStateFlow(false)) initializeRepository(MutableStateFlow(false))
var testSources: List<SelfossModel.Source>? var testSources: List<SelfossModel.Source>?
@ -1006,7 +1008,7 @@ class RepositoryTest {
every { appSettingsService.isItemCachingEnabled() } returns false every { appSettingsService.isItemCachingEnabled() } returns false
every { appSettingsService.isUpdateSourcesEnabled() } returns false every { appSettingsService.isUpdateSourcesEnabled() } returns false
coEvery { api.sources() } returns SelfossModel.StatusAndData(success = true, data = sources) coEvery { api.sources() } returns StatusAndData(success = true, data = sources)
every { db.sourcesQueries.sources().executeAsList() } returns sourcesDB every { db.sourcesQueries.sources().executeAsList() } returns sourcesDB
initializeRepository(MutableStateFlow(false)) initializeRepository(MutableStateFlow(false))
var testSources: List<SelfossModel.Source>? var testSources: List<SelfossModel.Source>?
@ -1021,8 +1023,8 @@ class RepositoryTest {
@Test @Test
fun create_source() { fun create_source() {
coEvery { api.createSourceForVersion(any(), any(), any(), any(), any(), any()) } returns coEvery { api.createSourceForVersion(any(), any(), any(), any(), any()) } returns
SelfossModel.SuccessResponse(true) SuccessResponse(true)
initializeRepository() initializeRepository()
var response: Boolean var response: Boolean
@ -1043,7 +1045,6 @@ class RepositoryTest {
any(), any(),
any(), any(),
any(), any(),
any()
) )
} }
assertSame(true, response) assertSame(true, response)
@ -1051,8 +1052,8 @@ class RepositoryTest {
@Test @Test
fun create_source_but_response_fails() { fun create_source_but_response_fails() {
coEvery { api.createSourceForVersion(any(), any(), any(), any(), any(), any()) } returns coEvery { api.createSourceForVersion(any(), any(), any(), any(), any()) } returns
SelfossModel.SuccessResponse(false) SuccessResponse(false)
initializeRepository() initializeRepository()
var response: Boolean var response: Boolean
@ -1073,7 +1074,6 @@ class RepositoryTest {
any(), any(),
any(), any(),
any(), any(),
any()
) )
} }
assertSame(false, response) assertSame(false, response)
@ -1081,8 +1081,8 @@ class RepositoryTest {
@Test @Test
fun create_source_without_connection() { fun create_source_without_connection() {
coEvery { api.createSourceForVersion(any(), any(), any(), any(), any(), any()) } returns coEvery { api.createSourceForVersion(any(), any(), any(), any(), any()) } returns
SelfossModel.SuccessResponse(true) SuccessResponse(true)
initializeRepository(MutableStateFlow(false)) initializeRepository(MutableStateFlow(false))
var response: Boolean var response: Boolean
@ -1102,7 +1102,6 @@ class RepositoryTest {
any(), any(),
any(), any(),
any(), any(),
any(),
any() any()
) )
} }
@ -1111,7 +1110,7 @@ class RepositoryTest {
@Test @Test
fun delete_source() { fun delete_source() {
coEvery { api.deleteSource(any()) } returns SelfossModel.SuccessResponse(true) coEvery { api.deleteSource(any()) } returns SuccessResponse(true)
initializeRepository() initializeRepository()
var response: Boolean var response: Boolean
@ -1125,7 +1124,7 @@ class RepositoryTest {
@Test @Test
fun delete_source_but_response_fails() { fun delete_source_but_response_fails() {
coEvery { api.deleteSource(any()) } returns SelfossModel.SuccessResponse(false) coEvery { api.deleteSource(any()) } returns SuccessResponse(false)
initializeRepository() initializeRepository()
var response: Boolean var response: Boolean
@ -1139,7 +1138,7 @@ class RepositoryTest {
@Test @Test
fun delete_source_without_connection() { fun delete_source_without_connection() {
coEvery { api.deleteSource(any()) } returns SelfossModel.SuccessResponse(false) coEvery { api.deleteSource(any()) } returns SuccessResponse(false)
initializeRepository(MutableStateFlow(false)) initializeRepository(MutableStateFlow(false))
var response: Boolean var response: Boolean
@ -1153,7 +1152,7 @@ class RepositoryTest {
@Test @Test
fun update_remote() { fun update_remote() {
coEvery { api.update() } returns SelfossModel.StatusAndData( coEvery { api.update() } returns StatusAndData(
success = true, success = true,
data = "finished" data = "finished"
) )
@ -1170,7 +1169,7 @@ class RepositoryTest {
@Test @Test
fun update_remote_but_response_fails() { fun update_remote_but_response_fails() {
coEvery { api.update() } returns SelfossModel.StatusAndData( coEvery { api.update() } returns StatusAndData(
success = false, success = false,
data = "unallowed access" data = "unallowed access"
) )
@ -1187,7 +1186,7 @@ class RepositoryTest {
@Test @Test
fun update_remote_with_unallowed_access() { fun update_remote_with_unallowed_access() {
coEvery { api.update() } returns SelfossModel.StatusAndData( coEvery { api.update() } returns StatusAndData(
success = true, success = true,
data = "unallowed access" data = "unallowed access"
) )
@ -1204,7 +1203,7 @@ class RepositoryTest {
@Test @Test
fun update_remote_without_connection() { fun update_remote_without_connection() {
coEvery { api.update() } returns SelfossModel.StatusAndData( coEvery { api.update() } returns StatusAndData(
success = true, success = true,
data = "undocumented..." data = "undocumented..."
) )
@ -1221,7 +1220,7 @@ class RepositoryTest {
@Test @Test
fun login() { fun login() {
coEvery { api.login() } returns SelfossModel.SuccessResponse(success = true) coEvery { api.login() } returns SuccessResponse(success = true)
initializeRepository() initializeRepository()
var response: Boolean var response: Boolean
@ -1235,7 +1234,7 @@ class RepositoryTest {
@Test @Test
fun login_but_response_fails() { fun login_but_response_fails() {
coEvery { api.login() } returns SelfossModel.SuccessResponse(success = false) coEvery { api.login() } returns SuccessResponse(success = false)
initializeRepository() initializeRepository()
var response: Boolean var response: Boolean
@ -1249,7 +1248,7 @@ class RepositoryTest {
@Test @Test
fun login_but_without_connection() { fun login_but_without_connection() {
coEvery { api.login() } returns SelfossModel.SuccessResponse(success = true) coEvery { api.login() } returns SuccessResponse(success = true)
initializeRepository(MutableStateFlow(false)) initializeRepository(MutableStateFlow(false))
var response: Boolean var response: Boolean
@ -1297,9 +1296,9 @@ class RepositoryTest {
any() any()
) )
} returnsMany listOf( } returnsMany listOf(
SelfossModel.StatusAndData(success = true, data = generateTestApiItem(itemParameter1)), StatusAndData(success = true, data = generateTestApiItem(itemParameter1)),
SelfossModel.StatusAndData(success = true, data = generateTestApiItem(itemParameter2)), StatusAndData(success = true, data = generateTestApiItem(itemParameter2)),
SelfossModel.StatusAndData(success = true, data = generateTestApiItem(itemParameter1)), StatusAndData(success = true, data = generateTestApiItem(itemParameter1)),
) )
initializeRepository() initializeRepository()
@ -1323,7 +1322,7 @@ class RepositoryTest {
@Test @Test
fun cache_items_but_response_fails() { fun cache_items_but_response_fails() {
coEvery { api.getItems(any(), any(), any(), any(), any(), any(), any()) } returns coEvery { api.getItems(any(), any(), any(), any(), any(), any(), any()) } returns
SelfossModel.StatusAndData(success = false, data = generateTestApiItem()) StatusAndData(success = false, data = generateTestApiItem())
initializeRepository() initializeRepository()
repository.tagFilter = SelfossModel.Tag("Tag", "read", 0) repository.tagFilter = SelfossModel.Tag("Tag", "read", 0)
@ -1346,7 +1345,7 @@ class RepositoryTest {
@Test @Test
fun cache_items_without_connection() { fun cache_items_without_connection() {
coEvery { api.getItems(any(), any(), any(), any(), any(), any(), any()) } returns coEvery { api.getItems(any(), any(), any(), any(), any(), any(), any()) } returns
SelfossModel.StatusAndData(success = false, data = generateTestApiItem()) StatusAndData(success = false, data = generateTestApiItem())
initializeRepository(MutableStateFlow(false)) initializeRepository(MutableStateFlow(false))
repository.tagFilter = SelfossModel.Tag("Tag", "read", 0) repository.tagFilter = SelfossModel.Tag("Tag", "read", 0)

View File

@ -1,5 +1,6 @@
package bou.amine.apps.readerforselfossv2.DI package bou.amine.apps.readerforselfossv2.DI
import bou.amine.apps.readerforselfossv2.rest.MercuryApi
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 org.kodein.di.DI import org.kodein.di.DI
@ -10,4 +11,5 @@ import org.kodein.di.singleton
val networkModule by DI.Module { val networkModule by DI.Module {
bind<AppSettingsService>() with singleton { AppSettingsService() } bind<AppSettingsService>() with singleton { AppSettingsService() }
bind<SelfossApi>() with singleton { SelfossApi(instance()) } bind<SelfossApi>() with singleton { SelfossApi(instance()) }
bind<MercuryApi>() with singleton { MercuryApi() }
} }

View File

@ -0,0 +1,157 @@
package bou.amine.apps.readerforselfossv2.model
import bou.amine.apps.readerforselfossv2.utils.DateUtils
import bou.amine.apps.readerforselfossv2.utils.getHtmlDecoded
import kotlinx.serialization.KSerializer
import kotlinx.serialization.Serializable
import kotlinx.serialization.descriptors.PrimitiveKind
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
import kotlinx.serialization.json.*
class MercuryModel {
@Serializable
class ParsedContent(
val title: String,
val content: String?,
val lead_image_url: String?,
val url: String
)
@Serializable
data class Tag(
val tag: String,
val color: String,
val unread: Int
)
@Serializable
class Stats(
val total: Int,
val unread: Int,
val starred: Int
)
@Serializable
data class Spout(
val name: String,
val description: String
)
@Serializable
data class ApiVersion(
val version: String?,
val apiversion: String?
) {
fun getApiMajorVersion() : Int {
var versionNumber = 0
if (apiversion != null) {
versionNumber = apiversion.substringBefore(".").toInt()
}
return versionNumber
}
}
@Serializable
data class Source(
val id: Int,
val title: String,
@Serializable(with = TagsListSerializer::class)
val tags: List<String>,
val spout: String,
val error: String,
val icon: String?
)
@Serializable
data class Item(
val id: Int,
val datetime: String,
val title: String,
val content: String,
@Serializable(with = BooleanSerializer::class)
var unread: Boolean,
@Serializable(with = BooleanSerializer::class)
var starred: Boolean,
val thumbnail: String?,
val icon: String?,
val link: String,
val sourcetitle: String,
@Serializable(with = TagsListSerializer::class)
val tags: List<String>
) {
// TODO: maybe find a better way to handle these kind of urls
fun getLinkDecoded(): String {
var stringUrl: String
stringUrl =
if (link.startsWith("http://news.google.com/news/") || link.startsWith("https://news.google.com/news/")) {
if (link.contains("&amp;url=")) {
link.substringAfter("&amp;url=")
} else {
this.link.replace("&amp;", "&")
}
} else {
this.link.replace("&amp;", "&")
}
// handle :443 => https
if (stringUrl.contains(":443")) {
stringUrl = stringUrl.replace(":443", "").replace("http://", "https://")
}
// handle url not starting with http
if (stringUrl.startsWith("//")) {
stringUrl = "http:$stringUrl"
}
return stringUrl
}
fun sourceAndDateText(): String =
this.sourcetitle.getHtmlDecoded() + DateUtils.parseRelativeDate(this.datetime)
fun toggleStar(): Item {
this.starred = !this.starred
return this
}
}
// TODO: this seems to be super slow.
object TagsListSerializer : KSerializer<List<String>> {
override fun deserialize(decoder: Decoder): List<String> {
return when(val json = ((decoder as JsonDecoder).decodeJsonElement())) {
is JsonArray -> json.toList().map { it.toString() }
else -> json.toString().split(",")
}
}
override val descriptor: SerialDescriptor
get() = PrimitiveSerialDescriptor("tags", PrimitiveKind.STRING)
override fun serialize(encoder: Encoder, value: List<String>) {
TODO("Not yet implemented")
}
}
object BooleanSerializer : KSerializer<Boolean> {
override fun deserialize(decoder: Decoder): Boolean {
val json = ((decoder as JsonDecoder).decodeJsonElement()).jsonPrimitive
return if (json.booleanOrNull != null) {
json.boolean
} else {
json.int == 1
}
}
override val descriptor: SerialDescriptor
get() = PrimitiveSerialDescriptor("b", PrimitiveKind.BOOLEAN)
override fun serialize(encoder: Encoder, value: Boolean) {
TODO("Not yet implemented")
}
}
}

View File

@ -0,0 +1,48 @@
package bou.amine.apps.readerforselfossv2.model
import io.ktor.client.call.*
import io.ktor.client.statement.*
import io.ktor.http.*
import kotlinx.serialization.Serializable
@Serializable
class SuccessResponse(val success: Boolean) {
val isSuccess: Boolean
get() = success
}
class StatusAndData<T>(val success: Boolean, val data: T? = null) {
companion object {
fun <T> succes(d: T): StatusAndData<T> {
return StatusAndData(true, d)
}
fun <T> error(): StatusAndData<T> {
return StatusAndData(false)
}
}
}
suspend fun responseOrSuccessIf404(r: HttpResponse): SuccessResponse {
return if (r.status === HttpStatusCode.NotFound) {
SuccessResponse(true)
} else {
maybeResponse(r)
}
}
suspend fun maybeResponse(r: HttpResponse): SuccessResponse {
return if (r.status.isSuccess()) {
r.body()
} else {
SuccessResponse(false)
}
}
suspend inline fun <reified T> bodyOrFailure(r: HttpResponse): StatusAndData<T> {
return if (r.status.isSuccess()) {
StatusAndData.succes(r.body())
} else {
StatusAndData.error()
}
}

View File

@ -20,12 +20,6 @@ class SelfossModel {
val unread: Int val unread: Int
) )
@Serializable
class SuccessResponse(val success: Boolean) {
val isSuccess: Boolean
get() = success
}
@Serializable @Serializable
class Stats( class Stats(
val total: Int, val total: Int,
@ -152,16 +146,4 @@ class SelfossModel {
TODO("Not yet implemented") TODO("Not yet implemented")
} }
} }
class StatusAndData<T>(val success: Boolean, val data: T? = null) {
companion object {
fun <T> succes(d: T): StatusAndData<T> {
return StatusAndData(true, d)
}
fun <T> error(): StatusAndData<T> {
return StatusAndData(false)
}
}
}
} }

View File

@ -3,6 +3,7 @@ package bou.amine.apps.readerforselfossv2.repository
import bou.amine.apps.readerforselfossv2.dao.* import bou.amine.apps.readerforselfossv2.dao.*
import bou.amine.apps.readerforselfossv2.model.NetworkUnavailableException import bou.amine.apps.readerforselfossv2.model.NetworkUnavailableException
import bou.amine.apps.readerforselfossv2.model.SelfossModel 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.rest.SelfossApi
import bou.amine.apps.readerforselfossv2.service.AppSettingsService import bou.amine.apps.readerforselfossv2.service.AppSettingsService
import bou.amine.apps.readerforselfossv2.utils.* import bou.amine.apps.readerforselfossv2.utils.*
@ -40,7 +41,7 @@ class Repository(private val api: SelfossApi, private val appSettingsService: Ap
suspend fun getNewerItems(): ArrayList<SelfossModel.Item> { suspend fun getNewerItems(): ArrayList<SelfossModel.Item> {
// TODO: Use the updatedSince parameter // TODO: Use the updatedSince parameter
var fetchedItems: SelfossModel.StatusAndData<List<SelfossModel.Item>> = SelfossModel.StatusAndData.error() var fetchedItems: StatusAndData<List<SelfossModel.Item>> = StatusAndData.error()
var fromDB = false var fromDB = false
if (isNetworkAvailable()) { if (isNetworkAvailable()) {
fetchedItems = api.getItems( fetchedItems = api.getItems(
@ -65,7 +66,7 @@ class Repository(private val api: SelfossApi, private val appSettingsService: Ap
if (sourceFilter != null) { if (sourceFilter != null) {
dbItems = dbItems.filter { it.sourcetitle == sourceFilter!!.title } dbItems = dbItems.filter { it.sourcetitle == sourceFilter!!.title }
} }
fetchedItems = SelfossModel.StatusAndData.succes( fetchedItems = StatusAndData.succes(
dbItems.map { it.toView() } dbItems.map { it.toView() }
) )
} }
@ -81,7 +82,7 @@ class Repository(private val api: SelfossApi, private val appSettingsService: Ap
} }
suspend fun getOlderItems(): ArrayList<SelfossModel.Item> { suspend fun getOlderItems(): ArrayList<SelfossModel.Item> {
var fetchedItems: SelfossModel.StatusAndData<List<SelfossModel.Item>> = SelfossModel.StatusAndData.error() var fetchedItems: StatusAndData<List<SelfossModel.Item>> = StatusAndData.error()
if (isNetworkAvailable()) { if (isNetworkAvailable()) {
val offset = items.size val offset = items.size
fetchedItems = api.getItems( fetchedItems = api.getItems(
@ -339,8 +340,7 @@ class Repository(private val api: SelfossApi, private val appSettingsService: Ap
url, url,
spout, spout,
tags, tags,
filter, filter
appSettingsService.getApiVersion()
).isSuccess == true ).isSuccess == true
} }
@ -372,12 +372,29 @@ class Repository(private val api: SelfossApi, private val appSettingsService: Ap
val response = api.login() val response = api.login()
result = response.isSuccess == true result = response.isSuccess == true
} catch (cause: Throwable) { } catch (cause: Throwable) {
Napier.e(cause.stackTraceToString(), tag = "RepositoryImpl.updateRemote") Napier.e(cause.stackTraceToString(), tag = "RepositoryImpl.login")
} }
} }
return result return result
} }
suspend fun logout() {
if (isNetworkAvailable()) {
try {
val response = api.logout()
if (response.isSuccess) {
Napier.e("Couldn't logout.", tag = "RepositoryImpl.logout")
}
} catch (cause: Throwable) {
Napier.e(cause.stackTraceToString(), tag = "RepositoryImpl.logout")
} finally {
appSettingsService.clearAll()
}
} else {
appSettingsService.clearAll()
}
}
fun refreshLoginInformation(url: String, login: String, password: String) { fun refreshLoginInformation(url: String, login: String, password: String) {
appSettingsService.refreshLoginInformation(url, login, password) appSettingsService.refreshLoginInformation(url, login, password)
baseUrl = url baseUrl = url

View File

@ -0,0 +1,43 @@
package bou.amine.apps.readerforselfossv2.rest
import bou.amine.apps.readerforselfossv2.model.*
import io.github.aakira.napier.Napier
import io.ktor.client.*
import io.ktor.client.plugins.cache.*
import io.ktor.client.plugins.contentnegotiation.*
import io.ktor.client.plugins.logging.*
import io.ktor.client.request.*
import io.ktor.serialization.kotlinx.json.*
import kotlinx.serialization.json.Json
class MercuryApi() {
var client = createHttpClient()
private fun createHttpClient(): HttpClient {
return HttpClient {
install(ContentNegotiation) {
install(HttpCache)
json(Json {
prettyPrint = true
isLenient = true
ignoreUnknownKeys = true
})
}
install(Logging) {
logger = object : Logger {
override fun log(message: String) {
Napier.d(message, tag = "LogMercuryCalls")
}
}
level = LogLevel.INFO
}
expectSuccess = false
}
}
suspend fun query(url: String): StatusAndData<MercuryModel.ParsedContent> =
bodyOrFailure(client.get("https://amine-louveau.fr/parser.php") {
parameter("link", url)
})
}

View File

@ -1,12 +1,13 @@
package bou.amine.apps.readerforselfossv2.rest package bou.amine.apps.readerforselfossv2.rest
import bou.amine.apps.readerforselfossv2.model.SelfossModel import bou.amine.apps.readerforselfossv2.model.*
import bou.amine.apps.readerforselfossv2.service.AppSettingsService import bou.amine.apps.readerforselfossv2.service.AppSettingsService
import io.github.aakira.napier.Napier
import io.ktor.client.* import io.ktor.client.*
import io.ktor.client.call.*
import io.ktor.client.plugins.* import io.ktor.client.plugins.*
import io.ktor.client.plugins.cache.* import io.ktor.client.plugins.cache.*
import io.ktor.client.plugins.contentnegotiation.* import io.ktor.client.plugins.contentnegotiation.*
import io.ktor.client.plugins.cookies.*
import io.ktor.client.plugins.logging.* import io.ktor.client.plugins.logging.*
import io.ktor.client.request.* import io.ktor.client.request.*
import io.ktor.client.request.forms.* import io.ktor.client.request.forms.*
@ -20,7 +21,7 @@ class SelfossApi(private val appSettingsService: AppSettingsService) {
var client = createHttpClient() var client = createHttpClient()
private fun createHttpClient(): HttpClient { private fun createHttpClient(): HttpClient {
return HttpClient { val client = HttpClient {
install(ContentNegotiation) { install(ContentNegotiation) {
install(HttpCache) install(HttpCache)
json(Json { json(Json {
@ -32,7 +33,7 @@ class SelfossApi(private val appSettingsService: AppSettingsService) {
install(Logging) { install(Logging) {
logger = object : Logger { logger = object : Logger {
override fun log(message: String) { override fun log(message: String) {
appSettingsService.logApiCalls(message) Napier.d(message, tag = "LogApiCalls")
} }
} }
level = LogLevel.INFO level = LogLevel.INFO
@ -40,22 +41,26 @@ class SelfossApi(private val appSettingsService: AppSettingsService) {
install(HttpTimeout) { install(HttpTimeout) {
requestTimeoutMillis = appSettingsService.getApiTimeout() requestTimeoutMillis = appSettingsService.getApiTimeout()
} }
/* TODO: Auth as basic install(HttpCookies)
if (apiDetailsService.getUserName().isNotEmpty() && apiDetailsService.getPassword().isNotEmpty()) {
install(Auth) {
basic {
credentials {
BasicAuthCredentials(username = apiDetailsService.getUserName(), password = apiDetailsService.getPassword())
}
sendWithoutRequest {
true
}
}
}
}*/
expectSuccess = false expectSuccess = false
} }
client.plugin(HttpSend).intercept { request ->
val originalCall = execute(request)
if (originalCall.response.status == HttpStatusCode.Forbidden && shouldHavePostLogin() && hasLoginInfo()) {
Napier.i("Forbidden action, will try to login and retry", tag = "HttpSend")
if (login().isSuccess) {
Napier.i("Logged in worked", tag = "HttpSend")
execute(request)
}
originalCall
} else {
originalCall
}
}
return client
} }
fun url(path: String) = fun url(path: String) =
@ -66,12 +71,39 @@ class SelfossApi(private val appSettingsService: AppSettingsService) {
client = createHttpClient() client = createHttpClient()
} }
suspend fun login(): SelfossModel.SuccessResponse = // Api version was introduces after the POST login, so when there is a version, it should be available
maybeResponse(client.get(url("/login")) { private fun shouldHavePostLogin() = appSettingsService.getApiVersion() != -1
private fun hasLoginInfo() = appSettingsService.getUserName() != null && appSettingsService.getPassword() != null
suspend fun login(): SuccessResponse =
if (shouldHavePostLogin()) {
postLogin()
} else {
getLogin()
}
private suspend fun getLogin() = maybeResponse(client.get(url("/login")) {
parameter("username", appSettingsService.getUserName()) parameter("username", appSettingsService.getUserName())
parameter("password", appSettingsService.getPassword()) parameter("password", appSettingsService.getPassword())
}) })
private suspend fun postLogin() = maybeResponse(client.post(url("/login")) {
parameter("username", appSettingsService.getUserName())
parameter("password", appSettingsService.getPassword())
})
private fun shouldHaveNewLogout() = appSettingsService.getApiVersion() >= 5 // We are missing 4.1.0
suspend fun logout(): SuccessResponse =
if (shouldHaveNewLogout()) {
doLogout()
} else {
maybeLogoutIfAvailable()
}
private suspend fun maybeLogoutIfAvailable() = responseOrSuccessIf404(client.get(url("/logout")))
private suspend fun doLogout() = maybeResponse(client.delete(url("/api/session/current")))
suspend fun getItems( suspend fun getItems(
type: String, type: String,
offset: Int, offset: Int,
@ -80,10 +112,12 @@ class SelfossApi(private val appSettingsService: AppSettingsService) {
search: String?, search: String?,
updatedSince: String?, updatedSince: String?,
items: Int? = null items: Int? = null
): SelfossModel.StatusAndData<List<SelfossModel.Item>> = ): StatusAndData<List<SelfossModel.Item>> =
bodyOrFailure(client.get(url("/items")) { bodyOrFailure(client.get(url("/items")) {
if (!shouldHavePostLogin()) {
parameter("username", appSettingsService.getUserName()) parameter("username", appSettingsService.getUserName())
parameter("password", appSettingsService.getPassword()) parameter("password", appSettingsService.getPassword())
}
parameter("type", type) parameter("type", type)
parameter("tag", tag) parameter("tag", tag)
parameter("source", source) parameter("source", source)
@ -93,69 +127,89 @@ class SelfossApi(private val appSettingsService: AppSettingsService) {
parameter("offset", offset) parameter("offset", offset)
}) })
suspend fun stats(): SelfossModel.StatusAndData<SelfossModel.Stats> = suspend fun stats(): StatusAndData<SelfossModel.Stats> =
bodyOrFailure(client.get(url("/stats")) { bodyOrFailure(client.get(url("/stats")) {
if (!shouldHavePostLogin()) {
parameter("username", appSettingsService.getUserName()) parameter("username", appSettingsService.getUserName())
parameter("password", appSettingsService.getPassword()) parameter("password", appSettingsService.getPassword())
}
}) })
suspend fun tags(): SelfossModel.StatusAndData<List<SelfossModel.Tag>> = suspend fun tags(): StatusAndData<List<SelfossModel.Tag>> =
bodyOrFailure(client.get(url("/tags")) { bodyOrFailure(client.get(url("/tags")) {
if (!shouldHavePostLogin()) {
parameter("username", appSettingsService.getUserName()) parameter("username", appSettingsService.getUserName())
parameter("password", appSettingsService.getPassword()) parameter("password", appSettingsService.getPassword())
}
}) })
suspend fun update(): SelfossModel.StatusAndData<String> = suspend fun update(): StatusAndData<String> =
bodyOrFailure(client.get(url("/update")) { bodyOrFailure(client.get(url("/update")) {
if (!shouldHavePostLogin()) {
parameter("username", appSettingsService.getUserName()) parameter("username", appSettingsService.getUserName())
parameter("password", appSettingsService.getPassword()) parameter("password", appSettingsService.getPassword())
}
}) })
suspend fun spouts(): SelfossModel.StatusAndData<Map<String, SelfossModel.Spout>> = suspend fun spouts(): StatusAndData<Map<String, SelfossModel.Spout>> =
bodyOrFailure(client.get(url("/sources/spouts")) { bodyOrFailure(client.get(url("/sources/spouts")) {
if (!shouldHavePostLogin()) {
parameter("username", appSettingsService.getUserName()) parameter("username", appSettingsService.getUserName())
parameter("password", appSettingsService.getPassword()) parameter("password", appSettingsService.getPassword())
}
}) })
suspend fun sources(): SelfossModel.StatusAndData<ArrayList<SelfossModel.Source>> = suspend fun sources(): StatusAndData<ArrayList<SelfossModel.Source>> =
bodyOrFailure(client.get(url("/sources/list")) { bodyOrFailure(client.get(url("/sources/list")) {
if (!shouldHavePostLogin()) {
parameter("username", appSettingsService.getUserName()) parameter("username", appSettingsService.getUserName())
parameter("password", appSettingsService.getPassword()) parameter("password", appSettingsService.getPassword())
}
}) })
suspend fun version(): SelfossModel.StatusAndData<SelfossModel.ApiVersion> = suspend fun version(): StatusAndData<SelfossModel.ApiVersion> =
bodyOrFailure(client.get(url("/api/about"))) bodyOrFailure(client.get(url("/api/about")))
suspend fun markAsRead(id: String): SelfossModel.SuccessResponse = suspend fun markAsRead(id: String): SuccessResponse =
maybeResponse(client.post(url("/mark/$id")) { maybeResponse(client.post(url("/mark/$id")) {
if (!shouldHavePostLogin()) {
parameter("username", appSettingsService.getUserName()) parameter("username", appSettingsService.getUserName())
parameter("password", appSettingsService.getPassword()) parameter("password", appSettingsService.getPassword())
}
}) })
suspend fun unmarkAsRead(id: String): SelfossModel.SuccessResponse = suspend fun unmarkAsRead(id: String): SuccessResponse =
maybeResponse(client.post(url("/unmark/$id")) { maybeResponse(client.post(url("/unmark/$id")) {
if (!shouldHavePostLogin()) {
parameter("username", appSettingsService.getUserName()) parameter("username", appSettingsService.getUserName())
parameter("password", appSettingsService.getPassword()) parameter("password", appSettingsService.getPassword())
}
}) })
suspend fun starr(id: String): SelfossModel.SuccessResponse = suspend fun starr(id: String): SuccessResponse =
maybeResponse(client.post(url("/starr/$id")) { maybeResponse(client.post(url("/starr/$id")) {
if (!shouldHavePostLogin()) {
parameter("username", appSettingsService.getUserName()) parameter("username", appSettingsService.getUserName())
parameter("password", appSettingsService.getPassword()) parameter("password", appSettingsService.getPassword())
}
}) })
suspend fun unstarr(id: String): SelfossModel.SuccessResponse = suspend fun unstarr(id: String): SuccessResponse =
maybeResponse(client.post(url("/unstarr/$id")) { maybeResponse(client.post(url("/unstarr/$id")) {
if (!shouldHavePostLogin()) {
parameter("username", appSettingsService.getUserName()) parameter("username", appSettingsService.getUserName())
parameter("password", appSettingsService.getPassword()) parameter("password", appSettingsService.getPassword())
}
}) })
suspend fun markAllAsRead(ids: List<String>): SelfossModel.SuccessResponse = suspend fun markAllAsRead(ids: List<String>): SuccessResponse =
maybeResponse(client.submitForm( maybeResponse(client.submitForm(
url = url("/mark"), url = url("/mark"),
formParameters = Parameters.build { formParameters = Parameters.build {
if (!shouldHavePostLogin()) {
append("username", appSettingsService.getUserName()) append("username", appSettingsService.getUserName())
append("password", appSettingsService.getPassword()) append("password", appSettingsService.getPassword())
}
ids.map { append("ids[]", it) } ids.map { append("ids[]", it) }
} }
)) ))
@ -165,18 +219,17 @@ class SelfossApi(private val appSettingsService: AppSettingsService) {
url: String, url: String,
spout: String, spout: String,
tags: String, tags: String,
filter: String, filter: String
version: Int ): SuccessResponse =
): SelfossModel.SuccessResponse =
maybeResponse( maybeResponse(
if (version > 1) { if (appSettingsService.getApiVersion() > 1) {
createSource2(title, url, spout, tags, filter) createSource2(title, url, spout, tags, filter)
} else { } else {
createSource(title, url, spout, tags, filter) createSource(title, url, spout, tags, filter)
} }
) )
suspend fun createSource( private suspend fun createSource(
title: String, title: String,
url: String, url: String,
spout: String, spout: String,
@ -184,8 +237,13 @@ class SelfossApi(private val appSettingsService: AppSettingsService) {
filter: String filter: String
): HttpResponse = ): HttpResponse =
client.submitForm( client.submitForm(
url = url("/source?username=${appSettingsService.getUserName()}&password=${appSettingsService.getPassword()}"), url = url("/source"),
formParameters = Parameters.build { formParameters = Parameters.build {
// TODO: test this
if (!shouldHavePostLogin()) {
append("username", appSettingsService.getUserName())
append("password", appSettingsService.getPassword())
}
append("title", title) append("title", title)
append("url", url) append("url", url)
append("spout", spout) append("spout", spout)
@ -194,7 +252,7 @@ class SelfossApi(private val appSettingsService: AppSettingsService) {
} }
) )
suspend fun createSource2( private suspend fun createSource2(
title: String, title: String,
url: String, url: String,
spout: String, spout: String,
@ -202,8 +260,12 @@ class SelfossApi(private val appSettingsService: AppSettingsService) {
filter: String filter: String
): HttpResponse = ): HttpResponse =
client.submitForm( client.submitForm(
url = url("/source?username=${appSettingsService.getUserName()}&password=${appSettingsService.getPassword()}"), url = url("/source"),
formParameters = Parameters.build { formParameters = Parameters.build {
if (!shouldHavePostLogin()) {
append("username", appSettingsService.getUserName())
append("password", appSettingsService.getPassword())
}
append("title", title) append("title", title)
append("url", url) append("url", url)
append("spout", spout) append("spout", spout)
@ -212,25 +274,11 @@ class SelfossApi(private val appSettingsService: AppSettingsService) {
} }
) )
suspend fun deleteSource(id: Int): SelfossModel.SuccessResponse = suspend fun deleteSource(id: Int): SuccessResponse =
maybeResponse(client.delete(url("/source/$id")) { maybeResponse(client.delete(url("/source/$id")) {
if (!shouldHavePostLogin()) {
parameter("username", appSettingsService.getUserName()) parameter("username", appSettingsService.getUserName())
parameter("password", appSettingsService.getPassword()) parameter("password", appSettingsService.getPassword())
}
}) })
suspend fun maybeResponse(r: HttpResponse): SelfossModel.SuccessResponse {
return if (r.status.isSuccess()) {
r.body()
} else {
SelfossModel.SuccessResponse(false)
}
}
suspend inline fun <reified T> bodyOrFailure(r: HttpResponse): SelfossModel.StatusAndData<T> {
return if (r.status.isSuccess()) {
SelfossModel.StatusAndData.succes(r.body())
} else {
SelfossModel.StatusAndData.error()
}
}
} }

View File

@ -35,6 +35,8 @@ class AppSettingsService {
private var _fontSize: Int? = null private var _fontSize: Int? = null
private var _staticBar: Boolean? = null private var _staticBar: Boolean? = null
private var _font: String = "" private var _font: String = ""
private var _theme: Int? = null
private var _enableAnalytics: Boolean? = null
init { init {
@ -42,10 +44,6 @@ class AppSettingsService {
refreshUserSettings() refreshUserSettings()
} }
fun logApiCalls(message: String) {
Napier.d(message, tag = "LogApiCalls")
}
fun getApiVersion(): Int { fun getApiVersion(): Int {
if (_apiVersion == -1) { if (_apiVersion == -1) {
refreshApiVersion() refreshApiVersion()
@ -307,6 +305,17 @@ class AppSettingsService {
return _staticBar == true return _staticBar == true
} }
private fun refreshAnalyticsEnabled() {
_enableAnalytics = settings.getBoolean(ENABLE_ANALYTICS, true)
}
fun isAnalyticsEnabled(): Boolean {
if (_enableAnalytics != null) {
refreshAnalyticsEnabled()
}
return _enableAnalytics == true
}
private fun refreshFont() { private fun refreshFont() {
_font = settings.getString(READER_FONT, "") _font = settings.getString(READER_FONT, "")
} }
@ -318,6 +327,17 @@ class AppSettingsService {
return _font return _font
} }
private fun refreshCurrentTheme() {
_theme = settings.getString(CURRENT_THEME, "-1").toInt()
}
fun getCurrentTheme(): Int {
if (_theme == null) {
refreshCurrentTheme()
}
return _theme ?: -1
}
fun refreshApiSettings() { fun refreshApiSettings() {
refreshPassword() refreshPassword()
refreshUsername() refreshUsername()
@ -346,6 +366,8 @@ class AppSettingsService {
refreshFontSize() refreshFontSize()
refreshFont() refreshFont()
refreshStaticBarEnabled() refreshStaticBarEnabled()
refreshCurrentTheme()
refreshAnalyticsEnabled()
} }
fun refreshLoginInformation( fun refreshLoginInformation(
@ -444,5 +466,9 @@ class AppSettingsService {
const val INFINITE_LOADING = "infinite_loading" const val INFINITE_LOADING = "infinite_loading"
const val ITEMS_CACHING = "items_caching" const val ITEMS_CACHING = "items_caching"
const val CURRENT_THEME = "currentMode"
const val ENABLE_ANALYTICS = "enable_analytics"
} }
} }