Commit e7c15342 authored by Christophe Henry's avatar Christophe Henry

Merge branch 'fix-webview-api-21' into 'develop'

Fix webview api 21

See merge request !74
parents 45146842 225ca4b9
Pipeline #3598 canceled with stage
in 0 seconds
......@@ -11,7 +11,7 @@ android {
compileSdkVersion 28
defaultConfig {
applicationId "fr.chenry.android.freshrss"
minSdkVersion 23
minSdkVersion 21
targetSdkVersion 28
versionCode 12
versionName "1.2.2"
......@@ -114,6 +114,7 @@ dependencies {
def jsoup_version = '1.12.2'
def acraVersion = '5.5.0'
def autoservice_version = "1.0-rc6"
def android_test = "1.2.0"
// Linter
ktlint "com.github.shyiko:ktlint:0.31.0"
......@@ -187,9 +188,11 @@ dependencies {
// Tests
testImplementation "junit:junit:4.13"
androidTestImplementation "androidx.test:runner:1.2.0"
androidTestImplementation "androidx.test:rules:$android_test"
androidTestImplementation "androidx.test:runner:$android_test"
androidTestImplementation "androidx.test.ext:junit:1.1.1"
androidTestImplementation "androidx.test.espresso:espresso-core:$espresso_version"
androidTestImplementation "androidx.test.espresso:espresso-contrib:$espresso_version"
androidTestImplementation "com.github.javafaker:javafaker:1.0.2"
// Debug
......
package fr.chenry.android.freshrss.activities
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.action.ViewActions.click
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.contrib.DrawerActions
import androidx.test.espresso.contrib.DrawerMatchers.isClosed
import androidx.test.espresso.contrib.DrawerMatchers.isOpen
import androidx.test.espresso.matcher.ViewMatchers.*
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.LargeTest
import androidx.test.rule.ActivityTestRule
import fr.chenry.android.freshrss.R
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
@LargeTest
class MainActivityTest {
@get:Rule
val activityRule = ActivityTestRule(MainActivity::class.java)
@Test
fun `check-drawer-is-closed-when-adding-a-new-subscription`() {
// Open drawer
onView(withId(R.id.activity_main_navigation_drawer)).perform(DrawerActions.open())
onView(withId(R.id.activity_main_navigation_drawer)).check(matches(isOpen()))
onView(withText(R.string.title_add_subscription)).perform(click())
onView(withText(android.R.string.ok)).perform(click())
onView(withId(R.id.activity_main_navigation_drawer)).check(matches(isClosed()))
}
}
......@@ -10,6 +10,7 @@ import androidx.core.app.NotificationCompat
import fr.chenry.android.freshrss.R.drawable
import fr.chenry.android.freshrss.R.string
import fr.chenry.android.freshrss.store.Store
import fr.chenry.android.freshrss.store.database.models.VoidAccount
import fr.chenry.android.freshrss.utils.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runBlocking
......@@ -48,6 +49,9 @@ class RefresherService: Service() {
fun refresh(manual: Boolean = true): Promise<Unit, Exception> {
if(Store.refreshingPromise.value != null) return Store.refreshingPromise.value!!
// This case should not happen. Blocking only for testing
if((Store.account.value ?: VoidAccount) == VoidAccount) return Promise.ofSuccess(Unit)
if(!F.context.isConnectedToNetwork()) {
if(manual) {
Toast.makeText(
......
package fr.chenry.android.freshrss.activities
import android.content.res.AssetManager
import android.os.Bundle
import android.view.MenuItem
import android.widget.TextView
......@@ -22,6 +23,7 @@ import kotlinx.android.synthetic.main.activity_main.*
class MainActivity: AppCompatActivity() {
private val accountVM by viewModels<AccountVM>()
private val navigation: NavController by lazy {
Navigation.findNavController(this, R.id.main_activity_host_fragment)
}
......@@ -66,13 +68,19 @@ class MainActivity: AppCompatActivity() {
return super.onOptionsItemSelected(item)
}
fun onAddSubscriptionClick(@Suppress("UNUSED_PARAMETER") menuItem: MenuItem): Boolean =
// TODO: Remove this function when androidx.appcompat:appcompat:1.2.0 is released
// See https://stackoverflow.com/a/59961940
override fun getAssets(): AssetManager = resources.assets
fun onAddSubscriptionClick(@Suppress("UNUSED_PARAMETER") item: MenuItem) {
drawerLayout.closeDrawers()
AddSubscriptionDialog {Store.postAddSubscription(it).fail(this::e)}
.show(main_activity_host_fragment!!.childFragmentManager, AddSubscriptionDialog::class.java.canonicalName)
.let {true}
}
fun onSettingsItemClick(@Suppress("UNUSED_PARAMETER") menuItem: MenuItem): Boolean =
navigation.navigate(MainSubscriptionFragmentDirections.mainSubscriptionsToSettings()).let {true}
fun onSettingsItemClick(@Suppress("UNUSED_PARAMETER") item: MenuItem) =
navigation.navigate(MainSubscriptionFragmentDirections.mainSubscriptionsToSettings())
override fun onSupportNavigateUp(): Boolean =
navigation.navigateUp(appBarConfiguration) || super.onSupportNavigateUp()
......
......@@ -4,8 +4,7 @@ import android.app.Dialog
import android.content.ClipboardManager
import android.content.Context
import android.os.Bundle
import android.widget.EditText
import android.widget.TextView
import android.widget.*
import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.DialogFragment
import fr.chenry.android.freshrss.F
......@@ -19,39 +18,38 @@ class AddSubscriptionDialog(private val callback: (String) -> Unit): DialogFragm
}
private val dialogView by lazy {
requireActivity().layoutInflater.inflate(R.layout.fragment_add_subscription_dialog, null)
requireActivity().layoutInflater.inflate(
R.layout.fragment_add_subscription_dialog,
LinearLayout(context),
false
)
}
private val editText get() = dialogView.findViewById<EditText>(R.id.fragment_add_subscription_dialog_text_field)
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
return activity!!.let {
val builder = AlertDialog.Builder(it)
val result = builder.setView(dialogView)
.setPositiveButton(android.R.string.ok) {_, _ ->
callback(tryProcessYoutubeUrl(editText.text.toString()))
}
.setNegativeButton(android.R.string.cancel) {dialog, _ -> dialog.cancel()}
.create()
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog = requireActivity().let {
val result = AlertDialog.Builder(it).setView(dialogView)
.setPositiveButton(android.R.string.ok) {_, _ -> callback(tryProcessYoutubeUrl(editText.text.toString()))}
.setNegativeButton(android.R.string.cancel) {dialog, _ -> dialog.cancel()}
.create()
editText?.let {self ->
if(clipboard.hasPrimaryClip()) {
val item = clipboard.primaryClip!!.getItemAt(0)
val uris = when {
item.text != null -> item.text.toString().extractURLs()
item.uri != null -> listOf(item.uri)
else -> listOf()
}
editText?.let {self ->
if(clipboard.hasPrimaryClip()) {
val item = clipboard.primaryClip!!.getItemAt(0)
val uris = when {
item.text != null -> item.text.toString().extractURLs()
item.uri != null -> listOf(item.uri)
else -> listOf()
}
if(uris.isNotEmpty()) {
self.setText(uris[0].toString(), TextView.BufferType.EDITABLE)
self.selectAll()
}
if(uris.isNotEmpty()) {
self.setText(uris[0].toString(), TextView.BufferType.EDITABLE)
self.selectAll()
}
}
result
}
result
}
companion object {
......
package fr.chenry.android.freshrss.components.subscriptionarticles.webviewutils
import android.content.Context
import android.os.Build
import android.util.AttributeSet
import android.webkit.WebSettings.MENU_ITEM_NONE
import android.webkit.WebView
import androidx.fragment.app.Fragment
import fr.chenry.android.freshrss.F
......@@ -11,22 +9,19 @@ import fr.chenry.android.freshrss.R
import fr.chenry.android.freshrss.store.database.models.Article
import org.jsoup.Jsoup
// WebView crashing on Lollipop devices
// See https://stackoverflow.com/a/49024931
class FRSSWebView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0,
defStyleRes: Int = 0
): WebView(context.applicationContext, attrs, defStyleAttr, defStyleRes) {
): WebView(context, attrs, defStyleAttr, defStyleRes) {
init {
isFocusable = true
isFocusableInTouchMode = true
settings.javaScriptEnabled = false
settings.defaultTextEncodingName = "UTF-8"
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
settings.disabledActionModeMenuItems = MENU_ITEM_NONE
}
}
fun load(fragment: Fragment, article: Article) {
......@@ -61,13 +56,14 @@ class FRSSWebView @JvmOverloads constructor(
it.attr("data-original-href", linkToOriginalImage)
if(type.isNotBlank()) it.attr("data-original-type", type)
val newImage = it.clone()
link.after(newImage).remove()
newImage.after("""
newImage.after(
"""
|<a href="$linkToOriginalImage" class="link-to-original-image">
| ${F.getString(R.string.link_to_original_image)}
|</a>""".trimMargin())
|</a>""".trimMargin()
)
}
return doc.html()
......
......@@ -11,6 +11,8 @@ class FRSSWebViewClient(private val fragment: Fragment, private val sourceUrl: U
if (request?.url?.scheme?.startsWith("mailto") == true) {
val mt = MailTo.parse(request.url.toString())
fragment.startActivity(MailIntent(mt.to, mt.subject, mt.body, mt.cc))
return true
} else if (request?.url?.scheme?.startsWith("http") == true) {
if (request.url.isRelative && sourceUrl == null) return true
......@@ -18,8 +20,10 @@ class FRSSWebViewClient(private val fragment: Fragment, private val sourceUrl: U
sourceUrl!!.buildUpon().path(request.url.path ?: "").build() else request.url
fragment.startActivity(Intent(Intent.ACTION_VIEW).apply { data = url })
return true
}
return true
return false
}
}
package fr.chenry.android.freshrss.components.subscriptionarticles.webviewutils
import android.content.Intent
import android.content.Intent.*
import android.content.Intent.ACTION_SEND
import android.content.Intent.EXTRA_TEXT
import com.x5.template.Theme
import fr.chenry.android.freshrss.F
import fr.chenry.android.freshrss.R
import fr.chenry.android.freshrss.components.subscriptionarticles.webviewutils.chunkfilters.SentenceCapFilter
import fr.chenry.android.freshrss.components.subscriptionarticles.webviewutils.chunkfilters.StripFragmentFilter
import fr.chenry.android.freshrss.utils.nullIfBlank
object ShareIntent {
private val defaultTemplate = """
......@@ -29,7 +27,5 @@ object ShareIntent {
fun create(attributes: Map<String, String>) = Intent(ACTION_SEND).apply {
type = "text/plain"
putExtra(EXTRA_TEXT, format(attributes))
val feedName = attributes["subscription"]?.nullIfBlank() ?: F.getString(R.string.this_feed)
putExtra(EXTRA_SUBJECT, F.getString(R.string.share_article, feedName))
}
}
......@@ -116,8 +116,11 @@ object Store {
}
}
fun postAddSubscription(url: String): Promise<Unit, Exception> =
ensureToken().bind {
fun postAddSubscription(url: String): Promise<Unit, Exception> {
// This case should not happen. Blocking only for testing
if((account.value ?: VoidAccount) == VoidAccount) return Promise.ofSuccess(Unit)
return ensureToken().bind {
val addSubscriptionPromise = api.postAddSubscription(url)
addSubscriptionPromise successUi {id ->
......@@ -145,4 +148,5 @@ object Store {
addSubscriptionPromise.toSuccessVoid()
}
}
}
......@@ -52,7 +52,7 @@ class Api(val account: Account) {
.promise()
}
fun postReadStatus(itemId: ItemId, readStatus: ReadStatus): Promise<Unit, Exception> {
fun postReadStatus(itemId: String, readStatus: ReadStatus): Promise<Unit, Exception> {
val parameters = listOf(
"i" to itemId,
(if (readStatus == ReadStatus.READ) "a" else "r") to "user/-/state/com.google/read"
......
......@@ -4,7 +4,7 @@ import com.fasterxml.jackson.core.JsonParser
import com.fasterxml.jackson.databind.DeserializationContext
import com.fasterxml.jackson.databind.annotation.JsonDeserialize
import com.fasterxml.jackson.databind.deser.std.StringDeserializer
import fr.chenry.android.freshrss.utils.unescapeHtml4
import fr.chenry.android.freshrss.utils.unescape
typealias StreamId = String
......@@ -26,5 +26,5 @@ data class SubscriptionCategoryApiItem(
)
class HtmlEntitiesDeserializer : StringDeserializer() {
override fun deserialize(p: JsonParser?, ctxt: DeserializationContext?) = super.deserialize(p, ctxt).unescapeHtml4()
override fun deserialize(p: JsonParser?, ctxt: DeserializationContext?) = super.deserialize(p, ctxt).unescape()
}
......@@ -22,10 +22,10 @@ class Converters {
fun readStatusToString(liveData: ReadStatus) = liveData.name
@TypeConverter
fun listOfStringToString(list: List<String>) = list.map { it.escapeHtml4() }.joinToString(" ")
fun listOfStringToString(list: List<String>) = list.map { it.escape() }.joinToString(" ")
@TypeConverter
fun stringToListOfString(string: String) = string.split("\\s+".toRegex()).map { it.unescapeHtml4() }
fun stringToListOfString(string: String) = string.split("\\s+".toRegex()).map { it.unescape() }
@TypeConverter
fun bitmapToBlob(bitmap: Bitmap?) =
......
......@@ -6,7 +6,7 @@ import dev.matrix.roomigrant.GenerateRoomMigrations
import fr.chenry.android.freshrss.store.api.models.StreamId
import fr.chenry.android.freshrss.store.database.models.*
import fr.chenry.android.freshrss.utils.Try
import fr.chenry.android.freshrss.utils.escapeHtml4
import fr.chenry.android.freshrss.utils.escape
import kotlinx.coroutines.*
import org.joda.time.LocalDateTime
......@@ -28,7 +28,7 @@ abstract class FreshRSSDabatabase : RoomDatabase() {
// Articles
fun getArticlesByStreamId(streamId: StreamId) = getArticlesDAO().getByStreamId(streamId)
fun getArticleById(id: ItemId) = getArticlesDAO().getById(id)
fun getArticleById(id: String) = getArticlesDAO().getById(id)
fun getArticleByStreamIdAndUnread(streamId: StreamId) =
getArticlesDAO().getByStreamIdAndUnread(streamId, ReadStatus.UNREAD.name)
......@@ -67,9 +67,9 @@ abstract class FreshRSSDabatabase : RoomDatabase() {
fun insertSubscriptionImage(id: String, bitmap: Bitmap) = getSubscriptionsDAO().insertImage(id, bitmap)
fun getSubcriptionsById(id: String) = getSubscriptionsDAO().byId(id)
fun getSubcriptionsBySubscriptionCategory(subscriptionCategory: SubscriptionCategory) =
getSubscriptionsDAO().bySubscriptionCategory("%${subscriptionCategory.id.escapeHtml4()}%")
getSubscriptionsDAO().bySubscriptionCategory("%${subscriptionCategory.id.escape()}%")
fun getSubcriptionsBySubscriptionCategoryAndUnreadCount(subscriptionCategory: SubscriptionCategory) =
getSubscriptionsDAO().bySubscriptionCategoryAndUnreadCount("%${subscriptionCategory.id.escapeHtml4()}%")
getSubscriptionsDAO().bySubscriptionCategoryAndUnreadCount("%${subscriptionCategory.id.escape()}%")
fun getAllSubcriptions() = getSubscriptionsDAO().getAll()
fun getAllUnreadSubcriptions() = getSubscriptionsDAO().getAllUnread()
suspend fun getAllSubcriptionsIds() = getSubscriptionsDAO().getAllIds()
......
......@@ -21,7 +21,6 @@ import io.reactivex.Flowable
import kotlinx.android.parcel.Parcelize
import org.joda.time.LocalDateTime
typealias ItemId = String
typealias Articles = List<Article>
@Entity(tableName = "articles")
......@@ -92,7 +91,7 @@ interface ArticlesDAO {
fun getByStreamId(streamId: StreamId): LiveData<Articles>
@Query("SELECT * FROM articles WHERE id = :id")
fun getById(id: ItemId): Flowable<Articles>
fun getById(id: String): Flowable<Articles>
@Query("SELECT * FROM articles WHERE streamId = :streamId AND readStatus = :readStatus")
fun getByStreamIdAndUnread(streamId: StreamId, readStatus: String = ReadStatus.UNREAD.name): LiveData<Articles>
......
......@@ -4,7 +4,7 @@ import androidx.lifecycle.*
import fr.chenry.android.freshrss.F
import fr.chenry.android.freshrss.store.database.models.*
class SubscriptionArticleVM(articleId: ItemId) : ViewModel() {
class SubscriptionArticleVM(articleId: String) : ViewModel() {
val liveData: LiveData<Article>
val subscription: Subscription
private val source: LiveData<Articles>
......@@ -23,7 +23,7 @@ class SubscriptionArticleVM(articleId: ItemId) : ViewModel() {
}
}
class SubscriptionArticleVMF(private val articleId: ItemId) : ViewModelProvider.NewInstanceFactory() {
class SubscriptionArticleVMF(private val articleId: String) : ViewModelProvider.NewInstanceFactory() {
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
@Suppress("UNCHECKED_CAST")
return SubscriptionArticleVM(articleId) as T
......
......@@ -75,7 +75,7 @@ fun Request.authentify(account: Account) =
fun Request.authorize(account: Account) = this.header("T" to account.writeToken)
fun Any?.unit(): Unit = Unit
fun Any?.unit() = Unit
fun Any.v(message: String) = Log.v(this::class.qualifiedName, message).unit()
fun Any.v(message: Throwable) = Log.v(this::class.qualifiedName, "VERBOSE", message).unit()
fun Any.d(message: String) = Log.d(this::class.qualifiedName, message).unit()
......@@ -91,8 +91,14 @@ fun Any.wtf(message: Throwable) = Log.wtf(this::class.qualifiedName, "WTF", mess
fun String.addTrailingSlash() = if(this.endsWith("/")) this else "$this/"
fun String.removeNewLines() = this.replace("\\s+".toRegex(), " ")
fun String.escapeHtml4() = StringEscapeUtils.escapeHtml4(this).orEmpty()
fun String.unescapeHtml4() = StringEscapeUtils.unescapeHtml4(this).orEmpty()
fun String.escape() = StringEscapeUtils.escapeHtml4(this).orEmpty()
// See https://github.com/FreshRSS/FreshRSS/issues/2770#issuecomment-575208803
fun String?.unescape() = StringEscapeUtils.unescapeHtml4(this).orEmpty().let {s->
var res = s
listOf('’' to '\'', '"' to '"', '^' to '^', '?' to '?', '\' to '\\', '/' to '/', ',' to ',', ';' to ';')
.forEach {res = res.replace(it.first, it.second)}
res
}
fun String?.nullIfBlank() = if(this.isNullOrBlank()) null else this
fun String?.extractURLs(): List<Uri> = if(this.isNullOrBlank()) listOf() else {
this
......
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<menu
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:title="@string/title_add_subscription"
android:id="@+id/activity_main_drawer_add_subscription"
android:icon="@drawable/ic_add_black_24dp"
android:onClick="onAddSubscriptionClick" />
android:onClick="onAddSubscriptionClick"
app:showAsAction="withText"/>
<item android:title="@string/application">
<menu>
......@@ -12,7 +14,8 @@
android:title="@string/title_settings"
android:id="@+id/activity_main_drawer_settings"
android:icon="@drawable/ic_settings_black_24dp"
android:onClick="onSettingsItemClick" />
android:onClick="onSettingsItemClick"
app:showAsAction="withText"/>
</menu>
</item>
......
......@@ -35,7 +35,6 @@
<string name="refresh_frequency_30m">30 دقيقة</string>
<string name="title_settings">الإعدادات</string>
<string name="nav_header_subtitle">القائمة</string>
<string name="this_feed">هذا التدفق</string>
<string name="subscription_categories">الفئات</string>
<string name="good_evening_user">عمّ مساؤك %s</string>
<string name="good_morning_user">أسعدت صباحًا %s</string>
......
......@@ -27,7 +27,6 @@
<string name="title_unread">Non lus</string>
<string name="unread">non lu</string>
<string name="title_favorites">Favoris</string>
<string name="this_feed">ce flux</string>
<string name="read">lu</string>
<string name="prompt_login">Identifiant</string>
<string name="error_instance">Cette URL n\'est pas correcte</string>
......
......@@ -94,7 +94,6 @@
<string name="good_evening_user">Good evening%s</string>
<string name="subscription_categories">Categories</string>
<string name="share_article">Share article of %s</string>
<string name="this_feed">this feed</string>
<string name="instance_url_malformed">This URL is malformed</string>
<string name="no_internet_connection_avaible">There is no internet connection avaible for now, please retry later</string>
<!-- Other messages -->
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment