Compare commits

..

23 Commits

Author SHA1 Message Date
e68da7764f Settings for acra and analytics. Closes #98.
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2022-12-01 21:30:20 +01:00
c3ff894027 Initial integration. 2022-11-30 20:53:11 +01:00
7b68264dd7 Cleaning.
All checks were successful
continuous-integration/drone/push Build is passing
2022-11-21 20:20:27 +01:00
cfcf030bf8 Removing gradle props.
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone Build is passing
2022-11-14 13:38:10 +01:00
0e7d7a5835 Conditionnal siteId
Some checks failed
continuous-integration/drone/push Build is failing
2022-11-14 13:18:20 +01:00
0856ebb889 Removing matomo url from build config.
Some checks are pending
continuous-integration/drone/push Build is running
2022-11-14 13:10:17 +01:00
25bf68cf0c Merge pull request 'Initial Matomo integration.' (#101) from login into master
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone Build is passing
Reviewed-on: https://gitea.amine-louveau.fr/Louvorg/ReaderForSelfoss-multiplatform/pulls/101
2022-11-13 13:18:49 +00:00
afc6f392c6 Initial Matomo integration.
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2022-11-13 13:13:03 +01:00
a0b5e2052b Merge pull request 'Fixed theme reload issues.' (#100) from fix-theme-reload into master
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone Build is passing
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.
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
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
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: https://gitea.amine-louveau.fr/Louvorg/ReaderForSelfoss-multiplatform/pulls/92
2022-11-11 19:29:40 +00:00
dbe97f564e Revert imageview changes
All checks were successful
continuous-integration/drone/pr Build is passing
2022-11-11 09:40:36 +01:00
3a3bf03114 Bigger checktile.
All checks were successful
continuous-integration/drone/push Build is passing
2022-11-10 21:41:55 +01:00
c09a32e9ad Add checkerboard background to the images in the image view
All checks were successful
continuous-integration/drone/pr Build is passing
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
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone Build is passing
Reviewed-on: https://gitea.amine-louveau.fr/Louvorg/ReaderForSelfoss-multiplatform/pulls/96
2022-11-08 21:17:23 +00:00
9e8a25ed3e Fixing tests.
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2022-11-08 22:02:20 +01:00
8ea46e146b Cleaning.
Some checks failed
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2022-11-08 21:54:12 +01:00
5ecf3c3f87 Mercury api in common code. 2022-11-08 21:31:40 +01:00
325f103417 Timeout.
Some checks failed
continuous-integration/drone/push Build is failing
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
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: https://gitea.amine-louveau.fr/Louvorg/ReaderForSelfoss-multiplatform/pulls/95
2022-11-08 07:38:54 +00:00
87ea44754e Font update.
All checks were successful
continuous-integration/drone/push Build is passing
2022-11-07 22:36:20 +01:00
04dec50808 Theme should automatically change on phone settings change.
Some checks failed
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is passing
2022-11-07 22:07:35 +01:00
55 changed files with 741 additions and 375 deletions

View File

@ -8,7 +8,7 @@ steps:
commands:
- echo "---------------------------------------------------------"
- 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 "Analysing..."
- ./gradlew sonarqube -Dsonar.projectKey=RFS2 -Dsonar.host.url=$SONAR_HOST_URL -Dsonar.login=$SONAR_LOGIN
@ -90,7 +90,7 @@ steps:
commands:
- echo "---------------------------------------------------------"
- 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 "Generate APK"
- ./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.
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
val ignoreGitVersion: String by project
val acraVersion = "5.9.7"
plugins {
id("com.android.application")
@ -96,9 +97,6 @@ android {
proguardFiles(getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro")
}
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")
@ -142,12 +140,6 @@ dependencies {
implementation("com.mikepenz:aboutlibraries-core: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
implementation("com.ashokvarma.android:bottom-navigation-bar:2.2.0")
implementation("com.amulyakhare:com.amulyakhare.textdrawable:1.0.1")
@ -195,6 +187,12 @@ dependencies {
testImplementation("io.mockk:mockk:1.12.0")
testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.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> {

View File

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

View File

@ -15,7 +15,8 @@
android:supportsRtl="true"
android:networkSecurityConfig="@xml/network_security_config"
android:theme="@style/NoBar"
android:dataExtractionRules="@xml/data_extraction_rules">
android:dataExtractionRules="@xml/data_extraction_rules"
android:configChanges="uiMode">
<activity
android:name=".MainActivity"
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.repository.Repository
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.BottomNavigationItem
import com.ashokvarma.bottomnavigation.TextBadgeItem
@ -56,9 +59,15 @@ import com.mikepenz.materialdrawer.util.updateBadge
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
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.android.closestDI
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
@ -95,12 +104,16 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
override val di by closestDI()
private val repository : Repository by instance()
private val appSettingsService : AppSettingsService by instance()
private val tracker : Tracker by instance()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityHomeBinding.inflate(layoutInflater)
val view = binding.root
TrackHelper.track().screen("/home").with(tracker)
fromTabShortcut = intent.getIntExtra("shortcutTab", -1) != -1
repository.offlineOverride = intent.getBooleanExtra("startOffline", false)
@ -297,6 +310,8 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
handleBottomBarActions()
handleGDPRDialog(appSettingsService.settings.getBoolean("GDPR_shown", false))
handleRecurringTask()
CoroutineScope(Dispatchers.Main).launch {
@ -306,6 +321,26 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
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() {
DrawerImageLoader.init(object : AbstractDrawerImageLoader() {
override fun set(imageView: ImageView, uri: Uri, placeholder: Drawable, tag: String?) {
@ -478,6 +513,7 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
val gdColor = try {
Color.parseColor(it.color)
} catch (e: IllegalArgumentException) {
e.sendSilentlyWithAcra()
resources.getColor(R.color.colorPrimary)
}
gd.setColor(gdColor)

View File

@ -2,6 +2,7 @@ package bou.amine.apps.readerforselfossv2.android
import android.animation.Animator
import android.animation.AnimatorListenerAdapter
import android.annotation.SuppressLint
import android.content.Intent
import android.os.Bundle
import android.text.TextUtils
@ -24,6 +25,12 @@ import kotlinx.coroutines.launch
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.DimensionQueue
import org.matomo.sdk.extra.DownloadTracker
import org.matomo.sdk.extra.TrackHelper
import java.security.MessageDigest
class LoginActivity : AppCompatActivity(), DIAware {
@ -35,10 +42,17 @@ class LoginActivity : AppCompatActivity(), DIAware {
override val di by closestDI()
private val repository : Repository by instance()
private val appSettingsService : AppSettingsService by instance()
private val tracker : Tracker by instance()
override fun onCreate(savedInstanceState: Bundle?) {
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)
val view = binding.root
@ -56,6 +70,11 @@ class LoginActivity : AppCompatActivity(), DIAware {
handleActions()
}
@SuppressLint("WrongConstant") // Constant is fetched from the settings
private fun handleTheme() {
AppCompatDelegate.setDefaultNightMode(appSettingsService.getCurrentTheme())
}
private fun handleActions() {
binding.passwordView.setOnEditorActionListener(
@ -95,6 +114,15 @@ class LoginActivity : AppCompatActivity(), DIAware {
private fun goToMain() {
CoroutineScope(Dispatchers.Main).launch {
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)
startActivity(intent)

View File

@ -12,9 +12,7 @@ import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.ProcessLifecycleOwner
import androidx.multidex.MultiDexApplication
import androidx.preference.PreferenceManager
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.dao.DriverFactory
import bou.amine.apps.readerforselfossv2.dao.ReaderForSelfossDB
@ -31,7 +29,17 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.launch
import org.acra.ACRA
import org.acra.ReportField
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.matomo.sdk.Matomo
import org.matomo.sdk.Tracker
import org.matomo.sdk.TrackerBuilder
class MyApp : MultiDexApplication(), DIAware {
@ -42,6 +50,8 @@ class MyApp : MultiDexApplication(), DIAware {
bind<Repository>() with singleton { Repository(instance(), instance(), isConnectionAvailable, instance()) }
bind<ConnectivityStatus>() with singleton { ConnectivityStatus(applicationContext) }
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()
@ -56,28 +66,57 @@ class MyApp : MultiDexApplication(), DIAware {
super.onCreate()
Napier.base(DebugAntilog())
initDrawerImageLoader()
if (!ACRA.isACRASenderServiceProcess()) {
initDrawerImageLoader()
tryToHandleBug()
tryToHandleBug()
handleNotificationChannels()
handleNotificationChannels()
ProcessLifecycleOwner.get().lifecycle.addObserver(AppLifeCycleObserver(connectivityStatus, repository))
ProcessLifecycleOwner.get().lifecycle.addObserver(AppLifeCycleObserver(connectivityStatus, repository))
CoroutineScope(Dispatchers.Main).launch {
viewModel.networkAvailableProvider.collect { networkAvailable ->
val toastMessage = if (networkAvailable) {
repository.handleDBActions()
R.string.network_connectivity_retrieved
} else {
R.string.network_connectivity_lost
CoroutineScope(Dispatchers.Main).launch {
viewModel.networkAvailableProvider.collect { networkAvailable ->
val toastMessage = if (networkAvailable) {
repository.handleDBActions()
R.string.network_connectivity_retrieved
} else {
R.string.network_connectivity_lost
}
Toast.makeText(
applicationContext,
toastMessage,
Toast.LENGTH_SHORT
).show()
}
}
}
}
Toast.makeText(
applicationContext,
toastMessage,
Toast.LENGTH_SHORT
).show()
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
}
}
}
@ -122,10 +161,9 @@ class MyApp : MultiDexApplication(), DIAware {
val oldHandler = Thread.getDefaultUncaughtExceptionHandler()
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")
}) {
Unit
} else {
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 bou.amine.apps.readerforselfossv2.android.ImageActivity
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.model.ParecelableItem
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.model.SelfossModel
import bou.amine.apps.readerforselfossv2.repository.Repository
import bou.amine.apps.readerforselfossv2.rest.MercuryApi
import bou.amine.apps.readerforselfossv2.service.AppSettingsService
import bou.amine.apps.readerforselfossv2.utils.getHtmlDecoded
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.Dispatchers
import kotlinx.coroutines.launch
import org.acra.ktx.sendSilentlyWithAcra
import org.acra.ktx.sendWithAcra
import org.kodein.di.DI
import org.kodein.di.DIAware
import org.kodein.di.android.x.closestDI
import org.kodein.di.instance
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
import java.net.MalformedURLException
import java.net.URL
import java.util.*
@ -81,6 +79,9 @@ class ArticleFragment : Fragment(), DIAware {
private var font = ""
private var staticBar = false
private val mercuryApi : MercuryApi by instance()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@ -113,6 +114,7 @@ class ArticleFragment : Fragment(), DIAware {
typeface = try {
ResourcesCompat.getFont(requireContext(), resId)!!
} catch (e: java.lang.Exception) {
e.sendSilentlyWithAcra()
// Just to be sure
null
}
@ -218,6 +220,7 @@ class ArticleFragment : Fragment(), DIAware {
)
} catch (e: InflateException) {
e.sendSilentlyWithAcra()
AlertDialog.Builder(requireContext())
.setMessage(requireContext().getString(R.string.webview_dialog_issue_message))
.setTitle(requireContext().getString(R.string.webview_dialog_issue_title))
@ -249,88 +252,79 @@ class ArticleFragment : Fragment(), DIAware {
private fun getContentFromMercury() {
if (repository.isNetworkAvailable()) {
binding.progressBar.visibility = View.VISIBLE
val parser = MercuryApi()
parser.parseUrl(url).enqueue(
object : Callback<ParsedContent> {
override fun onResponse(
call: Call<ParsedContent>,
response: Response<ParsedContent>
) {
// TODO: clean all the following after finding the mercury content issue
try {
if (response.body() != null && response.body()!!.content != null && !response.body()!!.content.isNullOrEmpty()) {
try {
binding.titleView.text = response.body()!!.title
if (typeface != null) {
binding.titleView.typeface = typeface
}
try {
// Note: Mercury may return relative urls... If it does the url val will not be changed.
URL(response.body()!!.url)
url = response.body()!!.url
} catch (e: MalformedURLException) {
// Mercury returned a relative url. We do nothing.
}
} catch (e: Exception) {
CoroutineScope(Dispatchers.Main).launch {
val response = mercuryApi.query(url)
if (response.success) {
try {
if (response.data != null && response.data!!.content != null && !response.data!!.content.isNullOrEmpty()) {
try {
binding.titleView.text = response.data!!.title
if (typeface != null) {
binding.titleView.typeface = typeface
}
try {
contentText = response.body()!!.content.orEmpty()
htmlToWebview()
} catch (e: Exception) {
}
try {
if (response.body()!!.lead_image_url != null && !response.body()!!.lead_image_url.isNullOrEmpty() && context != null) {
binding.imageView.visibility = View.VISIBLE
try {
Glide
.with(requireContext())
.asBitmap()
.load(
response.body()!!.lead_image_url.orEmpty()
)
.apply(RequestOptions.fitCenterTransform())
.into(binding.imageView)
} catch (e: Exception) {
}
} else {
binding.imageView.visibility = View.GONE
}
} catch (e: Exception) {
if (context != null) {
}
}
try {
binding.nestedScrollView.scrollTo(0, 0)
binding.progressBar.visibility = View.GONE
} catch (e: Exception) {
if (context != null) {
}
}
} else {
try {
openInBrowserAfterFailing()
} catch (e: Exception) {
if (context != null) {
}
// Note: Mercury may return relative urls... If it does the url val will not be changed.
URL(response.data!!.url)
url = response.data!!.url
} catch (e: MalformedURLException) {
// Mercury returned a relative url
e.sendSilentlyWithAcra()
}
} catch (e: Exception) {
e.sendSilentlyWithAcra()
}
} catch (e: Exception) {
if (context != null) {
try {
contentText = response.data!!.content.orEmpty()
htmlToWebview()
} catch (e: Exception) {
e.sendSilentlyWithAcra()
}
try {
if (response.data!!.lead_image_url != null && !response.data!!.lead_image_url.isNullOrEmpty() && context != null) {
binding.imageView.visibility = View.VISIBLE
try {
Glide
.with(requireContext())
.asBitmap()
.load(
response.data!!.lead_image_url.orEmpty()
)
.apply(RequestOptions.fitCenterTransform())
.into(binding.imageView)
} catch (e: Exception) {
e.sendSilentlyWithAcra()
}
} else {
binding.imageView.visibility = View.GONE
}
} catch (e: Exception) {
e.sendSilentlyWithAcra()
}
try {
binding.nestedScrollView.scrollTo(0, 0)
binding.progressBar.visibility = View.GONE
} catch (e: Exception) {
e.sendSilentlyWithAcra()
}
} else {
try {
openInBrowserAfterFailing()
} catch (e: Exception) {
e.sendSilentlyWithAcra()
}
}
} catch (e: Exception) {
e.sendSilentlyWithAcra()
}
override fun onFailure(
call: Call<ParsedContent>,
t: Throwable
) = openInBrowserAfterFailing()
} else {
openInBrowserAfterFailing()
}
)
}
}
}
@ -369,19 +363,25 @@ class ArticleFragment : Fragment(), DIAware {
try {
val image = Glide.with(view).asBitmap().apply(glideOptions).load(url).submit().get()
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")) {
try {
val image = Glide.with(view).asBitmap().apply(glideOptions).load(url).submit().get()
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")) {
try {
val image = Glide.with(view).asBitmap().apply(glideOptions).load(url).submit().get()
return WebResourceResponse("image/jpg", "UTF-8", getBitmapInputStream(image, Bitmap.CompressFormat.WEBP))
}catch ( e : ExecutionException) {}
} catch ( e : ExecutionException) {
e.sendSilentlyWithAcra()
}
}
return super.shouldInterceptRequest(view, url)
@ -405,11 +405,13 @@ class ArticleFragment : Fragment(), DIAware {
val itemUrl = URL(url)
baseUrl = itemUrl.protocol + "://" + itemUrl.host
} catch (e: MalformedURLException) {
e.sendSilentlyWithAcra()
}
val fontName = when (font) {
getString(R.string.open_sans_font_id) -> "Open Sans"
getString(R.string.roboto_font_id) -> "Roboto"
getString(R.string.source_code_pro_font_id) -> "Source Code Pro"
else -> ""
}

View File

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

View File

@ -3,7 +3,6 @@ package bou.amine.apps.readerforselfossv2.android.model
import android.os.Parcel
import android.os.Parcelable
import bou.amine.apps.readerforselfossv2.model.SelfossModel
import com.google.gson.annotations.SerializedName
fun SelfossModel.Item.toParcelable() : ParecelableItem =
ParecelableItem(
@ -34,17 +33,17 @@ fun ParecelableItem.toModel() : SelfossModel.Item =
this.tags.split(",")
)
data class ParecelableItem(
@SerializedName("id") val id: Int,
@SerializedName("datetime") val datetime: String,
@SerializedName("title") val title: String,
@SerializedName("content") val content: String,
@SerializedName("unread") var unread: Boolean,
@SerializedName("starred") var starred: Boolean,
@SerializedName("thumbnail") val thumbnail: String?,
@SerializedName("icon") val icon: String?,
@SerializedName("link") val link: String,
@SerializedName("sourcetitle") val sourcetitle: String,
@SerializedName("tags") val tags: String
val id: Int,
val datetime: String,
val title: String,
val content: String,
var unread: Boolean,
var starred: Boolean,
val thumbnail: String?,
val icon: String?,
val link: String,
val sourcetitle: String,
val tags: String
) : Parcelable {
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.databinding.ActivitySettingsBinding
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"
class SettingsActivity : AppCompatActivity(),
PreferenceFragmentCompat.OnPreferenceStartFragmentCallback {
PreferenceFragmentCompat.OnPreferenceStartFragmentCallback, DIAware {
override val di by closestDI()
private val tracker : Tracker by instance()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val binding = ActivitySettingsBinding.inflate(layoutInflater)
TrackHelper.track().screen("/settings").with(tracker)
setContentView(binding.root)
if (savedInstanceState == null) {
supportFragmentManager
@ -91,6 +103,11 @@ class SettingsActivity : AppCompatActivity(),
class MainPreferenceFragment : PreferenceFragmentCompat() {
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
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()
if (input in 1..200) return@InputFilter null
} catch (nfe: NumberFormatException) {
nfe.sendSilentlyWithAcra()
Toast.makeText(activity, R.string.items_number_should_be_number, Toast.LENGTH_LONG).show()
}
""
@ -130,6 +148,7 @@ class SettingsActivity : AppCompatActivity(),
try {
editText.textSize = editable.toString().toInt().toFloat()
} catch (e: NumberFormatException) {
e.sendSilentlyWithAcra()
}
}
} }
@ -139,6 +158,7 @@ class SettingsActivity : AppCompatActivity(),
val input = (dest.toString() + source.toString()).toInt()
if (input > 0) return@InputFilter null
} catch (nfe: NumberFormatException) {
nfe.sendSilentlyWithAcra()
}
""
}

View File

@ -1,13 +1,9 @@
package bou.amine.apps.readerforselfossv2.android.utils
import android.app.Activity
import android.app.PendingIntent
import android.content.ActivityNotFoundException
import android.content.Context
import android.content.Intent
import android.graphics.BitmapFactory
import android.net.Uri
import android.os.Build
import android.text.Spannable
import android.text.style.ClickableSpan
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"?>
<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"
android:layout_width="match_parent"
android:layout_height="match_parent">
@ -9,8 +9,8 @@
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_centerVertical="true"
android:layout_centerHorizontal="true"
android:background="@android:color/black"
android:adjustViewBounds="true"
android:background="@drawable/checkerboard"
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_light">Light mode</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>

View File

@ -133,4 +133,9 @@
<string name="mode_system">Follow the system setting</string>
<string name="mode_light">Light mode</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>

View File

@ -133,4 +133,9 @@
<string name="mode_system">Follow the system setting</string>
<string name="mode_light">Light mode</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>

View File

@ -133,4 +133,9 @@
<string name="mode_system">Follow the system setting</string>
<string name="mode_light">Light mode</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>

View File

@ -133,4 +133,9 @@
<string name="mode_system">Utiliser les paramètres système</string>
<string name="mode_light">Thème clair</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>

View File

@ -133,4 +133,9 @@
<string name="mode_system">Follow the system setting</string>
<string name="mode_light">Light mode</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>

View File

@ -133,4 +133,9 @@
<string name="mode_system">Follow the system setting</string>
<string name="mode_light">Light mode</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>

View File

@ -133,4 +133,9 @@
<string name="mode_system">Follow the system setting</string>
<string name="mode_light">Light mode</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>

View File

@ -133,4 +133,9 @@
<string name="mode_system">Follow the system setting</string>
<string name="mode_light">Light mode</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>

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_light">Light mode</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>

View File

@ -133,4 +133,9 @@
<string name="mode_system">Follow the system setting</string>
<string name="mode_light">Light mode</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>

View File

@ -133,4 +133,9 @@
<string name="mode_system">Follow the system setting</string>
<string name="mode_light">Light mode</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>

View File

@ -133,4 +133,9 @@
<string name="mode_system">Follow the system setting</string>
<string name="mode_light">Light mode</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>

View File

@ -133,4 +133,9 @@
<string name="mode_system">Follow the system setting</string>
<string name="mode_light">Light mode</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>

View File

@ -133,4 +133,9 @@
<string name="mode_system">遵循系统设置</string>
<string name="mode_light">浅色模式</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>

View File

@ -133,4 +133,9 @@
<string name="mode_system">Follow the system setting</string>
<string name="mode_light">Light mode</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>

View File

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

View File

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

View File

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

View File

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

View File

@ -125,6 +125,7 @@
<string name="reader_text_align_left">Align left</string>
<string name="reader_text_align_justify">Justify</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="roboto_font_id" translatable="false">roboto</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_system">Follow the system setting</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>

View File

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

View File

@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<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">
<Preference
@ -17,9 +18,13 @@
android:title="@string/pref_header_offline"
android:icon="@drawable/ic_signal_wifi_off_black_24dp" />
<Preference
android:fragment="bou.amine.apps.readerforselfossv2.android.settings.SettingsActivity$ThemePreferenceFragment"
<ListPreference
android:defaultValue="0"
android:entries="@array/ModeTitles"
android:entryValues="@array/ModeValues"
android:key="currentMode"
android:title="@string/pref_header_theme"
app:useSimpleSummaryProvider="false"
android:icon="@drawable/ic_color_lens_black_24dp" />
<Preference
@ -32,4 +37,18 @@
android:title="@string/pref_header_experimental"
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>

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.TAG
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.service.AppSettingsService
import bou.amine.apps.readerforselfossv2.utils.ItemType
@ -47,11 +49,11 @@ class RepositoryTest {
every { appSettingsService.isItemCachingEnabled() } returns false
every { appSettingsService.isUpdateSourcesEnabled() } returns false
coEvery { api.version() } returns SelfossModel.StatusAndData(
coEvery { api.version() } returns StatusAndData(
success = true,
data = SelfossModel.ApiVersion("2.19-ba1e8e3", "4.0.0")
)
coEvery { api.stats() } returns SelfossModel.StatusAndData(
coEvery { api.stats() } returns StatusAndData(
success = true,
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() {
every { appSettingsService.getApiVersion() } returns 1
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
initializeRepository()
@ -98,11 +100,11 @@ class RepositoryTest {
@Test
fun get_api_1_date_with_api_4_version_stored() {
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()
itemParameters.datetime = "2021-04-23 11:45:32"
coEvery { api.getItems(any(), any(), any(), any(), any(), any(), any()) } returns
SelfossModel.StatusAndData(
StatusAndData(
success = true,
data = generateTestApiItem(itemParameters)
)
@ -118,7 +120,7 @@ class RepositoryTest {
@Test
fun get_newer_items() {
coEvery { api.getItems(any(), any(), any(), any(), any(), any(), any()) } returns
SelfossModel.StatusAndData(success = true, data = generateTestApiItem())
StatusAndData(success = true, data = generateTestApiItem())
initializeRepository()
runBlocking {
@ -133,7 +135,7 @@ class RepositoryTest {
@Test
fun get_all_newer_items() {
coEvery { api.getItems(any(), any(), any(), any(), any(), any(), any()) } returns
SelfossModel.StatusAndData(success = true, data = generateTestApiItem())
StatusAndData(success = true, data = generateTestApiItem())
initializeRepository()
repository.displayedItems = ItemType.ALL
@ -149,7 +151,7 @@ class RepositoryTest {
@Test
fun get_newer_starred_items() {
coEvery { api.getItems(any(), any(), any(), any(), any(), any(), any()) } returns
SelfossModel.StatusAndData(success = true, data = generateTestApiItem())
StatusAndData(success = true, data = generateTestApiItem())
initializeRepository()
repository.displayedItems = ItemType.STARRED
@ -242,7 +244,7 @@ class RepositoryTest {
@Test
fun get_older_items() {
coEvery { api.getItems(any(), any(), any(), any(), any(), any(), any()) } returns
SelfossModel.StatusAndData(success = true, data = generateTestApiItem())
StatusAndData(success = true, data = generateTestApiItem())
initializeRepository()
repository.items = ArrayList(generateTestApiItem())
@ -258,7 +260,7 @@ class RepositoryTest {
@Test
fun get_all_older_items() {
coEvery { api.getItems(any(), any(), any(), any(), any(), any(), any()) } returns
SelfossModel.StatusAndData(success = true, data = generateTestApiItem())
StatusAndData(success = true, data = generateTestApiItem())
initializeRepository()
repository.items = ArrayList(generateTestApiItem())
@ -275,7 +277,7 @@ class RepositoryTest {
@Test
fun get_older_starred_items() {
coEvery { api.getItems(any(), any(), any(), any(), any(), any(), any()) } returns
SelfossModel.StatusAndData(success = true, data = generateTestApiItem())
StatusAndData(success = true, data = generateTestApiItem())
initializeRepository()
repository.displayedItems = ItemType.STARRED
@ -308,7 +310,7 @@ class RepositoryTest {
@Test
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
@ -376,7 +378,7 @@ class RepositoryTest {
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
every { appSettingsService.isUpdateSourcesEnabled() } returns true
every { appSettingsService.isItemCachingEnabled() } returns true
@ -403,7 +405,7 @@ class RepositoryTest {
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
every { appSettingsService.isUpdateSourcesEnabled() } returns false
every { appSettingsService.isItemCachingEnabled() } returns true
@ -433,7 +435,7 @@ class RepositoryTest {
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
every { appSettingsService.isUpdateSourcesEnabled() } returns true
every { appSettingsService.isItemCachingEnabled() } returns false
@ -460,7 +462,7 @@ class RepositoryTest {
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
every { appSettingsService.isUpdateSourcesEnabled() } returns false
every { appSettingsService.isItemCachingEnabled() } returns false
@ -489,7 +491,7 @@ class RepositoryTest {
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
every { appSettingsService.isUpdateSourcesEnabled() } returns true
every { appSettingsService.isItemCachingEnabled() } returns true
@ -517,7 +519,7 @@ class RepositoryTest {
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
every { appSettingsService.isItemCachingEnabled() } returns false
every { appSettingsService.isUpdateSourcesEnabled() } returns true
@ -544,7 +546,7 @@ class RepositoryTest {
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
every { appSettingsService.isUpdateSourcesEnabled() } returns false
every { appSettingsService.isItemCachingEnabled() } returns true
@ -572,7 +574,7 @@ class RepositoryTest {
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
every { appSettingsService.isUpdateSourcesEnabled() } 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
initializeRepository()
var testSources: List<SelfossModel.Source>?
@ -681,7 +683,7 @@ class RepositoryTest {
every { appSettingsService.isUpdateSourcesEnabled() } returns false
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
initializeRepository()
var testSources: List<SelfossModel.Source>?
@ -738,7 +740,7 @@ class RepositoryTest {
every { appSettingsService.isUpdateSourcesEnabled() } returns true
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
initializeRepository()
var testSources: List<SelfossModel.Source>?
@ -792,7 +794,7 @@ class RepositoryTest {
every { appSettingsService.isUpdateSourcesEnabled() } 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
initializeRepository()
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
initializeRepository(MutableStateFlow(false))
var testSources: List<SelfossModel.Source>?
@ -898,7 +900,7 @@ class RepositoryTest {
every { appSettingsService.isItemCachingEnabled() } returns false
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
initializeRepository(MutableStateFlow(false))
var testSources: List<SelfossModel.Source>?
@ -952,7 +954,7 @@ class RepositoryTest {
every { appSettingsService.isItemCachingEnabled() } returns true
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
initializeRepository(MutableStateFlow(false))
var testSources: List<SelfossModel.Source>?
@ -1006,7 +1008,7 @@ class RepositoryTest {
every { appSettingsService.isItemCachingEnabled() } 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
initializeRepository(MutableStateFlow(false))
var testSources: List<SelfossModel.Source>?
@ -1022,7 +1024,7 @@ class RepositoryTest {
@Test
fun create_source() {
coEvery { api.createSourceForVersion(any(), any(), any(), any(), any(), any()) } returns
SelfossModel.SuccessResponse(true)
SuccessResponse(true)
initializeRepository()
var response: Boolean
@ -1052,7 +1054,7 @@ class RepositoryTest {
@Test
fun create_source_but_response_fails() {
coEvery { api.createSourceForVersion(any(), any(), any(), any(), any(), any()) } returns
SelfossModel.SuccessResponse(false)
SuccessResponse(false)
initializeRepository()
var response: Boolean
@ -1082,7 +1084,7 @@ class RepositoryTest {
@Test
fun create_source_without_connection() {
coEvery { api.createSourceForVersion(any(), any(), any(), any(), any(), any()) } returns
SelfossModel.SuccessResponse(true)
SuccessResponse(true)
initializeRepository(MutableStateFlow(false))
var response: Boolean
@ -1111,7 +1113,7 @@ class RepositoryTest {
@Test
fun delete_source() {
coEvery { api.deleteSource(any()) } returns SelfossModel.SuccessResponse(true)
coEvery { api.deleteSource(any()) } returns SuccessResponse(true)
initializeRepository()
var response: Boolean
@ -1125,7 +1127,7 @@ class RepositoryTest {
@Test
fun delete_source_but_response_fails() {
coEvery { api.deleteSource(any()) } returns SelfossModel.SuccessResponse(false)
coEvery { api.deleteSource(any()) } returns SuccessResponse(false)
initializeRepository()
var response: Boolean
@ -1139,7 +1141,7 @@ class RepositoryTest {
@Test
fun delete_source_without_connection() {
coEvery { api.deleteSource(any()) } returns SelfossModel.SuccessResponse(false)
coEvery { api.deleteSource(any()) } returns SuccessResponse(false)
initializeRepository(MutableStateFlow(false))
var response: Boolean
@ -1153,7 +1155,7 @@ class RepositoryTest {
@Test
fun update_remote() {
coEvery { api.update() } returns SelfossModel.StatusAndData(
coEvery { api.update() } returns StatusAndData(
success = true,
data = "finished"
)
@ -1170,7 +1172,7 @@ class RepositoryTest {
@Test
fun update_remote_but_response_fails() {
coEvery { api.update() } returns SelfossModel.StatusAndData(
coEvery { api.update() } returns StatusAndData(
success = false,
data = "unallowed access"
)
@ -1187,7 +1189,7 @@ class RepositoryTest {
@Test
fun update_remote_with_unallowed_access() {
coEvery { api.update() } returns SelfossModel.StatusAndData(
coEvery { api.update() } returns StatusAndData(
success = true,
data = "unallowed access"
)
@ -1204,7 +1206,7 @@ class RepositoryTest {
@Test
fun update_remote_without_connection() {
coEvery { api.update() } returns SelfossModel.StatusAndData(
coEvery { api.update() } returns StatusAndData(
success = true,
data = "undocumented..."
)
@ -1221,7 +1223,7 @@ class RepositoryTest {
@Test
fun login() {
coEvery { api.login() } returns SelfossModel.SuccessResponse(success = true)
coEvery { api.login() } returns SuccessResponse(success = true)
initializeRepository()
var response: Boolean
@ -1235,7 +1237,7 @@ class RepositoryTest {
@Test
fun login_but_response_fails() {
coEvery { api.login() } returns SelfossModel.SuccessResponse(success = false)
coEvery { api.login() } returns SuccessResponse(success = false)
initializeRepository()
var response: Boolean
@ -1249,7 +1251,7 @@ class RepositoryTest {
@Test
fun login_but_without_connection() {
coEvery { api.login() } returns SelfossModel.SuccessResponse(success = true)
coEvery { api.login() } returns SuccessResponse(success = true)
initializeRepository(MutableStateFlow(false))
var response: Boolean
@ -1297,9 +1299,9 @@ class RepositoryTest {
any()
)
} returnsMany listOf(
SelfossModel.StatusAndData(success = true, data = generateTestApiItem(itemParameter1)),
SelfossModel.StatusAndData(success = true, data = generateTestApiItem(itemParameter2)),
SelfossModel.StatusAndData(success = true, data = generateTestApiItem(itemParameter1)),
StatusAndData(success = true, data = generateTestApiItem(itemParameter1)),
StatusAndData(success = true, data = generateTestApiItem(itemParameter2)),
StatusAndData(success = true, data = generateTestApiItem(itemParameter1)),
)
initializeRepository()
@ -1323,7 +1325,7 @@ class RepositoryTest {
@Test
fun cache_items_but_response_fails() {
coEvery { api.getItems(any(), any(), any(), any(), any(), any(), any()) } returns
SelfossModel.StatusAndData(success = false, data = generateTestApiItem())
StatusAndData(success = false, data = generateTestApiItem())
initializeRepository()
repository.tagFilter = SelfossModel.Tag("Tag", "read", 0)
@ -1346,7 +1348,7 @@ class RepositoryTest {
@Test
fun cache_items_without_connection() {
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))
repository.tagFilter = SelfossModel.Tag("Tag", "read", 0)

View File

@ -1,5 +1,6 @@
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.service.AppSettingsService
import org.kodein.di.DI
@ -10,4 +11,5 @@ import org.kodein.di.singleton
val networkModule by DI.Module {
bind<AppSettingsService>() with singleton { AppSettingsService() }
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,40 @@
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 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
)
@Serializable
class SuccessResponse(val success: Boolean) {
val isSuccess: Boolean
get() = success
}
@Serializable
class Stats(
val total: Int,
@ -152,16 +146,4 @@ class SelfossModel {
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.model.NetworkUnavailableException
import bou.amine.apps.readerforselfossv2.model.SelfossModel
import bou.amine.apps.readerforselfossv2.model.StatusAndData
import bou.amine.apps.readerforselfossv2.rest.SelfossApi
import bou.amine.apps.readerforselfossv2.service.AppSettingsService
import bou.amine.apps.readerforselfossv2.utils.*
@ -40,7 +41,7 @@ class Repository(private val api: SelfossApi, private val appSettingsService: Ap
suspend fun getNewerItems(): ArrayList<SelfossModel.Item> {
// 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
if (isNetworkAvailable()) {
fetchedItems = api.getItems(
@ -65,7 +66,7 @@ class Repository(private val api: SelfossApi, private val appSettingsService: Ap
if (sourceFilter != null) {
dbItems = dbItems.filter { it.sourcetitle == sourceFilter!!.title }
}
fetchedItems = SelfossModel.StatusAndData.succes(
fetchedItems = StatusAndData.succes(
dbItems.map { it.toView() }
)
}
@ -81,7 +82,7 @@ class Repository(private val api: SelfossApi, private val appSettingsService: Ap
}
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()) {
val offset = items.size
fetchedItems = api.getItems(

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,6 +1,6 @@
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 io.ktor.client.*
import io.ktor.client.call.*
@ -66,7 +66,7 @@ class SelfossApi(private val appSettingsService: AppSettingsService) {
client = createHttpClient()
}
suspend fun login(): SelfossModel.SuccessResponse =
suspend fun login(): SuccessResponse =
maybeResponse(client.get(url("/login")) {
parameter("username", appSettingsService.getUserName())
parameter("password", appSettingsService.getPassword())
@ -80,7 +80,7 @@ class SelfossApi(private val appSettingsService: AppSettingsService) {
search: String?,
updatedSince: String?,
items: Int? = null
): SelfossModel.StatusAndData<List<SelfossModel.Item>> =
): StatusAndData<List<SelfossModel.Item>> =
bodyOrFailure(client.get(url("/items")) {
parameter("username", appSettingsService.getUserName())
parameter("password", appSettingsService.getPassword())
@ -93,64 +93,64 @@ class SelfossApi(private val appSettingsService: AppSettingsService) {
parameter("offset", offset)
})
suspend fun stats(): SelfossModel.StatusAndData<SelfossModel.Stats> =
suspend fun stats(): StatusAndData<SelfossModel.Stats> =
bodyOrFailure(client.get(url("/stats")) {
parameter("username", appSettingsService.getUserName())
parameter("password", appSettingsService.getPassword())
})
suspend fun tags(): SelfossModel.StatusAndData<List<SelfossModel.Tag>> =
suspend fun tags(): StatusAndData<List<SelfossModel.Tag>> =
bodyOrFailure(client.get(url("/tags")) {
parameter("username", appSettingsService.getUserName())
parameter("password", appSettingsService.getPassword())
})
suspend fun update(): SelfossModel.StatusAndData<String> =
suspend fun update(): StatusAndData<String> =
bodyOrFailure(client.get(url("/update")) {
parameter("username", appSettingsService.getUserName())
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")) {
parameter("username", appSettingsService.getUserName())
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")) {
parameter("username", appSettingsService.getUserName())
parameter("password", appSettingsService.getPassword())
})
suspend fun version(): SelfossModel.StatusAndData<SelfossModel.ApiVersion> =
suspend fun version(): StatusAndData<SelfossModel.ApiVersion> =
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")) {
parameter("username", appSettingsService.getUserName())
parameter("password", appSettingsService.getPassword())
})
suspend fun unmarkAsRead(id: String): SelfossModel.SuccessResponse =
suspend fun unmarkAsRead(id: String): SuccessResponse =
maybeResponse(client.post(url("/unmark/$id")) {
parameter("username", appSettingsService.getUserName())
parameter("password", appSettingsService.getPassword())
})
suspend fun starr(id: String): SelfossModel.SuccessResponse =
suspend fun starr(id: String): SuccessResponse =
maybeResponse(client.post(url("/starr/$id")) {
parameter("username", appSettingsService.getUserName())
parameter("password", appSettingsService.getPassword())
})
suspend fun unstarr(id: String): SelfossModel.SuccessResponse =
suspend fun unstarr(id: String): SuccessResponse =
maybeResponse(client.post(url("/unstarr/$id")) {
parameter("username", appSettingsService.getUserName())
parameter("password", appSettingsService.getPassword())
})
suspend fun markAllAsRead(ids: List<String>): SelfossModel.SuccessResponse =
suspend fun markAllAsRead(ids: List<String>): SuccessResponse =
maybeResponse(client.submitForm(
url = url("/mark"),
formParameters = Parameters.build {
@ -167,7 +167,7 @@ class SelfossApi(private val appSettingsService: AppSettingsService) {
tags: String,
filter: String,
version: Int
): SelfossModel.SuccessResponse =
): SuccessResponse =
maybeResponse(
if (version > 1) {
createSource2(title, url, spout, tags, filter)
@ -212,25 +212,9 @@ 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")) {
parameter("username", appSettingsService.getUserName())
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 _staticBar: Boolean? = null
private var _font: String = ""
private var _theme: Int? = null
private var _enableAnalytics: Boolean? = null
init {
@ -307,6 +309,17 @@ class AppSettingsService {
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() {
_font = settings.getString(READER_FONT, "")
}
@ -318,6 +331,17 @@ class AppSettingsService {
return _font
}
private fun refreshCurrentTheme() {
_theme = settings.getString(CURRENT_THEME, "-1").toInt()
}
fun getCurrentTheme(): Int {
if (_theme == null) {
refreshCurrentTheme()
}
return _theme ?: -1
}
fun refreshApiSettings() {
refreshPassword()
refreshUsername()
@ -346,6 +370,8 @@ class AppSettingsService {
refreshFontSize()
refreshFont()
refreshStaticBarEnabled()
refreshCurrentTheme()
refreshAnalyticsEnabled()
}
fun refreshLoginInformation(
@ -444,5 +470,9 @@ class AppSettingsService {
const val INFINITE_LOADING = "infinite_loading"
const val ITEMS_CACHING = "items_caching"
const val CURRENT_THEME = "currentMode"
const val ENABLE_ANALYTICS = "enable_analytics"
}
}