chore: code style.
Some checks failed
Check PR code / Lint (pull_request) Successful in 59s
Check PR code / build (pull_request) Failing after 9m25s

This commit is contained in:
Amine Bouabdallaoui 2025-01-11 14:12:24 +01:00
parent 5035392aff
commit 6b5f6cbbe0
60 changed files with 910 additions and 795 deletions

27
.editorconfig Normal file
View File

@ -0,0 +1,27 @@
root = true
[*]
insert_final_newline = true
[{*.kt,*.kts}]
[*.{kt,kts}]
# Disable wildcard imports entirely
ij_kotlin_name_count_to_use_star_import = 2147483647
ij_kotlin_name_count_to_use_star_import_for_members = 2147483647
ij_kotlin_code_style_defaults = KOTLIN_OFFICIAL
end_of_line = lf
ij_kotlin_allow_trailing_comma = true
ij_kotlin_allow_trailing_comma_on_call_site = true
ij_kotlin_imports_layout = *, java.**, javax.**, kotlin.**, ^
ij_kotlin_packages_to_use_import_on_demand = unset
indent_size = 4
indent_style = space
ktlint_chain_method_rule_force_multiline_when_chain_operator_count_greater_or_equal_than = unset
ktlint_class_signature_rule_force_multiline_when_parameter_count_greater_or_equal_than = 1
ktlint_code_style = ktlint_official
ktlint_function_signature_body_expression_wrapping = multiline
ktlint_function_signature_rule_force_multiline_when_parameter_count_greater_or_equal_than = 2
ktlint_ignore_back_ticked_identifier = false
max_line_length = 140

View File

@ -20,9 +20,9 @@ jobs:
- name: Install detekt - name: Install detekt
run: 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 run: 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
- name: Linting... - name: Linting...
run: ktlint 'shared/**/*.kt' 'androidApp/**/*.kt' '!shared/build' || true run: ktlint 'shared/**/*.kt' 'androidApp/**/*.kt' '!shared/build'
- name: Detecting... - name: Detecting...
run: ./detekt-cli-1.23.1/bin/detekt-cli --all-rules --excludes '**/shared/build/**/*.kt' || true run: ./detekt-cli-1.23.1/bin/detekt-cli --all-rules --excludes '**/shared/build/**/*.kt' || true
build: build:
needs: Lint needs: Lint
uses: ./.gitea/workflows/common_build.yml uses: ./.gitea/workflows/common_build.yml

View File

@ -20,65 +20,72 @@ import org.hamcrest.Matchers.hasToString
fun performLogin(someUrl: String? = null) { fun performLogin(someUrl: String? = null) {
onView(withId(R.id.urlView)).perform(click()).perform( onView(withId(R.id.urlView)).perform(click()).perform(
typeTextIntoFocusedView( typeTextIntoFocusedView(
if (!someUrl.isNullOrEmpty()) someUrl else "http://10.0.2.2:8888" if (!someUrl.isNullOrEmpty()) someUrl else "http://10.0.2.2:8888",
) ),
) )
onView(withId(R.id.signInButton)).perform(click()) onView(withId(R.id.signInButton)).perform(click())
} }
fun loginAndInitHome() { fun loginAndInitHome() {
performLogin() performLogin()
onView(withText(R.string.gdpr_dialog_title)).check(matches(isDisplayed())) onView(withText(R.string.gdpr_dialog_title)).check(matches(isDisplayed()))
onView(withText("OK")).perform(click()) onView(withText("OK")).perform(click())
} }
fun changeAndCancelSetting(oldValue: String, newValue: String, openSettingItem: () -> Unit) { fun changeAndCancelSetting(
oldValue: String,
newValue: String,
openSettingItem: () -> Unit,
) {
openSettingItem() openSettingItem()
onView( onView(
withId(android.R.id.edit) withId(android.R.id.edit),
).perform(replaceText(newValue)) ).perform(replaceText(newValue))
onView( onView(
withId(android.R.id.button2) withId(android.R.id.button2),
).perform(click()) ).perform(click())
openSettingItem() openSettingItem()
onView( onView(
withId(android.R.id.edit) withId(android.R.id.edit),
).check(matches(withText(oldValue))) ).check(matches(withText(oldValue)))
onView( onView(
withText(newValue) withText(newValue),
).check(doesNotExist()) ).check(doesNotExist())
onView( onView(
withId(android.R.id.button2) withId(android.R.id.button2),
).perform(click()) ).perform(click())
} }
fun changeAndSaveSetting(oldValue: String, newValue: String, openSettingItem: () -> Unit) { fun changeAndSaveSetting(
oldValue: String,
newValue: String,
openSettingItem: () -> Unit,
) {
openSettingItem() openSettingItem()
onView( onView(
withId(android.R.id.edit) withId(android.R.id.edit),
).perform(replaceText(newValue)) ).perform(replaceText(newValue))
onView( onView(
withId(android.R.id.button1) withId(android.R.id.button1),
).perform(click()) ).perform(click())
openSettingItem() openSettingItem()
onView( onView(
withId(android.R.id.edit) withId(android.R.id.edit),
).check(matches(withText(newValue))) ).check(matches(withText(newValue)))
if (oldValue.isNotEmpty()) { if (oldValue.isNotEmpty()) {
onView( onView(
withText(oldValue) withText(oldValue),
).check(doesNotExist()) ).check(doesNotExist())
} }
onView( onView(
withId(android.R.id.button2) withId(android.R.id.button2),
).perform(click()) ).perform(click())
} }
fun testPreferencesFromArray( fun testPreferencesFromArray(
context: Context, context: Context,
@ArrayRes arrayRes: Int, @ArrayRes arrayRes: Int,
openSettingItem: () -> Unit openSettingItem: () -> Unit,
) { ) {
openSettingItem() openSettingItem()
context.resources.getStringArray(arrayRes).forEach { res -> context.resources.getStringArray(arrayRes).forEach { res ->
@ -90,7 +97,10 @@ fun testPreferencesFromArray(
} }
} }
fun testAddSourceWithUrl(url: String, sourceName: String) { fun testAddSourceWithUrl(
url: String,
sourceName: String,
) {
onView(withId(R.id.fab)) onView(withId(R.id.fab))
.perform(click()) .perform(click())
onView(withId(R.id.nameInput)) onView(withId(R.id.nameInput))
@ -106,4 +116,4 @@ fun testAddSourceWithUrl(url: String, sourceName: String) {
onView(withId(R.id.saveBtn)) onView(withId(R.id.saveBtn))
.perform(click()) .perform(click())
onView(withText(sourceName)).check(matches(isDisplayed())) onView(withText(sourceName)).check(matches(isDisplayed()))
} }

View File

@ -25,8 +25,9 @@ import org.hamcrest.Matcher
import org.hamcrest.Matchers import org.hamcrest.Matchers
import org.hamcrest.TypeSafeMatcher import org.hamcrest.TypeSafeMatcher
fun withError(
fun withError(@StringRes id: Int): TypeSafeMatcher<View?> { @StringRes id: Int,
): TypeSafeMatcher<View?> {
return object : TypeSafeMatcher<View?>() { return object : TypeSafeMatcher<View?>() {
override fun matchesSafely(view: View?): Boolean { override fun matchesSafely(view: View?): Boolean {
if (view == null) { if (view == null) {
@ -52,7 +53,9 @@ fun isPopupWindow(): Matcher<Root> {
return isPlatformPopup() return isPlatformPopup()
} }
fun withDrawable(@DrawableRes id: Int) = object : TypeSafeMatcher<View>() { fun withDrawable(
@DrawableRes id: Int,
) = object : TypeSafeMatcher<View>() {
override fun describeTo(description: Description) { override fun describeTo(description: Description) {
description.appendText("ImageView with drawable same as drawable with id $id") description.appendText("ImageView with drawable same as drawable with id $id")
} }
@ -68,43 +71,49 @@ fun withDrawable(@DrawableRes id: Int) = object : TypeSafeMatcher<View>() {
} }
} }
fun hasBottombarItemText(@StringRes id: Int): Matcher<View>? { fun hasBottombarItemText(
@StringRes id: Int,
): Matcher<View>? {
return allOf( return allOf(
withResourceName("fixed_bottom_navigation_icon"), withResourceName("fixed_bottom_navigation_icon"),
withParent( withParent(
allOf( allOf(
withResourceName("fixed_bottom_navigation_icon_container"), withResourceName("fixed_bottom_navigation_icon_container"),
hasSibling(withText(id)) hasSibling(withText(id)),
) ),
) ),
) )
} }
fun withSettingsCheckboxWidget(@StringRes id: Int): Matcher<View>? { fun withSettingsCheckboxWidget(
@StringRes id: Int,
): Matcher<View>? {
return allOf( return allOf(
withId(android.R.id.switch_widget), withId(android.R.id.switch_widget),
withParent( withParent(
withSettingsCheckboxFrame(id) withSettingsCheckboxFrame(id),
) ),
) )
} }
fun withSettingsCheckboxFrame(@StringRes id: Int): Matcher<View>? { fun withSettingsCheckboxFrame(
@StringRes id: Int,
): Matcher<View>? {
return allOf( return allOf(
withId(android.R.id.widget_frame), withId(android.R.id.widget_frame),
hasSibling( hasSibling(
allOf( allOf(
withClassName(Matchers.equalTo(RelativeLayout::class.java.name)), withClassName(Matchers.equalTo(RelativeLayout::class.java.name)),
withChild( withChild(
withText(id) withText(id),
) ),
) ),
) ),
) )
} }
fun openMenu() { fun openMenu() {
openActionBarOverflowOrOptionsMenu( openActionBarOverflowOrOptionsMenu(
ApplicationProvider.getApplicationContext<Context>() ApplicationProvider.getApplicationContext<Context>(),
) )
} }

View File

@ -23,7 +23,6 @@ import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
@LargeTest @LargeTest
class HomeActivityTest { class HomeActivityTest {
@get:Rule @get:Rule
val activityRule = ActivityScenarioRule(LoginActivity::class.java) val activityRule = ActivityScenarioRule(LoginActivity::class.java)
@ -36,13 +35,13 @@ class HomeActivityTest {
fun testMenu() { fun testMenu() {
onView(withId(R.id.action_search)).check(matches(isDisplayed())).check( onView(withId(R.id.action_search)).check(matches(isDisplayed())).check(
matches( matches(
isClickable() isClickable(),
) ),
) )
onView(withId(R.id.action_filter)).check(matches(isDisplayed())).check( onView(withId(R.id.action_filter)).check(matches(isDisplayed())).check(
matches( matches(
isClickable() isClickable(),
) ),
) )
openMenu() openMenu()
onView(withText(R.string.readAll)).check(matches(isDisplayed())) onView(withText(R.string.readAll)).check(matches(isDisplayed()))
@ -57,19 +56,19 @@ class HomeActivityTest {
fun testMenuActions() { fun testMenuActions() {
onView(withId(R.id.action_search)).perform(click()) onView(withId(R.id.action_search)).perform(click())
onView( onView(
withId(R.id.search_src_text) withId(R.id.search_src_text),
).check(matches(isFocused())) ).check(matches(isFocused()))
onView(isRoot()).perform(ViewActions.pressBack()) onView(isRoot()).perform(ViewActions.pressBack())
onView(withId(R.id.action_filter)).perform(click()) onView(withId(R.id.action_filter)).perform(click())
onView( onView(
withText(R.string.filter_item_sources) withText(R.string.filter_item_sources),
).check(matches(isDisplayed())) ).check(matches(isDisplayed()))
onView( onView(
withText(R.string.filter_item_tags) withText(R.string.filter_item_tags),
).check(matches(isDisplayed())) ).check(matches(isDisplayed()))
onView( onView(
withId(R.id.floatingActionButton2) withId(R.id.floatingActionButton2),
).check(matches(isDisplayed())) ).check(matches(isDisplayed()))
onView(isRoot()).perform(ViewActions.pressBack()) onView(isRoot()).perform(ViewActions.pressBack())
@ -107,14 +106,13 @@ class HomeActivityTest {
fun testEmptyView() { fun testEmptyView() {
onView(withId(R.id.emptyText)).check(matches(isDisplayed())) onView(withId(R.id.emptyText)).check(matches(isDisplayed()))
onView( onView(
hasBottombarItemText(R.string.tab_new) hasBottombarItemText(R.string.tab_new),
).check(matches(isDisplayed())).check(matches(isSelected())) ).check(matches(isDisplayed())).check(matches(isSelected()))
onView( onView(
hasBottombarItemText(R.string.tab_read) hasBottombarItemText(R.string.tab_read),
).check(matches(isDisplayed())).check(matches(not(isSelected()))) ).check(matches(isDisplayed())).check(matches(not(isSelected())))
onView( onView(
hasBottombarItemText(R.string.tab_favs) hasBottombarItemText(R.string.tab_favs),
).check(matches(isDisplayed())).check(matches(not(isSelected()))) ).check(matches(isDisplayed())).check(matches(not(isSelected())))
} }
}
}

View File

@ -23,7 +23,6 @@ import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
@LargeTest @LargeTest
class LoginActivityTest { class LoginActivityTest {
@get:Rule @get:Rule
val activityRule = ActivityScenarioRule(LoginActivity::class.java) val activityRule = ActivityScenarioRule(LoginActivity::class.java)
@ -52,11 +51,11 @@ class LoginActivityTest {
onView(withId(R.id.urlView)).check(matches(isDisplayed())) onView(withId(R.id.urlView)).check(matches(isDisplayed()))
onView(withId(R.id.selfSigned)).check(matches(isDisplayed())).check(matches(isNotChecked())) onView(withId(R.id.selfSigned)).check(matches(isDisplayed())).check(matches(isNotChecked()))
.check( .check(
matches(isClickable()) matches(isClickable()),
) )
onView(withId(R.id.withLogin)).check(matches(isDisplayed())) onView(withId(R.id.withLogin)).check(matches(isDisplayed()))
.check(matches(isNotChecked())).check( .check(matches(isNotChecked())).check(
matches(isClickable()) matches(isClickable()),
) )
} }
@ -80,4 +79,4 @@ class LoginActivityTest {
performLogin() performLogin()
onView(withText(R.string.gdpr_dialog_title)).check(matches(isDisplayed())) onView(withText(R.string.gdpr_dialog_title)).check(matches(isDisplayed()))
} }
} }

View File

@ -26,11 +26,9 @@ import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
@LargeTest @LargeTest
class SettingsActivityGeneralTest { class SettingsActivityGeneralTest {
@get:Rule @get:Rule
val activityRule = ActivityScenarioRule(LoginActivity::class.java) val activityRule = ActivityScenarioRule(LoginActivity::class.java)
@ -38,7 +36,7 @@ class SettingsActivityGeneralTest {
fun init() { fun init() {
loginAndInitHome() loginAndInitHome()
openActionBarOverflowOrOptionsMenu( openActionBarOverflowOrOptionsMenu(
ApplicationProvider.getApplicationContext() ApplicationProvider.getApplicationContext(),
) )
onView(withText(R.string.title_activity_settings)).perform(click()) onView(withText(R.string.title_activity_settings)).perform(click())
onView(withText(R.string.pref_header_general)).perform(click()) onView(withText(R.string.pref_header_general)).perform(click())
@ -48,68 +46,75 @@ class SettingsActivityGeneralTest {
fun testGeneral() { fun testGeneral() {
onView(withText(R.string.pref_api_items_number_title)).check(matches(isDisplayed())) onView(withText(R.string.pref_api_items_number_title)).check(matches(isDisplayed()))
onView( onView(
withSettingsCheckboxWidget(R.string.pref_general_infinite_loading_title) withSettingsCheckboxWidget(R.string.pref_general_infinite_loading_title),
).check( ).check(
matches( matches(
allOf( allOf(
isDisplayed(), not(isChecked()) isDisplayed(),
) not(isChecked()),
) ),
),
) )
onView(withText(R.string.pref_general_category_links)).check(matches(isDisplayed())) onView(withText(R.string.pref_general_category_links)).check(matches(isDisplayed()))
onView(withSettingsCheckboxWidget(R.string.pref_article_viewer_title)).check( onView(withSettingsCheckboxWidget(R.string.pref_article_viewer_title)).check(
matches( matches(
allOf( allOf(
isDisplayed(), isChecked() isDisplayed(),
) isChecked(),
) ),
),
) )
onView(withSettingsCheckboxWidget(R.string.reader_static_bar_title)).check( onView(withSettingsCheckboxWidget(R.string.reader_static_bar_title)).check(
matches( matches(
allOf( allOf(
isDisplayed(), not(isChecked()) isDisplayed(),
) not(isChecked()),
) ),
),
) )
onView(withSettingsCheckboxFrame(R.string.reader_static_bar_title)).check( onView(withSettingsCheckboxFrame(R.string.reader_static_bar_title)).check(
matches( matches(
isEnabled() isEnabled(),
) ),
) )
onView(withText(R.string.pref_general_category_displaying)).check(matches(isDisplayed())) onView(withText(R.string.pref_general_category_displaying)).check(matches(isDisplayed()))
onView(withSettingsCheckboxWidget(R.string.pref_switch_card_view_title)).check( onView(withSettingsCheckboxWidget(R.string.pref_switch_card_view_title)).check(
matches( matches(
allOf( allOf(
isDisplayed(), not(isChecked()) isDisplayed(),
) not(isChecked()),
) ),
),
) )
onView(withSettingsCheckboxWidget(R.string.card_height_title)).check( onView(withSettingsCheckboxWidget(R.string.card_height_title)).check(
matches( matches(
allOf( allOf(
isDisplayed(), not(isChecked()) isDisplayed(),
) not(isChecked()),
) ),
),
) )
onView(withSettingsCheckboxFrame(R.string.card_height_title)).check( onView(withSettingsCheckboxFrame(R.string.card_height_title)).check(
matches( matches(
not(isEnabled()) not(isEnabled()),
) ),
) )
onView(withSettingsCheckboxWidget(R.string.switch_unread_count_title)).check( onView(withSettingsCheckboxWidget(R.string.switch_unread_count_title)).check(
matches( matches(
allOf( allOf(
isDisplayed(), isChecked() isDisplayed(),
) isChecked(),
) ),
),
) )
onView(withId(R.id.settings)).perform(swipeUp()) onView(withId(R.id.settings)).perform(swipeUp())
onView(withSettingsCheckboxWidget(R.string.display_all_counts_title)).check( onView(withSettingsCheckboxWidget(R.string.display_all_counts_title)).check(
matches( matches(
allOf( allOf(
isDisplayed(), not(isChecked()) isDisplayed(),
) not(isChecked()),
) ),
),
) )
} }
@ -120,25 +125,25 @@ class SettingsActivityGeneralTest {
// Value check // Value check
onView( onView(
withId(android.R.id.edit) withId(android.R.id.edit),
).perform(replaceText("AVC")) ).perform(replaceText("AVC"))
.check(matches(withText(""))) .check(matches(withText("")))
// TODO: should check message error. Not working for api level 30+ // TODO: should check message error. Not working for api level 30+
onView( onView(
withId(android.R.id.edit) withId(android.R.id.edit),
).perform(replaceText("-1")) ).perform(replaceText("-1"))
.check(matches(withText(""))) .check(matches(withText("")))
// TODO: should check message error. Not working for api level 30+ // TODO: should check message error. Not working for api level 30+
onView( onView(
withId(android.R.id.edit) withId(android.R.id.edit),
).perform(replaceText("300")) ).perform(replaceText("300"))
.check(matches(withText(""))) .check(matches(withText("")))
onView( onView(
withId(android.R.id.edit) withId(android.R.id.edit),
).perform(typeTextIntoFocusedView("300")) ).perform(typeTextIntoFocusedView("300"))
.check(matches(withText("30"))) .check(matches(withText("30")))
onView( onView(
withId(android.R.id.edit) withId(android.R.id.edit),
).perform(replaceText("10")) ).perform(replaceText("10"))
.check(matches(withText("10"))) .check(matches(withText("10")))
onView(isRoot()).perform(ViewActions.pressBack()) onView(isRoot()).perform(ViewActions.pressBack())
@ -157,18 +162,18 @@ class SettingsActivityGeneralTest {
// article viewer settings // article viewer settings
onView(withSettingsCheckboxFrame(R.string.reader_static_bar_title)).check( onView(withSettingsCheckboxFrame(R.string.reader_static_bar_title)).check(
matches( matches(
isEnabled() isEnabled(),
) ),
) )
onView(withSettingsCheckboxWidget(R.string.pref_article_viewer_title)).perform(click()) onView(withSettingsCheckboxWidget(R.string.pref_article_viewer_title)).perform(click())
onView(withSettingsCheckboxFrame(R.string.reader_static_bar_title)).check( onView(withSettingsCheckboxFrame(R.string.reader_static_bar_title)).check(
matches( matches(
not(isEnabled()) not(isEnabled()),
) ),
) )
onView(withSettingsCheckboxFrame(R.string.card_height_title)).check(matches(not(isEnabled()))) onView(withSettingsCheckboxFrame(R.string.card_height_title)).check(matches(not(isEnabled())))
onView(withSettingsCheckboxWidget(R.string.pref_switch_card_view_title)).perform(click()) onView(withSettingsCheckboxWidget(R.string.pref_switch_card_view_title)).perform(click())
onView(withSettingsCheckboxFrame(R.string.card_height_title)).check(matches(isEnabled())) onView(withSettingsCheckboxFrame(R.string.card_height_title)).check(matches(isEnabled()))
} }
} }

View File

@ -21,11 +21,9 @@ import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
@LargeTest @LargeTest
class SettingsActivityOfflineTest { class SettingsActivityOfflineTest {
@get:Rule @get:Rule
val activityRule = ActivityScenarioRule(LoginActivity::class.java) val activityRule = ActivityScenarioRule(LoginActivity::class.java)
@ -38,7 +36,7 @@ class SettingsActivityOfflineTest {
} }
loginAndInitHome() loginAndInitHome()
openActionBarOverflowOrOptionsMenu( openActionBarOverflowOrOptionsMenu(
ApplicationProvider.getApplicationContext() ApplicationProvider.getApplicationContext(),
) )
onView(withText(R.string.title_activity_settings)).perform(click()) onView(withText(R.string.title_activity_settings)).perform(click())
onView(withText(R.string.pref_header_offline)).perform(click()) onView(withText(R.string.pref_header_offline)).perform(click())
@ -49,58 +47,63 @@ class SettingsActivityOfflineTest {
onView(withSettingsCheckboxWidget(R.string.pref_switch_periodic_refresh)).check( onView(withSettingsCheckboxWidget(R.string.pref_switch_periodic_refresh)).check(
matches( matches(
allOf( allOf(
isDisplayed(), not(isChecked()) isDisplayed(),
) not(isChecked()),
) ),
),
) )
onView(withSettingsCheckboxWidget(R.string.pref_switch_items_caching)).check( onView(withSettingsCheckboxWidget(R.string.pref_switch_items_caching)).check(
matches( matches(
allOf( allOf(
isDisplayed(), not(isChecked()) isDisplayed(),
) not(isChecked()),
) ),
),
) )
onView(withSettingsCheckboxFrame(R.string.pref_switch_items_caching)).check( onView(withSettingsCheckboxFrame(R.string.pref_switch_items_caching)).check(
matches( matches(
isEnabled() isEnabled(),
) ),
) )
onView(withText(R.string.pref_periodic_refresh_minutes_title)).check( onView(withText(R.string.pref_periodic_refresh_minutes_title)).check(
matches( matches(
allOf(isNotEnabled(), isDisplayed()) allOf(isNotEnabled(), isDisplayed()),
) ),
) )
onView(withSettingsCheckboxWidget(R.string.pref_switch_refresh_when_charging)).check( onView(withSettingsCheckboxWidget(R.string.pref_switch_refresh_when_charging)).check(
matches( matches(
allOf( allOf(
isDisplayed(), not(isChecked()) isDisplayed(),
) not(isChecked()),
) ),
),
) )
onView(withSettingsCheckboxFrame(R.string.pref_switch_refresh_when_charging)).check( onView(withSettingsCheckboxFrame(R.string.pref_switch_refresh_when_charging)).check(
matches( matches(
isNotEnabled() isNotEnabled(),
) ),
) )
onView(withSettingsCheckboxWidget(R.string.pref_switch_notify_new_items)).check( onView(withSettingsCheckboxWidget(R.string.pref_switch_notify_new_items)).check(
matches( matches(
allOf( allOf(
isDisplayed(), not(isChecked()) isDisplayed(),
) not(isChecked()),
) ),
),
) )
onView(withSettingsCheckboxFrame(R.string.pref_switch_notify_new_items)).check( onView(withSettingsCheckboxFrame(R.string.pref_switch_notify_new_items)).check(
matches( matches(
isNotEnabled() isNotEnabled(),
) ),
) )
onView(withSettingsCheckboxWidget(R.string.pref_switch_update_sources)).check( onView(withSettingsCheckboxWidget(R.string.pref_switch_update_sources)).check(
matches( matches(
allOf( allOf(
isDisplayed(), isChecked() isDisplayed(),
) isChecked(),
) ),
),
) )
} }
@ -111,50 +114,50 @@ class SettingsActivityOfflineTest {
onView(withText(R.string.pref_switch_items_caching_on)).check(matches(isDisplayed())) onView(withText(R.string.pref_switch_items_caching_on)).check(matches(isDisplayed()))
onView(withSettingsCheckboxFrame(R.string.pref_switch_items_caching)).check( onView(withSettingsCheckboxFrame(R.string.pref_switch_items_caching)).check(
matches( matches(
isEnabled() isEnabled(),
) ),
) )
onView(withText(R.string.pref_periodic_refresh_minutes_title)).check( onView(withText(R.string.pref_periodic_refresh_minutes_title)).check(
matches( matches(
isNotEnabled() isNotEnabled(),
) ),
) )
onView(withSettingsCheckboxFrame(R.string.pref_switch_refresh_when_charging)).check( onView(withSettingsCheckboxFrame(R.string.pref_switch_refresh_when_charging)).check(
matches( matches(
isNotEnabled() isNotEnabled(),
) ),
) )
onView(withSettingsCheckboxFrame(R.string.pref_switch_notify_new_items)).check( onView(withSettingsCheckboxFrame(R.string.pref_switch_notify_new_items)).check(
matches( matches(
isNotEnabled() isNotEnabled(),
) ),
) )
onView(withText(R.string.pref_switch_periodic_refresh_off)).check( onView(withText(R.string.pref_switch_periodic_refresh_off)).check(
matches( matches(
isDisplayed() isDisplayed(),
) ),
) )
onView(withSettingsCheckboxWidget(R.string.pref_switch_periodic_refresh)).perform(click()) onView(withSettingsCheckboxWidget(R.string.pref_switch_periodic_refresh)).perform(click())
onView(withText(R.string.pref_switch_periodic_refresh_on)).check( onView(withText(R.string.pref_switch_periodic_refresh_on)).check(
matches( matches(
isDisplayed() isDisplayed(),
) ),
) )
onView(withSettingsCheckboxFrame(R.string.pref_periodic_refresh_minutes_title)).check( onView(withSettingsCheckboxFrame(R.string.pref_periodic_refresh_minutes_title)).check(
matches( matches(
isEnabled() isEnabled(),
) ),
) )
onView(withSettingsCheckboxFrame(R.string.pref_switch_refresh_when_charging)).check( onView(withSettingsCheckboxFrame(R.string.pref_switch_refresh_when_charging)).check(
matches( matches(
isEnabled() isEnabled(),
) ),
) )
onView(withSettingsCheckboxFrame(R.string.pref_switch_notify_new_items)).check( onView(withSettingsCheckboxFrame(R.string.pref_switch_notify_new_items)).check(
matches( matches(
isEnabled() isEnabled(),
) ),
) )
changeAndCancelSetting("360", "123") { changeAndCancelSetting("360", "123") {
onView(withText(R.string.pref_periodic_refresh_minutes_title)).perform(click()) onView(withText(R.string.pref_periodic_refresh_minutes_title)).perform(click())
@ -166,4 +169,4 @@ class SettingsActivityOfflineTest {
onView(withSettingsCheckboxFrame(R.string.pref_switch_notify_new_items)).perform(click()) onView(withSettingsCheckboxFrame(R.string.pref_switch_notify_new_items)).perform(click())
onView(withSettingsCheckboxWidget(R.string.pref_switch_update_sources)).perform(click()) onView(withSettingsCheckboxWidget(R.string.pref_switch_update_sources)).perform(click())
} }
} }

View File

@ -19,11 +19,9 @@ import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
@LargeTest @LargeTest
class SettingsActivityReaderTest { class SettingsActivityReaderTest {
@get:Rule @get:Rule
val activityRule = ActivityScenarioRule(LoginActivity::class.java) val activityRule = ActivityScenarioRule(LoginActivity::class.java)
@ -36,7 +34,7 @@ class SettingsActivityReaderTest {
} }
loginAndInitHome() loginAndInitHome()
openActionBarOverflowOrOptionsMenu( openActionBarOverflowOrOptionsMenu(
ApplicationProvider.getApplicationContext() ApplicationProvider.getApplicationContext(),
) )
onView(withText(R.string.title_activity_settings)).perform(click()) onView(withText(R.string.title_activity_settings)).perform(click())
onView(withText(R.string.pref_header_viewer)).perform(click()) onView(withText(R.string.pref_header_viewer)).perform(click())
@ -47,11 +45,12 @@ class SettingsActivityReaderTest {
onView(withSettingsCheckboxFrame(R.string.pref_switch_actions_pager_scroll)).check( onView(withSettingsCheckboxFrame(R.string.pref_switch_actions_pager_scroll)).check(
matches( matches(
allOf( allOf(
isDisplayed(), not( isDisplayed(),
isChecked() not(
) isChecked(),
) ),
) ),
),
) )
onView(withText(R.string.pref_content_reader_font_size)).check(matches(isDisplayed())) onView(withText(R.string.pref_content_reader_font_size)).check(matches(isDisplayed()))
onView(withText(R.string.settings_reader_font)).check(matches(isDisplayed())) onView(withText(R.string.settings_reader_font)).check(matches(isDisplayed()))
@ -61,14 +60,14 @@ class SettingsActivityReaderTest {
fun testReaderActions() { fun testReaderActions() {
onView(withText(R.string.pref_switch_actions_pager_scroll_off)).check( onView(withText(R.string.pref_switch_actions_pager_scroll_off)).check(
matches( matches(
isDisplayed() isDisplayed(),
) ),
) )
onView(withSettingsCheckboxFrame(R.string.pref_switch_actions_pager_scroll)).perform(click()) onView(withSettingsCheckboxFrame(R.string.pref_switch_actions_pager_scroll)).perform(click())
onView(withText(R.string.pref_switch_actions_pager_scroll_on)).check( onView(withText(R.string.pref_switch_actions_pager_scroll_on)).check(
matches( matches(
isDisplayed() isDisplayed(),
) ),
) )
onView(withText(R.string.pref_content_reader_font_size)).perform(click()) onView(withText(R.string.pref_content_reader_font_size)).perform(click())
@ -83,4 +82,4 @@ class SettingsActivityReaderTest {
onView(withText(R.string.settings_reader_font)).perform(click()) onView(withText(R.string.settings_reader_font)).perform(click())
} }
} }
} }

View File

@ -20,7 +20,6 @@ import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
@LargeTest @LargeTest
class SettingsActivityTest { class SettingsActivityTest {
@get:Rule @get:Rule
val activityRule = ActivityScenarioRule(LoginActivity::class.java) val activityRule = ActivityScenarioRule(LoginActivity::class.java)
lateinit var context: Context lateinit var context: Context
@ -35,10 +34,8 @@ class SettingsActivityTest {
onView(withText(R.string.title_activity_settings)).perform(click()) onView(withText(R.string.title_activity_settings)).perform(click())
} }
@Test @Test
fun testAllSettings() { fun testAllSettings() {
onView(withText(R.string.pref_header_general)).check(matches(isDisplayed())) onView(withText(R.string.pref_header_general)).check(matches(isDisplayed()))
onView(withText(R.string.pref_header_viewer)).check(matches(isDisplayed())) onView(withText(R.string.pref_header_viewer)).check(matches(isDisplayed()))
onView(withText(R.string.pref_header_offline)).check(matches(isDisplayed())) onView(withText(R.string.pref_header_offline)).check(matches(isDisplayed()))
@ -48,14 +45,13 @@ class SettingsActivityTest {
matches( matches(
allOf( allOf(
isDisplayed(), isDisplayed(),
not(isSelected()) not(isSelected()),
) ),
) ),
) )
onView(withText(R.string.action_about)).check(matches(isDisplayed())) onView(withText(R.string.action_about)).check(matches(isDisplayed()))
} }
@Test @Test
fun testThemes() { fun testThemes() {
testPreferencesFromArray(context, R.array.ModeTitles) { testPreferencesFromArray(context, R.array.ModeTitles) {
@ -63,7 +59,6 @@ class SettingsActivityTest {
} }
} }
@Test @Test
fun testExperimentail() { fun testExperimentail() {
onView(withText(R.string.pref_header_experimental)).perform(click()) onView(withText(R.string.pref_header_experimental)).perform(click())
@ -75,13 +70,11 @@ class SettingsActivityTest {
} }
} }
@Test @Test
fun testBugReports() { fun testBugReports() {
onView(withText(R.string.pref_switch_disable_acra)).perform(click()) onView(withText(R.string.pref_switch_disable_acra)).perform(click())
} }
@Test @Test
fun testLinks() { fun testLinks() {
onView(withText(R.string.pref_header_links)).perform(click()) onView(withText(R.string.pref_header_links)).perform(click())
@ -91,10 +84,9 @@ class SettingsActivityTest {
onView(withText(R.string.translation)).check(matches(isDisplayed())) onView(withText(R.string.translation)).check(matches(isDisplayed()))
} }
@Test @Test
fun testAbout() { fun testAbout() {
onView(withText(R.string.action_about)).perform(click()) onView(withText(R.string.action_about)).perform(click())
onView(withText("ACRA")).check(matches(isDisplayed())) onView(withText("ACRA")).check(matches(isDisplayed()))
} }
} }

View File

@ -41,11 +41,10 @@ class SourcesActivityTest {
fun addSource() { fun addSource() {
testAddSourceWithUrl( testAddSourceWithUrl(
"https://lorem-rss.herokuapp.com/feed?unit=year&interval=1&length=10", "https://lorem-rss.herokuapp.com/feed?unit=year&interval=1&length=10",
sourceName sourceName,
) )
} }
@Test @Test
fun addSourceCheckContent() { fun addSourceCheckContent() {
testAddSourceWithUrl("https://news.google.com/rss?hl=en-US&gl=US&ceid=US:en", sourceName) testAddSourceWithUrl("https://news.google.com/rss?hl=en-US&gl=US&ceid=US:en", sourceName)
@ -54,7 +53,7 @@ class SourcesActivityTest {
onView(withText(R.string.menu_home_refresh)).perform(click()) onView(withText(R.string.menu_home_refresh)).perform(click())
onView(withText(R.string.refresh_dialog_message)).check(matches(isDisplayed())) onView(withText(R.string.refresh_dialog_message)).check(matches(isDisplayed()))
onView( onView(
withId(android.R.id.button1) withId(android.R.id.button1),
).perform(click()) ).perform(click())
Thread.sleep(10000) Thread.sleep(10000)
onView(withId(R.id.swipeRefreshLayout)).perform(swipeDown()) onView(withId(R.id.swipeRefreshLayout)).perform(swipeDown())
@ -74,10 +73,9 @@ class SourcesActivityTest {
onView(withText(sourceName)).check(doesNotExist()) onView(withText(sourceName)).check(doesNotExist())
} }
private fun goToSources() { private fun goToSources() {
openMenu() openMenu()
onView(withText(R.string.menu_home_sources)) onView(withText(R.string.menu_home_sources))
.perform(click()) .perform(click())
} }
} }

View File

@ -35,7 +35,7 @@ import bou.amine.apps.readerforselfossv2.android.utils.openUrlInBrowser
import bou.amine.apps.readerforselfossv2.model.SelfossModel import bou.amine.apps.readerforselfossv2.model.SelfossModel
import bou.amine.apps.readerforselfossv2.repository.Repository import bou.amine.apps.readerforselfossv2.repository.Repository
import bou.amine.apps.readerforselfossv2.service.AppSettingsService import bou.amine.apps.readerforselfossv2.service.AppSettingsService
import bou.amine.apps.readerforselfossv2.utils.ItemType import bou.amine.apps.readerforselfossv2.utils.Enums.ItemType
import com.ashokvarma.bottomnavigation.BottomNavigationBar import com.ashokvarma.bottomnavigation.BottomNavigationBar
import com.ashokvarma.bottomnavigation.BottomNavigationItem import com.ashokvarma.bottomnavigation.BottomNavigationItem
import com.ashokvarma.bottomnavigation.TextBadgeItem import com.ashokvarma.bottomnavigation.TextBadgeItem
@ -49,7 +49,10 @@ import org.kodein.di.instance
import java.security.MessageDigest import java.security.MessageDigest
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAware { class HomeActivity :
AppCompatActivity(),
SearchView.OnQueryTextListener,
DIAware {
private var items: ArrayList<SelfossModel.Item> = ArrayList() private var items: ArrayList<SelfossModel.Item> = ArrayList()
private var elementsShown: ItemType = ItemType.UNREAD private var elementsShown: ItemType = ItemType.UNREAD
@ -171,11 +174,12 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
getElementsAccordingToTab() getElementsAccordingToTab()
} }
} else { } else {
Toast.makeText( Toast
this@HomeActivity, .makeText(
"Found null when swiping at positon $position.", this@HomeActivity,
Toast.LENGTH_LONG, "Found null when swiping at positon $position.",
).show() Toast.LENGTH_LONG,
).show()
} }
} }
} }
@ -200,15 +204,18 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
tabNewBadge = tabNewBadge =
TextBadgeItem() TextBadgeItem()
.setText("") .setText("")
.setHideOnSelect(false).hide(false) .setHideOnSelect(false)
.hide(false)
tabArchiveBadge = tabArchiveBadge =
TextBadgeItem() TextBadgeItem()
.setText("") .setText("")
.setHideOnSelect(false).hide(false) .setHideOnSelect(false)
.hide(false)
tabStarredBadge = tabStarredBadge =
TextBadgeItem() TextBadgeItem()
.setText("") .setText("")
.setHideOnSelect(false).hide(false) .setHideOnSelect(false)
.hide(false)
if (appSettingsService.isDisplayUnreadCountEnabled()) { if (appSettingsService.isDisplayUnreadCountEnabled()) {
lifecycleScope.launch { lifecycleScope.launch {
@ -236,14 +243,12 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
BottomNavigationItem( BottomNavigationItem(
R.drawable.ic_tab_fiber_new_black_24dp, R.drawable.ic_tab_fiber_new_black_24dp,
getString(R.string.tab_new), getString(R.string.tab_new),
) ).setBadgeItem(tabNewBadge)
.setBadgeItem(tabNewBadge)
val tabArchive = val tabArchive =
BottomNavigationItem( BottomNavigationItem(
R.drawable.ic_tab_archive_black_24dp, R.drawable.ic_tab_archive_black_24dp,
getString(R.string.tab_read), getString(R.string.tab_read),
) ).setBadgeItem(tabArchiveBadge)
.setBadgeItem(tabArchiveBadge)
val tabStarred = val tabStarred =
BottomNavigationItem( BottomNavigationItem(
R.drawable.ic_tab_favorite_black_24dp, R.drawable.ic_tab_favorite_black_24dp,
@ -425,17 +430,17 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
binding.recyclerView.addOnScrollListener(recyclerViewScrollListener) binding.recyclerView.addOnScrollListener(recyclerViewScrollListener)
} }
private fun getLastVisibleItem(): Int { private fun getLastVisibleItem(): Int =
return when (val manager = binding.recyclerView.layoutManager) { when (val manager = binding.recyclerView.layoutManager) {
is StaggeredGridLayoutManager -> is StaggeredGridLayoutManager ->
manager.findLastCompletelyVisibleItemPositions( manager
null, .findLastCompletelyVisibleItemPositions(
).last() null,
).last()
is GridLayoutManager -> manager.findLastCompletelyVisibleItemPosition() is GridLayoutManager -> manager.findLastCompletelyVisibleItemPosition()
else -> 0 else -> 0
} }
}
private fun mayBeEmpty() = private fun mayBeEmpty() =
if (items.isEmpty()) { if (items.isEmpty()) {
@ -577,7 +582,8 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
messageRes: Int, messageRes: Int,
doFn: () -> Unit, doFn: () -> Unit,
) { ) {
AlertDialog.Builder(this@HomeActivity) AlertDialog
.Builder(this@HomeActivity)
.setMessage(messageRes) .setMessage(messageRes)
.setTitle(titleRes) .setTitle(titleRes)
.setPositiveButton(android.R.string.ok) { _, _ -> doFn() } .setPositiveButton(android.R.string.ok) { _, _ -> doFn() }
@ -589,7 +595,7 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
override fun onOptionsItemSelected(item: MenuItem): Boolean { override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) { when (item.itemId) {
R.id.issue_tracker -> { R.id.issue_tracker -> {
baseContext.openUrlInBrowser(AppSettingsService.trackerUrl) baseContext.openUrlInBrowser(AppSettingsService.TRACKER_URL)
return true return true
} }
@ -606,18 +612,19 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
CoroutineScope(Dispatchers.Main).launch { CoroutineScope(Dispatchers.Main).launch {
val updatedRemote = repository.updateRemote() val updatedRemote = repository.updateRemote()
if (updatedRemote) { if (updatedRemote) {
Toast.makeText( Toast
this@HomeActivity, .makeText(
R.string.refresh_success_response, this@HomeActivity,
Toast.LENGTH_LONG, R.string.refresh_success_response,
) Toast.LENGTH_LONG,
.show() ).show()
} else { } else {
Toast.makeText( Toast
this@HomeActivity, .makeText(
R.string.refresh_failer_message, this@HomeActivity,
Toast.LENGTH_SHORT, R.string.refresh_failer_message,
).show() Toast.LENGTH_SHORT,
).show()
} }
CountingIdlingResourceSingleton.decrement() CountingIdlingResourceSingleton.decrement()
} }
@ -633,25 +640,26 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
CoroutineScope(Dispatchers.Main).launch { CoroutineScope(Dispatchers.Main).launch {
val success = repository.markAllAsRead(items) val success = repository.markAllAsRead(items)
if (success) { if (success) {
Toast.makeText( Toast
this@HomeActivity, .makeText(
R.string.all_posts_read, this@HomeActivity,
Toast.LENGTH_SHORT, R.string.all_posts_read,
).show() Toast.LENGTH_SHORT,
).show()
tabNewBadge.removeBadge() tabNewBadge.removeBadge()
getElementsAccordingToTab() getElementsAccordingToTab()
} else { } else {
Toast.makeText( Toast
this@HomeActivity, .makeText(
R.string.all_posts_not_read, this@HomeActivity,
Toast.LENGTH_SHORT, R.string.all_posts_not_read,
).show() Toast.LENGTH_SHORT,
).show()
} }
handleListResult() handleListResult()
binding.swipeRefreshLayout.isRefreshing = false binding.swipeRefreshLayout.isRefreshing = false
CountingIdlingResourceSingleton.decrement() CountingIdlingResourceSingleton.decrement()
} }
} }
} }
@ -661,7 +669,7 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
R.id.action_disconnect -> { R.id.action_disconnect -> {
needsConfirmation( needsConfirmation(
R.string.confirm_disconnect_title, R.string.confirm_disconnect_title,
R.string.confirm_disconnect_description R.string.confirm_disconnect_description,
) { ) {
runBlocking { runBlocking {
repository.logout() repository.logout()
@ -702,7 +710,8 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
private fun handleRecurringTask() { private fun handleRecurringTask() {
if (appSettingsService.isPeriodicRefreshEnabled()) { if (appSettingsService.isPeriodicRefreshEnabled()) {
val myConstraints = val myConstraints =
Constraints.Builder() Constraints
.Builder()
.setRequiresBatteryNotLow(true) .setRequiresBatteryNotLow(true)
.setRequiresCharging(appSettingsService.isRefreshWhenChargingOnlyEnabled()) .setRequiresCharging(appSettingsService.isRefreshWhenChargingOnlyEnabled())
.setRequiresStorageNotLow(true) .setRequiresStorageNotLow(true)
@ -711,19 +720,19 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
val backgroundWork = val backgroundWork =
PeriodicWorkRequestBuilder<LoadingWorker>( PeriodicWorkRequestBuilder<LoadingWorker>(
appSettingsService.getRefreshMinutes(), appSettingsService.getRefreshMinutes(),
TimeUnit.MINUTES TimeUnit.MINUTES,
) ).setConstraints(myConstraints)
.setConstraints(myConstraints)
.addTag("selfoss-loading") .addTag("selfoss-loading")
.build() .build()
WorkManager.getInstance( WorkManager
baseContext, .getInstance(
).enqueueUniquePeriodicWork( baseContext,
"selfoss-loading", ).enqueueUniquePeriodicWork(
ExistingPeriodicWorkPolicy.KEEP, "selfoss-loading",
backgroundWork ExistingPeriodicWorkPolicy.KEEP,
) backgroundWork,
)
} }
} }
} }

View File

@ -30,7 +30,9 @@ import org.kodein.di.DIAware
import org.kodein.di.android.closestDI import org.kodein.di.android.closestDI
import org.kodein.di.instance import org.kodein.di.instance
class LoginActivity : AppCompatActivity(), DIAware { class LoginActivity :
AppCompatActivity(),
DIAware {
private var inValidCount: Int = 0 private var inValidCount: Int = 0
private var isWithLogin = false private var isWithLogin = false
@ -108,7 +110,7 @@ class LoginActivity : AppCompatActivity(), DIAware {
repository.updateApiInformation() repository.updateApiInformation()
ACRA.errorReporter.putCustomData( ACRA.errorReporter.putCustomData(
"SELFOSS_API_VERSION", "SELFOSS_API_VERSION",
appSettingsService.getApiVersion().toString() appSettingsService.getApiVersion().toString(),
) )
CountingIdlingResourceSingleton.decrement() CountingIdlingResourceSingleton.decrement()
} }
@ -151,11 +153,12 @@ class LoginActivity : AppCompatActivity(), DIAware {
repository.updateApiInformation() repository.updateApiInformation()
} catch (e: Exception) { } catch (e: Exception) {
if (e.message?.startsWith("No transformation found") == true) { if (e.message?.startsWith("No transformation found") == true) {
Toast.makeText( Toast
applicationContext, .makeText(
R.string.application_selfoss_only, applicationContext,
Toast.LENGTH_LONG, R.string.application_selfoss_only,
).show() Toast.LENGTH_LONG,
).show()
preferenceError() preferenceError()
showProgress(false) showProgress(false)
} }
@ -270,7 +273,7 @@ class LoginActivity : AppCompatActivity(), DIAware {
return when (item.itemId) { return when (item.itemId) {
R.id.issue_tracker -> { R.id.issue_tracker -> {
val browserIntent = val browserIntent =
Intent(Intent.ACTION_VIEW, Uri.parse(AppSettingsService.trackerUrl)) Intent(Intent.ACTION_VIEW, Uri.parse(AppSettingsService.TRACKER_URL))
startActivity(browserIntent) startActivity(browserIntent)
return true return true
} }
@ -280,9 +283,9 @@ class LoginActivity : AppCompatActivity(), DIAware {
.withAboutIconShown(true) .withAboutIconShown(true)
.withAboutVersionShown(true) .withAboutVersionShown(true)
.withAboutSpecial2("Bug reports") .withAboutSpecial2("Bug reports")
.withAboutSpecial2Description(AppSettingsService.trackerUrl) .withAboutSpecial2Description(AppSettingsService.TRACKER_URL)
.withAboutSpecial1("Project Page") .withAboutSpecial1("Project Page")
.withAboutSpecial1Description(AppSettingsService.sourceUrl) .withAboutSpecial1Description(AppSettingsService.SOURCE_URL)
.start(this) .start(this)
true true
} }
@ -290,4 +293,4 @@ class LoginActivity : AppCompatActivity(), DIAware {
else -> super.onOptionsItemSelected(item) else -> super.onOptionsItemSelected(item)
} }
} }
} }

View File

@ -9,11 +9,11 @@ import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.ProcessLifecycleOwner import androidx.lifecycle.ProcessLifecycleOwner
import androidx.multidex.MultiDexApplication import androidx.multidex.MultiDexApplication
import bou.amine.apps.readerforselfossv2.DI.networkModule
import bou.amine.apps.readerforselfossv2.android.testing.TestingHelper import bou.amine.apps.readerforselfossv2.android.testing.TestingHelper
import bou.amine.apps.readerforselfossv2.android.viewmodel.AppViewModel import bou.amine.apps.readerforselfossv2.android.viewmodel.AppViewModel
import bou.amine.apps.readerforselfossv2.dao.DriverFactory import bou.amine.apps.readerforselfossv2.dao.DriverFactory
import bou.amine.apps.readerforselfossv2.dao.ReaderForSelfossDB import bou.amine.apps.readerforselfossv2.dao.ReaderForSelfossDB
import bou.amine.apps.readerforselfossv2.di.networkModule
import bou.amine.apps.readerforselfossv2.repository.Repository import bou.amine.apps.readerforselfossv2.repository.Repository
import bou.amine.apps.readerforselfossv2.service.AppSettingsService import bou.amine.apps.readerforselfossv2.service.AppSettingsService
import com.github.ln_12.library.ConnectivityStatus import com.github.ln_12.library.ConnectivityStatus
@ -36,21 +36,23 @@ import org.kodein.di.bind
import org.kodein.di.instance import org.kodein.di.instance
import org.kodein.di.singleton import org.kodein.di.singleton
class MyApp : MultiDexApplication(), DIAware { class MyApp :
MultiDexApplication(),
DIAware {
override val di by DI.lazy { override val di by DI.lazy {
bind<AppSettingsService>() with singleton { AppSettingsService(ACRA.isACRASenderServiceProcess() || TestingHelper().isUnitTest()) } bind<AppSettingsService>() with singleton { AppSettingsService(ACRA.isACRASenderServiceProcess() || TestingHelper().isUnitTest()) }
import(networkModule) import(networkModule)
bind<DriverFactory>() with singleton { DriverFactory(applicationContext) } bind<DriverFactory>() with singleton { DriverFactory(applicationContext) }
bind<ReaderForSelfossDB>() with singleton { ReaderForSelfossDB(driverFactory.createDriver()) } bind<ReaderForSelfossDB>() with singleton { ReaderForSelfossDB(driverFactory.createDriver()) }
bind<Repository>() with bind<Repository>() with
singleton { singleton {
Repository( Repository(
instance(), instance(),
instance(), instance(),
isConnectionAvailable, isConnectionAvailable,
instance(), instance(),
) )
} }
bind<ConnectivityStatus>() with singleton { ConnectivityStatus(applicationContext) } bind<ConnectivityStatus>() with singleton { ConnectivityStatus(applicationContext) }
bind<AppViewModel>() with singleton { AppViewModel(repository = instance()) } bind<AppViewModel>() with singleton { AppViewModel(repository = instance()) }
} }
@ -89,11 +91,12 @@ class MyApp : MultiDexApplication(), DIAware {
R.string.network_connectivity_lost R.string.network_connectivity_lost
} }
Toast.makeText( Toast
applicationContext, .makeText(
toastMessage, applicationContext,
Toast.LENGTH_SHORT, toastMessage,
).show() Toast.LENGTH_SHORT,
).show()
} }
} }
} }
@ -151,13 +154,13 @@ class MyApp : MultiDexApplication(), DIAware {
val name = getString(R.string.notification_channel_sync) val name = getString(R.string.notification_channel_sync)
val importance = NotificationManager.IMPORTANCE_LOW val importance = NotificationManager.IMPORTANCE_LOW
val mChannel = NotificationChannel(AppSettingsService.syncChannelId, name, importance) val mChannel = NotificationChannel(AppSettingsService.SYNC_CHANNEL_ID, name, importance)
val newItemsChannelname = getString(R.string.new_items_channel_sync) val newItemsChannelname = getString(R.string.new_items_channel_sync)
val newItemsChannelimportance = NotificationManager.IMPORTANCE_DEFAULT val newItemsChannelimportance = NotificationManager.IMPORTANCE_DEFAULT
val newItemsChannelmChannel = val newItemsChannelmChannel =
NotificationChannel( NotificationChannel(
AppSettingsService.newItemsChannelId, AppSettingsService.NEW_ITEMS_CHANNEL_ID,
newItemsChannelname, newItemsChannelname,
newItemsChannelimportance, newItemsChannelimportance,
) )
@ -199,4 +202,4 @@ class MyApp : MultiDexApplication(), DIAware {
super.onPause(owner) super.onPause(owner)
} }
} }
} }

View File

@ -103,8 +103,7 @@ class ReaderActivity : AppCompatActivity(), DIAware {
FragmentStateAdapter(fa) { FragmentStateAdapter(fa) {
override fun getItemCount(): Int = allItems.size override fun getItemCount(): Int = allItems.size
override fun createFragment(position: Int): Fragment = override fun createFragment(position: Int): Fragment = ArticleFragment.newInstance(allItems[position])
ArticleFragment.newInstance(allItems[position])
} }
override fun onKeyDown( override fun onKeyDown(
@ -229,4 +228,4 @@ class ReaderActivity : AppCompatActivity(), DIAware {
startActivity(intent) startActivity(intent)
overridePendingTransition(0, 0) overridePendingTransition(0, 0)
} }
} }

View File

@ -81,4 +81,4 @@ class SourcesActivity : AppCompatActivity(), DIAware {
startActivity(Intent(this@SourcesActivity, UpsertSourceActivity::class.java)) startActivity(Intent(this@SourcesActivity, UpsertSourceActivity::class.java))
} }
} }
} }

View File

@ -49,7 +49,10 @@ class ItemCardAdapter(
return ViewHolder(binding) return ViewHolder(binding)
} }
private fun handleClickListeners(holderBinding: CardItemBinding, position: Int) { private fun handleClickListeners(
holderBinding: CardItemBinding,
position: Int,
) {
holderBinding.favButton.setOnClickListener { holderBinding.favButton.setOnClickListener {
val item = items[position] val item = items[position]
if (item.starred) { if (item.starred) {
@ -96,12 +99,13 @@ class ItemCardAdapter(
binding.title.setLinkTextColor(c.resources.getColor(R.color.colorAccent)) binding.title.setLinkTextColor(c.resources.getColor(R.color.colorAccent))
binding.sourceTitleAndDate.text = try { binding.sourceTitleAndDate.text =
itm.sourceAuthorAndDate() try {
} catch (e: Exception) { itm.sourceAuthorAndDate()
e.sendSilentlyWithAcraWithName("ItemCardAdapter parse date") } catch (e: Exception) {
itm.sourceAuthorOnly() e.sendSilentlyWithAcraWithName("ItemCardAdapter parse date")
} itm.sourceAuthorOnly()
}
if (!appSettingsService.isFullHeightCardsEnabled()) { if (!appSettingsService.isFullHeightCardsEnabled()) {
binding.itemImage.maxHeight = imageMaxHeight binding.itemImage.maxHeight = imageMaxHeight
@ -126,4 +130,4 @@ class ItemCardAdapter(
} }
inner class ViewHolder(val binding: CardItemBinding) : RecyclerView.ViewHolder(binding.root) inner class ViewHolder(val binding: CardItemBinding) : RecyclerView.ViewHolder(binding.root)
} }

View File

@ -53,12 +53,13 @@ class ItemListAdapter(
binding.title.setLinkTextColor(c.resources.getColor(R.color.colorAccent)) binding.title.setLinkTextColor(c.resources.getColor(R.color.colorAccent))
binding.sourceTitleAndDate.text = try { binding.sourceTitleAndDate.text =
itm.sourceAuthorAndDate() try {
} catch (e: Exception) { itm.sourceAuthorAndDate()
e.sendSilentlyWithAcraWithName("ItemListAdapter parse date") } catch (e: Exception) {
itm.sourceAuthorOnly() e.sendSilentlyWithAcraWithName("ItemListAdapter parse date")
} itm.sourceAuthorOnly()
}
if (itm.getThumbnail(repository.baseUrl).isEmpty()) { if (itm.getThumbnail(repository.baseUrl).isEmpty()) {
if (itm.getIcon(repository.baseUrl).isEmpty()) { if (itm.getIcon(repository.baseUrl).isEmpty()) {
@ -73,4 +74,4 @@ class ItemListAdapter(
} }
inner class ViewHolder(val binding: ListItemBinding) : RecyclerView.ViewHolder(binding.root) inner class ViewHolder(val binding: ListItemBinding) : RecyclerView.ViewHolder(binding.root)
} }

View File

@ -11,14 +11,16 @@ import bou.amine.apps.readerforselfossv2.android.utils.openItemUrl
import bou.amine.apps.readerforselfossv2.model.SelfossModel import bou.amine.apps.readerforselfossv2.model.SelfossModel
import bou.amine.apps.readerforselfossv2.repository.Repository import bou.amine.apps.readerforselfossv2.repository.Repository
import bou.amine.apps.readerforselfossv2.service.AppSettingsService import bou.amine.apps.readerforselfossv2.service.AppSettingsService
import bou.amine.apps.readerforselfossv2.utils.ItemType import bou.amine.apps.readerforselfossv2.utils.Enums.ItemType
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.kodein.di.DIAware import org.kodein.di.DIAware
abstract class ItemsAdapter<VH : RecyclerView.ViewHolder?> : RecyclerView.Adapter<VH>(), DIAware { abstract class ItemsAdapter<VH : RecyclerView.ViewHolder?> :
RecyclerView.Adapter<VH>(),
DIAware {
abstract val items: ArrayList<SelfossModel.Item> abstract val items: ArrayList<SelfossModel.Item>
abstract val repository: Repository abstract val repository: Repository
abstract val binding: ViewBinding abstract val binding: ViewBinding
@ -45,8 +47,7 @@ abstract class ItemsAdapter<VH : RecyclerView.ViewHolder?> : RecyclerView.Adapte
app.findViewById(R.id.coordLayout), app.findViewById(R.id.coordLayout),
R.string.marked_as_read, R.string.marked_as_read,
Snackbar.LENGTH_LONG, Snackbar.LENGTH_LONG,
) ).setAction(R.string.undo_string) {
.setAction(R.string.undo_string) {
unreadItemAtIndex(item, position, false) unreadItemAtIndex(item, position, false)
} }
@ -66,8 +67,7 @@ abstract class ItemsAdapter<VH : RecyclerView.ViewHolder?> : RecyclerView.Adapte
app.findViewById(R.id.coordLayout), app.findViewById(R.id.coordLayout),
R.string.marked_as_unread, R.string.marked_as_unread,
Snackbar.LENGTH_LONG, Snackbar.LENGTH_LONG,
) ).setAction(R.string.undo_string) {
.setAction(R.string.undo_string) {
readItemAtIndex(item, position, false) readItemAtIndex(item, position, false)
} }
@ -77,7 +77,10 @@ abstract class ItemsAdapter<VH : RecyclerView.ViewHolder?> : RecyclerView.Adapte
s.show() s.show()
} }
protected fun handleLinkOpening(holderBinding: ViewBinding, position: Int) { protected fun handleLinkOpening(
holderBinding: ViewBinding,
position: Int,
) {
holderBinding.root.setOnClickListener { holderBinding.root.setOnClickListener {
repository.setReaderItems(items) repository.setReaderItems(items)
c.openItemUrl( c.openItemUrl(

View File

@ -23,11 +23,13 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.kodein.di.DIAware import org.kodein.di.DIAware
import org.kodein.di.instance import org.kodein.di.instance
import java.util.* import java.util.Timer
import kotlin.concurrent.schedule import kotlin.concurrent.schedule
class LoadingWorker(val context: Context, params: WorkerParameters) : class LoadingWorker(
Worker(context, params), val context: Context,
params: WorkerParameters,
) : Worker(context, params),
DIAware { DIAware {
override val di by lazy { (applicationContext as MyApp).di } override val di by lazy { (applicationContext as MyApp).di }
private val repository: Repository by instance() private val repository: Repository by instance()
@ -40,12 +42,13 @@ class LoadingWorker(val context: Context, params: WorkerParameters) :
applicationContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager applicationContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
val notification = val notification =
NotificationCompat.Builder(applicationContext, AppSettingsService.syncChannelId) NotificationCompat
.Builder(applicationContext, AppSettingsService.SYNC_CHANNEL_ID)
.setContentTitle(context.getString(R.string.loading_notification_title)) .setContentTitle(context.getString(R.string.loading_notification_title))
.setContentText(context.getString(R.string.loading_notification_text)) .setContentText(context.getString(R.string.loading_notification_text))
.setOngoing(true) .setOngoing(true)
.setPriority(PRIORITY_LOW) .setPriority(PRIORITY_LOW)
.setChannelId(AppSettingsService.syncChannelId) .setChannelId(AppSettingsService.SYNC_CHANNEL_ID)
.setSmallIcon(R.drawable.ic_stat_cloud_download_black_24dp) .setSmallIcon(R.drawable.ic_stat_cloud_download_black_24dp)
notificationManager.notify(1, notification.build()) notificationManager.notify(1, notification.build())
@ -87,19 +90,18 @@ class LoadingWorker(val context: Context, params: WorkerParameters) :
PendingIntent.getActivity(context, 0, intent, pflags) PendingIntent.getActivity(context, 0, intent, pflags)
val newItemsNotification = val newItemsNotification =
NotificationCompat.Builder( NotificationCompat
applicationContext, .Builder(
AppSettingsService.newItemsChannelId, applicationContext,
) AppSettingsService.NEW_ITEMS_CHANNEL_ID,
.setContentTitle(context.getString(R.string.new_items_notification_title)) ).setContentTitle(context.getString(R.string.new_items_notification_title))
.setContentText( .setContentText(
context.getString( context.getString(
R.string.new_items_notification_text, R.string.new_items_notification_text,
newSize, newSize,
), ),
) ).setPriority(PRIORITY_DEFAULT)
.setPriority(PRIORITY_DEFAULT) .setChannelId(AppSettingsService.NEW_ITEMS_CHANNEL_ID)
.setChannelId(AppSettingsService.newItemsChannelId)
.setContentIntent(pendingIntent) .setContentIntent(pendingIntent)
.setAutoCancel(true) .setAutoCancel(true)
.setSmallIcon(R.drawable.ic_tab_fiber_new_black_24dp) .setSmallIcon(R.drawable.ic_tab_fiber_new_black_24dp)

View File

@ -115,12 +115,13 @@ class ArticleFragment : Fragment(), DIAware {
contentText = item.content contentText = item.content
contentTitle = item.title.getHtmlDecoded() contentTitle = item.title.getHtmlDecoded()
contentImage = item.getThumbnail(repository.baseUrl) contentImage = item.getThumbnail(repository.baseUrl)
contentSource = try { contentSource =
item.sourceAuthorAndDate() try {
} catch (e: Exception) { item.sourceAuthorAndDate()
e.sendSilentlyWithAcraWithName("Article Fragment parse date") } catch (e: Exception) {
item.sourceAuthorOnly() e.sendSilentlyWithAcraWithName("Article Fragment parse date")
} item.sourceAuthorOnly()
}
allImages = item.getImages() allImages = item.getImages()
fontSize = appSettingsService.getFontSize() fontSize = appSettingsService.getFontSize()
@ -337,7 +338,10 @@ class ArticleFragment : Fragment(), DIAware {
view: WebView?, view: WebView?,
url: String, url: String,
): Boolean { ): Boolean {
return if (context != null && url.isUrlValid() && binding.webcontent.hitTestResult.type != WebView.HitTestResult.SRC_IMAGE_ANCHOR_TYPE) { return if (context != null &&
url.isUrlValid() &&
binding.webcontent.hitTestResult.type != WebView.HitTestResult.SRC_IMAGE_ANCHOR_TYPE
) {
requireContext().openUrlInBrowser(url) requireContext().openUrlInBrowser(url)
true true
} else { } else {
@ -422,7 +426,6 @@ class ArticleFragment : Fragment(), DIAware {
context.theme.resolveAttribute(R.attr.colorOnSurface, colorOnSurface, true) context.theme.resolveAttribute(R.attr.colorOnSurface, colorOnSurface, true)
context.theme.resolveAttribute(R.attr.colorSurface, colorSurface, true) context.theme.resolveAttribute(R.attr.colorSurface, colorSurface, true)
} catch (e: IllegalStateException) { } catch (e: IllegalStateException) {
e.sendSilentlyWithAcraWithName("Context issue when setting attributes, but context wasn't null before") e.sendSilentlyWithAcraWithName("Context issue when setting attributes, but context wasn't null before")
} }
@ -458,7 +461,7 @@ class ArticleFragment : Fragment(), DIAware {
binding.webcontent.setOnTouchListener { _, event -> binding.webcontent.setOnTouchListener { _, event ->
gestureDetector.onTouchEvent( gestureDetector.onTouchEvent(
event event,
) )
} }
@ -598,9 +601,9 @@ class ArticleFragment : Fragment(), DIAware {
fun performClick(): Boolean { fun performClick(): Boolean {
if (allImages != null && ( if (allImages != null && (
binding.webcontent.hitTestResult.type == WebView.HitTestResult.IMAGE_TYPE || binding.webcontent.hitTestResult.type == WebView.HitTestResult.IMAGE_TYPE ||
binding.webcontent.hitTestResult.type == WebView.HitTestResult.SRC_IMAGE_ANCHOR_TYPE binding.webcontent.hitTestResult.type == WebView.HitTestResult.SRC_IMAGE_ANCHOR_TYPE
) )
) { ) {
val position: Int = allImages.indexOf(binding.webcontent.hitTestResult.extra) val position: Int = allImages.indexOf(binding.webcontent.hitTestResult.extra)
@ -612,4 +615,4 @@ class ArticleFragment : Fragment(), DIAware {
} }
return false return false
} }
} }

View File

@ -190,4 +190,4 @@ class FilterSheetFragment : BottomSheetDialogFragment(), DIAware {
companion object { companion object {
const val TAG = "FilterModalBottomSheet" const val TAG = "FilterModalBottomSheet"
} }
} }

View File

@ -40,4 +40,4 @@ fun String.toTextDrawableString(): String {
} }
} }
return textDrawable.toString() return textDrawable.toString()
} }

View File

@ -64,15 +64,14 @@ class SettingsActivity :
outState.putCharSequence(TITLE_TAG, title) outState.putCharSequence(TITLE_TAG, title)
} }
override fun onSupportNavigateUp(): Boolean { override fun onSupportNavigateUp(): Boolean =
return if (supportFragmentManager.popBackStackImmediate()) { if (supportFragmentManager.popBackStackImmediate()) {
supportActionBar?.title = getText(R.string.title_activity_settings) supportActionBar?.title = getText(R.string.title_activity_settings)
false false
} else { } else {
super.onBackPressed() super.onBackPressed()
true true
} }
}
override fun onPreferenceStartFragment( override fun onPreferenceStartFragment(
caller: PreferenceFragmentCompat, caller: PreferenceFragmentCompat,
@ -81,15 +80,17 @@ class SettingsActivity :
// Instantiate the new Fragment // Instantiate the new Fragment
val args = pref.extras val args = pref.extras
val fragment = val fragment =
supportFragmentManager.fragmentFactory.instantiate( supportFragmentManager.fragmentFactory
classLoader, .instantiate(
pref.fragment.toString(), classLoader,
).apply { pref.fragment.toString(),
arguments = args ).apply {
setTargetFragment(caller, 0) arguments = args
} setTargetFragment(caller, 0)
}
// Replace the existing Fragment with the new Fragment // Replace the existing Fragment with the new Fragment
supportFragmentManager.beginTransaction() supportFragmentManager
.beginTransaction()
.replace(R.id.settings, fragment) .replace(R.id.settings, fragment)
.addToBackStack(null) .addToBackStack(null)
.commit() .commit()
@ -108,7 +109,7 @@ class SettingsActivity :
preferenceManager.findPreference<Preference>(CURRENT_THEME)?.onPreferenceChangeListener = preferenceManager.findPreference<Preference>(CURRENT_THEME)?.onPreferenceChangeListener =
Preference.OnPreferenceChangeListener { _, newValue -> Preference.OnPreferenceChangeListener { _, newValue ->
AppCompatDelegate.setDefaultNightMode( AppCompatDelegate.setDefaultNightMode(
newValue.toString().toInt() newValue.toString().toInt(),
) // ListPreference Only takes string-arrays ¯\_(ツ)_/¯ ) // ListPreference Only takes string-arrays ¯\_(ツ)_/¯
true true
} }
@ -144,11 +145,12 @@ class SettingsActivity :
val input: Int = (dest.toString() + source.toString()).toInt() val input: Int = (dest.toString() + source.toString()).toInt()
if (input in 1..200) return@InputFilter null if (input in 1..200) return@InputFilter null
} catch (nfe: NumberFormatException) { } catch (nfe: NumberFormatException) {
Toast.makeText( Toast
activity, .makeText(
R.string.items_number_should_be_number, activity,
Toast.LENGTH_LONG R.string.items_number_should_be_number,
).show() Toast.LENGTH_LONG,
).show()
} }
"" ""
}, },
@ -234,19 +236,19 @@ class SettingsActivity :
preferenceManager.findPreference<Preference>("trackerLink")?.onPreferenceClickListener = preferenceManager.findPreference<Preference>("trackerLink")?.onPreferenceClickListener =
Preference.OnPreferenceClickListener { Preference.OnPreferenceClickListener {
openUrl(AppSettingsService.trackerUrl) openUrl(AppSettingsService.TRACKER_URL)
true true
} }
preferenceManager.findPreference<Preference>("sourceLink")?.onPreferenceClickListener = preferenceManager.findPreference<Preference>("sourceLink")?.onPreferenceClickListener =
Preference.OnPreferenceClickListener { Preference.OnPreferenceClickListener {
openUrl(AppSettingsService.sourceUrl) openUrl(AppSettingsService.SOURCE_URL)
false false
} }
preferenceManager.findPreference<Preference>("translation")?.onPreferenceClickListener = preferenceManager.findPreference<Preference>("translation")?.onPreferenceClickListener =
Preference.OnPreferenceClickListener { Preference.OnPreferenceClickListener {
openUrl(AppSettingsService.translationUrl) openUrl(AppSettingsService.TRANSLATION_URL)
false false
} }
} }
@ -260,4 +262,4 @@ class SettingsActivity :
setPreferencesFromResource(R.xml.pref_experimental, rootKey) setPreferencesFromResource(R.xml.pref_experimental, rootKey)
} }
} }
} }

View File

@ -3,7 +3,6 @@ package bou.amine.apps.readerforselfossv2.android.testing
import androidx.test.espresso.idling.CountingIdlingResource import androidx.test.espresso.idling.CountingIdlingResource
object CountingIdlingResourceSingleton { object CountingIdlingResourceSingleton {
private const val RESOURCE = "GLOBAL" private const val RESOURCE = "GLOBAL"
@JvmField @JvmField
@ -18,4 +17,4 @@ object CountingIdlingResourceSingleton {
countingIdlingResource.decrement() countingIdlingResource.decrement()
} }
} }
} }

View File

@ -2,7 +2,6 @@ package bou.amine.apps.readerforselfossv2.android.testing
import android.os.Build import android.os.Build
class TestingHelper { class TestingHelper {
fun isUnitTest(): Boolean { fun isUnitTest(): Boolean {
var device = Build.DEVICE var device = Build.DEVICE
@ -16,4 +15,4 @@ class TestingHelper {
} }
return device == "robolectric" && product == "robolectric" return device == "robolectric" && product == "robolectric"
} }
} }

View File

@ -18,7 +18,6 @@ import bou.amine.apps.readerforselfossv2.model.SelfossModel
import bou.amine.apps.readerforselfossv2.utils.toStringUriWithHttp import bou.amine.apps.readerforselfossv2.utils.toStringUriWithHttp
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
fun Context.openItemUrl( fun Context.openItemUrl(
currentItem: Int, currentItem: Int,
linkDecoded: String, linkDecoded: String,
@ -42,8 +41,7 @@ fun Context.openItemUrl(
} }
} }
fun String.isUrlValid(): Boolean = fun String.isUrlValid(): Boolean = this.toHttpUrlOrNull() != null && Patterns.WEB_URL.matcher(this).matches()
this.toHttpUrlOrNull() != null && Patterns.WEB_URL.matcher(this).matches()
fun String.isBaseUrlInvalid(): Boolean { fun String.isBaseUrlInvalid(): Boolean {
val baseUrl = this.toHttpUrlOrNull() val baseUrl = this.toHttpUrlOrNull()
@ -61,7 +59,6 @@ fun Context.openItemUrlInBrowserAsNewTask(i: SelfossModel.Item) {
} }
fun Context.openUrlInBrowserAsNewTask(url: String) { fun Context.openUrlInBrowserAsNewTask(url: String) {
val intent = Intent(Intent.ACTION_VIEW) val intent = Intent(Intent.ACTION_VIEW)
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
intent.data = Uri.parse(url) intent.data = Uri.parse(url)
@ -80,7 +77,6 @@ fun Context.mayBeStartActivity(intent: Intent) {
} catch (e: ActivityNotFoundException) { } catch (e: ActivityNotFoundException) {
Toast.makeText(this, getString(R.string.no_browser), Toast.LENGTH_SHORT).show() Toast.makeText(this, getString(R.string.no_browser), Toast.LENGTH_SHORT).show()
} }
} }
class LinkOnTouchListener : View.OnTouchListener { class LinkOnTouchListener : View.OnTouchListener {
@ -122,4 +118,4 @@ class LinkOnTouchListener : View.OnTouchListener {
} }
return ret return ret
} }
} }

View File

@ -6,4 +6,4 @@ import org.acra.ktx.sendSilentlyWithAcra
fun Throwable.sendSilentlyWithAcraWithName(name: String) { fun Throwable.sendSilentlyWithAcraWithName(name: String) {
ACRA.errorReporter.putCustomData("error_source", name) ACRA.errorReporter.putCustomData("error_source", name)
this.sendSilentlyWithAcra() this.sendSilentlyWithAcra()
} }

View File

@ -8,22 +8,19 @@ import org.acra.config.CoreConfiguration
import org.acra.config.ReportingAdministrator import org.acra.config.ReportingAdministrator
import org.acra.data.CrashReportData import org.acra.data.CrashReportData
@AutoService(ReportingAdministrator::class) @AutoService(ReportingAdministrator::class)
class AcraReportingAdministrator : ReportingAdministrator { class AcraReportingAdministrator : ReportingAdministrator {
override fun shouldStartCollecting( override fun shouldStartCollecting(
context: Context, context: Context,
config: CoreConfiguration, config: CoreConfiguration,
reportBuilder: ReportBuilder reportBuilder: ReportBuilder,
): Boolean { ): Boolean =
return reportBuilder.exception !is DeadSystemException && (reportBuilder.exception != null && reportBuilder.exception!!::class.simpleName != "CannotDeliverBroadcastException") reportBuilder.exception !is DeadSystemException &&
} (reportBuilder.exception != null && reportBuilder.exception!!::class.simpleName != "CannotDeliverBroadcastException")
override fun shouldSendReport( override fun shouldSendReport(
context: Context, context: Context,
config: CoreConfiguration, config: CoreConfiguration,
crashReportData: CrashReportData crashReportData: CrashReportData,
): Boolean { ): Boolean = crashReportData.get("BRAND") != "redroid"
return crashReportData.get("BRAND") != "redroid" }
}
}

View File

@ -19,11 +19,10 @@ class AppViewModel(private val repository: Repository) : ViewModel() {
if (isConnected && !wasConnected && repository.connectionMonitored) { if (isConnected && !wasConnected && repository.connectionMonitored) {
_networkAvailableProvider.emit(true) _networkAvailableProvider.emit(true)
wasConnected = true wasConnected = true
} else if (!isConnected && wasConnected && repository.connectionMonitored) } else if (!isConnected && wasConnected && repository.connectionMonitored) {
{ _networkAvailableProvider.emit(false)
_networkAvailableProvider.emit(false) wasConnected = false
wasConnected = false }
}
} }
} }
} }

View File

@ -11,13 +11,17 @@ fun dialogMessage(): String {
return latestDialog.findViewById<TextView>(android.R.id.message)?.text.toString() return latestDialog.findViewById<TextView>(android.R.id.message)?.text.toString()
} }
fun Menu.assertClickable(@IdRes id: Int) { fun Menu.assertClickable(
@IdRes id: Int,
) {
this.assertVisible(id) this.assertVisible(id)
val item = this.findItem(id) val item = this.findItem(id)
assertTrue(item.isEnabled) assertTrue(item.isEnabled)
} }
fun Menu.assertVisible(@IdRes id: Int) { fun Menu.assertVisible(
@IdRes id: Int,
) {
val item = this.findItem(id) val item = this.findItem(id)
assertTrue(item.isVisible) assertTrue(item.isVisible)
} }

View File

@ -11,10 +11,8 @@ import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
import org.robolectric.Robolectric import org.robolectric.Robolectric
@RunWith(RobotElectriqueRunner::class)
@RunWith(RobotElectriqueRunnerclass::class)
class LoginActivityTest { class LoginActivityTest {
@Test @Test
fun login_shouldDisplay() { fun login_shouldDisplay() {
Robolectric.buildActivity(LoginActivity::class.java).use { controller -> Robolectric.buildActivity(LoginActivity::class.java).use { controller ->
@ -74,4 +72,4 @@ class LoginActivityTest {
assertEquals(expectedIntent.component, actual.component) assertEquals(expectedIntent.component, actual.component)
} }
}*/ }*/
} }

View File

@ -3,10 +3,8 @@ package bou.amine.apps.readerforselfossv2.android.tests.robolectric
import org.robolectric.RobolectricTestRunner import org.robolectric.RobolectricTestRunner
import org.robolectric.annotation.Config import org.robolectric.annotation.Config
class RobotElectriqueRunnerclass(testClass: Class<*>?) : class RobotElectriqueRunner(
RobolectricTestRunner(testClass) { testClass: Class<*>?,
) : RobolectricTestRunner(testClass) {
override fun buildGlobalConfig(): Config { override fun buildGlobalConfig(): Config = Config.Builder().setSdk(25, 30, 33).build()
return Config.Builder().setSdk(25, 30, 33).build() }
}
}

View File

@ -42,14 +42,15 @@ private const val FEED_URL = "https://test.com/feed"
private const val TAGS = "Test, New" private const val TAGS = "Test, New"
private const val NUMBER_ARTICLES = 100
private const val NUMBER_UNREAD = 50
private const val NUMBER_STARRED = 20
class RepositoryTest { class RepositoryTest {
private val db = mockk<ReaderForSelfossDB>(relaxed = true) private val db = mockk<ReaderForSelfossDB>(relaxed = true)
private val appSettingsService = mockk<AppSettingsService>() private val appSettingsService = mockk<AppSettingsService>()
private val api = mockk<SelfossApi>() private val api = mockk<SelfossApi>()
private val NUMBER_ARTICLES = 100
private val NUMBER_UNREAD = 50
private val NUMBER_STARRED = 20
private lateinit var repository: Repository private lateinit var repository: Repository
private fun initializeRepository( private fun initializeRepository(
@ -75,19 +76,20 @@ class RepositoryTest {
every { appSettingsService.isUpdateSourcesEnabled() } returns false every { appSettingsService.isUpdateSourcesEnabled() } returns false
coEvery { api.apiInformation() } returns coEvery { api.apiInformation() } returns
StatusAndData( StatusAndData(
success = true, success = true,
data = SelfossModel.ApiInformation( data =
SelfossModel.ApiInformation(
"2.19-ba1e8e3", "2.19-ba1e8e3",
"4.0.0", "4.0.0",
SelfossModel.ApiConfiguration(false, true) SelfossModel.ApiConfiguration(false, true),
), ),
) )
coEvery { api.stats() } returns coEvery { api.stats() } returns
StatusAndData( StatusAndData(
success = true, success = true,
data = SelfossModel.Stats(NUMBER_ARTICLES, NUMBER_UNREAD, NUMBER_STARRED), data = SelfossModel.Stats(NUMBER_ARTICLES, NUMBER_UNREAD, NUMBER_STARRED),
) )
every { db.itemsQueries.deleteItemsWhereSource(any()) } returns Unit every { db.itemsQueries.deleteItemsWhereSource(any()) } returns Unit
every { db.itemsQueries.items().executeAsList() } returns generateTestDBItems() every { db.itemsQueries.items().executeAsList() } returns generateTestDBItems()
@ -117,7 +119,7 @@ class RepositoryTest {
fun get_api_4_date_with_api_1_version_stored() { fun get_api_4_date_with_api_1_version_stored() {
every { appSettingsService.getApiVersion() } returns 1 every { appSettingsService.getApiVersion() } returns 1
coEvery { api.getItems(any(), any(), any(), any(), any(), any(), any()) } returns coEvery { api.getItems(any(), any(), any(), any(), any(), any(), any()) } returns
StatusAndData(success = true, data = generateTestApiItem()) StatusAndData(success = true, data = generateTestApiItem())
every { appSettingsService.updateApiVersion(any()) } returns Unit every { appSettingsService.updateApiVersion(any()) } returns Unit
initializeRepository() initializeRepository()
@ -133,14 +135,15 @@ class RepositoryTest {
fun get_public_access() { fun get_public_access() {
every { appSettingsService.updatePublicAccess(any()) } returns Unit every { appSettingsService.updatePublicAccess(any()) } returns Unit
coEvery { api.apiInformation() } returns coEvery { api.apiInformation() } returns
StatusAndData( StatusAndData(
success = true, success = true,
data = SelfossModel.ApiInformation( data =
SelfossModel.ApiInformation(
"2.19-ba1e8e3", "2.19-ba1e8e3",
"4.0.0", "4.0.0",
SelfossModel.ApiConfiguration(true, true) SelfossModel.ApiConfiguration(true, true),
), ),
) )
every { appSettingsService.getUserName() } returns "" every { appSettingsService.getUserName() } returns ""
initializeRepository() initializeRepository()
@ -153,14 +156,15 @@ class RepositoryTest {
fun get_public_access_username_not_empty() { fun get_public_access_username_not_empty() {
every { appSettingsService.updatePublicAccess(any()) } returns Unit every { appSettingsService.updatePublicAccess(any()) } returns Unit
coEvery { api.apiInformation() } returns coEvery { api.apiInformation() } returns
StatusAndData( StatusAndData(
success = true, success = true,
data = SelfossModel.ApiInformation( data =
SelfossModel.ApiInformation(
"2.19-ba1e8e3", "2.19-ba1e8e3",
"4.0.0", "4.0.0",
SelfossModel.ApiConfiguration(true, true) SelfossModel.ApiConfiguration(true, true),
), ),
) )
every { appSettingsService.getUserName() } returns "username" every { appSettingsService.getUserName() } returns "username"
initializeRepository() initializeRepository()
@ -173,14 +177,15 @@ class RepositoryTest {
fun get_public_access_no_auth() { fun get_public_access_no_auth() {
every { appSettingsService.updatePublicAccess(any()) } returns Unit every { appSettingsService.updatePublicAccess(any()) } returns Unit
coEvery { api.apiInformation() } returns coEvery { api.apiInformation() } returns
StatusAndData( StatusAndData(
success = true, success = true,
data = SelfossModel.ApiInformation( data =
SelfossModel.ApiInformation(
"2.19-ba1e8e3", "2.19-ba1e8e3",
"4.0.0", "4.0.0",
SelfossModel.ApiConfiguration(true, false) SelfossModel.ApiConfiguration(true, false),
), ),
) )
every { appSettingsService.getUserName() } returns "" every { appSettingsService.getUserName() } returns ""
initializeRepository() initializeRepository()
@ -193,14 +198,15 @@ class RepositoryTest {
fun get_public_access_disabled() { fun get_public_access_disabled() {
every { appSettingsService.updatePublicAccess(any()) } returns Unit every { appSettingsService.updatePublicAccess(any()) } returns Unit
coEvery { api.apiInformation() } returns coEvery { api.apiInformation() } returns
StatusAndData( StatusAndData(
success = true, success = true,
data = SelfossModel.ApiInformation( data =
SelfossModel.ApiInformation(
"2.19-ba1e8e3", "2.19-ba1e8e3",
"4.0.0", "4.0.0",
SelfossModel.ApiConfiguration(false, true) SelfossModel.ApiConfiguration(false, true),
), ),
) )
every { appSettingsService.getUserName() } returns "" every { appSettingsService.getUserName() } returns ""
initializeRepository() initializeRepository()
@ -216,10 +222,10 @@ class RepositoryTest {
val itemParameters = FakeItemParameters() val itemParameters = FakeItemParameters()
itemParameters.datetime = "2021-04-23 11:45:32" itemParameters.datetime = "2021-04-23 11:45:32"
coEvery { api.getItems(any(), any(), any(), any(), any(), any(), any()) } returns coEvery { api.getItems(any(), any(), any(), any(), any(), any(), any()) } returns
StatusAndData( StatusAndData(
success = true, success = true,
data = generateTestApiItem(itemParameters), data = generateTestApiItem(itemParameters),
) )
initializeRepository() initializeRepository()
runBlocking { runBlocking {
@ -232,7 +238,7 @@ class RepositoryTest {
@Test @Test
fun get_newer_items() { fun get_newer_items() {
coEvery { api.getItems(any(), any(), any(), any(), any(), any(), any()) } returns coEvery { api.getItems(any(), any(), any(), any(), any(), any(), any()) } returns
StatusAndData(success = true, data = generateTestApiItem()) StatusAndData(success = true, data = generateTestApiItem())
initializeRepository() initializeRepository()
runBlocking { runBlocking {
@ -247,7 +253,7 @@ class RepositoryTest {
@Test @Test
fun get_all_newer_items() { fun get_all_newer_items() {
coEvery { api.getItems(any(), any(), any(), any(), any(), any(), any()) } returns coEvery { api.getItems(any(), any(), any(), any(), any(), any(), any()) } returns
StatusAndData(success = true, data = generateTestApiItem()) StatusAndData(success = true, data = generateTestApiItem())
initializeRepository() initializeRepository()
repository.displayedItems = ItemType.ALL repository.displayedItems = ItemType.ALL
@ -263,7 +269,7 @@ class RepositoryTest {
@Test @Test
fun get_newer_starred_items() { fun get_newer_starred_items() {
coEvery { api.getItems(any(), any(), any(), any(), any(), any(), any()) } returns coEvery { api.getItems(any(), any(), any(), any(), any(), any(), any()) } returns
StatusAndData(success = true, data = generateTestApiItem()) StatusAndData(success = true, data = generateTestApiItem())
initializeRepository() initializeRepository()
repository.displayedItems = ItemType.STARRED repository.displayedItems = ItemType.STARRED
@ -302,8 +308,8 @@ class RepositoryTest {
coEvery { db.itemsQueries.items().executeAsList() } returns generateTestDBItems( coEvery { db.itemsQueries.items().executeAsList() } returns generateTestDBItems(
itemParameter1, itemParameter1,
) + ) +
generateTestDBItems(itemParameter2) + generateTestDBItems(itemParameter2) +
generateTestDBItems(itemParameter3) generateTestDBItems(itemParameter3)
every { appSettingsService.isItemCachingEnabled() } returns true every { appSettingsService.isItemCachingEnabled() } returns true
@ -330,8 +336,8 @@ class RepositoryTest {
coEvery { db.itemsQueries.items().executeAsList() } returns generateTestDBItems( coEvery { db.itemsQueries.items().executeAsList() } returns generateTestDBItems(
itemParameter1, itemParameter1,
) + ) +
generateTestDBItems(itemParameter2) + generateTestDBItems(itemParameter2) +
generateTestDBItems(itemParameter3) generateTestDBItems(itemParameter3)
every { appSettingsService.isItemCachingEnabled() } returns true every { appSettingsService.isItemCachingEnabled() } returns true
@ -360,7 +366,7 @@ class RepositoryTest {
@Test @Test
fun get_older_items() { fun get_older_items() {
coEvery { api.getItems(any(), any(), any(), any(), any(), any(), any()) } returns coEvery { api.getItems(any(), any(), any(), any(), any(), any(), any()) } returns
StatusAndData(success = true, data = generateTestApiItem()) StatusAndData(success = true, data = generateTestApiItem())
initializeRepository() initializeRepository()
repository.items = ArrayList(generateTestApiItem()) repository.items = ArrayList(generateTestApiItem())
@ -376,7 +382,7 @@ class RepositoryTest {
@Test @Test
fun get_all_older_items() { fun get_all_older_items() {
coEvery { api.getItems(any(), any(), any(), any(), any(), any(), any()) } returns coEvery { api.getItems(any(), any(), any(), any(), any(), any(), any()) } returns
StatusAndData(success = true, data = generateTestApiItem()) StatusAndData(success = true, data = generateTestApiItem())
initializeRepository() initializeRepository()
repository.items = ArrayList(generateTestApiItem()) repository.items = ArrayList(generateTestApiItem())
@ -393,7 +399,7 @@ class RepositoryTest {
@Test @Test
fun get_older_starred_items() { fun get_older_starred_items() {
coEvery { api.getItems(any(), any(), any(), any(), any(), any(), any()) } returns coEvery { api.getItems(any(), any(), any(), any(), any(), any(), any()) } returns
StatusAndData(success = true, data = generateTestApiItem()) StatusAndData(success = true, data = generateTestApiItem())
initializeRepository() initializeRepository()
repository.displayedItems = ItemType.STARRED repository.displayedItems = ItemType.STARRED
@ -833,7 +839,7 @@ class RepositoryTest {
@Test @Test
fun create_source() { fun create_source() {
coEvery { api.createSourceForVersion(any(), any(), any(), any()) } returns coEvery { api.createSourceForVersion(any(), any(), any(), any()) } returns
SuccessResponse(true) SuccessResponse(true)
initializeRepository() initializeRepository()
var response: Boolean var response: Boolean
@ -861,7 +867,7 @@ class RepositoryTest {
@Test @Test
fun create_source_but_response_fails() { fun create_source_but_response_fails() {
coEvery { api.createSourceForVersion(any(), any(), any(), any()) } returns coEvery { api.createSourceForVersion(any(), any(), any(), any()) } returns
SuccessResponse(false) SuccessResponse(false)
initializeRepository() initializeRepository()
var response: Boolean var response: Boolean
@ -889,7 +895,7 @@ class RepositoryTest {
@Test @Test
fun create_source_without_connection() { fun create_source_without_connection() {
coEvery { api.createSourceForVersion(any(), any(), any(), any()) } returns coEvery { api.createSourceForVersion(any(), any(), any(), any()) } returns
SuccessResponse(true) SuccessResponse(true)
initializeRepository(MutableStateFlow(false)) initializeRepository(MutableStateFlow(false))
var response: Boolean var response: Boolean
@ -962,10 +968,10 @@ class RepositoryTest {
@Test @Test
fun update_remote() { fun update_remote() {
coEvery { api.update() } returns coEvery { api.update() } returns
StatusAndData( StatusAndData(
success = true, success = true,
data = "finished", data = "finished",
) )
initializeRepository() initializeRepository()
var response: Boolean var response: Boolean
@ -980,10 +986,10 @@ class RepositoryTest {
@Test @Test
fun update_remote_but_response_fails() { fun update_remote_but_response_fails() {
coEvery { api.update() } returns coEvery { api.update() } returns
StatusAndData( StatusAndData(
success = false, success = false,
data = "unallowed access", data = "unallowed access",
) )
initializeRepository() initializeRepository()
var response: Boolean var response: Boolean
@ -998,10 +1004,10 @@ class RepositoryTest {
@Test @Test
fun update_remote_with_unallowed_access() { fun update_remote_with_unallowed_access() {
coEvery { api.update() } returns coEvery { api.update() } returns
StatusAndData( StatusAndData(
success = true, success = true,
data = "unallowed access", data = "unallowed access",
) )
initializeRepository() initializeRepository()
var response: Boolean var response: Boolean
@ -1016,10 +1022,10 @@ class RepositoryTest {
@Test @Test
fun update_remote_without_connection() { fun update_remote_without_connection() {
coEvery { api.update() } returns coEvery { api.update() } returns
StatusAndData( StatusAndData(
success = true, success = true,
data = "undocumented...", data = "undocumented...",
) )
initializeRepository(MutableStateFlow(false)) initializeRepository(MutableStateFlow(false))
var response: Boolean var response: Boolean
@ -1109,11 +1115,11 @@ class RepositoryTest {
any(), any(),
) )
} returnsMany } returnsMany
listOf( listOf(
StatusAndData(success = true, data = generateTestApiItem(itemParameter1)), StatusAndData(success = true, data = generateTestApiItem(itemParameter1)),
StatusAndData(success = true, data = generateTestApiItem(itemParameter2)), StatusAndData(success = true, data = generateTestApiItem(itemParameter2)),
StatusAndData(success = true, data = generateTestApiItem(itemParameter1)), StatusAndData(success = true, data = generateTestApiItem(itemParameter1)),
) )
initializeRepository() initializeRepository()
prepareSearch() prepareSearch()
@ -1127,7 +1133,7 @@ class RepositoryTest {
@Test @Test
fun cache_items_but_response_fails() { fun cache_items_but_response_fails() {
coEvery { api.getItems(any(), any(), any(), any(), any(), any(), any()) } returns coEvery { api.getItems(any(), any(), any(), any(), any(), any(), any()) } returns
StatusAndData(success = false, data = generateTestApiItem()) StatusAndData(success = false, data = generateTestApiItem())
initializeRepository() initializeRepository()
prepareSearch() prepareSearch()
@ -1141,7 +1147,7 @@ class RepositoryTest {
@Test @Test
fun cache_items_without_connection() { fun cache_items_without_connection() {
coEvery { api.getItems(any(), any(), any(), any(), any(), any(), any()) } returns coEvery { api.getItems(any(), any(), any(), any(), any(), any(), any()) } returns
StatusAndData(success = false, data = generateTestApiItem()) StatusAndData(success = false, data = generateTestApiItem())
initializeRepository(MutableStateFlow(false)) initializeRepository(MutableStateFlow(false))
prepareSearch() prepareSearch()
@ -1168,4 +1174,4 @@ class RepositoryTest {
) )
repository.searchFilter = "search" repository.searchFilter = "search"
} }
} }

View File

@ -3,8 +3,8 @@ package bou.amine.apps.readerforselfossv2.tests.repository
import bou.amine.apps.readerforselfossv2.dao.ITEM import bou.amine.apps.readerforselfossv2.dao.ITEM
import bou.amine.apps.readerforselfossv2.model.SelfossModel import bou.amine.apps.readerforselfossv2.model.SelfossModel
fun generateTestDBItems(item: FakeItemParameters = FakeItemParameters()): List<ITEM> { fun generateTestDBItems(item: FakeItemParameters = FakeItemParameters()): List<ITEM> =
return listOf( listOf(
ITEM( ITEM(
id = item.id, id = item.id,
datetime = item.datetime, datetime = item.datetime,
@ -20,10 +20,9 @@ fun generateTestDBItems(item: FakeItemParameters = FakeItemParameters()): List<I
author = item.author, author = item.author,
), ),
) )
}
fun generateTestApiItem(item: FakeItemParameters = FakeItemParameters()): List<SelfossModel.Item> { fun generateTestApiItem(item: FakeItemParameters = FakeItemParameters()): List<SelfossModel.Item> =
return listOf( listOf(
SelfossModel.Item( SelfossModel.Item(
id = item.id.toInt(), id = item.id.toInt(),
datetime = item.datetime, datetime = item.datetime,
@ -39,14 +38,13 @@ fun generateTestApiItem(item: FakeItemParameters = FakeItemParameters()): List<S
author = item.author, author = item.author,
), ),
) )
}
class FakeItemParameters { class FakeItemParameters {
var id = "20" var id = "20"
var datetime = "2022-09-09T03:32:01-04:00" var datetime = "2022-09-09T03:32:01-04:00"
val title = "Etica della ricerca sotto i riflettori." val title = "Etica della ricerca sotto i riflettori."
val content = val content =
"<p><strong>Luigi Campanella, già Presidente SCI</strong></p>\n<p>Letica della scienza è di certo ambito di cui continuiamo a scoprire nuovi aspetti e risvolti.</p>\n<p>Lultimo è quello delle intelligenze artificiali capaci di creare opere complesse basate su immagini e parole memorizzate con il rischio di fake news e di contenuti disturbanti.</p>\n<p>Per evitare che ciò accada si sta procedendo filtrando secondo criteri di autocensura i dati da cui lintelligenza artificiale parte.</p>\n<p>Comincia ad intravedersi un futuro prossimo di competizione fra autori umani ed artificiali nel quale sarà importante, quando i loro prodotti saranno indistinguibili, dichiararne lorigine.</p>\n<p>Come si comprende, si conferma che gli aspetti etici dellinnovazione e della ricerca si diversificato sempre di più.</p>\n<p>La biologia molecolare e la genetica già in passato hanno posto allattenzione comune aspetti di etica della scienza che hanno indotto a nuove riflessioni circa i limiti delle ricerche.</p>\n<p>Largomento, sempre attuale, torna sulle prime pagine a seguito della pubblicazione di una ricerca della Università di Cambridge che ha sviluppato una struttura cellulare di un topo con un cuore che batte regolarmente.</p>\n<img src=\"https://ilblogdellasci.files.wordpress.com/2022/09/image002-1.png?w=481\" alt=\"\" width=\"697\" height=\"430\" /><img src=\"https://ilblogdellasci.files.wordpress.com/2022/09/image003-1.png?w=906\" alt=\"\" /><p>Magdalena Zernicka-Goetz</p>\n<img src=\"https://ilblogdellasci.files.wordpress.com/2022/09/image004.jpg?w=474\" alt=\"\" width=\"622\" height=\"465\" /><p>Gianluca Amadei</p>\n<p>Del gruppo fa parte anche uno scienziato italiano Gianluca Amadei,che dinnanzi alle obiezioni di natura etica sulla realizzazione della vita artificiale si è affrettato a sostenere che non è creare nuove vite il fine primario della ricerca, ma quello di salvare quelle esistenti, di dare contributi essenziali alla medicina citando il caso del fallimento tuttora non interpretato di alcune gravidanze e di superare la sperimentazione animale, così contribuendo positivamente alla soluzione di un altro dilemma etico.</p>\n<p>Lembrione sintetico ha ovviamente come primo traguardo il contributo ai trapianti oggi drammaticamente carenti nellofferta rispetto alla domanda, con attese fino a 4 anni per i trapianti di cuore ed a 2 anni per quelli di fegato. Il lavoro dovrebbe adesso continuare presso lAteneo di Padova per creare nuovi organi e nuovi farmaci.</p>" "<p><strong>Luigi Campanella, già Presidente SCI</strong></p>\n<p>Letica della scienza è di certo ambito di cui continuiamo</p>"
var unread = true var unread = true
var starred = true var starred = true
val thumbnail = null val thumbnail = null
@ -56,4 +54,4 @@ class FakeItemParameters {
var sourcetitle = "La Chimica e la Società" var sourcetitle = "La Chimica e la Società"
var tags = "Chimica, Testing" var tags = "Chimica, Testing"
var author = "Someone important" var author = "Someone important"
} }

View File

@ -9,7 +9,7 @@ actual class DriverFactory(private val context: Context) {
return AndroidSqliteDriver( return AndroidSqliteDriver(
ReaderForSelfossDB.Schema, ReaderForSelfossDB.Schema,
context, context,
"ReaderForSelfossV2-android.db" "ReaderForSelfossV2-android.db",
) )
} }
} }

View File

@ -8,16 +8,18 @@ class NaiveTrustManager : X509TrustManager {
override fun checkClientTrusted( override fun checkClientTrusted(
chain: Array<out X509Certificate>?, chain: Array<out X509Certificate>?,
authType: String?, authType: String?,
) {} ) {
}
override fun checkServerTrusted( override fun checkServerTrusted(
chain: Array<out X509Certificate>?, chain: Array<out X509Certificate>?,
authType: String?, authType: String?,
) {} ) {
}
override fun getAcceptedIssuers(): Array<out X509Certificate> = arrayOf() override fun getAcceptedIssuers(): Array<out X509Certificate> = arrayOf()
} }
actual fun setupInsecureHTTPEngine(config: CIOEngineConfig) { actual fun setupInsecureHttpEngine(config: CIOEngineConfig) {
config.https.trustManager = NaiveTrustManager() config.https.trustManager = NaiveTrustManager()
} }

View File

@ -1,8 +1,7 @@
package bou.amine.apps.readerforselfossv2.utils package bou.amine.apps.readerforselfossv2.utils
import android.text.format.DateUtils import android.text.format.DateUtils
import io.github.aakira.napier.Napier import kotlinx.datetime.Clock
import kotlinx.datetime.*
actual class DateUtils { actual class DateUtils {
actual companion object { actual companion object {

View File

@ -4,19 +4,13 @@ import android.net.Uri
import android.text.Html import android.text.Html
import bou.amine.apps.readerforselfossv2.model.SelfossModel import bou.amine.apps.readerforselfossv2.model.SelfossModel
import org.jsoup.Jsoup import org.jsoup.Jsoup
import java.util.* import java.util.Locale
actual fun String.getHtmlDecoded(): String { actual fun String.getHtmlDecoded(): String = Html.fromHtml(this).toString()
return Html.fromHtml(this).toString()
}
actual fun SelfossModel.Item.getIcon(baseUrl: String): String { actual fun SelfossModel.Item.getIcon(baseUrl: String): String = constructUrl(baseUrl, "favicons", icon)
return constructUrl(baseUrl, "favicons", icon)
}
actual fun SelfossModel.Item.getThumbnail(baseUrl: String): String { actual fun SelfossModel.Item.getThumbnail(baseUrl: String): String = constructUrl(baseUrl, "thumbnails", thumbnail)
return constructUrl(baseUrl, "thumbnails", thumbnail)
}
actual fun SelfossModel.Item.getImages(): ArrayList<String> { actual fun SelfossModel.Item.getImages(): ArrayList<String> {
val allImages = ArrayList<String>() val allImages = ArrayList<String>()
@ -34,16 +28,14 @@ actual fun SelfossModel.Item.getImages(): ArrayList<String> {
return allImages return allImages
} }
actual fun SelfossModel.Source.getIcon(baseUrl: String): String { actual fun SelfossModel.Source.getIcon(baseUrl: String): String = constructUrl(baseUrl, "favicons", icon)
return constructUrl(baseUrl, "favicons", icon)
}
actual fun constructUrl( actual fun constructUrl(
baseUrl: String, baseUrl: String,
path: String, path: String,
file: String?, file: String?,
): String { ): String =
return if (file == null || file == "null" || file.isEmpty()) { if (file == null || file == "null" || file.isEmpty()) {
"" ""
} else { } else {
val baseUriBuilder = Uri.parse(baseUrl).buildUpon() val baseUriBuilder = Uri.parse(baseUrl).buildUpon()
@ -51,4 +43,3 @@ actual fun constructUrl(
baseUriBuilder.toString() baseUriBuilder.toString()
} }
}

View File

@ -4,4 +4,4 @@ import app.cash.sqldelight.db.SqlDriver
expect class DriverFactory { expect class DriverFactory {
fun createDriver(): SqlDriver fun createDriver(): SqlDriver
} }

View File

@ -1,4 +1,4 @@
package bou.amine.apps.readerforselfossv2.DI package bou.amine.apps.readerforselfossv2.di
import bou.amine.apps.readerforselfossv2.rest.MercuryApi import bou.amine.apps.readerforselfossv2.rest.MercuryApi
import bou.amine.apps.readerforselfossv2.rest.SelfossApi import bou.amine.apps.readerforselfossv2.rest.SelfossApi

View File

@ -7,7 +7,7 @@ class MercuryModel {
class ParsedContent( class ParsedContent(
val title: String? = null, val title: String? = null,
val content: String? = null, val content: String? = null,
val lead_image_url: String? = null, // NOSONAR val lead_image_url: String? = null,
val url: String? = null, val url: String? = null,
val error: Boolean? = null, val error: Boolean? = null,
val message: String? = null, val message: String? = null,

View File

@ -3,19 +3,20 @@ package bou.amine.apps.readerforselfossv2.model
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
@Serializable @Serializable
class SuccessResponse(val success: Boolean) { class SuccessResponse(
val success: Boolean,
) {
val isSuccess: Boolean val isSuccess: Boolean
get() = success get() = success
} }
class StatusAndData<T>(val success: Boolean, val data: T? = null) { class StatusAndData<T>(
val success: Boolean,
val data: T? = null,
) {
companion object { companion object {
fun <T> succes(d: T): StatusAndData<T> { fun <T> succes(d: T): StatusAndData<T> = StatusAndData(true, d)
return StatusAndData(true, d)
}
fun <T> error(): StatusAndData<T> { fun <T> error(): StatusAndData<T> = StatusAndData(false)
return StatusAndData(false)
}
} }
} }

View File

@ -141,7 +141,7 @@ class SelfossModel {
} }
if (stringUrl.isEmptyOrNullOrNullString()) { if (stringUrl.isEmptyOrNullOrNullString()) {
throw Exception("Link ${link} was translated to ${stringUrl}, but was empty. Handle this.") throw Exception("Link $link was translated to $stringUrl, but was empty. Handle this.")
} }
return stringUrl return stringUrl
@ -172,12 +172,11 @@ class SelfossModel {
// TODO: this seems to be super slow. // TODO: this seems to be super slow.
object TagsListSerializer : KSerializer<List<String>> { object TagsListSerializer : KSerializer<List<String>> {
override fun deserialize(decoder: Decoder): List<String> { override fun deserialize(decoder: Decoder): List<String> =
return when (val json = ((decoder as JsonDecoder).decodeJsonElement())) { when (val json = ((decoder as JsonDecoder).decodeJsonElement())) {
is JsonArray -> json.toList().map { it.toString().replace("^\"|\"$".toRegex(), "") } is JsonArray -> json.toList().map { it.toString().replace("^\"|\"$".toRegex(), "") }
else -> json.toString().split(",") else -> json.toString().split(",")
} }
}
override val descriptor: SerialDescriptor override val descriptor: SerialDescriptor
get() = PrimitiveSerialDescriptor("tags", PrimitiveKind.STRING) get() = PrimitiveSerialDescriptor("tags", PrimitiveKind.STRING)
@ -188,7 +187,7 @@ class SelfossModel {
) { ) {
encoder.encodeCollection( encoder.encodeCollection(
PrimitiveSerialDescriptor("tags", PrimitiveKind.STRING), PrimitiveSerialDescriptor("tags", PrimitiveKind.STRING),
value.size value.size,
) { this.toString() } ) { this.toString() }
} }
} }
@ -204,10 +203,11 @@ class SelfossModel {
} }
override val descriptor: SerialDescriptor override val descriptor: SerialDescriptor
get() = PrimitiveSerialDescriptor( get() =
"BooleanOrIntForSomeSelfossVersions", PrimitiveSerialDescriptor(
PrimitiveKind.BOOLEAN "BooleanOrIntForSomeSelfossVersions",
) PrimitiveKind.BOOLEAN,
)
override fun serialize( override fun serialize(
encoder: Encoder, encoder: Encoder,
@ -216,4 +216,4 @@ class SelfossModel {
TODO("Not yet implemented") TODO("Not yet implemented")
} }
} }
} }

View File

@ -1,12 +1,21 @@
package bou.amine.apps.readerforselfossv2.repository package bou.amine.apps.readerforselfossv2.repository
import bou.amine.apps.readerforselfossv2.dao.* import bou.amine.apps.readerforselfossv2.dao.ACTION
import bou.amine.apps.readerforselfossv2.dao.DriverFactory
import bou.amine.apps.readerforselfossv2.dao.ITEM
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.NetworkUnavailableException import bou.amine.apps.readerforselfossv2.model.NetworkUnavailableException
import bou.amine.apps.readerforselfossv2.model.SelfossModel import bou.amine.apps.readerforselfossv2.model.SelfossModel
import bou.amine.apps.readerforselfossv2.model.StatusAndData import bou.amine.apps.readerforselfossv2.model.StatusAndData
import bou.amine.apps.readerforselfossv2.rest.SelfossApi import bou.amine.apps.readerforselfossv2.rest.SelfossApi
import bou.amine.apps.readerforselfossv2.service.AppSettingsService import bou.amine.apps.readerforselfossv2.service.AppSettingsService
import bou.amine.apps.readerforselfossv2.utils.* import bou.amine.apps.readerforselfossv2.utils.Enums.ItemType
import bou.amine.apps.readerforselfossv2.utils.getHtmlDecoded
import bou.amine.apps.readerforselfossv2.utils.toEntity
import bou.amine.apps.readerforselfossv2.utils.toParsedDate
import bou.amine.apps.readerforselfossv2.utils.toView
import io.github.aakira.napier.Napier import io.github.aakira.napier.Napier
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@ -27,26 +36,26 @@ class Repository(
var displayedItems = ItemType.UNREAD var displayedItems = ItemType.UNREAD
private var _tagFilter = MutableStateFlow<SelfossModel.Tag?>(null) private var tagFilterFlow = MutableStateFlow<SelfossModel.Tag?>(null)
var tagFilter = _tagFilter.asStateFlow() var tagFilter = tagFilterFlow.asStateFlow()
private var _sourceFilter = MutableStateFlow<SelfossModel.Source?>(null) private var sourceFilterFlow = MutableStateFlow<SelfossModel.Source?>(null)
var sourceFilter = _sourceFilter.asStateFlow() var sourceFilter = sourceFilterFlow.asStateFlow()
var searchFilter: String? = null var searchFilter: String? = null
var offlineOverride = false var offlineOverride = false
private val _badgeUnread = MutableStateFlow(0) private val badgeUnreadFlow = MutableStateFlow(0)
val badgeUnread = _badgeUnread.asStateFlow() val badgeUnread = badgeUnreadFlow.asStateFlow()
private val _badgeAll = MutableStateFlow(0) private val badgeAllFlow = MutableStateFlow(0)
val badgeAll = _badgeAll.asStateFlow() val badgeAll = badgeAllFlow.asStateFlow()
private val _badgeStarred = MutableStateFlow(0) private val badgeStarredFlow = MutableStateFlow(0)
val badgeStarred = _badgeStarred.asStateFlow() val badgeStarred = badgeStarredFlow.asStateFlow()
private var fetchedTags = false private var fetchedTags = false
private var fetchedSources = false private var fetchedSources = false
private var _readerItems = ArrayList<SelfossModel.Item>() private var readerItems = ArrayList<SelfossModel.Item>()
private var _selectedSource: SelfossModel.SourceDetail? = null private var selectedSource: SelfossModel.SourceDetail? = null
suspend fun getNewerItems(): ArrayList<SelfossModel.Item> { suspend fun getNewerItems(): ArrayList<SelfossModel.Item> {
var fetchedItems: StatusAndData<List<SelfossModel.Item>> = StatusAndData.error() var fetchedItems: StatusAndData<List<SelfossModel.Item>> = StatusAndData.error()
@ -135,17 +144,17 @@ class Repository(
if (isNetworkAvailable()) { if (isNetworkAvailable()) {
val response = api.stats() val response = api.stats()
if (response.success && response.data != null) { if (response.success && response.data != null) {
_badgeUnread.value = response.data.unread ?: 0 badgeUnreadFlow.value = response.data.unread ?: 0
_badgeAll.value = response.data.total badgeAllFlow.value = response.data.total
_badgeStarred.value = response.data.starred ?: 0 badgeStarredFlow.value = response.data.starred ?: 0
success = true success = true
} }
} else if (appSettingsService.isItemCachingEnabled()) { } else if (appSettingsService.isItemCachingEnabled()) {
// TODO: do this differently, because it's not efficient // TODO: do this differently, because it's not efficient
val dbItems = getDBItems() val dbItems = getDBItems()
_badgeUnread.value = dbItems.filter { item -> item.unread }.size badgeUnreadFlow.value = dbItems.filter { item -> item.unread }.size
_badgeStarred.value = dbItems.filter { item -> item.starred }.size badgeStarredFlow.value = dbItems.filter { item -> item.starred }.size
_badgeAll.value = dbItems.size badgeAllFlow.value = dbItems.size
success = true success = true
} }
return success return success
@ -170,8 +179,8 @@ class Repository(
} }
} }
suspend fun getSpouts(): Map<String, SelfossModel.Spout> { suspend fun getSpouts(): Map<String, SelfossModel.Spout> =
return if (isNetworkAvailable()) { if (isNetworkAvailable()) {
val spouts = api.spouts() val spouts = api.spouts()
if (spouts.success && spouts.data != null) { if (spouts.success && spouts.data != null) {
spouts.data spouts.data
@ -181,7 +190,6 @@ class Repository(
} else { } else {
throw NetworkUnavailableException() throw NetworkUnavailableException()
} }
}
suspend fun getSourcesDetailsOrStats(): ArrayList<SelfossModel.Source> { suspend fun getSourcesDetailsOrStats(): ArrayList<SelfossModel.Source> {
var sources = ArrayList<SelfossModel.Source>() var sources = ArrayList<SelfossModel.Source>()
@ -234,14 +242,13 @@ class Repository(
return success return success
} }
private suspend fun markAsReadById(id: Int): Boolean { private suspend fun markAsReadById(id: Int): Boolean =
return if (isNetworkAvailable()) { if (isNetworkAvailable()) {
api.markAsRead(id.toString()).isSuccess api.markAsRead(id.toString()).isSuccess
} else { } else {
insertDBAction(id.toString(), read = true) insertDBAction(id.toString(), read = true)
true true
} }
}
suspend fun unmarkAsRead(item: SelfossModel.Item): Boolean { suspend fun unmarkAsRead(item: SelfossModel.Item): Boolean {
val success = unmarkAsReadById(item.id) val success = unmarkAsReadById(item.id)
@ -252,14 +259,13 @@ class Repository(
return success return success
} }
private suspend fun unmarkAsReadById(id: Int): Boolean { private suspend fun unmarkAsReadById(id: Int): Boolean =
return if (isNetworkAvailable()) { if (isNetworkAvailable()) {
api.unmarkAsRead(id.toString()).isSuccess api.unmarkAsRead(id.toString()).isSuccess
} else { } else {
insertDBAction(id.toString(), unread = true) insertDBAction(id.toString(), unread = true)
true true
} }
}
suspend fun starr(item: SelfossModel.Item): Boolean { suspend fun starr(item: SelfossModel.Item): Boolean {
val success = starrById(item.id) val success = starrById(item.id)
@ -270,14 +276,13 @@ class Repository(
return success return success
} }
private suspend fun starrById(id: Int): Boolean { private suspend fun starrById(id: Int): Boolean =
return if (isNetworkAvailable()) { if (isNetworkAvailable()) {
api.starr(id.toString()).isSuccess api.starr(id.toString()).isSuccess
} else { } else {
insertDBAction(id.toString(), starred = true) insertDBAction(id.toString(), starred = true)
true true
} }
}
suspend fun unstarr(item: SelfossModel.Item): Boolean { suspend fun unstarr(item: SelfossModel.Item): Boolean {
val success = unstarrById(item.id) val success = unstarrById(item.id)
@ -288,14 +293,13 @@ class Repository(
return success return success
} }
private suspend fun unstarrById(id: Int): Boolean { private suspend fun unstarrById(id: Int): Boolean =
return if (isNetworkAvailable()) { if (isNetworkAvailable()) {
api.unstarr(id.toString()).isSuccess api.unstarr(id.toString()).isSuccess
} else { } else {
insertDBAction(id.toString(), starred = true) insertDBAction(id.toString(), starred = true)
true true
} }
}
suspend fun markAllAsRead(items: ArrayList<SelfossModel.Item>): Boolean { suspend fun markAllAsRead(items: ArrayList<SelfossModel.Item>): Boolean {
var success = false var success = false
@ -312,7 +316,7 @@ class Repository(
private fun markAsReadLocally(item: SelfossModel.Item) { private fun markAsReadLocally(item: SelfossModel.Item) {
if (item.unread) { if (item.unread) {
item.unread = false item.unread = false
_badgeUnread.value -= 1 badgeUnreadFlow.value -= 1
} }
CoroutineScope(Dispatchers.Main).launch { CoroutineScope(Dispatchers.Main).launch {
@ -323,7 +327,7 @@ class Repository(
private fun unmarkAsReadLocally(item: SelfossModel.Item) { private fun unmarkAsReadLocally(item: SelfossModel.Item) {
if (!item.unread) { if (!item.unread) {
item.unread = true item.unread = true
_badgeUnread.value += 1 badgeUnreadFlow.value += 1
} }
CoroutineScope(Dispatchers.Main).launch { CoroutineScope(Dispatchers.Main).launch {
@ -334,7 +338,7 @@ class Repository(
private fun starrLocally(item: SelfossModel.Item) { private fun starrLocally(item: SelfossModel.Item) {
if (!item.starred) { if (!item.starred) {
item.starred = true item.starred = true
_badgeStarred.value += 1 badgeStarredFlow.value += 1
} }
CoroutineScope(Dispatchers.Main).launch { CoroutineScope(Dispatchers.Main).launch {
@ -345,7 +349,7 @@ class Repository(
private fun unstarrLocally(item: SelfossModel.Item) { private fun unstarrLocally(item: SelfossModel.Item) {
if (item.starred) { if (item.starred) {
item.starred = false item.starred = false
_badgeStarred.value -= 1 badgeStarredFlow.value -= 1
} }
CoroutineScope(Dispatchers.Main).launch { CoroutineScope(Dispatchers.Main).launch {
@ -361,12 +365,13 @@ class Repository(
): Boolean { ): Boolean {
var response = false var response = false
if (isNetworkAvailable()) { if (isNetworkAvailable()) {
response = api.createSourceForVersion( response = api
title, .createSourceForVersion(
url, title,
spout, url,
tags, spout,
).isSuccess == true tags,
).isSuccess == true
} }
return response return response
@ -407,13 +412,12 @@ class Repository(
return success return success
} }
suspend fun updateRemote(): Boolean { suspend fun updateRemote(): Boolean =
return if (isNetworkAvailable()) { if (isNetworkAvailable()) {
api.update().data.equals("finished") api.update().data.equals("finished")
} else { } else {
false false
} }
}
suspend fun login(): Boolean { suspend fun login(): Boolean {
var result = false var result = false
@ -422,7 +426,7 @@ class Repository(
val response = api.login() val response = api.login()
result = response.isSuccess == true result = response.isSuccess == true
} catch (cause: Throwable) { } catch (cause: Throwable) {
Napier.e("login failed", cause, tag = "RepositoryImpl.login") Napier.e("login failed", cause, tag = "Repository.login")
} }
} }
return result return result
@ -436,7 +440,7 @@ class Repository(
// a random rss feed, that would throw a NoTransformationFoundException // a random rss feed, that would throw a NoTransformationFoundException
fetchFailed = !api.getItemsWithoutCatch().success fetchFailed = !api.getItemsWithoutCatch().success
} catch (e: Throwable) { } catch (e: Throwable) {
Napier.e("checkIfFetchFails failed", e, tag = "RepositoryImpl.shouldBeSelfossInstance") Napier.e("checkIfFetchFails failed", e, tag = "Repository.shouldBeSelfossInstance")
} }
} }
@ -448,10 +452,10 @@ class Repository(
try { try {
val response = api.logout() val response = api.logout()
if (!response.isSuccess) { if (!response.isSuccess) {
Napier.e("Couldn't logout.", tag = "RepositoryImpl.logout") Napier.e("Couldn't logout.", tag = "Repository.logout")
} }
} catch (cause: Throwable) { } catch (cause: Throwable) {
Napier.e("logout failed", cause, tag = "RepositoryImpl.logout") Napier.e("logout failed", cause, tag = "Repository.logout")
} }
appSettingsService.clearAll() appSettingsService.clearAll()
} else { } else {
@ -578,16 +582,19 @@ class Repository(
markAsReadById(action.articleid.toInt()), markAsReadById(action.articleid.toInt()),
action, action,
) )
action.unread -> action.unread ->
doAndReportOnFail( doAndReportOnFail(
unmarkAsReadById(action.articleid.toInt()), unmarkAsReadById(action.articleid.toInt()),
action, action,
) )
action.starred -> action.starred ->
doAndReportOnFail( doAndReportOnFail(
starrById(action.articleid.toInt()), starrById(action.articleid.toInt()),
action, action,
) )
action.unstarred -> action.unstarred ->
doAndReportOnFail( doAndReportOnFail(
unstarrById(action.articleid.toInt()), unstarrById(action.articleid.toInt()),
@ -607,34 +614,30 @@ class Repository(
} }
fun setTagFilter(tag: SelfossModel.Tag?) { fun setTagFilter(tag: SelfossModel.Tag?) {
_tagFilter.value = tag tagFilterFlow.value = tag
} }
fun setSourceFilter(source: SelfossModel.Source?) { fun setSourceFilter(source: SelfossModel.Source?) {
_sourceFilter.value = source sourceFilterFlow.value = source
} }
fun setReaderItems(readerItems: ArrayList<SelfossModel.Item>) { fun setReaderItems(readerItems: ArrayList<SelfossModel.Item>) {
_readerItems = readerItems this.readerItems = readerItems
} }
fun getReaderItems(): ArrayList<SelfossModel.Item> { fun getReaderItems(): ArrayList<SelfossModel.Item> = readerItems
return _readerItems
}
fun migrate(driverFactory: DriverFactory) { fun migrate(driverFactory: DriverFactory) {
ReaderForSelfossDB.Schema.migrate(driverFactory.createDriver(), 0, 1) ReaderForSelfossDB.Schema.migrate(driverFactory.createDriver(), 0, 1)
} }
fun setSelectedSource(source: SelfossModel.SourceDetail) { fun setSelectedSource(source: SelfossModel.SourceDetail) {
_selectedSource = source selectedSource = source
} }
fun unsetSelectedSource() { fun unsetSelectedSource() {
_selectedSource = null selectedSource = null
} }
fun getSelectedSource(): SelfossModel.SourceDetail? { fun getSelectedSource(): SelfossModel.SourceDetail? = selectedSource
return _selectedSource
}
} }

View File

@ -48,4 +48,4 @@ class MercuryApi {
parameter("link", url) parameter("link", url)
}, },
) )
} }

View File

@ -3,23 +3,28 @@ package bou.amine.apps.readerforselfossv2.rest
import bou.amine.apps.readerforselfossv2.model.StatusAndData import bou.amine.apps.readerforselfossv2.model.StatusAndData
import bou.amine.apps.readerforselfossv2.model.SuccessResponse import bou.amine.apps.readerforselfossv2.model.SuccessResponse
import io.github.aakira.napier.Napier import io.github.aakira.napier.Napier
import io.ktor.client.* import io.ktor.client.HttpClient
import io.ktor.client.call.* import io.ktor.client.call.body
import io.ktor.client.request.* import io.ktor.client.request.HttpRequestBuilder
import io.ktor.client.request.forms.* import io.ktor.client.request.delete
import io.ktor.client.statement.* import io.ktor.client.request.forms.submitForm
import io.ktor.http.* import io.ktor.client.request.get
import io.ktor.client.request.post
import io.ktor.client.request.url
import io.ktor.client.statement.HttpResponse
import io.ktor.http.HttpStatusCode
import io.ktor.http.Parameters
import io.ktor.http.isSuccess
suspend fun responseOrSuccessIf404(r: HttpResponse?): SuccessResponse { suspend fun responseOrSuccessIf404(r: HttpResponse?): SuccessResponse =
return if (r != null && r.status === HttpStatusCode.NotFound) { if (r != null && r.status === HttpStatusCode.NotFound) {
SuccessResponse(true) SuccessResponse(true)
} else { } else {
maybeResponse(r) maybeResponse(r)
} }
}
suspend fun maybeResponse(r: HttpResponse?): SuccessResponse { suspend fun maybeResponse(r: HttpResponse?): SuccessResponse =
return if (r != null && r.status.isSuccess()) { if (r != null && r.status.isSuccess()) {
r.body() r.body()
} else { } else {
if (r != null) { if (r != null) {
@ -27,7 +32,6 @@ suspend fun maybeResponse(r: HttpResponse?): SuccessResponse {
} }
SuccessResponse(false) SuccessResponse(false)
} }
}
suspend inline fun <reified T> bodyOrFailure(r: HttpResponse?): StatusAndData<T> { suspend inline fun <reified T> bodyOrFailure(r: HttpResponse?): StatusAndData<T> {
try { try {
@ -98,4 +102,4 @@ suspend fun HttpClient.tryToSubmitForm(
url(url) url(url)
block() block()
} }
} }

View File

@ -33,16 +33,18 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
expect fun setupInsecureHTTPEngine(config: CIOEngineConfig) expect fun setupInsecureHttpEngine(config: CIOEngineConfig)
class SelfossApi(private val appSettingsService: AppSettingsService) { class SelfossApi(
private val appSettingsService: AppSettingsService,
) {
var client = createHttpClient() var client = createHttpClient()
fun createHttpClient() = fun createHttpClient() =
HttpClient(CIO) { HttpClient(CIO) {
if (appSettingsService.getSelfSigned()) { if (appSettingsService.getSelfSigned()) {
engine { engine {
setupInsecureHTTPEngine(this) setupInsecureHttpEngine(this)
} }
} }
install(HttpCache) install(HttpCache)
@ -105,12 +107,14 @@ class SelfossApi(private val appSettingsService: AppSettingsService) {
private fun hasLoginInfo() = private fun hasLoginInfo() =
appSettingsService.getUserName().isNotEmpty() && appSettingsService.getUserName().isNotEmpty() &&
appSettingsService.getPassword() appSettingsService
.isNotEmpty() .getPassword()
.isNotEmpty()
suspend fun login(): SuccessResponse = suspend fun login(): SuccessResponse =
if (appSettingsService.getUserName().isNotEmpty() && if (appSettingsService.getUserName().isNotEmpty() &&
appSettingsService.getPassword() appSettingsService
.getPassword()
.isNotEmpty() .isNotEmpty()
) { ) {
if (shouldHavePostLogin()) { if (shouldHavePostLogin()) {
@ -127,8 +131,10 @@ class SelfossApi(private val appSettingsService: AppSettingsService) {
client.tryToGet(url("/login")) { client.tryToGet(url("/login")) {
parameter("username", appSettingsService.getUserName()) parameter("username", appSettingsService.getUserName())
parameter("password", appSettingsService.getPassword()) parameter("password", appSettingsService.getPassword())
if (appSettingsService.getBasicUserName() if (appSettingsService
.isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty() .getBasicUserName()
.isNotEmpty() &&
appSettingsService.getBasicPassword().isNotEmpty()
) { ) {
headers { headers {
append( append(
@ -150,8 +156,10 @@ class SelfossApi(private val appSettingsService: AppSettingsService) {
client.tryToPost(url("/login")) { client.tryToPost(url("/login")) {
parameter("username", appSettingsService.getUserName()) parameter("username", appSettingsService.getUserName())
parameter("password", appSettingsService.getPassword()) parameter("password", appSettingsService.getPassword())
if (appSettingsService.getBasicUserName() if (appSettingsService
.isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty() .getBasicUserName()
.isNotEmpty() &&
appSettingsService.getBasicPassword().isNotEmpty()
) { ) {
headers { headers {
append( append(
@ -168,8 +176,7 @@ class SelfossApi(private val appSettingsService: AppSettingsService) {
}, },
) )
private fun shouldHaveNewLogout() = private fun shouldHaveNewLogout() = appSettingsService.getApiVersion() >= 5 // We are missing 4.1.0
appSettingsService.getApiVersion() >= 5 // We are missing 4.1.0
suspend fun logout(): SuccessResponse = suspend fun logout(): SuccessResponse =
if (shouldHaveNewLogout()) { if (shouldHaveNewLogout()) {
@ -181,8 +188,10 @@ class SelfossApi(private val appSettingsService: AppSettingsService) {
private suspend fun maybeLogoutIfAvailable() = private suspend fun maybeLogoutIfAvailable() =
responseOrSuccessIf404( responseOrSuccessIf404(
client.tryToGet(url("/logout")) { client.tryToGet(url("/logout")) {
if (appSettingsService.getBasicUserName() if (appSettingsService
.isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty() .getBasicUserName()
.isNotEmpty() &&
appSettingsService.getBasicPassword().isNotEmpty()
) { ) {
headers { headers {
append( append(
@ -202,8 +211,10 @@ class SelfossApi(private val appSettingsService: AppSettingsService) {
private suspend fun doLogout() = private suspend fun doLogout() =
maybeResponse( maybeResponse(
client.tryToDelete(url("/api/session/current")) { client.tryToDelete(url("/api/session/current")) {
if (appSettingsService.getBasicUserName() if (appSettingsService
.isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty() .getBasicUserName()
.isNotEmpty() &&
appSettingsService.getBasicPassword().isNotEmpty()
) { ) {
headers { headers {
append( append(
@ -242,8 +253,10 @@ class SelfossApi(private val appSettingsService: AppSettingsService) {
parameter("updatedsince", updatedSince) parameter("updatedsince", updatedSince)
parameter("items", items ?: appSettingsService.getItemsNumber()) parameter("items", items ?: appSettingsService.getItemsNumber())
parameter("offset", offset) parameter("offset", offset)
if (appSettingsService.getBasicUserName() if (appSettingsService
.isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty() .getBasicUserName()
.isNotEmpty() &&
appSettingsService.getBasicPassword().isNotEmpty()
) { ) {
headers { headers {
append( append(
@ -269,8 +282,10 @@ class SelfossApi(private val appSettingsService: AppSettingsService) {
} }
parameter("type", "all") parameter("type", "all")
parameter("items", 1) parameter("items", 1)
if (appSettingsService.getBasicUserName() if (appSettingsService
.isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty() .getBasicUserName()
.isNotEmpty() &&
appSettingsService.getBasicPassword().isNotEmpty()
) { ) {
headers { headers {
append( append(
@ -294,8 +309,10 @@ class SelfossApi(private val appSettingsService: AppSettingsService) {
parameter("username", appSettingsService.getUserName()) parameter("username", appSettingsService.getUserName())
parameter("password", appSettingsService.getPassword()) parameter("password", appSettingsService.getPassword())
} }
if (appSettingsService.getBasicUserName() if (appSettingsService
.isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty() .getBasicUserName()
.isNotEmpty() &&
appSettingsService.getBasicPassword().isNotEmpty()
) { ) {
headers { headers {
append( append(
@ -319,8 +336,10 @@ class SelfossApi(private val appSettingsService: AppSettingsService) {
parameter("username", appSettingsService.getUserName()) parameter("username", appSettingsService.getUserName())
parameter("password", appSettingsService.getPassword()) parameter("password", appSettingsService.getPassword())
} }
if (appSettingsService.getBasicUserName() if (appSettingsService
.isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty() .getBasicUserName()
.isNotEmpty() &&
appSettingsService.getBasicPassword().isNotEmpty()
) { ) {
headers { headers {
append( append(
@ -344,8 +363,10 @@ class SelfossApi(private val appSettingsService: AppSettingsService) {
parameter("username", appSettingsService.getUserName()) parameter("username", appSettingsService.getUserName())
parameter("password", appSettingsService.getPassword()) parameter("password", appSettingsService.getPassword())
} }
if (appSettingsService.getBasicUserName() if (appSettingsService
.isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty() .getBasicUserName()
.isNotEmpty() &&
appSettingsService.getBasicPassword().isNotEmpty()
) { ) {
headers { headers {
append( append(
@ -369,8 +390,10 @@ class SelfossApi(private val appSettingsService: AppSettingsService) {
parameter("username", appSettingsService.getUserName()) parameter("username", appSettingsService.getUserName())
parameter("password", appSettingsService.getPassword()) parameter("password", appSettingsService.getPassword())
} }
if (appSettingsService.getBasicUserName() if (appSettingsService
.isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty() .getBasicUserName()
.isNotEmpty() &&
appSettingsService.getBasicPassword().isNotEmpty()
) { ) {
headers { headers {
append( append(
@ -394,8 +417,10 @@ class SelfossApi(private val appSettingsService: AppSettingsService) {
parameter("username", appSettingsService.getUserName()) parameter("username", appSettingsService.getUserName())
parameter("password", appSettingsService.getPassword()) parameter("password", appSettingsService.getPassword())
} }
if (appSettingsService.getBasicUserName() if (appSettingsService
.isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty() .getBasicUserName()
.isNotEmpty() &&
appSettingsService.getBasicPassword().isNotEmpty()
) { ) {
headers { headers {
append( append(
@ -419,8 +444,10 @@ class SelfossApi(private val appSettingsService: AppSettingsService) {
parameter("username", appSettingsService.getUserName()) parameter("username", appSettingsService.getUserName())
parameter("password", appSettingsService.getPassword()) parameter("password", appSettingsService.getPassword())
} }
if (appSettingsService.getBasicUserName() if (appSettingsService
.isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty() .getBasicUserName()
.isNotEmpty() &&
appSettingsService.getBasicPassword().isNotEmpty()
) { ) {
headers { headers {
append( append(
@ -440,8 +467,10 @@ class SelfossApi(private val appSettingsService: AppSettingsService) {
suspend fun apiInformation(): StatusAndData<SelfossModel.ApiInformation> = suspend fun apiInformation(): StatusAndData<SelfossModel.ApiInformation> =
bodyOrFailure( bodyOrFailure(
client.tryToGet(url("/api/about")) { client.tryToGet(url("/api/about")) {
if (appSettingsService.getBasicUserName() if (appSettingsService
.isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty() .getBasicUserName()
.isNotEmpty() &&
appSettingsService.getBasicPassword().isNotEmpty()
) { ) {
headers { headers {
append( append(
@ -465,8 +494,10 @@ class SelfossApi(private val appSettingsService: AppSettingsService) {
parameter("username", appSettingsService.getUserName()) parameter("username", appSettingsService.getUserName())
parameter("password", appSettingsService.getPassword()) parameter("password", appSettingsService.getPassword())
} }
if (appSettingsService.getBasicUserName() if (appSettingsService
.isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty() .getBasicUserName()
.isNotEmpty() &&
appSettingsService.getBasicPassword().isNotEmpty()
) { ) {
headers { headers {
append( append(
@ -490,8 +521,10 @@ class SelfossApi(private val appSettingsService: AppSettingsService) {
parameter("username", appSettingsService.getUserName()) parameter("username", appSettingsService.getUserName())
parameter("password", appSettingsService.getPassword()) parameter("password", appSettingsService.getPassword())
} }
if (appSettingsService.getBasicUserName() if (appSettingsService
.isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty() .getBasicUserName()
.isNotEmpty() &&
appSettingsService.getBasicPassword().isNotEmpty()
) { ) {
headers { headers {
append( append(
@ -515,8 +548,10 @@ class SelfossApi(private val appSettingsService: AppSettingsService) {
parameter("username", appSettingsService.getUserName()) parameter("username", appSettingsService.getUserName())
parameter("password", appSettingsService.getPassword()) parameter("password", appSettingsService.getPassword())
} }
if (appSettingsService.getBasicUserName() if (appSettingsService
.isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty() .getBasicUserName()
.isNotEmpty() &&
appSettingsService.getBasicPassword().isNotEmpty()
) { ) {
headers { headers {
append( append(
@ -540,8 +575,10 @@ class SelfossApi(private val appSettingsService: AppSettingsService) {
parameter("username", appSettingsService.getUserName()) parameter("username", appSettingsService.getUserName())
parameter("password", appSettingsService.getPassword()) parameter("password", appSettingsService.getPassword())
} }
if (appSettingsService.getBasicUserName() if (appSettingsService
.isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty() .getBasicUserName()
.isNotEmpty() &&
appSettingsService.getBasicPassword().isNotEmpty()
) { ) {
headers { headers {
append( append(
@ -563,16 +600,18 @@ class SelfossApi(private val appSettingsService: AppSettingsService) {
client.tryToSubmitForm( client.tryToSubmitForm(
url = url("/mark"), url = url("/mark"),
formParameters = formParameters =
Parameters.build { Parameters.build {
if (!shouldHavePostLogin()) { if (!shouldHavePostLogin()) {
append("username", appSettingsService.getUserName()) append("username", appSettingsService.getUserName())
append("password", appSettingsService.getPassword()) append("password", appSettingsService.getPassword())
} }
ids.map { append("ids[]", it) } ids.map { append("ids[]", it) }
}, },
block = { block = {
if (appSettingsService.getBasicUserName() if (appSettingsService
.isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty() .getBasicUserName()
.isNotEmpty() &&
appSettingsService.getBasicPassword().isNotEmpty()
) { ) {
headers { headers {
append( append(
@ -614,19 +653,21 @@ class SelfossApi(private val appSettingsService: AppSettingsService) {
client.tryToSubmitForm( client.tryToSubmitForm(
url = url("/source"), url = url("/source"),
formParameters = formParameters =
Parameters.build { Parameters.build {
if (!shouldHavePostLogin()) { if (!shouldHavePostLogin()) {
append("username", appSettingsService.getUserName()) append("username", appSettingsService.getUserName())
append("password", appSettingsService.getPassword()) append("password", appSettingsService.getPassword())
} }
append("title", title) append("title", title)
append("url", url) append("url", url)
append("spout", spout) append("spout", spout)
append(tagsParamName, tags) append(tagsParamName, tags)
}, },
block = { block = {
if (appSettingsService.getBasicUserName() if (appSettingsService
.isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty() .getBasicUserName()
.isNotEmpty() &&
appSettingsService.getBasicPassword().isNotEmpty()
) { ) {
headers { headers {
append( append(
@ -669,19 +710,21 @@ class SelfossApi(private val appSettingsService: AppSettingsService) {
client.tryToSubmitForm( client.tryToSubmitForm(
url = url("/source/$id"), url = url("/source/$id"),
formParameters = formParameters =
Parameters.build { Parameters.build {
if (!shouldHavePostLogin()) { if (!shouldHavePostLogin()) {
append("username", appSettingsService.getUserName()) append("username", appSettingsService.getUserName())
append("password", appSettingsService.getPassword()) append("password", appSettingsService.getPassword())
} }
append("title", title) append("title", title)
append("url", url) append("url", url)
append("spout", spout) append("spout", spout)
append(tagsParamName, tags) append(tagsParamName, tags)
}, },
block = { block = {
if (appSettingsService.getBasicUserName() if (appSettingsService
.isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty() .getBasicUserName()
.isNotEmpty() &&
appSettingsService.getBasicPassword().isNotEmpty()
) { ) {
headers { headers {
append( append(
@ -705,8 +748,10 @@ class SelfossApi(private val appSettingsService: AppSettingsService) {
parameter("username", appSettingsService.getUserName()) parameter("username", appSettingsService.getUserName())
parameter("password", appSettingsService.getPassword()) parameter("password", appSettingsService.getPassword())
} }
if (appSettingsService.getBasicUserName() if (appSettingsService
.isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty() .getBasicUserName()
.isNotEmpty() &&
appSettingsService.getBasicPassword().isNotEmpty()
) { ) {
headers { headers {
append( append(
@ -722,4 +767,4 @@ class SelfossApi(private val appSettingsService: AppSettingsService) {
} }
}, },
) )
} }

View File

@ -121,4 +121,4 @@ class ACRASettings : Settings {
longs.remove(key) longs.remove(key)
strings.remove(key) strings.remove(key)
} }
} }

View File

@ -2,7 +2,9 @@ package bou.amine.apps.readerforselfossv2.service
import com.russhwolf.settings.Settings import com.russhwolf.settings.Settings
class AppSettingsService(acraSenderServiceProcess: Boolean = false) { class AppSettingsService(
acraSenderServiceProcess: Boolean = false,
) {
val settings: Settings = val settings: Settings =
if (acraSenderServiceProcess) { if (acraSenderServiceProcess) {
ACRASettings() ACRASettings()
@ -11,37 +13,37 @@ class AppSettingsService(acraSenderServiceProcess: Boolean = false) {
} }
// Api related // Api related
private var _apiVersion: Int = -1 private var apiVersion: Int = -1
private var _publicAccess: Boolean? = null private var publicAccess: Boolean? = null
private var _selfSigned: Boolean? = null private var selfSigned: Boolean? = null
private var _baseUrl: String = "" private var baseUrl: String = ""
private var _userName: String = "" private var userName: String = ""
private var _basicUserName: String = "" private var basicUserName: String = ""
private var _password: String = "" private var password: String = ""
private var _basicPassword: String = "" private var basicPassword: String = ""
// User settings related // User settings related
private var _itemsCaching: Boolean? = null private var itemsCaching: Boolean? = null
private var _articleViewer: Boolean? = null private var articleViewer: Boolean? = null
private var _shouldBeCardView: Boolean? = null private var shouldBeCardView: Boolean? = null
private var _displayUnreadCount: Boolean? = null private var displayUnreadCount: Boolean? = null
private var _displayAllCount: Boolean? = null private var displayAllCount: Boolean? = null
private var _fullHeightCards: Boolean? = null private var fullHeightCards: Boolean? = null
private var _updateSources: Boolean? = null private var updateSources: Boolean? = null
private var _periodicRefresh: Boolean? = null private var periodicRefresh: Boolean? = null
private var _refreshWhenChargingOnly: Boolean? = null private var refreshWhenChargingOnly: Boolean? = null
private var _infiniteLoading: Boolean? = null private var infiniteLoading: Boolean? = null
private var _notifyNewItems: Boolean? = null private var notifyNewItems: Boolean? = null
private var _itemsNumber: Int? = null private var itemsNumber: Int? = null
private var _apiTimeout: Long? = null private var apiTimeout: Long? = null
private var _refreshMinutes: Long = 360 private var refreshMinutes: Long = 360
private var _markOnScroll: Boolean? = null private var markOnScroll: Boolean? = null
private var _activeAlignment: Int? = null private var activeAlignment: Int? = null
private var _fontSize: Int? = null private var fontSize: Int? = null
private var _staticBar: Boolean? = null private var staticBar: Boolean? = null
private var _font: String = "" private var font: String = ""
private var _theme: Int? = null private var theme: Int? = null
init { init {
refreshApiSettings() refreshApiSettings()
@ -49,11 +51,11 @@ class AppSettingsService(acraSenderServiceProcess: Boolean = false) {
} }
fun getApiVersion(): Int { fun getApiVersion(): Int {
if (_apiVersion == -1) { if (apiVersion == -1) {
refreshApiVersion() refreshApiVersion()
return _apiVersion return apiVersion
} }
return _apiVersion return apiVersion
} }
fun updateApiVersion(apiMajorVersion: Int) { fun updateApiVersion(apiMajorVersion: Int) {
@ -62,14 +64,14 @@ class AppSettingsService(acraSenderServiceProcess: Boolean = false) {
} }
private fun refreshApiVersion() { private fun refreshApiVersion() {
_apiVersion = settings.getInt(API_VERSION_MAJOR, -1) apiVersion = settings.getInt(API_VERSION_MAJOR, -1)
} }
fun getPublicAccess(): Boolean { fun getPublicAccess(): Boolean {
if (_publicAccess == null) { if (publicAccess == null) {
refreshPublicAccess() refreshPublicAccess()
} }
return _publicAccess!! return publicAccess!!
} }
fun updatePublicAccess(publicAccess: Boolean) { fun updatePublicAccess(publicAccess: Boolean) {
@ -78,14 +80,14 @@ class AppSettingsService(acraSenderServiceProcess: Boolean = false) {
} }
private fun refreshPublicAccess() { private fun refreshPublicAccess() {
_publicAccess = settings.getBoolean(API_PUBLIC_ACCESS, false) publicAccess = settings.getBoolean(API_PUBLIC_ACCESS, false)
} }
fun getSelfSigned(): Boolean { fun getSelfSigned(): Boolean {
if (_selfSigned == null) { if (selfSigned == null) {
refreshSelfSigned() refreshSelfSigned()
} }
return _selfSigned!! return selfSigned!!
} }
fun updateSelfSigned(selfSigned: Boolean) { fun updateSelfSigned(selfSigned: Boolean) {
@ -94,53 +96,53 @@ class AppSettingsService(acraSenderServiceProcess: Boolean = false) {
} }
private fun refreshSelfSigned() { private fun refreshSelfSigned() {
_selfSigned = settings.getBoolean(API_SELF_SIGNED, false) selfSigned = settings.getBoolean(API_SELF_SIGNED, false)
} }
fun getBaseUrl(): String { fun getBaseUrl(): String {
if (_baseUrl.isEmpty()) { if (baseUrl.isEmpty()) {
refreshBaseUrl() refreshBaseUrl()
} }
return _baseUrl return baseUrl
} }
fun getUserName(): String { fun getUserName(): String {
if (_userName.isEmpty()) { if (userName.isEmpty()) {
refreshUsername() refreshUsername()
} }
return _userName return userName
} }
fun getPassword(): String { fun getPassword(): String {
if (_password.isEmpty()) { if (password.isEmpty()) {
refreshPassword() refreshPassword()
} }
return _password return password
} }
fun getBasicUserName(): String { fun getBasicUserName(): String {
if (_basicUserName.isEmpty()) { if (basicUserName.isEmpty()) {
refreshBasicUsername() refreshBasicUsername()
} }
return _basicUserName return basicUserName
} }
fun getBasicPassword(): String { fun getBasicPassword(): String {
if (_basicPassword.isEmpty()) { if (basicPassword.isEmpty()) {
refreshBasicPassword() refreshBasicPassword()
} }
return _basicPassword return basicPassword
} }
fun getItemsNumber(): Int { fun getItemsNumber(): Int {
if (_itemsNumber == null) { if (itemsNumber == null) {
refreshItemsNumber() refreshItemsNumber()
} }
return _itemsNumber!! return itemsNumber!!
} }
private fun refreshItemsNumber() { private fun refreshItemsNumber() {
_itemsNumber = itemsNumber =
try { try {
settings.getString(API_ITEMS_NUMBER, "20").toInt() settings.getString(API_ITEMS_NUMBER, "20").toInt()
} catch (e: Exception) { } catch (e: Exception) {
@ -150,16 +152,16 @@ class AppSettingsService(acraSenderServiceProcess: Boolean = false) {
} }
fun getApiTimeout(): Long { fun getApiTimeout(): Long {
if (_apiTimeout == null) { if (apiTimeout == null) {
refreshApiTimeout() refreshApiTimeout()
} }
return _apiTimeout!! return apiTimeout!!
} }
private fun secToMs(n: Long) = n * 1000 private fun secToMs(n: Long) = n * 1000
private fun refreshApiTimeout() { private fun refreshApiTimeout() {
_apiTimeout = apiTimeout =
secToMs( secToMs(
try { try {
val settingsTimeout = settings.getString(API_TIMEOUT, "60") val settingsTimeout = settings.getString(API_TIMEOUT, "60")
@ -177,229 +179,229 @@ class AppSettingsService(acraSenderServiceProcess: Boolean = false) {
} }
private fun refreshBaseUrl() { private fun refreshBaseUrl() {
_baseUrl = settings.getString(BASE_URL, "") baseUrl = settings.getString(BASE_URL, "")
} }
private fun refreshUsername() { private fun refreshUsername() {
_userName = settings.getString(LOGIN, "") userName = settings.getString(LOGIN, "")
} }
private fun refreshPassword() { private fun refreshPassword() {
_password = settings.getString(PASSWORD, "") password = settings.getString(PASSWORD, "")
} }
private fun refreshBasicUsername() { private fun refreshBasicUsername() {
_basicUserName = settings.getString(BASIC_LOGIN, "") basicUserName = settings.getString(BASIC_LOGIN, "")
} }
private fun refreshBasicPassword() { private fun refreshBasicPassword() {
_basicPassword = settings.getString(BASIC_PASSWORD, "") basicPassword = settings.getString(BASIC_PASSWORD, "")
} }
private fun refreshArticleViewerEnabled() { private fun refreshArticleViewerEnabled() {
_articleViewer = settings.getBoolean(PREFER_ARTICLE_VIEWER, true) articleViewer = settings.getBoolean(PREFER_ARTICLE_VIEWER, true)
} }
fun isArticleViewerEnabled(): Boolean { fun isArticleViewerEnabled(): Boolean {
if (_articleViewer != null) { if (articleViewer != null) {
refreshArticleViewerEnabled() refreshArticleViewerEnabled()
} }
return _articleViewer == true return articleViewer == true
} }
private fun refreshShouldBeCardViewEnabled() { private fun refreshShouldBeCardViewEnabled() {
_shouldBeCardView = settings.getBoolean(CARD_VIEW_ACTIVE, false) shouldBeCardView = settings.getBoolean(CARD_VIEW_ACTIVE, false)
} }
fun isCardViewEnabled(): Boolean { fun isCardViewEnabled(): Boolean {
if (_shouldBeCardView != null) { if (shouldBeCardView != null) {
refreshShouldBeCardViewEnabled() refreshShouldBeCardViewEnabled()
} }
return _shouldBeCardView == true return shouldBeCardView == true
} }
private fun refreshDisplayUnreadCountEnabled() { private fun refreshDisplayUnreadCountEnabled() {
_displayUnreadCount = settings.getBoolean(DISPLAY_UNREAD_COUNT, true) displayUnreadCount = settings.getBoolean(DISPLAY_UNREAD_COUNT, true)
} }
fun isDisplayUnreadCountEnabled(): Boolean { fun isDisplayUnreadCountEnabled(): Boolean {
if (_displayUnreadCount != null) { if (displayUnreadCount != null) {
refreshDisplayUnreadCountEnabled() refreshDisplayUnreadCountEnabled()
} }
return _displayUnreadCount == true return displayUnreadCount == true
} }
private fun refreshDisplayAllCountEnabled() { private fun refreshDisplayAllCountEnabled() {
_displayAllCount = settings.getBoolean(DISPLAY_OTHER_COUNT, false) displayAllCount = settings.getBoolean(DISPLAY_OTHER_COUNT, false)
} }
fun isDisplayAllCountEnabled(): Boolean { fun isDisplayAllCountEnabled(): Boolean {
if (_displayAllCount != null) { if (displayAllCount != null) {
refreshDisplayAllCountEnabled() refreshDisplayAllCountEnabled()
} }
return _displayAllCount == true return displayAllCount == true
} }
private fun refreshFullHeightCardsEnabled() { private fun refreshFullHeightCardsEnabled() {
_fullHeightCards = settings.getBoolean(FULL_HEIGHT_CARDS, false) fullHeightCards = settings.getBoolean(FULL_HEIGHT_CARDS, false)
} }
fun isFullHeightCardsEnabled(): Boolean { fun isFullHeightCardsEnabled(): Boolean {
if (_fullHeightCards != null) { if (fullHeightCards != null) {
refreshFullHeightCardsEnabled() refreshFullHeightCardsEnabled()
} }
return _fullHeightCards == true return fullHeightCards == true
} }
private fun refreshUpdateSourcesEnabled() { private fun refreshUpdateSourcesEnabled() {
_updateSources = settings.getBoolean(UPDATE_SOURCES, true) updateSources = settings.getBoolean(UPDATE_SOURCES, true)
} }
fun isUpdateSourcesEnabled(): Boolean { fun isUpdateSourcesEnabled(): Boolean {
if (_updateSources != null) { if (updateSources != null) {
refreshUpdateSourcesEnabled() refreshUpdateSourcesEnabled()
} }
return _updateSources == true return updateSources == true
} }
private fun refreshPeriodicRefreshEnabled() { private fun refreshPeriodicRefreshEnabled() {
_periodicRefresh = settings.getBoolean(PERIODIC_REFRESH, false) periodicRefresh = settings.getBoolean(PERIODIC_REFRESH, false)
} }
fun isPeriodicRefreshEnabled(): Boolean { fun isPeriodicRefreshEnabled(): Boolean {
if (_periodicRefresh != null) { if (periodicRefresh != null) {
refreshPeriodicRefreshEnabled() refreshPeriodicRefreshEnabled()
} }
return _periodicRefresh == true return periodicRefresh == true
} }
private fun refreshRefreshWhenChargingOnlyEnabled() { private fun refreshRefreshWhenChargingOnlyEnabled() {
_refreshWhenChargingOnly = settings.getBoolean(REFRESH_WHEN_CHARGING, false) refreshWhenChargingOnly = settings.getBoolean(REFRESH_WHEN_CHARGING, false)
} }
fun isRefreshWhenChargingOnlyEnabled(): Boolean { fun isRefreshWhenChargingOnlyEnabled(): Boolean {
if (_refreshWhenChargingOnly != null) { if (refreshWhenChargingOnly != null) {
refreshRefreshWhenChargingOnlyEnabled() refreshRefreshWhenChargingOnlyEnabled()
} }
return _refreshWhenChargingOnly == true return refreshWhenChargingOnly == true
} }
private fun refreshRefreshMinutes() { private fun refreshRefreshMinutes() {
_refreshMinutes = settings.getString(PERIODIC_REFRESH_MINUTES, "360").toLong() refreshMinutes = settings.getString(PERIODIC_REFRESH_MINUTES, "360").toLong()
if (_refreshMinutes <= 15) { if (refreshMinutes <= 15) {
_refreshMinutes = 15 refreshMinutes = 15
} }
} }
fun getRefreshMinutes(): Long { fun getRefreshMinutes(): Long {
if (_refreshMinutes != 360L) { if (refreshMinutes != 360L) {
refreshRefreshMinutes() refreshRefreshMinutes()
} }
return _refreshMinutes return refreshMinutes
} }
private fun refreshInfiniteLoadingEnabled() { private fun refreshInfiniteLoadingEnabled() {
_infiniteLoading = settings.getBoolean(INFINITE_LOADING, false) infiniteLoading = settings.getBoolean(INFINITE_LOADING, false)
} }
fun isInfiniteLoadingEnabled(): Boolean { fun isInfiniteLoadingEnabled(): Boolean {
if (_infiniteLoading != null) { if (infiniteLoading != null) {
refreshInfiniteLoadingEnabled() refreshInfiniteLoadingEnabled()
} }
return _infiniteLoading == true return infiniteLoading == true
} }
private fun refreshItemCachingEnabled() { private fun refreshItemCachingEnabled() {
_itemsCaching = settings.getBoolean(ITEMS_CACHING, false) itemsCaching = settings.getBoolean(ITEMS_CACHING, false)
} }
fun isItemCachingEnabled(): Boolean { fun isItemCachingEnabled(): Boolean {
if (_itemsCaching != null) { if (itemsCaching != null) {
refreshItemCachingEnabled() refreshItemCachingEnabled()
} }
return _itemsCaching == true return itemsCaching == true
} }
private fun refreshNotifyNewItemsEnabled() { private fun refreshNotifyNewItemsEnabled() {
_notifyNewItems = settings.getBoolean(NOTIFY_NEW_ITEMS, false) notifyNewItems = settings.getBoolean(NOTIFY_NEW_ITEMS, false)
} }
fun isNotifyNewItemsEnabled(): Boolean { fun isNotifyNewItemsEnabled(): Boolean {
if (_notifyNewItems != null) { if (notifyNewItems != null) {
refreshNotifyNewItemsEnabled() refreshNotifyNewItemsEnabled()
} }
return _notifyNewItems == true return notifyNewItems == true
} }
private fun refreshMarkOnScrollEnabled() { private fun refreshMarkOnScrollEnabled() {
_markOnScroll = settings.getBoolean(MARK_ON_SCROLL, false) markOnScroll = settings.getBoolean(MARK_ON_SCROLL, false)
} }
fun isMarkOnScrollEnabled(): Boolean { fun isMarkOnScrollEnabled(): Boolean {
if (_markOnScroll != null) { if (markOnScroll != null) {
refreshMarkOnScrollEnabled() refreshMarkOnScrollEnabled()
} }
return _markOnScroll == true return markOnScroll == true
} }
private fun refreshActiveAllignment() { private fun refreshActiveAllignment() {
_activeAlignment = settings.getInt(TEXT_ALIGN, JUSTIFY) activeAlignment = settings.getInt(TEXT_ALIGN, JUSTIFY)
} }
fun getActiveAllignment(): Int { fun getActiveAllignment(): Int {
if (_activeAlignment != null) { if (activeAlignment != null) {
refreshActiveAllignment() refreshActiveAllignment()
} }
return _activeAlignment ?: JUSTIFY return activeAlignment ?: JUSTIFY
} }
fun changeAllignment(allignment: Int) { fun changeAllignment(allignment: Int) {
settings.putInt(TEXT_ALIGN, allignment) settings.putInt(TEXT_ALIGN, allignment)
_activeAlignment = allignment activeAlignment = allignment
} }
private fun refreshFontSize() { private fun refreshFontSize() {
_fontSize = settings.getString(READER_FONT_SIZE, "16").toInt() fontSize = settings.getString(READER_FONT_SIZE, "16").toInt()
} }
fun getFontSize(): Int { fun getFontSize(): Int {
if (_fontSize != null) { if (fontSize != null) {
refreshFontSize() refreshFontSize()
} }
return _fontSize ?: 16 return fontSize ?: 16
} }
private fun refreshStaticBarEnabled() { private fun refreshStaticBarEnabled() {
_staticBar = settings.getBoolean(READER_STATIC_BAR, false) staticBar = settings.getBoolean(READER_STATIC_BAR, false)
} }
fun isStaticBarEnabled(): Boolean { fun isStaticBarEnabled(): Boolean {
if (_staticBar != null) { if (staticBar != null) {
refreshStaticBarEnabled() refreshStaticBarEnabled()
} }
return _staticBar == true return staticBar == true
} }
private fun refreshFont() { private fun refreshFont() {
_font = settings.getString(READER_FONT, "") font = settings.getString(READER_FONT, "")
} }
fun getFont(): String { fun getFont(): String {
if (_font.isEmpty()) { if (font.isEmpty()) {
refreshFont() refreshFont()
} }
return _font return font
} }
private fun refreshCurrentTheme() { private fun refreshCurrentTheme() {
_theme = settings.getString(CURRENT_THEME, "-1").toInt() theme = settings.getString(CURRENT_THEME, "-1").toInt()
} }
fun getCurrentTheme(): Int { fun getCurrentTheme(): Int {
if (_theme == null) { if (theme == null) {
refreshCurrentTheme() refreshCurrentTheme()
} }
return _theme ?: -1 return theme ?: -1
} }
fun refreshApiSettings() { fun refreshApiSettings() {
@ -478,15 +480,15 @@ class AppSettingsService(acraSenderServiceProcess: Boolean = false) {
} }
companion object { companion object {
const val translationUrl = "https://crwd.in/readerforselfoss" const val TRANSLATION_URL = "https://crwd.in/readerforselfoss"
const val sourceUrl = "https://gitea.amine-bouabdallaoui.fr/Louvorg/ReaderForSelfoss-multiplatform" const val SOURCE_URL = "https://gitea.amine-bouabdallaoui.fr/Louvorg/ReaderForSelfoss-multiplatform"
const val trackerUrl = "https://gitea.amine-bouabdallaoui.fr/Louvorg/ReaderForSelfoss-multiplatform/issues" const val TRACKER_URL = "https://gitea.amine-bouabdallaoui.fr/Louvorg/ReaderForSelfoss-multiplatform/issues"
const val syncChannelId = "sync-channel-id" const val SYNC_CHANNEL_ID = "sync-channel-id"
const val newItemsChannelId = "new-items-channel-id" const val NEW_ITEMS_CHANNEL_ID = "new-items-channel-id"
const val JUSTIFY = 1 const val JUSTIFY = 1

View File

@ -82,4 +82,4 @@ fun SelfossModel.Tag.getColorHexCode(): String =
"#$char1$char1$char2$char2$char3$char3" "#$char1$char1$char2$char2$char3$char3"
} else { } else {
this.color this.color
} }

View File

@ -1,12 +1,17 @@
package bou.amine.apps.readerforselfossv2.utils package bou.amine.apps.readerforselfossv2.utils
enum class ItemType(val position: Int, val type: String) { object Enums {
UNREAD(1, "unread"), enum class ItemType(
ALL(2, "all"), val position: Int,
STARRED(3, "starred"), val type: String,
; ) {
UNREAD(1, "unread"),
ALL(2, "all"),
STARRED(3, "starred"),
;
companion object { companion object {
fun fromInt(value: Int) = values().first { it.position == value } fun fromInt(value: Int) = values().first { it.position == value }
}
} }
} }

View File

@ -8,12 +8,11 @@ import kotlin.test.Test
import kotlin.test.assertEquals import kotlin.test.assertEquals
class DatesTest { class DatesTest {
private val newVersionDateVariant = "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 newVersionDate = "2013-04-07T13:43:00+01:00"
private val newVersionDate2 = "2013-04-07T13:43:00-01:00" private val newVersionDate2 = "2013-04-07T13:43:00-01:00"
private val oldVersionDate = "2013-05-07 13:46:00" private val oldVersionDate = "2013-05-07 13:46:00"
private val oldVersionDateVariant = "2021-03-21 10:32:00.000000" private val oldVersionDateVariant = "2021-03-21 10:32:00.000000"
@Test @Test
fun new_version_date_should_be_parsed() { fun new_version_date_should_be_parsed() {
@ -24,6 +23,7 @@ class DatesTest {
assertEquals(expected, date) assertEquals(expected, date)
} }
@Test @Test
fun new_version_date2_should_be_parsed() { fun new_version_date2_should_be_parsed() {
val date = newVersionDate2.toParsedDate() val date = newVersionDate2.toParsedDate()

View File

@ -7,4 +7,4 @@ actual class DriverFactory {
actual fun createDriver(): SqlDriver { actual fun createDriver(): SqlDriver {
return NativeSqliteDriver(ReaderForSelfossDB.Schema, "ReaderForSelfossV2-IOS.db") return NativeSqliteDriver(ReaderForSelfossDB.Schema, "ReaderForSelfossV2-IOS.db")
} }
} }

View File

@ -2,5 +2,5 @@ package bou.amine.apps.readerforselfossv2.rest
import io.ktor.client.engine.cio.CIOEngineConfig import io.ktor.client.engine.cio.CIOEngineConfig
actual fun setupInsecureHTTPEngine(config: CIOEngineConfig) { actual fun setupInsecureHttpEngine(config: CIOEngineConfig) {
} }

View File

@ -6,4 +6,4 @@ actual class DateUtils {
TODO("Not yet implemented") TODO("Not yet implemented")
} }
} }
} }

View File

@ -6,4 +6,4 @@ actual class DateUtils actual constructor() {
TODO("Not yet implemented") TODO("Not yet implemented")
} }
} }
} }

View File

@ -7,4 +7,4 @@ actual class DriverFactory {
actual fun createDriver(): SqlDriver { actual fun createDriver(): SqlDriver {
return NativeSqliteDriver(ReaderForSelfossDB.Schema, "ReaderForSelfossV2-IOS.db") return NativeSqliteDriver(ReaderForSelfossDB.Schema, "ReaderForSelfossV2-IOS.db")
} }
} }

View File

@ -2,5 +2,5 @@ package bou.amine.apps.readerforselfossv2.rest
import io.ktor.client.engine.cio.CIOEngineConfig import io.ktor.client.engine.cio.CIOEngineConfig
actual fun setupInsecureHTTPEngine(config: CIOEngineConfig) { actual fun setupInsecureHttpEngine(config: CIOEngineConfig) {
} }

View File

@ -6,4 +6,4 @@ actual class DateUtils {
TODO("Not yet implemented") TODO("Not yet implemented")
} }
} }
} }