Commit d8239a65 authored by Christophe Henry's avatar Christophe Henry
Browse files

Merge branch 'fix-open-browser-problem' into 'develop'

Fix #106: 'Open' button has disapeared

Closes #106

See merge request !119
parents d09d41b8 29d5e9b6
Pipeline #4838 passed with stage
in 0 seconds
# Development
# 1.3.4
## Bug fixes
* Fix [#106](https://git.feneas.org/christophehenry/freshrss-android/-/issues/106) regression introduced in 1.3.3 ([!119](https://git.feneas.org/christophehenry/freshrss-android/-/merge_requests/119))
# 1.3.3
## Features
* Add possibility to enable a debug mode ([!108](https://git.feneas.org/christophehenry/freshrss-android/-/merge_requests/108))
......
......@@ -109,7 +109,7 @@ dependencies {
def fragment_version = "1.2.5"
def lifecycle_version = "2.2.0"
def room_version = "2.2.5"
def roomigrant_version = "0.2.6"
def roomigrant_version = "0.2.0"
def jackson_version = "2.11.2"
def espresso_version = "3.3.0"
def espresso_idling_version = "3.3.0"
......@@ -160,8 +160,8 @@ dependencies {
implementation "androidx.room:room-runtime:$room_version"
implementation "androidx.room:room-rxjava2:$room_version"
implementation "io.reactivex.rxjava2:rxandroid:2.1.1"
implementation "com.github.RBusarow.Roomigrant:RoomigrantLib:$roomigrant_version"
kapt "com.github.RBusarow.Roomigrant:RoomigrantCompiler:$roomigrant_version"
implementation "com.github.MatrixDev.Roomigrant:RoomigrantLib:$roomigrant_version"
kapt "com.github.MatrixDev.Roomigrant:RoomigrantCompiler:$roomigrant_version"
// HTTP and promises
implementation "com.fasterxml.jackson.module:jackson-module-kotlin:$jackson_version"
......
......@@ -5,37 +5,31 @@ import androidx.core.view.GravityCompat
import androidx.drawerlayout.widget.DrawerLayout
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.NoActivityResumedException
import androidx.test.espresso.action.ViewActions
import androidx.test.espresso.action.ViewActions.click
import androidx.test.espresso.action.ViewActions.*
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.rules.ActivityScenarioRule
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.LargeTest
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.rule.ActivityTestRule
import androidx.test.runner.lifecycle.ActivityLifecycleMonitorRegistry
import androidx.test.runner.lifecycle.Stage
import fr.chenry.android.freshrss.R
import fr.chenry.android.freshrss.utils.FreshRSSBaseTest
import junit.framework.TestCase.assertFalse
import org.junit.Assert.*
import org.junit.Rule
import org.junit.Test
import org.junit.rules.ExpectedException
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
@LargeTest
class MainActivityTest: FreshRSSBaseTest() {
@get:Rule
val activityRule = ActivityTestRule(MainActivity::class.java)
@get:Rule
var exceptionRule = ExpectedException.none()!!
val activityRule = ActivityScenarioRule(MainActivity::class.java)
@Test
fun `drawer-is-closed-when-adding-a-new-subscription`() {
......@@ -52,13 +46,13 @@ class MainActivityTest: FreshRSSBaseTest() {
fun `drawer-is-closed-before-application-exits`() {
onView(withId(R.id.activity_main_navigation_drawer)).perform(DrawerActions.open())
onView(withId(R.id.activity_main_navigation_drawer)).check(matches(isOpen()))
onView(isRoot()).perform(ViewActions.pressBack())
onView(isRoot()).perform(pressBack())
onView(withId(R.id.activity_main_navigation_drawer)).check(matches(isClosed()))
// We assert that pressing back again exists the app
exceptionRule.expect(NoActivityResumedException::class.java)
onView(isRoot()).perform(ViewActions.pressBack())
assertThrows(NoActivityResumedException::class.java) {
onView(isRoot()).perform(pressBack())
}
}
// See https://stackoverflow.com/a/60341723/3459089
......
......@@ -3,31 +3,30 @@ package fr.chenry.android.freshrss.components.articles
import android.app.Activity.RESULT_OK
import android.app.Instrumentation
import android.content.Intent
import androidx.fragment.app.testing.FragmentScenario
import androidx.fragment.app.testing.launchFragmentInContainer
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.action.ViewActions.*
import androidx.test.espresso.assertion.ViewAssertions.*
import androidx.test.espresso.intent.Intents
import androidx.test.espresso.intent.Intents.intended
import androidx.test.espresso.intent.Intents.intending
import androidx.test.espresso.intent.matcher.IntentMatchers.*
import androidx.test.espresso.intent.rule.IntentsTestRule
import androidx.test.espresso.matcher.ViewMatchers.*
import androidx.test.espresso.web.assertion.WebViewAssertions.webMatches
import androidx.test.espresso.web.sugar.Web.onWebView
import androidx.test.espresso.web.webdriver.DriverAtoms.findElement
import androidx.test.espresso.web.webdriver.DriverAtoms.getText
import androidx.test.espresso.web.webdriver.Locator
import androidx.test.ext.junit.rules.ActivityScenarioRule
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.LargeTest
import androidx.test.internal.runner.junit4.statement.UiThreadStatement.runOnUiThread
import fr.chenry.android.freshrss.MainNavDirections
import fr.chenry.android.freshrss.R
import fr.chenry.android.freshrss.activities.MainActivity
import fr.chenry.android.freshrss.components.articles.webviewutils.FRSSWebView
import fr.chenry.android.freshrss.components.articles.webviewutils.ShareIntent
import fr.chenry.android.freshrss.utils.*
import fr.chenry.android.freshrss.utils.DisableAnimationsRule
import fr.chenry.android.freshrss.utils.FreshRSSBaseTest
import fr.chenry.android.freshrss.utils.factories.ArticleFactory
import fr.chenry.android.freshrss.utils.factories.SubscriptionFactory
import kotlinx.coroutines.runBlocking
import org.hamcrest.Matcher
import org.hamcrest.Matchers.*
......@@ -36,16 +35,30 @@ import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
@LargeTest
@Ignore("Something makes the instrumented tests to randomly crash for no obvious reason")
class ArticleDetailFragmentTest: FreshRSSBaseTest() {
private val bundle = ArticleDetailFragmentArgs(application.article.id).toBundle()
private val brokenArticle = ArticleFactory.createArticle(streamId = application.feed.id).let {
it.copy(href = it.href.replace("https?://".toRegex(), ""))
}
@get:Rule
val activityRule = IntentsTestRule(MainActivity::class.java)
private val bundle = ArticleDetailFragmentArgs(application.article.id).toBundle()
@get:Rule
@get:Rule(order = 0)
val disableAnimationsRule = DisableAnimationsRule()
@get:Rule(order = 1)
val activityRule = ActivityScenarioRule(MainActivity::class.java)
@Before
fun setUp() {
Intents.init()
runBlocking {
application.db.insertArticles(listOf(brokenArticle))
}
}
@After
fun tearDown() = Intents.release()
@Test
fun `should-display-article`() {
launchFragmentInContainer<ArticleDetailFragment>(
......@@ -70,7 +83,8 @@ class ArticleDetailFragmentTest: FreshRSSBaseTest() {
fragmentArgs = bundle
)
val expectedExtraText = ShareIntent.format(ShareIntent.getTemplateAttributes(application.feed.title, application.article))
val expectedExtraText =
ShareIntent.format(ShareIntent.getTemplateAttributes(application.feed.title, application.article))
intending(any(Intent::class.java)).respondWithFunction {
Instrumentation.ActivityResult(RESULT_OK, Intent())
......@@ -100,4 +114,14 @@ class ArticleDetailFragmentTest: FreshRSSBaseTest() {
onView(withId(R.id.fab_open_browser)).perform(click())
intended(allOf(hasAction(Intent.ACTION_VIEW), hasData(application.article.url)))
}
@Test
fun `should-not-display-browser-button-when-no-app-can-open`() {
launchFragmentInContainer<ArticleDetailFragment>(
themeResId = R.style.AppTheme,
fragmentArgs = ArticleDetailFragmentArgs(brokenArticle.id).toBundle()
)
onView(withId(R.id.fab_open_browser)).check(matches(not(isDisplayed())))
}
}
package fr.chenry.android.freshrss.components.navigationdrawer
import androidx.annotation.StringRes
import androidx.fragment.app.testing.launchFragmentInContainer
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.action.ViewActions.*
......@@ -8,31 +7,30 @@ import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.contrib.DrawerActions
import androidx.test.espresso.contrib.DrawerMatchers
import androidx.test.espresso.matcher.ViewMatchers.*
import androidx.test.ext.junit.rules.ActivityScenarioRule
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.LargeTest
import androidx.test.rule.ActivityTestRule
import fr.chenry.android.freshrss.R
import fr.chenry.android.freshrss.activities.MainActivity
import fr.chenry.android.freshrss.utils.*
import org.hamcrest.Matchers.equalTo
import org.hamcrest.Matchers.matchesPattern
import org.hamcrest.Matchers.*
import org.junit.*
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
@LargeTest
class SettingsFragmentTest: FreshRSSBaseTest() {
@get:Rule
val activityRule = ActivityTestRule(MainActivity::class.java)
val activityRule = ActivityScenarioRule(MainActivity::class.java)
@get:Rule
val disableAnimationsRule = DisableAnimationsRule()
@Before
fun setUp() {
activityRule.activity.requireF().preferences.reset()
activityRule.scenario.onActivity {
it.requireF().preferences.reset()
}
}
@Test
......@@ -43,12 +41,14 @@ class SettingsFragmentTest: FreshRSSBaseTest() {
onView(isRoot()).check {_, noViewFoundException ->
if(noViewFoundException != null) throw noViewFoundException
val activity = activityRule.activity as MainActivity
activityRule.scenario.onActivity {
assertThat(
activity.navigation.currentDestination!!,
equalTo(activity.navigation.graph.findNode(R.id.settingsFragment))
it.navigation.currentDestination!!,
equalTo(it.navigation.graph.findNode(R.id.settingsFragment))
)
}
}
}
@Test
......
......@@ -15,11 +15,9 @@ class DisableAnimationsRule: TestRule {
override fun apply(base: Statement, description: Description?) = object: Statement() {
override fun evaluate() {
disableAnimations()
try{
base.evaluate()
} finally {
val result = runCatching(base::evaluate)
enableAnimations()
}
if(result.isFailure) throw result.exceptionOrNull()!!
}
}
......
......@@ -10,13 +10,9 @@ import fr.chenry.android.freshrss.utils.factories.ArticleFactory
import fr.chenry.android.freshrss.utils.factories.SubscriptionFactory
import kotlinx.coroutines.runBlocking
@Suppress("unused")
class FreshRSSTestRunner: AndroidJUnitRunner() {
override fun newApplication(cl: ClassLoader, className: String, context: Context): Application {
return super.newApplication(cl, FreshRSSTestApplication::class.java.name, context)
}
override fun newApplication(cl: ClassLoader, className: String, context: Context): Application =
super.newApplication(cl, FreshRSSTestApplication::class.java.name, context)
class FreshRSSTestApplication: FreshRSSApplication() {
val feed = SubscriptionFactory.createSubscriptions(1)[0]
......@@ -33,6 +29,9 @@ class FreshRSSTestRunner: AndroidJUnitRunner() {
}
}
// Disable ACRA
override fun initAcra() = NOOP()
companion object {
const val TEST_DB_NAME = "freshrss-test.db"
}
......
......@@ -10,7 +10,9 @@ object ArticleFactory {
fun createArticle(
id: String = TestUtils.idNumber().valid(),
title: String = TestUtils.starTrek().character(),
href: String = TestUtils.internet().url(),
href: String = TestUtils.internet().url().let {
if(!"https?://.*".toRegex().matches(it)) "https://$it" else it
},
author: String = TestUtils.book().author(),
content: String = TestUtils.lorem().paragraph(5),
streamId: String = TestUtils.idNumber().valid(),
......
<?xml version="1.0" encoding="utf-8"?>
<manifest
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:dist="http://schemas.android.com/apk/distribution"
xmlns:tools="http://schemas.android.com/tools"
package="fr.chenry.android.freshrss">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<dist:module dist:instant="true" />
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" />
<application
android:name=".FreshRSSApplication"
......@@ -36,24 +34,19 @@
android:theme="@style/AppTheme.NoActionBar">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity
android:name=".activities.LoginActivity"
android:label="@string/app_name"
android:persistableMode="persistAcrossReboots" />
<activity
android:name=".activities.MainActivity" android:label="@string/app_name">
<intent-filter>
<action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" />
android:persistableMode="persistAcrossReboots"
/>
<data android:mimeType="text/plain" />
</intent-filter>
</activity>
<activity
android:name=".activities.MainActivity"
android:label="@string/app_name"
/>
</application>
</manifest>
\ No newline at end of file
......@@ -5,6 +5,7 @@ import android.app.NotificationChannel
import android.content.Context
import android.content.SharedPreferences
import android.os.Build
import androidx.annotation.VisibleForTesting
import androidx.core.app.NotificationManagerCompat
import androidx.room.Room
import androidx.work.*
......@@ -38,7 +39,6 @@ import kotlin.math.max
resChannelImportance = NotificationManagerCompat.IMPORTANCE_MAX
)
open class FreshRSSApplication: Application(), SharedPreferences.OnSharedPreferenceChangeListener {
open val preferences: FreshRSSPreferences by lazy {FreshRSSPreferences(this)}
open val workManager by lazy {WorkManager.getInstance(this)}
open val notificationManager: NotificationManagerCompat by lazy {NotificationManagerCompat.from(this)}
......@@ -50,7 +50,7 @@ open class FreshRSSApplication: Application(), SharedPreferences.OnSharedPrefere
override fun attachBaseContext(base: Context?) {
super.attachBaseContext(base)
ACRA.init(this)
initAcra()
}
override fun onCreate() {
......@@ -112,6 +112,11 @@ open class FreshRSSApplication: Application(), SharedPreferences.OnSharedPrefere
) enqueuePeriodicWork(max(0, initialDelay))
}
// Allows to disengage ACRA in tests
@VisibleForTesting
open fun initAcra() = ACRA.init(this)
@VisibleForTesting
fun isPeriodicRefreshEnabled() = preferences.refreshFrequency > 0
private suspend fun enqueuePeriodicWork(initialDelay: Long = 0, initialDelayTimeUnit: TimeUnit = TimeUnit.MINUTES) =
......
package fr.chenry.android.freshrss.components.articles
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.view.*
import android.widget.Toast
......@@ -44,26 +45,27 @@ class ArticleDetailFragment: Fragment(), View.OnClickListener {
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val view = inflater.inflate(R.layout.fragment_subscription_article_detail, container, false)
val view = FragmentSubscriptionArticleDetailBinding.inflate(inflater, container, false)
supportActionBar?.subtitle = article.title
val canOpenBrowser = browserIntent.resolveActivity(requireActivity().packageManager) != null
val activityInfos = browserIntent.resolveActivityInfo(requireActivity().packageManager, browserIntent.flags)
FragmentSubscriptionArticleDetailBinding.bind(view).apply {
view.apply {
article = this@ArticleDetailFragment.article
onClickListener = this@ArticleDetailFragment
this.canOpenBrowser = canOpenBrowser
}
canOpenBrowser = activityInfos?.exported ?: false
view.findViewById<FRSSWebView>(R.id.fragment_subscription_article_detail_web_view).apply {
load(this@ArticleDetailFragment, article)
if(requireF().preferences.retainScrollPosition) scrollY = article.scrollPosition
fragmentSubscriptionArticleDetailWebView.apply {
load(this@ArticleDetailFragment, this@ArticleDetailFragment.article)
if(requireF().preferences.retainScrollPosition)
scrollY = this@ArticleDetailFragment.article.scrollPosition
}
}
if(article.readStatus == ReadStatus.UNREAD) setReadStatus(ReadStatus.READ)
return view
return view.root
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
......@@ -140,7 +142,13 @@ class ArticleDetailFragment: Fragment(), View.OnClickListener {
val ctx = requireContext()
GlobalScope.launch {
/**
* Major mistake: Kotlin's [Result] cannot be used as the result of a [GlobalScope.launch]
* call. An intermediary function uses it as the indicator that the coroutine failed.
* Now I now why using [Result] as a return type in Kotlin a forbidden...
*/
@Suppress("DeferredResultUnused")
GlobalScope.async {
val result = Store.postReadStatus(article, readStatus)
result.onFailure {
......
......@@ -198,10 +198,9 @@ object Store {
suspend fun postAddSubscriptionAsync(url: String) = withContext(Dispatchers.IO) {
async {
executeSafeApiCall {postAddSubscription(url)}.mapCatching {
if(it.isSucess) {
if(!it.isSucess) throw Exception(it.error)
getSubscriptions()
db().getSubcriptionsById(it.streamId).blockingFirst(listOf()).first()
} else throw Exception(it.error)
}
}
}
......
......@@ -65,7 +65,7 @@ object TagsConverter: Converter<ResponseBody, List<FeedTagItem>> {
override fun convert(value: ResponseBody): List<FeedTagItem> = kotlin.runCatching {
value.use {
JSONObject(value.string()).optJSONArray("tags")?.let {jsonArray ->
(0 until (jsonArray.length() ?: 0)).mapNotNull {
(0 until (jsonArray.length())).mapNotNull {
val result = JACKSON_OBJECT_MAPPER.readValue<FeedTagItem>(jsonArray.optJSONObject(it).toString())
if(result.type == null) null else result
}
......
......@@ -39,7 +39,7 @@ class InstanceUrl(val protocole: String, base: String) {
fun parse(url: String): InstanceUrl {
"(.*://)".toPattern().matcher(url).let {
if (it.find()) {
val protocol = it.group(0)
val protocol = it.group(0)!!
if (authorizedProtocols.contains(protocol)) return InstanceUrl(protocol, url)
}
}
......
......@@ -22,13 +22,14 @@
android:layout_height="wrap_content"
android:layout_gravity="bottom|center_horizontal"
android:gravity="center"
android:orientation="horizontal">
android:orientation="horizontal"
android:weightSum="1">
<com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
android:id="@+id/fab_share"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_weight=".5"
android:layout_margin="16dp"
android:onClick="@{onClickListener::onClick}"
android:text="@string/share"
......@@ -38,11 +39,11 @@
android:id="@+id/fab_open_browser"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_weight=".5"
android:layout_margin="16dp"
android:onClick="@{onClickListener::onClick}"
android:text="@string/original_page"
android:visibility="@{canOpenBrowser ? View.GONE : View.VISIBLE, default=visible}"
android:visibility="@{canOpenBrowser ? View.VISIBLE : View.GONE, default=visible}"
app:icon="@drawable/ic_web_black_24dp" />
</LinearLayout>
</FrameLayout>
......
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