Commit 5c8e414b authored by Christophe Henry's avatar Christophe Henry

Fix webview crash on Android Lollipop

parent 45146842
......@@ -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
}
}
......@@ -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"
......
......@@ -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)
......
......@@ -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
......
<?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>
......
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