Compare commits

..

15 Commits

Author SHA1 Message Date
aminecmi 68aedb7641 chore: verbose. 2023-10-11 21:13:10 +02:00
Amine Louveau 754d526b49 chore: cleaning ci steps and upgrading dependencies.
## Types of changes

- [ ] I have read the **CONTRIBUTING** document.
- [ ] My code follows the code style of this project.
- [ ] I have updated the documentation accordingly.
- [ ] I have added tests to cover my changes.
- [ ] All new and existing tests passed.
- [ ] This is **NOT** translation related.

This closes issue #XXX

This is implements feature #YYY

This finishes chore #ZZZ

Co-authored-by: davidoskky <davidoskky@yahoo.it>
Co-authored-by: aminecmi <aminecmi@gmail.com>
Reviewed-on: https://gitea.amine-louveau.fr/Louvorg/ReaderForSelfoss-multiplatform/pulls/150
2023-10-10 20:52:26 +00:00
Amine Louveau c458871569 feat: Self signed ssl support.
Reviewed-on: https://gitea.amine-louveau.fr/Louvorg/ReaderForSelfoss-multiplatform/pulls/141
2023-09-17 18:28:47 +00:00
davidoskky 056825aa0c Revert xmlns changes 2023-09-17 12:01:32 +02:00
davidoskky 16b19fc5ce Revert version upgrades 2023-09-17 11:59:08 +02:00
davidoskky 4ad4a23ed8 Revert to private functions 2023-09-12 00:38:37 +02:00
davidoskky d8c215eacc Reintroduce removed parameter 2023-09-12 00:36:56 +02:00
davidoskky 2b446ab22b Revert dependency version changes 2023-09-12 00:36:04 +02:00
davidoskky a029d8a7dc Move api client creation function within api class 2023-09-10 21:33:28 +02:00
davidoskky 4482234e1a Remove unused strings file 2023-09-10 21:15:04 +02:00
davidoskky b5de30f561 Add translations 2023-09-10 21:14:43 +02:00
davidoskky 70ad5f322c Handle most HTTP client creation in common code 2023-09-10 21:14:43 +02:00
davidoskky d167092c83 Add a login switch to disable SSL verification 2023-09-10 20:24:22 +02:00
davidoskky c4f4bafe85 Add a switch in the login screen to disable SSL 2023-07-13 14:55:48 +02:00
davidoskky ed06b22a77 Tentative self signed ssl support 2023-07-13 14:55:48 +02:00
43 changed files with 280 additions and 246 deletions
+37 -30
View File
@@ -3,35 +3,38 @@ type: docker
name: test
steps:
- name: Lint
failure: ignore
image: mingc/android-build-box:latest
commands:
- echo "---------------------------------------------------------"
- echo "Install linters..."
- curl -sSLO https://github.com/pinterest/ktlint/releases/download/1.0.0/ktlint && chmod a+x ktlint && mv ktlint /usr/local/bin/
- curl -sSLO https://github.com/detekt/detekt/releases/download/v1.23.1/detekt-cli-1.23.1.zip && unzip detekt-cli-1.23.1.zip
- echo "---------------------------------------------------------"
- echo "Linting..."
- ktlint || true
- echo "---------------------------------------------------------"
- echo "Detecting..."
- ./detekt-cli-1.23.1/bin/detekt-cli --all-rules || true
- echo "---------------------------------------------------------"
command_timeout: 1m
- name: BuildAndTest
image: mingc/android-build-box:latest
commands:
- echo "---------------------------------------------------------"
- echo "Configure gradle..."
- 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
- mkdir -p ~/.gradle && echo "org.gradle.daemon=false\nignoreGitVersion=true\nsystemProp.org.gradle.internal.http.connectionTimeout=180000\nsystemProp.org.gradle.internal.http.socketTimeout=180000" >> ~/.gradle/gradle.properties
- echo "---------------------------------------------------------"
- echo "Building..."
- ./gradlew build -x test
- echo "Configure java..."
- . ~/.bash_profile
- jenv global 17.0
- java --version
- date
- echo "---------------------------------------------------------"
- echo "Testing..."
- echo "Building and testing..."
- ./gradlew build
- echo "---------------------------------------------------------"
- ./gradlew koverMergedXmlReport
environment:
TZ: Europe/Paris
SONAR_HOST_URL:
from_secret: sonarScannerHostUrl
SONAR_LOGIN:
from_secret: sonarScannerLogin
- name: Analyse
image: kytay/sonar-node-plugin
settings:
sonar_host:
from_secret: sonarScannerHostUrl
sonar_token:
from_secret: sonarScannerLogin
use_node_version: 16.18.1
sonar_debug: true
sonar_project_settings: ./sonar-project.properties
trigger:
event:
- push
@@ -50,20 +53,24 @@ steps:
- git fetch --tags -p
- PREV=$(git describe --tags --abbrev=0)
- ./build.sh --publish --from-ci
- git remote add pushing https://$GITEA_USR:$GITEA_PASS@gitea.amine-louveau.fr/Louvorg/ReaderForSelfoss-multiplatform.git
- VER=$(git describe --tags --abbrev=0)
- CHANGELOG=$(git log $PREV..HEAD --pretty="- %s")
- echo "**$VER**\n\n$CHANGELOG\n\n--------------------------------------------------------------------\n\n$(cat CHANGELOG.md)" > CHANGELOG.md
- git add CHANGELOG.md
- git commit -m "Changelog for $VER [CI SKIP]"
- git push pushing master
- git push pushing --tags
environment:
TZ: Europe/Paris
GITEA_USR:
from_secret: giteaUsr
GITEA_PASS:
from_secret: giteaPass
- name: git-push
image: appleboy/drone-git-push
settings:
branch: master
remote:
from_secret: remoteUrl
followtags: true
ssh_key:
from_secret: privateKey
skip_verify: true
- name: scpFiles
image: appleboy/drone-scp
@@ -107,10 +114,10 @@ steps:
- git fetch --tags
- echo "---------------------------------------------------------"
- echo "Configure gradle..."
- mkdir -p ~/.gradle && echo "org.gradle.daemon=false\nignoreGitVersion=false\npushCache=false\nsystemProp.org.gradle.internal.http.connectionTimeout=180000\nsystemProp.org.gradle.internal.http.socketTimeout=180000" >> ~/.gradle/gradle.properties
- mkdir -p ~/.gradle && echo "org.gradle.daemon=false\nignoreGitVersion=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
- ./gradlew :androidApp:assembleGithubConfigRelease
- echo "---------------------------------------------------------"
- echo "Get Key"
- wget https://amine-louveau.fr/key
+1 -1
View File
@@ -71,7 +71,7 @@ android {
}
defaultConfig {
applicationId = "bou.amine.apps.readerforselfossv2.android"
minSdk = 21
minSdk = 25
targetSdk = 34
versionCode = versionCodeFromGit()
versionName = versionNameFromGit()
@@ -139,9 +139,12 @@ class LoginActivity : AppCompatActivity(), DIAware {
showProgress(true)
appSettingsService.updateSelfSigned(binding.selfSigned.isChecked)
repository.refreshLoginInformation(url, login, password)
CoroutineScope(Dispatchers.Main).launch {
repository.updateApiInformation()
val result = repository.login()
if (result) {
val (errorFetching, displaySelfossOnly) = repository.shouldBeSelfossInstance()
@@ -5,7 +5,6 @@ import android.content.res.ColorStateList
import android.os.Bundle
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.LinearLayoutManager
import bou.amine.apps.readerforselfossv2.android.adapters.SourcesListAdapter
import bou.amine.apps.readerforselfossv2.android.databinding.ActivitySourcesBinding
@@ -37,10 +36,8 @@ class SourcesActivity : AppCompatActivity(), DIAware {
supportActionBar?.setDisplayHomeAsUpEnabled(true)
supportActionBar?.setDisplayShowHomeEnabled(true)
binding.fab.rippleColor = ContextCompat.getColor(this, R.color.colorAccentDark)
binding.fab.backgroundTintList =
ColorStateList.valueOf(ContextCompat.getColor(this, R.color.colorAccent))
binding.fab.rippleColor = resources.getColor(R.color.colorAccentDark)
binding.fab.backgroundTintList = ColorStateList.valueOf(resources.getColor(R.color.colorAccent))
}
override fun onStop() {
@@ -6,7 +6,6 @@ import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView.ScaleType
import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.RecyclerView
import bou.amine.apps.readerforselfossv2.android.R
import bou.amine.apps.readerforselfossv2.android.databinding.CardItemBinding
@@ -61,7 +60,7 @@ class ItemCardAdapter(
binding.title.setOnTouchListener(LinkOnTouchListener())
binding.title.setLinkTextColor(ContextCompat.getColor(c, R.color.colorAccent))
binding.title.setLinkTextColor(c.resources.getColor(R.color.colorAccent))
binding.sourceTitleAndDate.text = itm.sourceAuthorAndDate()
@@ -4,7 +4,6 @@ import android.app.Activity
import android.content.Context
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.RecyclerView
import bou.amine.apps.readerforselfossv2.android.R
import bou.amine.apps.readerforselfossv2.android.databinding.ListItemBinding
@@ -45,8 +44,7 @@ class ItemListAdapter(
binding.title.setOnTouchListener(LinkOnTouchListener())
binding.title.setLinkTextColor(ContextCompat.getColor(c, R.color.colorAccent))
binding.title.setLinkTextColor(c.resources.getColor(R.color.colorAccent))
binding.sourceTitleAndDate.text = itm.sourceAuthorAndDate()
@@ -10,21 +10,13 @@ import android.graphics.drawable.ColorDrawable
import android.net.Uri
import android.os.Bundle
import android.util.TypedValue
import android.view.GestureDetector
import android.view.InflateException
import android.view.LayoutInflater
import android.view.MenuItem
import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
import android.webkit.WebResourceRequest
import android.view.*
import android.webkit.WebResourceResponse
import android.webkit.WebSettings
import android.webkit.WebView
import android.webkit.WebViewClient
import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import androidx.core.content.ContextCompat
import androidx.core.widget.NestedScrollView
import androidx.fragment.app.Fragment
import bou.amine.apps.readerforselfossv2.android.ImageActivity
@@ -43,7 +35,6 @@ 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.ImageMimeType
import bou.amine.apps.readerforselfossv2.utils.getHtmlDecoded
import bou.amine.apps.readerforselfossv2.utils.getImages
import bou.amine.apps.readerforselfossv2.utils.getThumbnail
@@ -63,7 +54,7 @@ import org.kodein.di.instance
import java.net.MalformedURLException
import java.net.SocketTimeoutException
import java.net.URL
import java.util.Locale
import java.util.*
import java.util.concurrent.ExecutionException
@@ -125,15 +116,9 @@ class ArticleFragment : Fragment(), DIAware {
fab = binding.fab
fab.backgroundTintList =
ColorStateList.valueOf(
ContextCompat.getColor(
requireContext(),
R.color.colorAccent
)
)
fab.backgroundTintList = ColorStateList.valueOf(resources.getColor(R.color.colorAccent))
fab.rippleColor = ContextCompat.getColor(requireContext(), R.color.colorAccentDark)
fab.rippleColor = resources.getColor(R.color.colorAccentDark)
val floatingToolbar: FloatingToolbar = handleFloatingToolbar()
@@ -218,8 +203,7 @@ class ArticleFragment : Fragment(), DIAware {
}
floatingToolbar.attachFab(fab)
floatingToolbar.background =
ColorDrawable(ContextCompat.getColor(requireContext(), R.color.colorAccent))
floatingToolbar.background = ColorDrawable(resources.getColor(R.color.colorAccent))
floatingToolbar.setClickListener(
object : FloatingToolbar.ItemClickListener {
@@ -304,7 +288,7 @@ class ArticleFragment : Fragment(), DIAware {
contentText = data.content.orEmpty()
htmlToWebview()
handleLeadImage(data.lead_image_url)
handleLeadImage(data?.lead_image_url)
binding.nestedScrollView.scrollTo(0, 0)
binding.progressBar.visibility = View.GONE
@@ -329,19 +313,11 @@ class ArticleFragment : Fragment(), DIAware {
private fun handleImageLoading() {
binding.webcontent.webViewClient = object : WebViewClient() {
override fun shouldOverrideUrlLoading(
view: WebView?,
request: WebResourceRequest?
): Boolean {
val url = request?.url?.toString()
return if (context != null && url != null && url.isUrlValid() && binding.webcontent.hitTestResult.type != WebView.HitTestResult.SRC_IMAGE_ANCHOR_TYPE) {
@Deprecated("Deprecated in Java")
override fun shouldOverrideUrlLoading(view: WebView?, url: String): Boolean {
return if (context != null && url.isUrlValid() && binding.webcontent.hitTestResult.type != WebView.HitTestResult.SRC_IMAGE_ANCHOR_TYPE) {
try {
requireContext().startActivity(
Intent(
Intent.ACTION_VIEW,
Uri.parse(url)
)
)
requireContext().startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(url)))
} catch (e: ActivityNotFoundException) {
e.sendSilentlyWithAcraWithName("activityNotFound > $url")
}
@@ -351,42 +327,50 @@ class ArticleFragment : Fragment(), DIAware {
}
}
override fun shouldInterceptRequest(
view: WebView,
request: WebResourceRequest
): WebResourceResponse? {
val url = request.url.toString()
@Deprecated("Deprecated in Java")
override fun shouldInterceptRequest(view: WebView, url: String): WebResourceResponse? {
val glideOptions = RequestOptions.diskCacheStrategyOf(DiskCacheStrategy.ALL)
val supportedExtensions = ImageMimeType.values().map { it.extension }
val matchingExtension = supportedExtensions.find {
url.lowercase(Locale.US).contains(it)
}
matchingExtension?.let {
if (url.lowercase(Locale.US).contains(".jpg") || url.lowercase(Locale.US)
.contains(".jpeg")
) {
try {
val image = Glide.with(view)
.asBitmap()
.apply(glideOptions)
.load(url)
.submit()
.get()
val mimeType = ImageMimeType.findMimeTypeByExtension(it)
if (mimeType != null) {
return WebResourceResponse(
mimeType,
"UTF-8",
getBitmapInputStream(image, Bitmap.CompressFormat.WEBP)
)
}
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) {
// Do nothing
}
} 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) {
// Do nothing
}
} 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) {
// Do nothing
}
}
return super.shouldInterceptRequest(view, request)
return super.shouldInterceptRequest(view, url)
}
}
}
@@ -398,23 +382,13 @@ class ArticleFragment : Fragment(), DIAware {
binding.webcontent.settings.standardFontFamily = a.getString(0)
a.recycle()
binding.webcontent.visibility = View.VISIBLE
val colorOnSurface = TypedValue()
requireContext().theme.resolveAttribute(
com.google.android.material.R.attr.colorOnSurface,
colorOnSurface,
true
)
requireContext().theme.resolveAttribute(R.attr.colorOnSurface, colorOnSurface, true)
val colorSurface = TypedValue()
requireContext().theme.resolveAttribute(
com.google.android.material.R.attr.colorSurface,
colorSurface,
true
)
requireContext().theme.resolveAttribute(R.attr.colorSurface, colorSurface, true)
binding.webcontent.settings.useWideViewPort = true
binding.webcontent.settings.loadWithOverviewMode = true
@@ -483,7 +457,7 @@ class ArticleFragment : Fragment(), DIAware {
| color: ${
String.format(
"#%06X",
0xFFFFFF and ContextCompat.getColor(requireContext(), R.color.colorAccent)
0xFFFFFF and resources.getColor(R.color.colorAccent)
)
} !important;
| }
@@ -12,7 +12,6 @@ import android.view.View
import android.view.View.GONE
import android.view.View.VISIBLE
import android.view.ViewGroup
import androidx.core.content.ContextCompat
import bou.amine.apps.readerforselfossv2.android.HomeActivity
import bou.amine.apps.readerforselfossv2.android.R
import bou.amine.apps.readerforselfossv2.android.databinding.FilterFragmentBinding
@@ -21,7 +20,7 @@ import bou.amine.apps.readerforselfossv2.repository.Repository
import bou.amine.apps.readerforselfossv2.utils.getHtmlDecoded
import bou.amine.apps.readerforselfossv2.utils.getIcon
import com.bumptech.glide.Glide
import com.bumptech.glide.request.target.CustomViewTarget
import com.bumptech.glide.request.target.ViewTarget
import com.bumptech.glide.request.transition.Transition
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import com.google.android.material.chip.Chip
@@ -89,7 +88,7 @@ class FilterSheetFragment : BottomSheetDialogFragment(), DIAware {
Glide.with(context)
.load(source.getIcon(repository.baseUrl))
.into(object : CustomViewTarget<Chip, Drawable>(c) {
.into(object : ViewTarget<Chip?, Drawable?>(c) {
override fun onResourceReady(
resource: Drawable,
transition: Transition<in Drawable?>?
@@ -101,13 +100,6 @@ class FilterSheetFragment : BottomSheetDialogFragment(), DIAware {
}
}
override fun onLoadFailed(errorDrawable: Drawable?) {
c.chipIcon = errorDrawable
}
override fun onResourceCleared(placeholder: Drawable?) {
c.chipIcon = placeholder
}
})
c.text = source.title.getHtmlDecoded()
@@ -164,7 +156,7 @@ class FilterSheetFragment : BottomSheetDialogFragment(), DIAware {
Color.parseColor(tag.color)
} catch (e: IllegalArgumentException) {
e.sendSilentlyWithAcraWithName("color issue " + tag.color)
ContextCompat.getColor(context, R.color.colorPrimary)
resources.getColor(R.color.colorPrimary)
}
gd.setColor(gdColor)
gd.shape = GradientDrawable.RECTANGLE
@@ -71,25 +71,23 @@ class SettingsActivity : AppCompatActivity(),
}
override fun onPreferenceStartFragment(
caller: PreferenceFragmentCompat,
pref: Preference
caller: PreferenceFragmentCompat,
pref: Preference
): Boolean {
val fragmentClassName = pref.fragment ?: return false
// Instantiate the new Fragment
val args = pref.extras
val fragment = supportFragmentManager.fragmentFactory.instantiate(
classLoader,
fragmentClassName
classLoader,
pref.fragment.toString()
).apply {
arguments = args
setTargetFragment(caller, 0)
}
// Replace the existing Fragment with the new Fragment
supportFragmentManager.beginTransaction()
.replace(R.id.settings, fragment)
.addToBackStack(null)
.commit()
.replace(R.id.settings, fragment)
.addToBackStack(null)
.commit()
title = pref.title
supportActionBar?.title = title
return true
@@ -95,7 +95,6 @@ class LinkOnTouchListener : View.OnTouchListener {
if (link.isNotEmpty()) {
if (action == MotionEvent.ACTION_UP) {
link[0].onClick(widget)
widget.performClick()
}
ret = true
}
@@ -51,6 +51,13 @@
android:maxLines="1"
android:minHeight="48dp" />
<com.google.android.material.switchmaterial.SwitchMaterial
android:id="@+id/selfSigned"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/disable_ssl"
android:textAlignment="viewStart" />
<com.google.android.material.switchmaterial.SwitchMaterial
android:id="@+id/withLogin"
android:layout_width="match_parent"
@@ -130,4 +130,5 @@
<string name="update_source">Update source</string>
<string name="confirm_disconnect_title">Disconnect ?</string>
<string name="confirm_disconnect_description">You will be disconnected from your selfoss instance.</string>
<string name="disable_ssl">Disable SSL</string>
</resources>
@@ -130,4 +130,5 @@
<string name="update_source">Update source</string>
<string name="confirm_disconnect_title">Disconnect ?</string>
<string name="confirm_disconnect_description">You will be disconnected from your selfoss instance.</string>
<string name="disable_ssl">Disable SSL</string>
</resources>
@@ -130,4 +130,5 @@
<string name="update_source">Update source</string>
<string name="confirm_disconnect_title">Disconnect ?</string>
<string name="confirm_disconnect_description">You will be disconnected from your selfoss instance.</string>
<string name="disable_ssl">Disable SSL</string>
</resources>
@@ -130,4 +130,5 @@
<string name="update_source">Update source</string>
<string name="confirm_disconnect_title">Disconnect ?</string>
<string name="confirm_disconnect_description">You will be disconnected from your selfoss instance.</string>
<string name="disable_ssl">Disable SSL</string>
</resources>
@@ -130,4 +130,5 @@
<string name="update_source">Update source</string>
<string name="confirm_disconnect_title">Disconnect ?</string>
<string name="confirm_disconnect_description">You will be disconnected from your selfoss instance.</string>
<string name="disable_ssl">Disable SSL</string>
</resources>
@@ -130,4 +130,5 @@
<string name="update_source">Update source</string>
<string name="confirm_disconnect_title">Disconnect ?</string>
<string name="confirm_disconnect_description">You will be disconnected from your selfoss instance.</string>
<string name="disable_ssl">Disable SSL</string>
</resources>
@@ -130,4 +130,5 @@
<string name="update_source">Update source</string>
<string name="confirm_disconnect_title">Disconnect ?</string>
<string name="confirm_disconnect_description">You will be disconnected from your selfoss instance.</string>
<string name="disable_ssl">Disable SSL</string>
</resources>
@@ -130,4 +130,5 @@
<string name="update_source">Update source</string>
<string name="confirm_disconnect_title">Disconnect ?</string>
<string name="confirm_disconnect_description">You will be disconnected from your selfoss instance.</string>
<string name="disable_ssl">Disable SSL</string>
</resources>
@@ -130,4 +130,5 @@
<string name="update_source">Update source</string>
<string name="confirm_disconnect_title">Disconnect ?</string>
<string name="confirm_disconnect_description">You will be disconnected from your selfoss instance.</string>
<string name="disable_ssl">Disable SSL</string>
</resources>
@@ -1,11 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<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>
<string name="menu_home_filter">Filters</string>
<string name="application_selfoss_only">This app only works with a Selfoss instance, and no other RSS feed.</string>
<string name="menu_home_sources">Sources</string>
<string name="update_source">Update source</string>
</resources>
@@ -130,4 +130,5 @@
<string name="update_source">Update source</string>
<string name="confirm_disconnect_title">Disconnect ?</string>
<string name="confirm_disconnect_description">You will be disconnected from your selfoss instance.</string>
<string name="disable_ssl">Disable SSL</string>
</resources>
@@ -130,4 +130,5 @@
<string name="update_source">Update source</string>
<string name="confirm_disconnect_title">Disconnect ?</string>
<string name="confirm_disconnect_description">You will be disconnected from your selfoss instance.</string>
<string name="disable_ssl">Disable SSL</string>
</resources>
@@ -130,4 +130,5 @@
<string name="update_source">Update source</string>
<string name="confirm_disconnect_title">Disconnect ?</string>
<string name="confirm_disconnect_description">You will be disconnected from your selfoss instance.</string>
<string name="disable_ssl">Disable SSL</string>
</resources>
@@ -130,4 +130,5 @@
<string name="update_source">Update source</string>
<string name="confirm_disconnect_title">Disconnect ?</string>
<string name="confirm_disconnect_description">You will be disconnected from your selfoss instance.</string>
<string name="disable_ssl">Disable SSL</string>
</resources>
@@ -130,4 +130,5 @@
<string name="update_source">Update source</string>
<string name="confirm_disconnect_title">Disconnect ?</string>
<string name="confirm_disconnect_description">You will be disconnected from your selfoss instance.</string>
<string name="disable_ssl">Disable SSL</string>
</resources>
@@ -130,4 +130,5 @@
<string name="update_source">更新源</string>
<string name="confirm_disconnect_title">Disconnect ?</string>
<string name="confirm_disconnect_description">You will be disconnected from your selfoss instance.</string>
<string name="disable_ssl">Disable SSL</string>
</resources>
@@ -130,4 +130,5 @@
<string name="update_source">Update source</string>
<string name="confirm_disconnect_title">Disconnect ?</string>
<string name="confirm_disconnect_description">You will be disconnected from your selfoss instance.</string>
<string name="disable_ssl">Disable SSL</string>
</resources>
@@ -6,6 +6,7 @@
<string name="error_invalid_password">"Password not long enough"</string>
<string name="error_field_required">"Field required"</string>
<string name="prompt_url">"Url"</string>
<string name="disable_ssl">"Disable SSL"</string>
<string name="withLoginSwitch">"Login required ?"</string>
<string name="login_url_problem">"Oops. You may need to add a \"/\" at the end of the url."</string>
<string name="prompt_login">"Username"</string>
+31 -20
View File
@@ -9,38 +9,49 @@ import org.junit.Test
class DatesTest {
private val v3Date = "2013-04-07T13:43:00+01:00"
private val v4Date = "2013-04-07 13:43:00"
private val bug1Date = "2022-12-24T17:00:08+00"
private val newVersionDateVariant = "2022-12-24T17:00:08+00"
private val newVersionDate = "2013-04-07T13:43:00+01:00"
private val oldVersionDate = "2013-05-07 13:46:00"
private val oldVersionDateVariant = "2021-03-21 10:32:00.000000"
@Test
fun v3_date_should_be_parsed() {
val date = DateUtils.parseDate(v3Date)
val expected =
LocalDateTime(2013, 4, 7, 14, 43, 0, 0).toInstant(TimeZone.currentSystemDefault())
.toEpochMilliseconds()
assertEquals(date, expected)
}
@Test
fun v4_date_should_be_parsed() {
val date = DateUtils.parseDate(v4Date)
fun new_version_date_should_be_parsed() {
val date = DateUtils.parseDate(newVersionDate)
val expected =
LocalDateTime(2013, 4, 7, 13, 43, 0, 0).toInstant(TimeZone.currentSystemDefault())
.toEpochMilliseconds()
assertEquals(date, expected)
assertEquals(expected, date)
}
@Test
fun bug1_date_should_be_parsed() {
val date = DateUtils.parseDate(bug1Date)
fun old_version_date_should_be_parsed() {
val date = DateUtils.parseDate(oldVersionDate)
val expected =
LocalDateTime(2022, 12, 24, 18, 0, 8, 0).toInstant(TimeZone.currentSystemDefault())
LocalDateTime(2013, 5, 7, 13, 46, 0, 0).toInstant(TimeZone.currentSystemDefault())
.toEpochMilliseconds()
assertEquals(date, expected)
assertEquals(expected, date)
}
@Test
fun old_version_variant_date_should_be_parsed() {
val date = DateUtils.parseDate(oldVersionDateVariant)
val expected =
LocalDateTime(2021, 3, 21, 10, 32, 0, 0).toInstant(TimeZone.currentSystemDefault())
.toEpochMilliseconds()
assertEquals(expected, date)
}
@Test
fun new_version_variant_date_should_be_parsed() {
val date = DateUtils.parseDate(newVersionDateVariant)
val expected =
LocalDateTime(2022, 12, 24, 17, 0, 8, 0).toInstant(TimeZone.currentSystemDefault())
.toEpochMilliseconds()
assertEquals(expected, date)
}
}
+2 -2
View File
@@ -7,8 +7,8 @@ buildscript {
plugins {
//trick: for the same plugin versions in all sub-modules
id("com.android.application").version("8.1.0").apply(false)
id("com.android.library").version("8.1.0").apply(false)
id("com.android.application").version("8.1.2").apply(false)
id("com.android.library").version("8.1.2").apply(false)
id("org.jetbrains.kotlin.android").version("1.9.10").apply(false)
kotlin("multiplatform").version("1.9.10").apply(false)
id("com.mikepenz.aboutlibraries.plugin").version("10.5.1").apply(false)
+1 -2
View File
@@ -19,11 +19,10 @@ kotlin.code.style=official
android.useAndroidX=true
#android.nonTransitiveRClass=true
android.enableJetifier=true
android.nonTransitiveRClass=true
android.nonTransitiveRClass=false
#MPP
kotlin.mpp.enableCInteropCommonization=true
org.gradle.parallel=true
org.gradle.caching=true
ignoreGitVersion=false
kotlin.native.cacheKind.iosX64=none
pushCache=true
-13
View File
@@ -1,5 +1,3 @@
val pushCache: String by settings
pluginManagement {
repositories {
// maven { url = uri("https://nexus.amine-louveau.fr/repository/maven-public/")}
@@ -17,17 +15,6 @@ dependencyResolutionManagement {
}
}
buildCache {
remote<HttpBuildCache> {
url = uri("http://18.0.0.7:3071/cache/")
isAllowInsecureProtocol = true
isAllowUntrustedServer = true
isUseExpectContinue = true
isPush = (pushCache == "true")
}
}
rootProject.name = "ReaderForSelfossV2"
include(":androidApp")
include(":shared")
+3 -2
View File
@@ -36,6 +36,7 @@ kotlin {
implementation("io.ktor:ktor-serialization-kotlinx-json:$ktorVersion")
implementation("io.ktor:ktor-client-logging:$ktorVersion")
implementation("io.ktor:ktor-client-auth:$ktorVersion")
implementation("io.ktor:ktor-client-cio:$ktorVersion")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.1")
implementation("org.jsoup:jsoup:1.15.4")
@@ -102,10 +103,10 @@ kotlin {
}
android {
compileSdk = 33
compileSdk = 34
sourceSets["main"].manifest.srcFile("src/androidMain/AndroidManifest.xml")
defaultConfig {
minSdk = 21
minSdk = 25
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
@@ -0,0 +1,17 @@
package bou.amine.apps.readerforselfossv2.rest
import io.ktor.client.engine.cio.CIOEngineConfig
import java.security.cert.X509Certificate
import javax.net.ssl.X509TrustManager
class NaiveTrustManager : X509TrustManager {
override fun checkClientTrusted(chain: Array<out X509Certificate>?, authType: String?) {}
override fun checkServerTrusted(chain: Array<out X509Certificate>?, authType: String?) {}
override fun getAcceptedIssuers(): Array<out X509Certificate> = arrayOf()
}
actual fun setupInsecureHTTPEngine(config: CIOEngineConfig) {
config.https.trustManager = NaiveTrustManager()
}
@@ -6,16 +6,25 @@ import kotlinx.datetime.*
actual class DateUtils {
actual companion object {
// Possible formats are
// yyyy-mm-dd hh:mm:ss format
private val oldVersionFormat = "\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}(.()\\d*)?".toRegex()
// yyyy-MM-dd'T'HH:mm:ss[.SSS]XXX (RFC3339)
private val newVersionFormat = "\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\+\\d{2}(:\\d{2})?".toRegex()
// We may need to consider moving the formatting to platform specific code, even if the tests are doubled
// For now, we handle this in a hacky way, because kotlin only accepts iso formats
actual fun parseDate(dateString: String): Long {
return try {
Instant.parse(dateString).toEpochMilliseconds()
} catch (e: Exception) {
var str = dateString.replace(" ", "T")
if (str.matches("\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\+\\d{2}".toRegex())) {
str = str.split("+")[0]
}
LocalDateTime.parse(str).toInstant(TimeZone.currentSystemDefault()).toEpochMilliseconds()
var isoDateString: String = if (dateString.matches(oldVersionFormat)) {
dateString.replace(" ", "T")
} else if (dateString.matches(newVersionFormat)) {
dateString.split("+")[0]
} else {
throw Exception("Unrecognized format for $dateString")
}
return LocalDateTime.parse(isoDateString).toInstant(TimeZone.currentSystemDefault()).toEpochMilliseconds()
}
actual fun parseRelativeDate(dateString: String): String {
@@ -1,19 +1,13 @@
package bou.amine.apps.readerforselfossv2.utils
import android.net.Uri
import android.os.Build
import android.text.Html
import bou.amine.apps.readerforselfossv2.model.SelfossModel
import org.jsoup.Jsoup
import java.util.*
actual fun String.getHtmlDecoded(): String {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
Html.fromHtml(this, Html.FROM_HTML_MODE_COMPACT).toString()
} else {
@Suppress("DEPRECATION")
Html.fromHtml(this).toString()
}
return Html.fromHtml(this).toString()
}
actual fun SelfossModel.Item.getIcon(baseUrl: String): String {
@@ -25,10 +19,19 @@ actual fun SelfossModel.Item.getThumbnail(baseUrl: String): String {
}
actual fun SelfossModel.Item.getImages(): ArrayList<String> {
val doc = Jsoup.parse(content)
val images = doc.getElementsByTag("img")
val allImages = ArrayList<String>()
return ArrayList(images.map { it.attr("src") })
for ( image in Jsoup.parse(content).getElementsByTag("img")) {
val url = image.attr("src")
if (url.lowercase(Locale.US).contains(".jpg") ||
url.lowercase(Locale.US).contains(".jpeg") ||
url.lowercase(Locale.US).contains(".png") ||
url.lowercase(Locale.US).contains(".webp"))
{
allImages.add(url)
}
}
return allImages
}
actual fun SelfossModel.Source.getIcon(baseUrl: String): String {
@@ -5,31 +5,46 @@ import bou.amine.apps.readerforselfossv2.model.StatusAndData
import bou.amine.apps.readerforselfossv2.model.SuccessResponse
import bou.amine.apps.readerforselfossv2.service.AppSettingsService
import io.github.aakira.napier.Napier
import io.ktor.client.*
import io.ktor.client.plugins.*
import io.ktor.client.plugins.auth.providers.*
import io.ktor.client.plugins.cache.*
import io.ktor.client.plugins.contentnegotiation.*
import io.ktor.client.plugins.cookies.*
import io.ktor.client.plugins.logging.*
import io.ktor.client.request.*
import io.ktor.client.statement.*
import io.ktor.http.*
import io.ktor.serialization.kotlinx.json.*
import io.ktor.util.*
import io.ktor.utils.io.charsets.*
import io.ktor.utils.io.core.*
import io.ktor.client.HttpClient
import io.ktor.client.engine.cio.CIO
import io.ktor.client.engine.cio.CIOEngineConfig
import io.ktor.client.plugins.HttpRequestRetry
import io.ktor.client.plugins.HttpTimeout
import io.ktor.client.plugins.auth.providers.BasicAuthCredentials
import io.ktor.client.plugins.cache.HttpCache
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
import io.ktor.client.plugins.cookies.HttpCookies
import io.ktor.client.plugins.logging.LogLevel
import io.ktor.client.plugins.logging.Logger
import io.ktor.client.plugins.logging.Logging
import io.ktor.client.request.get
import io.ktor.client.request.headers
import io.ktor.client.request.parameter
import io.ktor.client.statement.HttpResponse
import io.ktor.http.HttpHeaders
import io.ktor.http.HttpStatusCode
import io.ktor.http.Parameters
import io.ktor.serialization.kotlinx.json.json
import io.ktor.util.encodeBase64
import io.ktor.utils.io.charsets.Charsets
import io.ktor.utils.io.core.toByteArray
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.serialization.json.Json
expect fun setupInsecureHTTPEngine(config: CIOEngineConfig)
class SelfossApi(private val appSettingsService: AppSettingsService) {
var client = createHttpClient()
private fun createHttpClient(): HttpClient {
val client = HttpClient {
fun createHttpClient() =
HttpClient(CIO) {
if (appSettingsService.getSelfSigned()) {
engine {
setupInsecureHTTPEngine(this)
}
}
install(ContentNegotiation) {
install(HttpCache)
json(Json {
@@ -60,7 +75,7 @@ class SelfossApi(private val appSettingsService: AppSettingsService) {
Napier.i("Will modify", tag = "HttpSend")
CoroutineScope(Dispatchers.Main).launch {
Napier.i("Will login", tag = "HttpSend")
this@SelfossApi.login()
login()
Napier.i("Did login", tag = "HttpSend")
}
}
@@ -68,10 +83,6 @@ class SelfossApi(private val appSettingsService: AppSettingsService) {
expectSuccess = false
}
return client
}
fun url(path: String) =
"${appSettingsService.getBaseUrl()}$path"
@@ -8,6 +8,7 @@ class AppSettingsService(acraSenderServiceProcess: Boolean = false) {
// Api related
private var _apiVersion: Int = -1
private var _publicAccess: Boolean? = null
private var _selfSigned: Boolean? = null
private var _baseUrl: String = ""
private var _userName: String = ""
private var _basicUserName: String = ""
@@ -77,6 +78,22 @@ class AppSettingsService(acraSenderServiceProcess: Boolean = false) {
_publicAccess = settings.getBoolean(API_PUBLIC_ACCESS, false)
}
fun getSelfSigned(): Boolean {
if (_selfSigned == null) {
refreshSelfSigned()
}
return _selfSigned!!
}
fun updateSelfSigned(selfSigned: Boolean) {
settings.putBoolean(API_SELF_SIGNED, selfSigned)
refreshSelfSigned()
}
private fun refreshSelfSigned() {
_selfSigned = settings.getBoolean(API_SELF_SIGNED, false)
}
fun getBaseUrl(): String {
if (_baseUrl.isEmpty()) {
refreshBaseUrl()
@@ -383,6 +400,7 @@ class AppSettingsService(acraSenderServiceProcess: Boolean = false) {
refreshBaseUrl()
refreshApiVersion()
refreshPublicAccess()
refreshSelfSigned()
}
fun refreshUserSettings() {
@@ -468,6 +486,8 @@ class AppSettingsService(acraSenderServiceProcess: Boolean = false) {
const val API_PUBLIC_ACCESS = "apiPublicAccess"
const val API_SELF_SIGNED = "apiSelfSigned"
const val API_ITEMS_NUMBER = "prefer_api_items_number"
const val API_TIMEOUT = "api_timeout"
@@ -17,7 +17,7 @@ fun SOURCE.toView(): SelfossModel.SourceDetail =
this.id.toInt(),
this.title,
null,
this.tags.split(","),
this.tags?.split(","),
this.spout,
this.error,
this.icon,
@@ -8,20 +8,4 @@ enum class ItemType(val position: Int, val type: String) {
companion object {
fun fromInt(value: Int) = values().first { it.position == value }
}
}
enum class ImageMimeType(val extension: String, val mimeType: String) {
JPG(".jpg", "image/jpeg"),
JPEG(".jpeg", "image/jpeg"),
PNG(".png", "image/png"),
WEBP(".webp", "image/webp"),
GIF(".gif", "image/gif"),
SVG(".svg", "image/svg+xml"),
BMP(".bmp", "image/bmp"),
TIFF(".tiff", "image/tiff"),
TIF(".tif", "image/tiff");
companion object {
fun findMimeTypeByExtension(ext: String) = values().find { it.extension == ext }?.mimeType
}
}
@@ -0,0 +1,6 @@
package bou.amine.apps.readerforselfossv2.rest
import io.ktor.client.engine.cio.CIOEngineConfig
actual fun setupInsecureHTTPEngine(config: CIOEngineConfig) {
}
@@ -0,0 +1,6 @@
package bou.amine.apps.readerforselfossv2.rest
import io.ktor.client.engine.cio.CIOEngineConfig
actual fun setupInsecureHTTPEngine(config: CIOEngineConfig) {
}