├── .github ├── issue_template.md └── workflows │ └── android.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── app ├── .gitignore ├── build.gradle.kts ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── email │ │ └── schaal │ │ └── ocreader │ │ ├── APIDispatcher.kt │ │ ├── DatabaseTest.kt │ │ ├── ItemPagerActivityTest.kt │ │ └── TestGenerator.kt │ ├── debug │ └── res │ │ ├── values │ │ └── strings.xml │ │ └── xml-v25 │ │ └── shortcuts.xml │ ├── main │ ├── AndroidManifest.xml │ ├── assets │ │ └── font │ │ │ └── crimsontextregular.ttf │ ├── ic_launcher-web.png │ ├── java │ │ └── email │ │ │ └── schaal │ │ │ └── ocreader │ │ │ ├── ItemPageFragment.kt │ │ │ ├── ItemPagerActivity.kt │ │ │ ├── ListActivity.kt │ │ │ ├── LoginFlowActivity.kt │ │ │ ├── ManageFeedsActivity.kt │ │ │ ├── OCReaderApplication.kt │ │ │ ├── Preferences.kt │ │ │ ├── SettingsActivity.kt │ │ │ ├── SettingsFragment.kt │ │ │ ├── api │ │ │ ├── API.kt │ │ │ ├── APIv12Interface.kt │ │ │ ├── CommonAPI.kt │ │ │ ├── Level.kt │ │ │ ├── OCSAPI.kt │ │ │ └── json │ │ │ │ ├── APILevels.kt │ │ │ │ ├── Collections.kt │ │ │ │ ├── FeedJsonTypeAdapter.kt │ │ │ │ ├── ItemJsonTypeAdapter.kt │ │ │ │ ├── NewsError.kt │ │ │ │ ├── Status.kt │ │ │ │ ├── TypeAdapters.kt │ │ │ │ ├── UserJsonTypeAdapter.kt │ │ │ │ └── v2 │ │ │ │ └── SyncResponse.kt │ │ │ ├── database │ │ │ ├── FeedViewModel.kt │ │ │ ├── FolderViewModel.kt │ │ │ ├── LiveRealmObject.kt │ │ │ ├── LiveRealmResults.kt │ │ │ ├── PagerViewModel.kt │ │ │ ├── Queries.kt │ │ │ ├── RealmViewModel.kt │ │ │ └── model │ │ │ │ ├── AllUnreadFolder.kt │ │ │ │ ├── Feed.kt │ │ │ │ ├── Folder.kt │ │ │ │ ├── FreshFolder.kt │ │ │ │ ├── Insertable.kt │ │ │ │ ├── Item.kt │ │ │ │ ├── SpecialFolder.kt │ │ │ │ ├── StarredFolder.kt │ │ │ │ ├── TemporaryFeed.kt │ │ │ │ ├── TreeItem.kt │ │ │ │ └── User.kt │ │ │ ├── http │ │ │ └── HttpManager.kt │ │ │ ├── service │ │ │ ├── SyncType.kt │ │ │ └── SyncWorker.kt │ │ │ ├── ui │ │ │ └── loginflow │ │ │ │ ├── LoginFlowFragment.kt │ │ │ │ └── LoginFlowWebViewFragment.kt │ │ │ ├── util │ │ │ ├── ColorGenerator.kt │ │ │ ├── FaviconLoader.kt │ │ │ ├── FeedColors.kt │ │ │ ├── LoginError.kt │ │ │ ├── MyAppGlideModule.kt │ │ │ ├── StringUtils.kt │ │ │ └── TextDrawable.kt │ │ │ └── view │ │ │ ├── AddNewFeedDialogFragment.kt │ │ │ ├── ArticleWebView.kt │ │ │ ├── DividerItemDecoration.kt │ │ │ ├── FeedManageListener.kt │ │ │ ├── FeedsAdapter.kt │ │ │ ├── FolderBottomSheetDialogFragment.kt │ │ │ ├── FolderSpinnerAdapter.kt │ │ │ ├── ItemViewHolder.kt │ │ │ ├── LiveItemsAdapter.kt │ │ │ ├── LoginViewModel.kt │ │ │ ├── NestedScrollWebView.kt │ │ │ ├── ProgressFloatingActionButton.kt │ │ │ ├── ScrollAwareFABBehavior.kt │ │ │ ├── TreeItemsAdapter.kt │ │ │ └── UserBottomSheetDialogFragment.kt │ └── res │ │ ├── drawable │ │ ├── account_circle.xml │ │ ├── favicon_background.xml │ │ ├── fresh.xml │ │ ├── ic_add.xml │ │ ├── ic_app_shortcut_feed.xml │ │ ├── ic_app_shortcut_fresh.xml │ │ ├── ic_app_shortcut_star.xml │ │ ├── ic_check_box.xml │ │ ├── ic_check_box_outline_blank.xml │ │ ├── ic_delete.xml │ │ ├── ic_done_above.xml │ │ ├── ic_done_all.xml │ │ ├── ic_feed_icon.xml │ │ ├── ic_folder.xml │ │ ├── ic_info.xml │ │ ├── ic_launcher_background.xml │ │ ├── ic_launcher_foreground.xml │ │ ├── ic_open_in_browser.xml │ │ ├── ic_play_arrow.xml │ │ ├── ic_play_circle_outline.xml │ │ ├── ic_refresh.xml │ │ ├── ic_settings.xml │ │ ├── ic_share.xml │ │ ├── ic_star.xml │ │ ├── ic_star_outline.xml │ │ ├── ic_warning.xml │ │ └── selectable_background.xml │ │ ├── font │ │ ├── crimson.xml │ │ └── crimsontextregular.ttf │ │ ├── layout │ │ ├── activity_item_pager.xml │ │ ├── activity_list.xml │ │ ├── activity_manage_feeds.xml │ │ ├── activity_settings.xml │ │ ├── bottom_navigationview.xml │ │ ├── dialog_folders.xml │ │ ├── fontpreference.xml │ │ ├── fragment_add_new_feed.xml │ │ ├── fragment_item_pager.xml │ │ ├── fragment_login_flow_web_view.xml │ │ ├── list_divider.xml │ │ ├── list_empty.xml │ │ ├── list_error.xml │ │ ├── list_feed.xml │ │ ├── list_folder.xml │ │ ├── list_item.xml │ │ ├── list_loadmore.xml │ │ ├── login_flow_activity.xml │ │ ├── login_flow_fragment.xml │ │ ├── spinner_folder.xml │ │ ├── spinner_folder_dropdown.xml │ │ ├── toolbar.xml │ │ ├── toolbar_pager.xml │ │ └── user_bottomsheet.xml │ │ ├── menu │ │ ├── menu_item_bottomsheet.xml │ │ ├── menu_item_list_action.xml │ │ ├── menu_item_list_bottom.xml │ │ ├── menu_item_list_top.xml │ │ └── menu_item_pager_bottom.xml │ │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── values-de │ │ └── strings.xml │ │ ├── values-fr │ │ └── strings.xml │ │ ├── values-it │ │ └── strings.xml │ │ ├── values-night │ │ └── colors.xml │ │ ├── values-ru │ │ └── strings.xml │ │ ├── values │ │ ├── arrays.xml │ │ ├── attrs.xml │ │ ├── colors.xml │ │ ├── dimens.xml │ │ ├── html.xml │ │ ├── ids.xml │ │ ├── md_colors.xml │ │ ├── strings.xml │ │ └── styles.xml │ │ ├── xml-v25 │ │ └── shortcuts.xml │ │ └── xml │ │ ├── network_security_config.xml │ │ └── preferences.xml │ └── test │ ├── java │ └── email │ │ └── schaal │ │ └── ocreader │ │ ├── APITest.kt │ │ ├── FaviconLoaderTest.kt │ │ ├── JsonTest.kt │ │ ├── StringUtilsTest.kt │ │ └── TestApplication.kt │ └── resources │ └── email │ └── schaal │ └── ocreader │ └── robolectric.properties ├── build.gradle.kts ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── icons ├── app_shortcut.svg ├── create_launcher_icon_from_svg.sh ├── feature_graphic.svg ├── ic_done_above.svg └── ic_launcher_main.svg ├── metadata └── en-US │ ├── full_description.txt │ ├── images │ ├── icon.png │ └── phoneScreenshots │ │ ├── drawer.png │ │ ├── itempager-dark.png │ │ ├── itempager.png │ │ └── list.png │ ├── short_description.txt │ ├── title.txt │ └── video.txt └── settings.gradle.kts /.github/issue_template.md: -------------------------------------------------------------------------------- 1 | ## Description of the issue 2 | 3 | 4 | ## Expected behavior 5 | 6 | 7 | ## Steps to reproduce 8 | 9 | 1. 10 | 2. 11 | 3. 12 | 13 | ## Your Environment 14 | 15 | * OCReader version: 16 | * Nextcloud news version: 17 | * Android version: 18 | 19 | ### Logcat 20 | 21 | 22 | ``` 23 | Please paste the relevant logcat output here 24 | ``` 25 | -------------------------------------------------------------------------------- /.github/workflows/android.yml: -------------------------------------------------------------------------------- 1 | name: Android CI 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | build: 11 | 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - uses: actions/checkout@v2 16 | - name: set up JDK 1.8 17 | uses: actions/setup-java@v1 18 | with: 19 | java-version: 1.8 20 | - name: Cache Gradle packages 21 | uses: actions/cache@v1 22 | with: 23 | path: ~/.gradle/caches 24 | key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle') }} 25 | restore-keys: ${{ runner.os }}-gradle 26 | - name: Test and build with Gradle 27 | run: ./gradlew test assembleRelease 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/ 5 | .DS_Store 6 | /build 7 | /captures 8 | *.apk 9 | /app/release/ 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Android CI](https://github.com/schaal/ocreader/workflows/Android%20CI/badge.svg)](https://github.com/schaal/ocreader/actions?query=workflow%3A%22Android+CI%22) 2 | 3 | # [OCReader][1] - Android client for [Nextcloud News][0] 4 | 5 | OCReader is a client for a [Nextcloud News][0] instance (needs at least version 8.8.2). 6 | 7 | [Get it on F-Droid](https://f-droid.org/app/email.schaal.ocreader) 10 | 11 | ## Screenshots 12 | 13 | ![Drawer](metadata/en-US/images/phoneScreenshots/drawer.png) 14 | ![List View](metadata/en-US/images/phoneScreenshots/list.png) 15 | ![Item page](metadata/en-US/images/phoneScreenshots/itempager.png) 16 | 17 | ### Dark theme 18 | 19 | ![Item page with dark theme](metadata/en-US/images/phoneScreenshots/itempager-dark.png) 20 | 21 | ## License 22 | OCReader is released under the [GNU General Public License version 3](https://www.gnu.org/licenses/gpl-3.0) (or later versions). 23 | 24 | [TextDrawable](https://github.com/amulyakhare/TextDrawable) licensed under the MIT License, Copyright (c) 2014 Amulya Khare 25 | 26 | ## Contact 27 | Maintainer: [Daniel Schaal](https://github.com/schaal) <> 28 | 29 | [0]: https://github.com/nextcloud/news 30 | [1]: https://github.com/schaal/ocreader 31 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # For more details, see 2 | # http://developer.android.com/guide/developing/tools/proguard.html 3 | 4 | -dontobfuscate 5 | 6 | # AboutLibraries 7 | #-keep class .R 8 | #-keep class **.R$* { 9 | # ; 10 | #} -------------------------------------------------------------------------------- /app/src/androidTest/java/email/schaal/ocreader/APIDispatcher.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2016 Daniel Schaal 3 | * 4 | * This file is part of OCReader. 5 | * 6 | * OCReader is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU General Public License as published by 8 | * the Free Software Foundation, either version 3 of the License, or 9 | * (at your option) any later version. 10 | * 11 | * OCReader is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | * GNU General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU General Public License 17 | * along with OCReader. If not, see . 18 | * 19 | */ 20 | package email.schaal.ocreader 21 | 22 | import okhttp3.mockwebserver.Dispatcher 23 | import okhttp3.mockwebserver.MockResponse 24 | import okhttp3.mockwebserver.RecordedRequest 25 | 26 | /** 27 | * Created by daniel on 15.10.16. 28 | */ 29 | internal class APIDispatcher : Dispatcher() { 30 | var version = "8.8.2" 31 | @Throws(InterruptedException::class) 32 | override fun dispatch(request: RecordedRequest): MockResponse { 33 | val PATH_PREFIX = "/index.php/apps/news/api" 34 | if (request.path == PATH_PREFIX) return MockResponse().setBody("{ \"apiLevels\": [\"v1-2\"]}") else if (request.path == "$PATH_PREFIX/v1-2/status") return MockResponse().setBody(String.format("{ \"version\": \"%s\", \"warnings\": { \"improperlyConfiguredCron\": false }}", version)) 35 | return MockResponse().setResponseCode(404) 36 | } 37 | } -------------------------------------------------------------------------------- /app/src/androidTest/java/email/schaal/ocreader/DatabaseTest.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2015-2016 Daniel Schaal 3 | * 4 | * This file is part of OCReader. 5 | * 6 | * OCReader is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU General Public License as published by 8 | * the Free Software Foundation, either version 3 of the License, or 9 | * (at your option) any later version. 10 | * 11 | * OCReader is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | * GNU General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU General Public License 17 | * along with OCReader. If not, see . 18 | * 19 | */ 20 | package email.schaal.ocreader 21 | 22 | import androidx.test.ext.junit.rules.ActivityScenarioRule 23 | import androidx.test.ext.junit.runners.AndroidJUnit4 24 | import email.schaal.ocreader.database.Queries 25 | import email.schaal.ocreader.database.model.Feed 26 | import email.schaal.ocreader.database.model.Folder 27 | import email.schaal.ocreader.database.model.Item 28 | import io.realm.Realm 29 | import io.realm.kotlin.where 30 | import org.junit.* 31 | import org.junit.runner.RunWith 32 | 33 | /** 34 | * [Testing Fundamentals](http://d.android.com/tools/testing/testing_android.html) 35 | */ 36 | @RunWith(AndroidJUnit4::class) 37 | class DatabaseTest { 38 | @get:Rule 39 | var activityTestRule = ActivityScenarioRule(ListActivity::class.java) 40 | 41 | @Before 42 | fun setUp() { 43 | Queries.resetDatabase() 44 | } 45 | 46 | @After 47 | fun tearDown() { 48 | Queries.resetDatabase() 49 | } 50 | 51 | @Test 52 | fun testDatabaseSetup() { 53 | val realm = Realm.getDefaultInstance() 54 | realm.close() 55 | Assert.assertTrue(realm.isClosed) 56 | } 57 | 58 | @Test 59 | fun testFolderInsert() { 60 | Realm.getDefaultInstance().use { realm -> 61 | realm.beginTransaction() 62 | TestGenerator.testFolder.insert(realm) 63 | realm.commitTransaction() 64 | Folder.get(realm, 1).let { dbFolder -> 65 | Assert.assertNotNull(dbFolder) 66 | Assert.assertEquals(dbFolder?.name, TestGenerator.FOLDER_TITLE) 67 | } 68 | } 69 | } 70 | 71 | @Test 72 | fun testFeedInsert() { 73 | Realm.getDefaultInstance().use { realm -> 74 | realm.beginTransaction() 75 | TestGenerator.testFeed.insert(realm) 76 | realm.commitTransaction() 77 | Feed.get(realm, 1).let { feed -> 78 | Assert.assertNotNull(feed) 79 | Assert.assertEquals(feed?.name, TestGenerator.FEED_TITLE) 80 | } 81 | } 82 | } 83 | 84 | @Test 85 | fun testItemInsert() { 86 | Realm.getDefaultInstance().use { realm -> 87 | realm.beginTransaction() 88 | TestGenerator.testFeed.insert(realm) 89 | TestGenerator.testItem.insert(realm) 90 | realm.commitTransaction() 91 | realm.where().findFirst().let { item -> 92 | Assert.assertEquals(item?.id, 1) 93 | Assert.assertEquals(item?.title, TestGenerator.ITEM_TITLE) 94 | Assert.assertEquals(item?.body, TestGenerator.BODY) 95 | Assert.assertEquals(item?.author, TestGenerator.AUTHOR) 96 | Assert.assertNull(item?.enclosureLink) 97 | } 98 | } 99 | } 100 | } -------------------------------------------------------------------------------- /app/src/androidTest/java/email/schaal/ocreader/ItemPagerActivityTest.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2017. Daniel Schaal 3 | * 4 | * This file is part of ocreader. 5 | * 6 | * ocreader is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU General Public License as published by 8 | * the Free Software Foundation, either version 3 of the License, or 9 | * (at your option) any later version. 10 | * 11 | * ocreader is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | * GNU General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU General Public License 17 | * along with Foobar. If not, see . 18 | */ 19 | package email.schaal.ocreader 20 | 21 | import android.content.Intent 22 | import androidx.test.espresso.Espresso 23 | import androidx.test.espresso.action.ViewActions 24 | import androidx.test.espresso.matcher.ViewMatchers 25 | import androidx.test.ext.junit.runners.AndroidJUnit4 26 | import androidx.test.filters.LargeTest 27 | import androidx.test.rule.ActivityTestRule 28 | import email.schaal.ocreader.ItemPagerActivity 29 | import email.schaal.ocreader.database.model.Item 30 | import org.junit.Rule 31 | import org.junit.Test 32 | import org.junit.runner.RunWith 33 | import java.util.* 34 | 35 | /** 36 | * Created by daniel on 16.04.17. 37 | */ 38 | @RunWith(AndroidJUnit4::class) 39 | @LargeTest 40 | class ItemPagerActivityTest { 41 | @Rule 42 | val activityTestRule = ActivityTestRule(ItemPagerActivity::class.java, true, false) 43 | 44 | @Test 45 | @Throws(Exception::class) 46 | fun testItemPagerActivity() { 47 | val intent = Intent(Intent.ACTION_VIEW) 48 | val items = ArrayList() 49 | items.add(TestGenerator.getTestItem(1)) 50 | items.add(TestGenerator.getTestItem(2)) 51 | intent.putExtra("ARG_ITEMS", items) 52 | activityTestRule.launchActivity(intent) 53 | Espresso.onView(ViewMatchers.withId(R.id.container)).perform(ViewActions.swipeLeft()) 54 | Espresso.onView(ViewMatchers.withId(R.id.container)).perform(ViewActions.swipeRight()) 55 | } 56 | } -------------------------------------------------------------------------------- /app/src/androidTest/java/email/schaal/ocreader/TestGenerator.kt: -------------------------------------------------------------------------------- 1 | package email.schaal.ocreader 2 | 3 | import email.schaal.ocreader.database.model.Feed 4 | import email.schaal.ocreader.database.model.Folder 5 | import email.schaal.ocreader.database.model.Item 6 | import java.util.* 7 | 8 | /** 9 | * Created by daniel on 14.10.16. 10 | */ 11 | internal object TestGenerator { 12 | const val FOLDER_TITLE = "TestFolderTitle" 13 | const val FEED_TITLE = "TestFeedTitle" 14 | const val ITEM_TITLE = "TestItemTitle" 15 | const val BODY = "

TestBody

" 16 | const val AUTHOR = "TestAuthor" 17 | 18 | val testFolder: Folder 19 | get() { 20 | return Folder().apply { 21 | id = 1L 22 | name = FOLDER_TITLE 23 | } 24 | } 25 | 26 | val testFeed: Feed 27 | get() = getTestFeed(1) 28 | 29 | fun getTestFeed(id: Long): Feed { 30 | return Feed().apply { 31 | this.id = id 32 | folderId = 0L 33 | name = FEED_TITLE 34 | } 35 | } 36 | 37 | val testItem: Item 38 | get() = getTestItem(1) 39 | 40 | fun getTestItem(id: Long): Item { 41 | return Item().apply { 42 | this.id = id 43 | title = ITEM_TITLE 44 | body = BODY 45 | author = AUTHOR 46 | feedId = 1 47 | feed = testFeed 48 | lastModified = Date() 49 | } 50 | } 51 | } -------------------------------------------------------------------------------- /app/src/debug/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 20 | 21 | 22 | OCDebug 23 | 24 | -------------------------------------------------------------------------------- /app/src/debug/res/xml-v25/shortcuts.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 12 | 15 | 16 | 17 | 22 | 26 | 29 | 30 | 31 | 36 | 40 | 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /app/src/main/assets/font/crimsontextregular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/schaal/ocreader/c0311a0ccbac01a930772a5d7d593b2367ae9ddc/app/src/main/assets/font/crimsontextregular.ttf -------------------------------------------------------------------------------- /app/src/main/ic_launcher-web.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/schaal/ocreader/c0311a0ccbac01a930772a5d7d593b2367ae9ddc/app/src/main/ic_launcher-web.png -------------------------------------------------------------------------------- /app/src/main/java/email/schaal/ocreader/OCReaderApplication.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2017. Daniel Schaal 3 | * 4 | * This file is part of ocreader. 5 | * 6 | * ocreader is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU General Public License as published by 8 | * the Free Software Foundation, either version 3 of the License, or 9 | * (at your option) any later version. 10 | * 11 | * ocreader is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | * GNU General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU General Public License 17 | * along with Foobar. If not, see . 18 | */ 19 | package email.schaal.ocreader 20 | 21 | import android.app.Application 22 | import androidx.appcompat.app.AppCompatDelegate 23 | import androidx.preference.PreferenceManager 24 | import email.schaal.ocreader.database.Queries 25 | import email.schaal.ocreader.database.model.Item 26 | 27 | /** 28 | * Application base class to setup the singletons 29 | */ 30 | class OCReaderApplication : Application() { 31 | override fun onCreate() { 32 | super.onCreate() 33 | 34 | val preferences = PreferenceManager.getDefaultSharedPreferences(this) 35 | 36 | preferences.edit().apply { 37 | // Migrate to apptoken 38 | if (preferences.contains(Preferences.PASSWORD.key)) { 39 | remove(Preferences.USERNAME.key) 40 | remove(Preferences.PASSWORD.key) 41 | remove(Preferences.URL.key) 42 | } 43 | 44 | // Migrate updatedAt to lastModified 45 | if (Preferences.SORT_FIELD.getString(preferences) == "updatedAt") 46 | putString(Preferences.SORT_FIELD.key, Item::lastModified.name) 47 | 48 | // Directly observer WorkManager 49 | remove("sync_running") 50 | }.apply() 51 | 52 | AppCompatDelegate.setDefaultNightMode(Preferences.getNightMode(preferences)) 53 | 54 | Queries.init(this) 55 | } 56 | } -------------------------------------------------------------------------------- /app/src/main/java/email/schaal/ocreader/SettingsFragment.kt: -------------------------------------------------------------------------------- 1 | package email.schaal.ocreader 2 | 3 | import android.os.Bundle 4 | import androidx.preference.Preference 5 | import androidx.preference.PreferenceFragmentCompat 6 | import androidx.preference.PreferenceManager 7 | import com.bumptech.glide.Glide 8 | import email.schaal.ocreader.database.Queries 9 | import kotlinx.coroutines.Dispatchers 10 | import kotlinx.coroutines.runBlocking 11 | 12 | /** 13 | * Preference Fragment 14 | */ 15 | class SettingsFragment : PreferenceFragmentCompat() { 16 | override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { 17 | setPreferencesFromResource(R.xml.preferences, rootKey) 18 | findPreference("reset_database")?.apply { 19 | setOnPreferenceClickListener { 20 | Queries.resetDatabase() 21 | Glide.get(context).apply { 22 | runBlocking(Dispatchers.IO) { 23 | clearDiskCache() 24 | } 25 | clearMemory() 26 | } 27 | PreferenceManager.getDefaultSharedPreferences(requireContext()) 28 | .edit() 29 | .putBoolean(Preferences.SYS_NEEDS_UPDATE_AFTER_SYNC.key, true) 30 | .apply() 31 | it.title = getString(R.string.database_was_reset) 32 | it.isEnabled = false 33 | true 34 | } 35 | } 36 | } 37 | } -------------------------------------------------------------------------------- /app/src/main/java/email/schaal/ocreader/api/APIv12Interface.kt: -------------------------------------------------------------------------------- 1 | package email.schaal.ocreader.api 2 | 3 | import email.schaal.ocreader.api.json.* 4 | import retrofit2.Response 5 | import retrofit2.http.* 6 | 7 | interface APIv12Interface { 8 | /* SERVER **/ 9 | @GET("status") 10 | suspend fun status(): Status 11 | 12 | /** FOLDERS */ 13 | @GET("folders") 14 | suspend fun folders(): Folders 15 | 16 | /** FEEDS */ 17 | @GET("feeds") 18 | suspend fun feeds(): Feeds 19 | 20 | @POST("feeds") 21 | suspend fun createFeed(@Body feedMap: Map): Feeds 22 | 23 | @PUT("feeds/{feedId}/move") 24 | suspend fun moveFeed(@Path("feedId") feedId: Long, @Body folderIdMap: Map): Response 25 | 26 | @DELETE("feeds/{feedId}") 27 | suspend fun deleteFeed(@Path("feedId") feedId: Long): Response 28 | 29 | /** ITEMS */ 30 | @GET("items") 31 | suspend fun items( 32 | @Query("batchSize") batchSize: Long, 33 | @Query("offset") offset: Long, 34 | @Query("type") type: Int, 35 | @Query("id") id: Long, 36 | @Query("getRead") getRead: Boolean, 37 | @Query("oldestFirst") oldestFirst: Boolean 38 | ): Items 39 | 40 | @GET("items/updated") 41 | suspend fun updatedItems( 42 | @Query("lastModified") lastModified: Long, 43 | @Query("type") type: Int, 44 | @Query("id") id: Long 45 | ): Items 46 | 47 | @PUT("items/read/multiple") 48 | suspend fun markItemsRead(@Body items: ItemIds): Response 49 | 50 | @PUT("items/unread/multiple") 51 | suspend fun markItemsUnread(@Body items: ItemIds): Response 52 | 53 | @PUT("items/star/multiple") 54 | suspend fun markItemsStarred(@Body itemMap: ItemMap): Response 55 | 56 | @PUT("items/unstar/multiple") 57 | suspend fun markItemsUnstarred(@Body itemMap: ItemMap): Response 58 | } -------------------------------------------------------------------------------- /app/src/main/java/email/schaal/ocreader/api/CommonAPI.kt: -------------------------------------------------------------------------------- 1 | package email.schaal.ocreader.api 2 | 3 | import email.schaal.ocreader.api.json.APILevels 4 | import retrofit2.Response 5 | import retrofit2.http.GET 6 | 7 | interface CommonAPI { 8 | @GET(API.API_ROOT) 9 | suspend fun apiLevels(): Response 10 | } -------------------------------------------------------------------------------- /app/src/main/java/email/schaal/ocreader/api/Level.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2017. Daniel Schaal 3 | * 4 | * This file is part of ocreader. 5 | * 6 | * ocreader is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU General Public License as published by 8 | * the Free Software Foundation, either version 3 of the License, or 9 | * (at your option) any later version. 10 | * 11 | * ocreader is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | * GNU General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU General Public License 17 | * along with Foobar. If not, see . 18 | */ 19 | package email.schaal.ocreader.api 20 | 21 | import android.content.Context 22 | import email.schaal.ocreader.R 23 | import email.schaal.ocreader.http.HttpManager 24 | 25 | /** 26 | * Created by daniel on 26.05.17. 27 | */ 28 | enum class Level(val level: String, val isSupported: Boolean) { 29 | V2("v2", false), 30 | V12("v1-2", true); 31 | 32 | companion object { 33 | fun getAPI(context: Context, level: Level, httpManager: HttpManager): API { 34 | return when (level) { 35 | V12 -> API(context, httpManager) 36 | V2 -> throw IllegalStateException(context.getString(R.string.error_not_compatible)) 37 | } 38 | } 39 | 40 | operator fun get(level: String): Level? { 41 | for (supportedLevel in values()) { 42 | if (supportedLevel.level == level) return supportedLevel 43 | } 44 | return null 45 | } 46 | } 47 | 48 | } -------------------------------------------------------------------------------- /app/src/main/java/email/schaal/ocreader/api/OCSAPI.kt: -------------------------------------------------------------------------------- 1 | package email.schaal.ocreader.api 2 | 3 | import email.schaal.ocreader.database.model.User 4 | import retrofit2.http.GET 5 | import retrofit2.http.Headers 6 | import retrofit2.http.Path 7 | 8 | interface OCSAPI { 9 | @Headers("OCS-APIRequest: true") 10 | @GET("ocs/v1.php/cloud/users/{userId}?format=json") 11 | suspend fun user(@Path("userId") userId: String): User 12 | } -------------------------------------------------------------------------------- /app/src/main/java/email/schaal/ocreader/api/json/APILevels.kt: -------------------------------------------------------------------------------- 1 | package email.schaal.ocreader.api.json 2 | 3 | import com.squareup.moshi.JsonClass 4 | import email.schaal.ocreader.api.Level 5 | 6 | /** 7 | * API response containing supported API levels 8 | */ 9 | @JsonClass(generateAdapter = true) 10 | data class APILevels( 11 | val apiLevels: List = emptyList() 12 | ) { 13 | fun highestSupportedApi(): Level? { 14 | for (level in Level.values()) 15 | if (level.isSupported && apiLevels.contains(level.level)) 16 | return level 17 | return null 18 | } 19 | } -------------------------------------------------------------------------------- /app/src/main/java/email/schaal/ocreader/api/json/Collections.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2020. Daniel Schaal 3 | * 4 | * This file is part of ocreader. 5 | * 6 | * ocreader is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU General Public License as published by 8 | * the Free Software Foundation, either version 3 of the License, or 9 | * (at your option) any later version. 10 | * 11 | * ocreader is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | * GNU General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU General Public License 17 | * along with Foobar. If not, see . 18 | */ 19 | 20 | package email.schaal.ocreader.api.json 21 | 22 | import com.squareup.moshi.JsonClass 23 | import email.schaal.ocreader.database.model.Feed 24 | import email.schaal.ocreader.database.model.Folder 25 | import email.schaal.ocreader.database.model.Item 26 | 27 | /** 28 | * Class to deserialize the json response for feeds 29 | */ 30 | @JsonClass(generateAdapter = true) 31 | class Feeds ( 32 | val feeds: List, 33 | val starredCount: Int = 0, 34 | val newestItemId: Long? = null 35 | ) 36 | 37 | /** 38 | * Class to deserialize the json response for folders 39 | */ 40 | @JsonClass(generateAdapter = true) 41 | class Folders(val folders: List) 42 | 43 | /** 44 | * Class to deserialize the json response for items 45 | */ 46 | @JsonClass(generateAdapter = true) 47 | class Items(val items: List) 48 | 49 | /** 50 | * Aggregates item ids, used to mark multiple items as read 51 | */ 52 | @JsonClass(generateAdapter = true) 53 | data class ItemIds(val items: List) { 54 | constructor(sourceItems: Iterable) : this(sourceItems.map { it.id }) 55 | } 56 | 57 | /** 58 | * Aggregates feedIds and guidHashes, used to mark multiple items as starred 59 | */ 60 | @JsonClass(generateAdapter = true) 61 | data class ItemMap(val items: List) { 62 | @JsonClass(generateAdapter = true) 63 | data class MappedItem(val feedId: Long, val guidHash: String?) 64 | 65 | constructor(sourceItems: Iterable) : this(sourceItems.map { 66 | MappedItem(it.feedId, it.guidHash) 67 | }) 68 | } -------------------------------------------------------------------------------- /app/src/main/java/email/schaal/ocreader/api/json/FeedJsonTypeAdapter.kt: -------------------------------------------------------------------------------- 1 | package email.schaal.ocreader.api.json 2 | 3 | import com.squareup.moshi.FromJson 4 | import com.squareup.moshi.JsonClass 5 | import com.squareup.moshi.ToJson 6 | import email.schaal.ocreader.database.model.Feed 7 | import email.schaal.ocreader.util.cleanString 8 | import java.util.* 9 | 10 | class FeedJsonTypeAdapter { 11 | @FromJson 12 | fun fromJson(jsonFeed: JsonFeed) : Feed { 13 | return Feed().apply { 14 | id = jsonFeed.id 15 | folderId = jsonFeed.folderId 16 | url = jsonFeed.url 17 | name = jsonFeed.title.cleanString() 18 | link = jsonFeed.link?.ifBlank { null } 19 | faviconLink = jsonFeed.faviconLink?.ifBlank { null } 20 | added = Date(jsonFeed.added * 1000) 21 | unreadCount = jsonFeed.unreadCount ?: 0 22 | ordering = jsonFeed.ordering 23 | pinned = jsonFeed.pinned 24 | updateErrorCount = jsonFeed.updateErrorCount ?: 0 25 | lastUpdateError = jsonFeed.lastUpdateError 26 | } 27 | } 28 | 29 | @ToJson 30 | fun toJson(feed: Feed) : Map { 31 | return mapOf( 32 | "id" to feed.id, 33 | "title" to feed.name 34 | ) 35 | } 36 | } 37 | 38 | @JsonClass(generateAdapter = true) 39 | class JsonFeed( 40 | val id: Long, 41 | val folderId: Long?, 42 | val url: String, 43 | val title: String, 44 | val link: String?, 45 | val faviconLink: String?, 46 | val added: Long, 47 | val unreadCount: Int?, 48 | val ordering: Int, 49 | val pinned: Boolean, 50 | val updateErrorCount: Int?, 51 | val lastUpdateError: String? 52 | ) -------------------------------------------------------------------------------- /app/src/main/java/email/schaal/ocreader/api/json/ItemJsonTypeAdapter.kt: -------------------------------------------------------------------------------- 1 | package email.schaal.ocreader.api.json 2 | 3 | import com.squareup.moshi.FromJson 4 | import com.squareup.moshi.JsonClass 5 | import com.squareup.moshi.ToJson 6 | import email.schaal.ocreader.database.model.Item 7 | import email.schaal.ocreader.util.cleanString 8 | import java.util.* 9 | 10 | class ItemJsonTypeAdapter { 11 | @FromJson 12 | fun fromJson(jsonItem: JsonItem) : Item { 13 | return Item().apply { 14 | id = jsonItem.id 15 | guid = jsonItem.guid 16 | guidHash = jsonItem.guidHash 17 | url = jsonItem.url 18 | title = jsonItem.title?.cleanString() 19 | author = jsonItem.author?.cleanString() 20 | pubDate = Date(jsonItem.pubDate * 1000) 21 | body = jsonItem.body 22 | enclosureLink = jsonItem.enclosureLink 23 | feedId = jsonItem.feedId 24 | unread = jsonItem.unread 25 | starred = jsonItem.starred 26 | lastModified = Date(jsonItem.lastModified * 1000) 27 | fingerprint = jsonItem.fingerprint 28 | contentHash = jsonItem.contentHash 29 | } 30 | } 31 | 32 | @ToJson 33 | fun toJson(item: Item) : Map { 34 | return mutableMapOf( 35 | "id" to item.id, 36 | "contentHash" to item.contentHash 37 | ). also { 38 | if(item.unreadChanged) it["isUnread"] = item.unread 39 | if(item.starredChanged) it["isStarred"] = item.starred 40 | } 41 | } 42 | } 43 | 44 | @JsonClass(generateAdapter = true) 45 | class JsonItem( 46 | val id: Long, 47 | val guid: String?, 48 | val guidHash: String?, 49 | val url: String?, 50 | val title: String?, 51 | val author: String?, 52 | val pubDate: Long, 53 | val body: String, 54 | val enclosureMime: String?, 55 | val enclosureLink: String?, 56 | val feedId: Long, 57 | val unread: Boolean, 58 | val starred: Boolean, 59 | val lastModified: Long, 60 | val fingerprint: String?, 61 | val contentHash: String? 62 | ) -------------------------------------------------------------------------------- /app/src/main/java/email/schaal/ocreader/api/json/NewsError.kt: -------------------------------------------------------------------------------- 1 | package email.schaal.ocreader.api.json 2 | 3 | /** 4 | * Class to decode error json response 5 | */ 6 | class NewsError { 7 | var message: String? = null 8 | } -------------------------------------------------------------------------------- /app/src/main/java/email/schaal/ocreader/api/json/Status.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2016 Daniel Schaal 3 | * 4 | * This file is part of OCReader. 5 | * 6 | * OCReader is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU General Public License as published by 8 | * the Free Software Foundation, either version 3 of the License, or 9 | * (at your option) any later version. 10 | * 11 | * OCReader is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | * GNU General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU General Public License 17 | * along with OCReader. If not, see . 18 | * 19 | */ 20 | package email.schaal.ocreader.api.json 21 | 22 | import com.github.zafarkhaja.semver.Version 23 | import com.squareup.moshi.JsonClass 24 | 25 | /** 26 | * Encapsulates the JSON response for the status api call 27 | */ 28 | @JsonClass(generateAdapter = true) 29 | class Status(val version: Version, 30 | val warnings: Map? = null) { 31 | 32 | val isImproperlyConfiguredCron: Boolean 33 | get() = warnings?.get("isImproperlyConfiguredCron") ?: false 34 | } -------------------------------------------------------------------------------- /app/src/main/java/email/schaal/ocreader/api/json/TypeAdapters.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2020. Daniel Schaal 3 | * 4 | * This file is part of ocreader. 5 | * 6 | * ocreader is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU General Public License as published by 8 | * the Free Software Foundation, either version 3 of the License, or 9 | * (at your option) any later version. 10 | * 11 | * ocreader is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | * GNU General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU General Public License 17 | * along with Foobar. If not, see . 18 | */ 19 | 20 | package email.schaal.ocreader.api.json 21 | 22 | import com.github.zafarkhaja.semver.Version 23 | import com.squareup.moshi.FromJson 24 | import com.squareup.moshi.ToJson 25 | import java.util.* 26 | 27 | class VersionTypeAdapter { 28 | @FromJson 29 | fun fromJson(versionString: String?): Version? { 30 | return Version.valueOf(versionString) 31 | } 32 | 33 | @ToJson 34 | fun toJson(version: Version): String { 35 | return version.toString() 36 | } 37 | } 38 | 39 | class DateTypeAdapter { 40 | @FromJson 41 | fun fromJson(timestamp: Long) : Date { 42 | return Date(timestamp * 1000) 43 | } 44 | 45 | @ToJson 46 | fun toJson(date: Date): Long { 47 | return date.time / 1000 48 | } 49 | } -------------------------------------------------------------------------------- /app/src/main/java/email/schaal/ocreader/api/json/UserJsonTypeAdapter.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2020. Daniel Schaal 3 | * 4 | * This file is part of ocreader. 5 | * 6 | * ocreader is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU General Public License as published by 8 | * the Free Software Foundation, either version 3 of the License, or 9 | * (at your option) any later version. 10 | * 11 | * ocreader is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | * GNU General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU General Public License 17 | * along with Foobar. If not, see . 18 | */ 19 | 20 | package email.schaal.ocreader.api.json 21 | 22 | import com.squareup.moshi.FromJson 23 | import com.squareup.moshi.JsonClass 24 | import email.schaal.ocreader.database.model.User 25 | 26 | class UserJsonTypeAdapter { 27 | @FromJson 28 | fun fromJson(ocs: OCS): User { 29 | return User(ocs.ocs.data.id, ocs.ocs.data.displayname) 30 | } 31 | } 32 | 33 | @JsonClass(generateAdapter = true) 34 | class OCS ( 35 | val ocs: JsonUserData 36 | ) 37 | 38 | @JsonClass(generateAdapter = true) 39 | class JsonUserData ( 40 | val data: JsonUser 41 | ) 42 | 43 | @JsonClass(generateAdapter = true) 44 | class JsonUser ( 45 | val id: String, 46 | val displayname: String 47 | ) -------------------------------------------------------------------------------- /app/src/main/java/email/schaal/ocreader/api/json/v2/SyncResponse.kt: -------------------------------------------------------------------------------- 1 | package email.schaal.ocreader.api.json.v2 2 | 3 | import email.schaal.ocreader.database.model.Feed 4 | import email.schaal.ocreader.database.model.Folder 5 | import email.schaal.ocreader.database.model.Item 6 | 7 | /** 8 | * API response for sync call 9 | */ 10 | class SyncResponse { 11 | var folders: List? = null 12 | var feeds: List? = null 13 | var items: List? = null 14 | 15 | } -------------------------------------------------------------------------------- /app/src/main/java/email/schaal/ocreader/database/FolderViewModel.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2020. Daniel Schaal 3 | * 4 | * This file is part of ocreader. 5 | * 6 | * ocreader is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU General Public License as published by 8 | * the Free Software Foundation, either version 3 of the License, or 9 | * (at your option) any later version. 10 | * 11 | * ocreader is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | * GNU General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU General Public License 17 | * along with Foobar. If not, see . 18 | */ 19 | 20 | package email.schaal.ocreader.database 21 | 22 | import email.schaal.ocreader.database.model.Feed 23 | import email.schaal.ocreader.database.model.Folder 24 | import io.realm.kotlin.where 25 | 26 | class FolderViewModel: RealmViewModel() { 27 | val foldersLiveData = LiveRealmResults(Folder.getAll(realm, false)) 28 | val feedsLiveData = LiveRealmResults(realm.where().sort(Feed::name.name).findAll()) 29 | } -------------------------------------------------------------------------------- /app/src/main/java/email/schaal/ocreader/database/LiveRealmResults.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018 Realm Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package email.schaal.ocreader.database 17 | 18 | import androidx.annotation.MainThread 19 | import androidx.lifecycle.MutableLiveData 20 | import io.realm.OrderedRealmCollectionChangeListener 21 | import io.realm.RealmModel 22 | import io.realm.RealmResults 23 | 24 | /** 25 | * This class represents a RealmResults wrapped inside a LiveData. 26 | * 27 | * Realm will always keep the RealmResults up-to-date whenever a change occurs on any thread, 28 | * and when that happens, the observer will be notified. 29 | * 30 | * The RealmResults will be observed until it is invalidated - meaning all local Realm instances on this thread are closed. 31 | * 32 | * @param the type of the RealmModel 33 | */ 34 | class LiveRealmResults @MainThread constructor(results: RealmResults) : MutableLiveData>() { 35 | private val results: RealmResults 36 | // The listener will notify the observers whenever a change occurs. 37 | // The results are modified in change. This could be expanded to also return the change set in a pair. 38 | private val listener = OrderedRealmCollectionChangeListener> { results, _ -> this@LiveRealmResults.value = results } 39 | // We should start observing and stop observing, depending on whether we have observers. 40 | /** 41 | * Starts observing the RealmResults, if it is still valid. 42 | */ 43 | override fun onActive() { 44 | super.onActive() 45 | if (results.isValid) { // invalidated results can no longer be observed. 46 | results.addChangeListener(listener) 47 | value = results 48 | } 49 | } 50 | 51 | /** 52 | * Stops observing the RealmResults. 53 | */ 54 | override fun onInactive() { 55 | super.onInactive() 56 | if (results.isValid) { 57 | results.removeChangeListener(listener) 58 | } 59 | } 60 | 61 | init { 62 | require(results.isValid) { "The provided RealmResults is no longer valid, the Realm instance it belongs to is closed. It can no longer be observed for changes." } 63 | this.results = results 64 | if (results.isLoaded) { 65 | // we should not notify observers when results aren't ready yet (async query). 66 | // however, synchronous query should be set explicitly. 67 | value = results 68 | } 69 | } 70 | } -------------------------------------------------------------------------------- /app/src/main/java/email/schaal/ocreader/database/PagerViewModel.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2020. Daniel Schaal 3 | * 4 | * This file is part of ocreader. 5 | * 6 | * ocreader is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU General Public License as published by 8 | * the Free Software Foundation, either version 3 of the License, or 9 | * (at your option) any later version. 10 | * 11 | * ocreader is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | * GNU General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU General Public License 17 | * along with Foobar. If not, see . 18 | */ 19 | 20 | package email.schaal.ocreader.database 21 | 22 | import email.schaal.ocreader.database.model.TemporaryFeed 23 | 24 | class PagerViewModel: RealmViewModel() { 25 | val pager = TemporaryFeed.getPagerTemporaryFeed(realm) 26 | 27 | fun updatePager() { 28 | TemporaryFeed.updatePagerTemporaryFeed(realm) 29 | } 30 | } -------------------------------------------------------------------------------- /app/src/main/java/email/schaal/ocreader/database/Queries.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2015-2016 Daniel Schaal 3 | * 4 | * This file is part of OCReader. 5 | * 6 | * OCReader is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU General Public License as published by 8 | * the Free Software Foundation, either version 3 of the License, or 9 | * (at your option) any later version. 10 | * 11 | * OCReader is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | * GNU General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU General Public License 17 | * along with OCReader. If not, see . 18 | * 19 | */ 20 | package email.schaal.ocreader.database 21 | 22 | import android.content.Context 23 | import android.util.Log 24 | import email.schaal.ocreader.database.model.* 25 | import io.realm.Realm 26 | import io.realm.RealmConfiguration 27 | import io.realm.exceptions.RealmPrimaryKeyConstraintException 28 | import io.realm.kotlin.createObject 29 | import io.realm.kotlin.where 30 | 31 | /** 32 | * Utility class containing some commonly used Queries for the Realm database. 33 | */ 34 | object Queries { 35 | private val TAG = Queries::class.java.name 36 | 37 | const val SCHEMA_VERSION = 13L 38 | 39 | private val initialData = Realm.Transaction { realm: Realm -> 40 | realm.where().findAll().deleteAllFromRealm() 41 | realm.where().findAll().deleteAllFromRealm() 42 | realm.where().findAll().deleteAllFromRealm() 43 | realm.where().findAll().deleteAllFromRealm() 44 | try { 45 | realm.createObject(TemporaryFeed.LIST_ID) 46 | realm.createObject(TemporaryFeed.PAGER_ID) 47 | } catch (e: RealmPrimaryKeyConstraintException) { 48 | } 49 | } 50 | 51 | fun init(context: Context) { 52 | Realm.init(context) 53 | val realmConfiguration = RealmConfiguration.Builder() 54 | .schemaVersion(SCHEMA_VERSION) 55 | .deleteRealmIfMigrationNeeded() 56 | .initialData(initialData) 57 | .compactOnLaunch() 58 | .allowWritesOnUiThread(true) 59 | .build() 60 | Realm.setDefaultConfiguration(realmConfiguration) 61 | 62 | try { 63 | Realm.getDefaultInstance().use { 64 | if(it.isEmpty) 65 | it.executeTransaction(initialData) 66 | } 67 | } catch(e: Exception) { 68 | Log.e(TAG, "Failed to open realm db", e) 69 | } 70 | } 71 | 72 | fun resetDatabase() { 73 | Log.w(TAG, "Database will be reset") 74 | Realm.getDefaultInstance().use { 75 | it.executeTransaction(initialData) 76 | } 77 | } 78 | 79 | } -------------------------------------------------------------------------------- /app/src/main/java/email/schaal/ocreader/database/RealmViewModel.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2020. Daniel Schaal 3 | * 4 | * This file is part of ocreader. 5 | * 6 | * ocreader is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU General Public License as published by 8 | * the Free Software Foundation, either version 3 of the License, or 9 | * (at your option) any later version. 10 | * 11 | * ocreader is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | * GNU General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU General Public License 17 | * along with Foobar. If not, see . 18 | */ 19 | 20 | package email.schaal.ocreader.database 21 | 22 | import android.util.Log 23 | import androidx.lifecycle.ViewModel 24 | import email.schaal.ocreader.database.model.Item 25 | import email.schaal.ocreader.database.model.Item.Companion.UNREAD 26 | import io.realm.Realm 27 | import io.realm.exceptions.RealmException 28 | import io.realm.kotlin.where 29 | 30 | open class RealmViewModel : ViewModel() { 31 | protected val realm: Realm by lazy { Realm.getDefaultInstance() } 32 | 33 | override fun onCleared() { 34 | Log.d(TAG, "onCleared called in ${this::class.simpleName}") 35 | realm.close() 36 | super.onCleared() 37 | } 38 | 39 | fun setItemUnread(unread: Boolean, vararg items: Item?) { 40 | realm.executeTransaction { 41 | try { 42 | for (item in items.filterNotNull()) { /* If the item has a fingerprint, mark all items with the same fingerprint 43 | as read 44 | */ 45 | if (item.fingerprint == null) { 46 | item.unread = unread 47 | } else { 48 | val sameItems = it.where() 49 | .equalTo(Item::fingerprint.name, item.fingerprint) 50 | .equalTo(UNREAD, !unread) 51 | .findAll() 52 | for (sameItem in sameItems) { 53 | sameItem.unread = unread 54 | } 55 | } 56 | } 57 | } catch (e: RealmException) { 58 | Log.e(TAG, "Failed to set item as unread", e) 59 | } 60 | } 61 | } 62 | 63 | fun setItemStarred(starred: Boolean, vararg items: Item?) { 64 | realm.executeTransaction { 65 | try { 66 | for (item in items.filterNotNull()) { 67 | item.starred = starred 68 | } 69 | } catch (e: RealmException) { 70 | Log.e(TAG, "Failed to set item as starred", e) 71 | } 72 | } 73 | } 74 | 75 | companion object { 76 | const val TAG = "RealmViewModel" 77 | } 78 | } -------------------------------------------------------------------------------- /app/src/main/java/email/schaal/ocreader/database/model/AllUnreadFolder.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2020. Daniel Schaal 3 | * 4 | * This file is part of ocreader. 5 | * 6 | * ocreader is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU General Public License as published by 8 | * the Free Software Foundation, either version 3 of the License, or 9 | * (at your option) any later version. 10 | * 11 | * ocreader is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | * GNU General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU General Public License 17 | * along with Foobar. If not, see . 18 | */ 19 | 20 | package email.schaal.ocreader.database.model 21 | 22 | import email.schaal.ocreader.R 23 | import io.realm.Realm 24 | import io.realm.Sort 25 | import io.realm.kotlin.where 26 | 27 | class AllUnreadFolder: SpecialFolder(R.string.unread_items) { 28 | companion object { 29 | const val ID: Long = -10 30 | } 31 | 32 | override fun treeItemId(): Long { 33 | return ID 34 | } 35 | 36 | override fun getIcon(): Int { 37 | return R.drawable.ic_feed_icon 38 | } 39 | 40 | override fun getCount(realm: Realm): Int { 41 | return realm.where().sum(Feed::unreadCount.name).toInt() 42 | } 43 | 44 | override fun getFeeds(realm: Realm, onlyUnread: Boolean): List { 45 | val query = realm.where() 46 | if(onlyUnread) 47 | query.greaterThan(Feed::unreadCount.name, 0) 48 | return query.sort(Feed::name.name, Sort.ASCENDING).findAll() 49 | } 50 | 51 | override fun getItems(realm: Realm, onlyUnread: Boolean): List { 52 | val query = realm.where() 53 | if(onlyUnread) 54 | query.equalTo(Item.UNREAD, true) 55 | return query.distinct(Item::fingerprint.name).findAll() 56 | } 57 | } -------------------------------------------------------------------------------- /app/src/main/java/email/schaal/ocreader/database/model/FreshFolder.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2020. Daniel Schaal 3 | * 4 | * This file is part of ocreader. 5 | * 6 | * ocreader is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU General Public License as published by 8 | * the Free Software Foundation, either version 3 of the License, or 9 | * (at your option) any later version. 10 | * 11 | * ocreader is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | * GNU General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU General Public License 17 | * along with Foobar. If not, see . 18 | */ 19 | 20 | package email.schaal.ocreader.database.model 21 | 22 | import email.schaal.ocreader.R 23 | import io.realm.Realm 24 | import io.realm.kotlin.where 25 | import java.util.* 26 | 27 | class FreshFolder: SpecialFolder(R.string.fresh_items) { 28 | companion object { 29 | private const val MAX_ARTICLE_AGE = 24 * 60 * 60 * 1000 30 | const val ID: Long = -12 31 | } 32 | 33 | override fun treeItemId(): Long { 34 | return ID 35 | } 36 | 37 | override fun getIcon(): Int { 38 | return R.drawable.fresh 39 | } 40 | 41 | override fun getCount(realm: Realm): Int { 42 | return realm.where() 43 | .equalTo(Item.UNREAD, true) 44 | .greaterThan(Item::pubDate.name, getDate()) 45 | .count() 46 | .toInt() 47 | } 48 | 49 | override fun getFeeds(realm: Realm, onlyUnread: Boolean): List { 50 | val freshFeeds = mutableListOf() 51 | for(item: Item in getItems(realm, false)) { 52 | val feed = item.feed 53 | if(!freshFeeds.contains(feed) && feed != null) 54 | freshFeeds.add(feed) 55 | } 56 | return freshFeeds 57 | } 58 | 59 | override fun getItems(realm: Realm, onlyUnread: Boolean): List { 60 | return realm.where() 61 | .equalTo(Item.UNREAD, true) 62 | .greaterThan(Item::pubDate.name, getDate()) 63 | .findAll() 64 | } 65 | 66 | private fun getDate() : Date { 67 | return Date(System.currentTimeMillis() - MAX_ARTICLE_AGE) 68 | } 69 | } -------------------------------------------------------------------------------- /app/src/main/java/email/schaal/ocreader/database/model/Insertable.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2020. Daniel Schaal 3 | * 4 | * This file is part of ocreader. 5 | * 6 | * ocreader is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU General Public License as published by 8 | * the Free Software Foundation, either version 3 of the License, or 9 | * (at your option) any later version. 10 | * 11 | * ocreader is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | * GNU General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU General Public License 17 | * along with Foobar. If not, see . 18 | */ 19 | 20 | package email.schaal.ocreader.database.model 21 | 22 | import io.realm.Realm 23 | 24 | interface Insertable { 25 | fun insert(realm: Realm) 26 | fun delete(realm: Realm) 27 | } -------------------------------------------------------------------------------- /app/src/main/java/email/schaal/ocreader/database/model/SpecialFolder.kt: -------------------------------------------------------------------------------- 1 | package email.schaal.ocreader.database.model 2 | 3 | import android.content.Context 4 | import androidx.annotation.StringRes 5 | 6 | abstract class SpecialFolder(@StringRes private val name_res: Int): TreeItem { 7 | override fun treeItemName(context: Context): String { 8 | return context.getString(name_res) 9 | } 10 | } -------------------------------------------------------------------------------- /app/src/main/java/email/schaal/ocreader/database/model/StarredFolder.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2020. Daniel Schaal 3 | * 4 | * This file is part of ocreader. 5 | * 6 | * ocreader is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU General Public License as published by 8 | * the Free Software Foundation, either version 3 of the License, or 9 | * (at your option) any later version. 10 | * 11 | * ocreader is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | * GNU General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU General Public License 17 | * along with Foobar. If not, see . 18 | */ 19 | 20 | package email.schaal.ocreader.database.model 21 | 22 | import email.schaal.ocreader.R 23 | import io.realm.Realm 24 | import io.realm.Sort 25 | import io.realm.kotlin.where 26 | 27 | class StarredFolder: SpecialFolder(R.string.starred_items) { 28 | companion object { 29 | const val ID: Long = -11 30 | } 31 | 32 | override fun treeItemId(): Long { 33 | return ID 34 | } 35 | 36 | override fun getIcon(): Int { 37 | return R.drawable.ic_star_outline 38 | } 39 | 40 | override fun getCount(realm: Realm): Int { 41 | return realm.where().equalTo(Item.STARRED, true).count().toInt() 42 | } 43 | 44 | override fun getFeeds(realm: Realm, onlyUnread: Boolean): List { 45 | return realm.where().greaterThan(Feed::starredCount.name, 0).sort(Feed::name.name, Sort.ASCENDING).findAll() 46 | } 47 | 48 | override fun getItems(realm: Realm, onlyUnread: Boolean): List { 49 | return realm.where().equalTo(Item.STARRED, true).findAll() 50 | } 51 | } -------------------------------------------------------------------------------- /app/src/main/java/email/schaal/ocreader/database/model/TemporaryFeed.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2015 Daniel Schaal 3 | * 4 | * This file is part of OCReader. 5 | * 6 | * OCReader is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU General Public License as published by 8 | * the Free Software Foundation, either version 3 of the License, or 9 | * (at your option) any later version. 10 | * 11 | * OCReader is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | * GNU General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU General Public License 17 | * along with OCReader. If not, see . 18 | * 19 | */ 20 | package email.schaal.ocreader.database.model 21 | 22 | import io.realm.Realm 23 | import io.realm.RealmList 24 | import io.realm.RealmModel 25 | import io.realm.annotations.PrimaryKey 26 | import io.realm.annotations.RealmClass 27 | import io.realm.kotlin.where 28 | 29 | /** 30 | * TemporaryFeed allows to store the currently displayed Items. 31 | */ 32 | @RealmClass 33 | open class TemporaryFeed() : RealmModel { 34 | @PrimaryKey 35 | var id: Long = 0 36 | var treeItemId: Long? = null 37 | var name: String = "" 38 | var items: RealmList? = null 39 | 40 | constructor(id: Long) : this() { 41 | this.id = id 42 | } 43 | 44 | companion object { 45 | const val LIST_ID = 0 46 | const val PAGER_ID = 1 47 | 48 | fun getListTemporaryFeed(realm: Realm): TemporaryFeed? { 49 | return realm.where().equalTo(TemporaryFeed::id.name, LIST_ID).findFirst() 50 | } 51 | 52 | fun getPagerTemporaryFeed(realm: Realm): TemporaryFeed? { 53 | return realm.where().equalTo(TemporaryFeed::id.name, PAGER_ID).findFirst() 54 | } 55 | 56 | fun updatePagerTemporaryFeed(realm: Realm) { 57 | realm.executeTransaction { realm1: Realm -> 58 | val listTempFeed = getListTemporaryFeed(realm1) 59 | val pagerTempFeed = getPagerTemporaryFeed(realm1) 60 | for (item in realm1.where().equalTo(Item::active.name, true).findAll()) { 61 | item.active = false 62 | } 63 | val listTempFeedItems = listTempFeed?.items 64 | if (listTempFeedItems != null) { 65 | for (item in listTempFeedItems) { 66 | item.active = true 67 | } 68 | } 69 | pagerTempFeed?.items = listTempFeedItems 70 | pagerTempFeed?.name = listTempFeed?.name ?: "" 71 | pagerTempFeed?.treeItemId = listTempFeed?.treeItemId ?: 0 72 | } 73 | } 74 | } 75 | } -------------------------------------------------------------------------------- /app/src/main/java/email/schaal/ocreader/database/model/TreeItem.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2020. Daniel Schaal 3 | * 4 | * This file is part of ocreader. 5 | * 6 | * ocreader is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU General Public License as published by 8 | * the Free Software Foundation, either version 3 of the License, or 9 | * (at your option) any later version. 10 | * 11 | * ocreader is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | * GNU General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU General Public License 17 | * along with Foobar. If not, see . 18 | */ 19 | 20 | package email.schaal.ocreader.database.model 21 | 22 | import android.content.Context 23 | import androidx.annotation.DrawableRes 24 | import io.realm.Realm 25 | 26 | interface TreeItem { 27 | fun treeItemId() : Long? 28 | fun treeItemName(context: Context): String 29 | @DrawableRes fun getIcon(): Int 30 | fun getCount(realm: Realm): Int 31 | fun getFeeds(realm: Realm, onlyUnread: Boolean = false): List 32 | fun getItems(realm: Realm, onlyUnread: Boolean = false): List 33 | } -------------------------------------------------------------------------------- /app/src/main/java/email/schaal/ocreader/database/model/User.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2020. Daniel Schaal 3 | * 4 | * This file is part of ocreader. 5 | * 6 | * ocreader is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU General Public License as published by 8 | * the Free Software Foundation, either version 3 of the License, or 9 | * (at your option) any later version. 10 | * 11 | * ocreader is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | * GNU General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU General Public License 17 | * along with Foobar. If not, see . 18 | */ 19 | 20 | package email.schaal.ocreader.database.model 21 | 22 | import io.realm.Realm 23 | import io.realm.RealmModel 24 | import io.realm.RealmObject 25 | import io.realm.annotations.PrimaryKey 26 | import io.realm.annotations.RealmClass 27 | import okhttp3.HttpUrl.Companion.toHttpUrlOrNull 28 | 29 | @RealmClass 30 | open class User( 31 | var userId: String = "", 32 | var displayName: String = "", 33 | ) : RealmModel, Insertable { 34 | @PrimaryKey var id = 0L 35 | 36 | override fun insert(realm: Realm) { 37 | realm.insertOrUpdate(this) 38 | } 39 | 40 | override fun delete(realm: Realm) { 41 | RealmObject.deleteFromRealm(this) 42 | } 43 | 44 | fun avatarUrl(baseUrl: String): String? { 45 | return baseUrl.toHttpUrlOrNull()?.resolve("avatar/${userId}/128")?.toString() 46 | } 47 | } -------------------------------------------------------------------------------- /app/src/main/java/email/schaal/ocreader/http/HttpManager.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2015-2016 Daniel Schaal 3 | * 4 | * This file is part of OCReader. 5 | * 6 | * OCReader is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU General Public License as published by 8 | * the Free Software Foundation, either version 3 of the License, or 9 | * (at your option) any later version. 10 | * 11 | * OCReader is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | * GNU General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU General Public License 17 | * along with OCReader. If not, see . 18 | * 19 | */ 20 | package email.schaal.ocreader.http 21 | 22 | import okhttp3.* 23 | import java.io.IOException 24 | import java.util.concurrent.TimeUnit 25 | 26 | /** 27 | * Utility class to setup the OkHttpClient and manage the credentials used to communicate with 28 | * the ownCloud instance. 29 | */ 30 | class HttpManager(username: String, password: String, url: HttpUrl) { 31 | val client: OkHttpClient = OkHttpClient.Builder() 32 | .connectTimeout(20, TimeUnit.SECONDS) 33 | .readTimeout(3, TimeUnit.MINUTES) 34 | .addInterceptor(AuthorizationInterceptor()) 35 | .build() 36 | val credentials: HostCredentials = HostCredentials(username, password, url) 37 | 38 | inner class HostCredentials constructor(username: String, password: String, url: HttpUrl) { 39 | val credentials: String = Credentials.basic(username, password) 40 | val rootUrl: HttpUrl = url 41 | } 42 | 43 | private inner class AuthorizationInterceptor : Interceptor { 44 | @Throws(IOException::class) 45 | override fun intercept(chain: Interceptor.Chain): Response { 46 | var request = chain.request() 47 | // only add Authorization header for urls on the configured owncloud host 48 | if (credentials.rootUrl.host == request.url.host) request = request.newBuilder() 49 | .addHeader("Authorization", credentials.credentials) 50 | .build() 51 | return chain.proceed(request) 52 | } 53 | } 54 | 55 | } -------------------------------------------------------------------------------- /app/src/main/java/email/schaal/ocreader/service/SyncType.kt: -------------------------------------------------------------------------------- 1 | package email.schaal.ocreader.service 2 | 3 | /** 4 | * Created by daniel on 02.04.17. 5 | */ 6 | enum class SyncType(val action: String) { 7 | FULL_SYNC("email.schaal.ocreader.action.FULL_SYNC"), 8 | SYNC_CHANGES_ONLY("email.schaal.ocreader.action.SYNC_CHANGES_ONLY"), 9 | LOAD_MORE("email.schaal.ocreader.action.LOAD_MORE"); 10 | 11 | companion object { 12 | operator fun get(action: String?): SyncType? = values().find { it.action == action } 13 | } 14 | 15 | } -------------------------------------------------------------------------------- /app/src/main/java/email/schaal/ocreader/service/SyncWorker.kt: -------------------------------------------------------------------------------- 1 | package email.schaal.ocreader.service 2 | 3 | import android.content.Context 4 | import androidx.lifecycle.LiveData 5 | import androidx.preference.PreferenceManager 6 | import androidx.work.* 7 | import email.schaal.ocreader.Preferences 8 | import email.schaal.ocreader.api.API 9 | import email.schaal.ocreader.util.LoginError 10 | 11 | class SyncWorker(context: Context, workerParams: WorkerParameters) : CoroutineWorker(context, workerParams) { 12 | companion object { 13 | const val KEY_SYNC_TYPE = "KEY_SYNC_TYPE" 14 | const val KEY_EXCEPTION = "KEY_EXCEPTION" 15 | const val WORK_ID = "WORK_ID_SYNC" 16 | 17 | fun sync(context: Context, syncType: SyncType): LiveData { 18 | val workManager = WorkManager.getInstance(context) 19 | val syncWork = OneTimeWorkRequestBuilder() 20 | .setInputData(workDataOf(KEY_SYNC_TYPE to syncType.action)) 21 | .build() 22 | workManager.enqueueUniqueWork(WORK_ID, ExistingWorkPolicy.KEEP, syncWork) 23 | return workManager.getWorkInfoByIdLiveData(syncWork.id) 24 | } 25 | 26 | fun getLiveData(context: Context): LiveData> { 27 | return WorkManager.getInstance(context).getWorkInfosForUniqueWorkLiveData(WORK_ID) 28 | } 29 | } 30 | 31 | override suspend fun doWork(): Result { 32 | val syncType: SyncType = SyncType[inputData.getString(KEY_SYNC_TYPE)] ?: SyncType.FULL_SYNC 33 | 34 | return try { 35 | API(applicationContext).sync(syncType) 36 | Result.success() 37 | } catch (e: Throwable) { 38 | e.printStackTrace() 39 | Result.failure( 40 | workDataOf( 41 | KEY_EXCEPTION to LoginError.getError( 42 | applicationContext, e).message 43 | ) 44 | ) 45 | } 46 | } 47 | } -------------------------------------------------------------------------------- /app/src/main/java/email/schaal/ocreader/ui/loginflow/LoginFlowFragment.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2020. Daniel Schaal 3 | * 4 | * This file is part of ocreader. 5 | * 6 | * ocreader is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU General Public License as published by 8 | * the Free Software Foundation, either version 3 of the License, or 9 | * (at your option) any later version. 10 | * 11 | * ocreader is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | * GNU General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU General Public License 17 | * along with Foobar. If not, see . 18 | */ 19 | 20 | package email.schaal.ocreader.ui.loginflow 21 | 22 | import android.os.Bundle 23 | import android.view.LayoutInflater 24 | import android.view.View 25 | import android.view.ViewGroup 26 | import android.widget.TextView 27 | import androidx.fragment.app.Fragment 28 | import email.schaal.ocreader.R 29 | import email.schaal.ocreader.databinding.LoginFlowFragmentBinding 30 | import okhttp3.HttpUrl.Companion.toHttpUrlOrNull 31 | 32 | class LoginFlowFragment : Fragment() { 33 | private lateinit var binding: LoginFlowFragmentBinding 34 | 35 | private var url: String? = null 36 | 37 | companion object { 38 | private const val ARG_URL = "URL" 39 | 40 | @JvmStatic 41 | fun newInstance(url: String? = null) = 42 | LoginFlowFragment().apply { 43 | url?.let { 44 | arguments = Bundle().apply { 45 | putString(ARG_URL, it) 46 | } 47 | } 48 | } 49 | } 50 | 51 | override fun onCreate(savedInstanceState: Bundle?) { 52 | super.onCreate(savedInstanceState) 53 | arguments?.let { 54 | url = it.getString(ARG_URL) 55 | } 56 | } 57 | 58 | override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, 59 | savedInstanceState: Bundle?): View { 60 | binding = LoginFlowFragmentBinding.inflate(inflater, container, false) 61 | binding.inputUrl.setText(url, TextView.BufferType.EDITABLE) 62 | return binding.root 63 | } 64 | 65 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 66 | super.onViewCreated(view, savedInstanceState) 67 | binding.buttonLogin.setOnClickListener { 68 | // Check if inputUrl starts with a scheme (http or https) 69 | val urlString = binding.inputUrl.text?.let { 70 | if(!it.startsWith("http")) 71 | "https://${it}" 72 | else 73 | it 74 | }?.toString() 75 | 76 | val url = urlString 77 | ?.toHttpUrlOrNull() 78 | ?.newBuilder() 79 | ?.addPathSegments("index.php/login/flow") 80 | 81 | if(url != null) { 82 | parentFragmentManager.beginTransaction() 83 | .replace(R.id.container, LoginFlowWebViewFragment.newInstance(url.toString())) 84 | .commit() 85 | } else { 86 | binding.inputUrl.error = getString(R.string.error_incorrect_url) 87 | } 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /app/src/main/java/email/schaal/ocreader/util/ColorGenerator.kt: -------------------------------------------------------------------------------- 1 | /* 2 | The MIT License (MIT) 3 | 4 | Copyright (c) 2014 Amulya Khare 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | */ 24 | package email.schaal.ocreader.util 25 | 26 | import androidx.annotation.ColorRes 27 | import email.schaal.ocreader.R 28 | import kotlin.math.abs 29 | 30 | /** 31 | * @author amulya 32 | */ 33 | internal class ColorGenerator private constructor(private val mColors: List) { 34 | @ColorRes 35 | fun getColor(key: Any?): Int { 36 | return if (key != null) mColors[abs(key.hashCode()) % mColors.size] else mColors[0] 37 | } 38 | 39 | companion object { 40 | val MATERIAL = ColorGenerator(listOf( 41 | R.color.tdb_Red, 42 | R.color.tdb_Purple, 43 | R.color.tdb_Indigo, 44 | R.color.tdb_Blue, 45 | R.color.tdb_LightBlue, 46 | R.color.tdb_Cyan, 47 | R.color.tdb_Teal, 48 | R.color.tdb_Green, 49 | R.color.tdb_LightGreen, 50 | R.color.tdb_Lime, 51 | R.color.tdb_Amber, 52 | R.color.tdb_DeepOrange, 53 | R.color.tdb_Brown, 54 | R.color.tdb_BlueGrey 55 | )) 56 | } 57 | 58 | } -------------------------------------------------------------------------------- /app/src/main/java/email/schaal/ocreader/util/FeedColors.kt: -------------------------------------------------------------------------------- 1 | package email.schaal.ocreader.util 2 | 3 | import androidx.annotation.ColorInt 4 | import androidx.palette.graphics.Palette 5 | import androidx.palette.graphics.Palette.Swatch 6 | import androidx.palette.graphics.Target 7 | 8 | /** 9 | * Generate text and background color for Feeds 10 | */ 11 | class FeedColors(private val palette: Palette?) { 12 | enum class Type { 13 | TEXT, BACKGROUND 14 | } 15 | 16 | internal constructor(@ColorInt color: Int?) : this( 17 | color?.let { Swatch(it, 1) } 18 | ?.let { Palette.Builder(listOf(it)).addTarget(Target.MUTED).generate() }) 19 | 20 | @ColorInt 21 | fun getColor(type: Type, @ColorInt defaultColor: Int): Int { 22 | return palette?.let { 23 | when (type) { 24 | Type.TEXT -> it.dominantSwatch 25 | Type.BACKGROUND -> it.mutedSwatch 26 | } 27 | }?.rgb ?: defaultColor 28 | } 29 | } -------------------------------------------------------------------------------- /app/src/main/java/email/schaal/ocreader/util/LoginError.kt: -------------------------------------------------------------------------------- 1 | package email.schaal.ocreader.util 2 | 3 | import android.content.Context 4 | import com.squareup.moshi.JsonClass 5 | import com.squareup.moshi.Moshi 6 | import email.schaal.ocreader.R 7 | import email.schaal.ocreader.api.API 8 | import retrofit2.HttpException 9 | import java.io.IOException 10 | import java.net.ConnectException 11 | import java.net.UnknownHostException 12 | import javax.net.ssl.SSLHandshakeException 13 | 14 | /** 15 | * Turn login errors into human-readable strings 16 | */ 17 | class LoginError private constructor(val section: Section, val message: String, val throwable: Throwable? = null) { 18 | enum class Section { 19 | URL, USER, PASSWORD, NONE, UNKNOWN 20 | } 21 | 22 | @JsonClass(generateAdapter = true) 23 | internal class ErrorMessage(val message: String) 24 | 25 | constructor(message: String) : this(Section.NONE, message) 26 | private constructor(throwable: Throwable) : this(Section.UNKNOWN, throwable.message ?: "", throwable) 27 | 28 | companion object { 29 | private fun getHttpError(context: Context, code: Int, e: HttpException): LoginError { 30 | return when (code) { 31 | 401 -> LoginError(Section.USER, context.getString(R.string.error_access_forbidden)) 32 | 403, 404 -> LoginError(Section.URL, context.getString(R.string.error_oc_not_found)) 33 | 405 -> LoginError(Section.URL, context.getString(R.string.ncnews_too_old, API.MIN_VERSION.toString())) 34 | else -> { 35 | val message = e.response()?.errorBody()?.source()?.let { source -> 36 | Moshi.Builder().build().adapter(ErrorMessage::class.java).fromJson(source)?.message } 37 | LoginError("${context.getString(R.string.http_error, code)}${message.let { ": $it" }}") 38 | } 39 | } 40 | } 41 | 42 | fun getError(context: Context, t: Throwable): LoginError { 43 | return when (t) { 44 | is HttpException -> getHttpError(context, t.code(), t) 45 | is UnknownHostException -> LoginError(Section.URL, context.getString(R.string.error_unknown_host)) 46 | is SSLHandshakeException -> LoginError(Section.URL, context.getString(R.string.untrusted_certificate)) 47 | is ConnectException -> LoginError(Section.URL, context.getString(R.string.could_not_connect)) 48 | is IOException -> return LoginError(Section.NONE, context.getString(R.string.ncnews_too_old, API.MIN_VERSION.toString())) 49 | else -> LoginError(t) 50 | } 51 | } 52 | } 53 | 54 | } -------------------------------------------------------------------------------- /app/src/main/java/email/schaal/ocreader/util/MyAppGlideModule.kt: -------------------------------------------------------------------------------- 1 | package email.schaal.ocreader.util 2 | 3 | import android.content.Context 4 | import com.bumptech.glide.GlideBuilder 5 | import com.bumptech.glide.annotation.GlideModule 6 | import com.bumptech.glide.module.AppGlideModule 7 | import com.bumptech.glide.request.RequestOptions 8 | import email.schaal.ocreader.R 9 | 10 | /** 11 | * Created by daniel on 7/15/17. 12 | */ 13 | @GlideModule 14 | class MyAppGlideModule : AppGlideModule() { 15 | override fun applyOptions(context: Context, builder: GlideBuilder) { 16 | builder.setDefaultRequestOptions(RequestOptions.placeholderOf(R.drawable.ic_feed_icon)) 17 | } 18 | } -------------------------------------------------------------------------------- /app/src/main/java/email/schaal/ocreader/util/StringUtils.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2015 Daniel Schaal 3 | * 4 | * This file is part of OCReader. 5 | * 6 | * OCReader is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU General Public License as published by 8 | * the Free Software Foundation, either version 3 of the License, or 9 | * (at your option) any later version. 10 | * 11 | * OCReader is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | * GNU General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU General Public License 17 | * along with OCReader. If not, see . 18 | * 19 | */ 20 | @file:JvmName("StringUtils") 21 | 22 | package email.schaal.ocreader.util 23 | 24 | import android.content.Context 25 | import android.graphics.Color 26 | import android.text.format.DateUtils 27 | import androidx.core.text.HtmlCompat 28 | import email.schaal.ocreader.R 29 | import email.schaal.ocreader.database.model.Feed 30 | import okhttp3.HttpUrl 31 | import java.util.* 32 | 33 | fun String.htmlLink(href: String?) = if(href != null) "${this}" else this 34 | 35 | /** 36 | * Utility functions to handle Strings. 37 | */ 38 | fun getByLine(context: Context, template: String, author: String?, feed: Feed?): String { 39 | return if (author != null && feed != null) { 40 | String.format(template, context.getString(R.string.by_author_on_feed, author, feed.name.htmlLink(feed.link))) 41 | } else if(author != null) { 42 | String.format(template, context.getString(R.string.by_author, author)) 43 | } else if (feed != null) { 44 | String.format(template, context.getString(R.string.on_feed, feed.name.htmlLink(feed.link))) 45 | } else { 46 | "" 47 | } 48 | } 49 | 50 | fun Date.getTimeSpanString(endDate: Date = Date()): CharSequence { 51 | return DateUtils.getRelativeTimeSpanString( 52 | this.time, 53 | endDate.time, 54 | DateUtils.MINUTE_IN_MILLIS, 55 | DateUtils.FORMAT_ABBREV_ALL) 56 | } 57 | 58 | fun String.cleanString(): String { 59 | return HtmlCompat.fromHtml(this, HtmlCompat.FROM_HTML_MODE_COMPACT).toString() 60 | } 61 | 62 | fun Int.asCssString(): String { // Use US locale so we always get a . as decimal separator for a valid css value 63 | return String.format(Locale.US, "rgba(%d,%d,%d,%.2f)", 64 | Color.red(this), 65 | Color.green(this), 66 | Color.blue(this), 67 | Color.alpha(this) / 255.0) 68 | } 69 | 70 | fun HttpUrl.buildBaseUrl(apiRoot: String): HttpUrl { 71 | return this 72 | .newBuilder() 73 | .addPathSegments(apiRoot) 74 | .build() 75 | } 76 | -------------------------------------------------------------------------------- /app/src/main/java/email/schaal/ocreader/view/AddNewFeedDialogFragment.kt: -------------------------------------------------------------------------------- 1 | package email.schaal.ocreader.view 2 | 3 | import android.app.AlertDialog 4 | import android.app.Dialog 5 | import android.content.Context 6 | import android.os.Bundle 7 | import androidx.fragment.app.FragmentActivity 8 | import com.google.android.material.bottomsheet.BottomSheetDialogFragment 9 | import email.schaal.ocreader.ManageFeedsActivity 10 | import email.schaal.ocreader.R 11 | import email.schaal.ocreader.database.model.Feed 12 | import email.schaal.ocreader.databinding.FragmentAddNewFeedBinding 13 | 14 | /** 15 | * Display form to add new Feed 16 | */ 17 | class AddNewFeedDialogFragment : BottomSheetDialogFragment() { 18 | private var listener: FeedManageListener? = null 19 | override fun onAttach(context: Context) { 20 | super.onAttach(context) 21 | listener = try { 22 | context as FeedManageListener 23 | } catch (e: ClassCastException) { 24 | throw ClassCastException("$context must implement FeedManageListener") 25 | } 26 | } 27 | 28 | override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { 29 | val activity = requireActivity() as ManageFeedsActivity 30 | 31 | val arguments = requireArguments() 32 | val feedId = arguments.getLong(ARG_FEED_ID, -1) 33 | val newFeed = feedId < 0 34 | 35 | val binding = FragmentAddNewFeedBinding.inflate(activity.layoutInflater).apply { 36 | feedUrl.isEnabled = newFeed 37 | folder.adapter = activity.folderSpinnerAdapter 38 | feedUrl.setText(arguments.getString(ARG_URL)) 39 | folder.setSelection(activity.folderSpinnerAdapter.getPosition( 40 | if (arguments.containsKey(ARG_FOLDER_ID)) arguments.getLong(ARG_FOLDER_ID) else null 41 | )) 42 | } 43 | 44 | return AlertDialog.Builder(activity).setTitle(if (newFeed) R.string.add_new_feed else R.string.edit_feed) 45 | .setPositiveButton(if (newFeed) R.string.add else R.string.save) { _, _ -> 46 | if (newFeed) 47 | listener?.addNewFeed(binding.feedUrl.text.toString(), binding.folder.selectedItemId, arguments.getBoolean(ARG_FINISH_AFTER_CLOSE, false)) 48 | else 49 | listener?.changeFeed(feedId, binding.folder.selectedItemId) 50 | } 51 | .setView(binding.root) 52 | .create() 53 | } 54 | 55 | companion object { 56 | const val ARG_URL = "url" 57 | const val ARG_FOLDER_ID = "folder_id" 58 | const val ARG_FINISH_AFTER_CLOSE = "finish_after_close" 59 | private const val ARG_FEED_ID = "feed_id" 60 | /** 61 | * Show feed edit dialog 62 | * @param activity Activity to get the FragmentManager 63 | * @param feed feed to edit or add (id < 0 means add new feed, id >=0 means edit existing feed) 64 | * @param finishAfterClose should the activity be finished after operation was successful 65 | */ 66 | fun show(activity: FragmentActivity, feed: Feed?, finishAfterClose: Boolean) { 67 | val dialogFragment = AddNewFeedDialogFragment() 68 | 69 | dialogFragment.arguments = Bundle().apply { 70 | putBoolean(ARG_FINISH_AFTER_CLOSE, finishAfterClose) 71 | feed?.let { feed -> 72 | putString(ARG_URL, feed.url) 73 | if (feed.id >= 0) { 74 | putLong(ARG_FEED_ID, feed.id) 75 | feed.folderId?.let { putLong(ARG_FOLDER_ID, it) } 76 | } 77 | } 78 | } 79 | dialogFragment.show(activity.supportFragmentManager, "newfeed") 80 | } 81 | } 82 | } -------------------------------------------------------------------------------- /app/src/main/java/email/schaal/ocreader/view/DividerItemDecoration.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2015 Daniel Schaal 3 | * 4 | * This file is part of OCReader. 5 | * 6 | * OCReader is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU General Public License as published by 8 | * the Free Software Foundation, either version 3 of the License, or 9 | * (at your option) any later version. 10 | * 11 | * OCReader is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | * GNU General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU General Public License 17 | * along with OCReader. If not, see . 18 | * 19 | */ 20 | package email.schaal.ocreader.view 21 | 22 | import android.content.Context 23 | import android.graphics.Canvas 24 | import android.graphics.Rect 25 | import android.graphics.drawable.Drawable 26 | import android.view.View 27 | import androidx.annotation.DimenRes 28 | import androidx.core.content.res.use 29 | import androidx.recyclerview.widget.LinearLayoutManager 30 | import androidx.recyclerview.widget.RecyclerView 31 | import androidx.recyclerview.widget.RecyclerView.ItemDecoration 32 | import email.schaal.ocreader.R 33 | 34 | class DividerItemDecoration(context: Context, @DimenRes insetRes: Int) : ItemDecoration() { 35 | private val mDivider: Drawable? = context.obtainStyledAttributes(intArrayOf(android.R.attr.listDivider)).use { 36 | it.getDrawable(0) 37 | } 38 | private val inset: Int = context.resources.getDimensionPixelSize(insetRes) 39 | private val size = mDivider?.intrinsicHeight ?: 0 40 | private val dividerRect = Rect(0, 0, 0, 0) 41 | 42 | override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, 43 | state: RecyclerView.State) { 44 | super.getItemOffsets(outRect, view, parent, state) 45 | if (mDivider == null || parent.getChildLayoutPosition(view) < 1) { 46 | return 47 | } 48 | outRect.top = size 49 | } 50 | 51 | override fun onDraw(c: Canvas, parent: RecyclerView, state: RecyclerView.State) { 52 | if (mDivider == null) return 53 | 54 | parent.layoutManager?.let { layoutManager -> 55 | val childCount = parent.childCount 56 | val paddingLeft = parent.paddingLeft 57 | val paddingLeftInset = paddingLeft + inset 58 | dividerRect.right = parent.width - parent.paddingRight 59 | loop@ for (i in 0 until childCount) { 60 | val child = parent.getChildAt(i) 61 | when (layoutManager.getItemViewType(child)) { 62 | R.id.viewtype_item -> if (i == childCount - 1 || i == childCount - 2 && layoutManager.getItemViewType( 63 | parent.getChildAt(i + 1) 64 | ) != R.id.viewtype_item 65 | ) dividerRect.left = paddingLeft else dividerRect.left = paddingLeftInset 66 | R.id.viewtype_empty, R.id.viewtype_loadmore -> dividerRect.left = paddingLeft 67 | R.id.viewtype_error -> continue@loop 68 | } 69 | dividerRect.top = 70 | child.bottom + (child.layoutParams as RecyclerView.LayoutParams).bottomMargin 71 | dividerRect.bottom = dividerRect.top + size 72 | mDivider.bounds = dividerRect 73 | mDivider.draw(c) 74 | } 75 | } 76 | } 77 | } -------------------------------------------------------------------------------- /app/src/main/java/email/schaal/ocreader/view/FeedManageListener.kt: -------------------------------------------------------------------------------- 1 | package email.schaal.ocreader.view 2 | 3 | import email.schaal.ocreader.database.model.Feed 4 | 5 | /** 6 | * Callbacks for feed management 7 | */ 8 | interface FeedManageListener { 9 | fun addNewFeed(url: String, folderId: Long, finishAfterAdd: Boolean) 10 | fun deleteFeed(feed: Feed) 11 | fun changeFeed(feedId: Long, folderId: Long) 12 | fun showFeedDialog(feed: Feed) 13 | } -------------------------------------------------------------------------------- /app/src/main/java/email/schaal/ocreader/view/FeedsAdapter.kt: -------------------------------------------------------------------------------- 1 | package email.schaal.ocreader.view 2 | 3 | import android.view.LayoutInflater 4 | import android.view.View 5 | import android.view.ViewGroup 6 | import androidx.recyclerview.widget.RecyclerView 7 | import email.schaal.ocreader.R 8 | import email.schaal.ocreader.database.model.Feed 9 | import email.schaal.ocreader.databinding.ListFeedBinding 10 | import email.schaal.ocreader.util.FaviconLoader 11 | import email.schaal.ocreader.util.FaviconLoader.FeedColorsListener 12 | import email.schaal.ocreader.util.FeedColors 13 | 14 | /** 15 | * RecyclerView Adapter for Feeds 16 | */ 17 | class FeedsAdapter(private val listener: FeedManageListener) : RecyclerView.Adapter() { 18 | var feeds: List = emptyList() 19 | set(value) { 20 | field = value 21 | notifyDataSetChanged() 22 | } 23 | 24 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { 25 | val binding = ListFeedBinding.inflate(LayoutInflater.from(parent.context), parent, false) 26 | return FeedViewHolder(binding, listener) 27 | } 28 | 29 | override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { 30 | if (holder is FeedViewHolder) { 31 | getItem(position)?.let { holder.bindFeed(it) } 32 | } 33 | } 34 | 35 | private fun getItem(position: Int): Feed? { 36 | return feeds[position] 37 | } 38 | 39 | override fun getItemId(index: Int): Long { 40 | return getItem(index)?.id ?: RecyclerView.NO_ID 41 | } 42 | 43 | override fun getItemViewType(position: Int): Int { 44 | return R.id.viewtype_item 45 | } 46 | 47 | /** 48 | * ViewHolder displaying a Feed 49 | */ 50 | private class FeedViewHolder internal constructor(private val binding: ListFeedBinding, private val listener: FeedManageListener) : RecyclerView.ViewHolder(binding.root), FeedColorsListener { 51 | fun bindFeed(feed: Feed) { 52 | binding.textViewTitle.text = feed.name 53 | binding.textViewFolder.text = feed.getFolderTitle(itemView.context) 54 | binding.deleteFeed.setOnClickListener { listener.deleteFeed(feed) } 55 | if (feed.isConsideredFailed()) { 56 | binding.feedFailure.visibility = View.VISIBLE 57 | binding.feedFailure.text = feed.lastUpdateError 58 | } else { 59 | binding.feedFailure.visibility = View.GONE 60 | } 61 | itemView.setOnClickListener { listener.showFeedDialog(feed) } 62 | FaviconLoader.Builder(binding.imageviewFavicon).build().load(itemView.context, feed, this) 63 | } 64 | 65 | override fun onGenerated(feedColors: FeedColors) {} 66 | override fun onStart() {} 67 | 68 | } 69 | 70 | init { 71 | setHasStableIds(true) 72 | } 73 | 74 | override fun getItemCount(): Int { 75 | return feeds.size 76 | } 77 | } -------------------------------------------------------------------------------- /app/src/main/java/email/schaal/ocreader/view/FolderBottomSheetDialogFragment.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2019. Daniel Schaal 3 | * 4 | * This file is part of ocreader. 5 | * 6 | * ocreader is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU General Public License as published by 8 | * the Free Software Foundation, either version 3 of the License, or 9 | * (at your option) any later version. 10 | * 11 | * ocreader is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | * GNU General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU General Public License 17 | * along with Foobar. If not, see . 18 | */ 19 | package email.schaal.ocreader.view 20 | 21 | import android.os.Bundle 22 | import android.view.LayoutInflater 23 | import android.view.View 24 | import android.view.ViewGroup 25 | import androidx.fragment.app.activityViewModels 26 | import androidx.lifecycle.Observer 27 | import androidx.preference.PreferenceManager 28 | import androidx.recyclerview.widget.LinearLayoutManager 29 | import com.google.android.material.bottomsheet.BottomSheetDialogFragment 30 | import email.schaal.ocreader.Preferences 31 | import email.schaal.ocreader.database.FeedViewModel 32 | import email.schaal.ocreader.database.FeedViewModel.FeedViewModelFactory 33 | import email.schaal.ocreader.database.model.AllUnreadFolder 34 | import email.schaal.ocreader.database.model.Folder 35 | import email.schaal.ocreader.database.model.TreeItem 36 | import email.schaal.ocreader.databinding.DialogFoldersBinding 37 | import email.schaal.ocreader.view.TreeItemsAdapter.TreeItemClickListener 38 | 39 | class FolderBottomSheetDialogFragment : BottomSheetDialogFragment() { 40 | private lateinit var binding: DialogFoldersBinding 41 | private val viewModel: FeedViewModel by activityViewModels { FeedViewModelFactory(requireContext()) } 42 | private lateinit var foldersAdapter: TreeItemsAdapter 43 | private var treeItemClickListener: TreeItemClickListener? = null 44 | 45 | override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { 46 | binding = DialogFoldersBinding.inflate(inflater, container, true) 47 | return binding.root 48 | } 49 | 50 | fun setTreeItemClickListener(treeItemClickListener: TreeItemClickListener) { 51 | this.treeItemClickListener = object : TreeItemClickListener { 52 | override fun onTreeItemClick(treeItem: TreeItem) { 53 | treeItemClickListener.onTreeItemClick(treeItem) 54 | dismiss() 55 | } 56 | } 57 | } 58 | 59 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 60 | super.onViewCreated(view, savedInstanceState) 61 | foldersAdapter = TreeItemsAdapter(requireContext(), viewModel.folders.value, treeItemClickListener, viewModel.topFolders) 62 | foldersAdapter.setSelectedTreeItemId(viewModel.selectedTreeItem.value?.treeItemId() ?: AllUnreadFolder.ID) 63 | 64 | viewModel.updateFolders(Preferences.SHOW_ONLY_UNREAD.getBoolean(PreferenceManager.getDefaultSharedPreferences(requireContext()))) 65 | viewModel.folders.observe(this, Observer { 66 | folders: List? -> foldersAdapter.updateTreeItems(folders) 67 | }) 68 | 69 | binding.recyclerViewFolders.adapter = foldersAdapter 70 | binding.recyclerViewFolders.layoutManager = LinearLayoutManager(requireContext()) 71 | } 72 | } -------------------------------------------------------------------------------- /app/src/main/java/email/schaal/ocreader/view/FolderSpinnerAdapter.kt: -------------------------------------------------------------------------------- 1 | package email.schaal.ocreader.view 2 | 3 | import android.content.Context 4 | import android.view.View 5 | import android.view.ViewGroup 6 | import android.widget.BaseAdapter 7 | import android.widget.TextView 8 | import androidx.annotation.LayoutRes 9 | import email.schaal.ocreader.R 10 | import email.schaal.ocreader.database.model.Folder 11 | 12 | /** 13 | * Adapter for Spinner to display Folders 14 | */ 15 | class FolderSpinnerAdapter(context: Context, private var folders: List = emptyList()) : BaseAdapter() { 16 | private val rootFolder: String = context.getString(R.string.root_folder) 17 | 18 | override fun getCount(): Int { 19 | return folders.size + 1 20 | } 21 | 22 | fun updateFolders(folders: List) { 23 | this.folders = folders 24 | notifyDataSetChanged() 25 | } 26 | 27 | override fun getItem(position: Int): Any { 28 | return if (position == 0) rootFolder else folders[position - 1].name 29 | } 30 | 31 | override fun getItemId(position: Int): Long { 32 | return if (position == 0) 0 else (folders[position - 1].id ?: 0) 33 | } 34 | 35 | override fun getItemViewType(position: Int): Int { 36 | return if (position == 0) VIEW_TYPE_NONE else VIEW_TYPE_FOLDER 37 | } 38 | 39 | override fun getDropDownView(position: Int, convertView: View?, parent: ViewGroup): View { 40 | return getSpinnerView(position, convertView, parent, R.layout.spinner_folder_dropdown) 41 | } 42 | 43 | override fun getView(position: Int, convertView: View?, parent: ViewGroup): View { 44 | return getSpinnerView(position, convertView, parent, R.layout.spinner_folder) 45 | } 46 | 47 | private fun getSpinnerView(position: Int, convertView: View?, parent: ViewGroup, @LayoutRes layout: Int): View { 48 | val view = convertView ?: View.inflate(parent.context, layout, null) 49 | (view as TextView).text = getItem(position) as String 50 | return view 51 | } 52 | 53 | override fun hasStableIds(): Boolean { 54 | return true 55 | } 56 | 57 | fun getPosition(folderId: Long?): Int { 58 | if (folderId == 0L || folderId == null) return 0 59 | var i = 1 60 | while (i - 1 < folders.size) { 61 | if (folders[i - 1].id == folderId) { 62 | return i 63 | } 64 | i++ 65 | } 66 | return -1 67 | } 68 | 69 | companion object { 70 | private const val VIEW_TYPE_NONE = 0 71 | private const val VIEW_TYPE_FOLDER = 1 72 | } 73 | 74 | } -------------------------------------------------------------------------------- /app/src/main/java/email/schaal/ocreader/view/LoginViewModel.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2020. Daniel Schaal 3 | * 4 | * This file is part of ocreader. 5 | * 6 | * ocreader is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU General Public License as published by 8 | * the Free Software Foundation, either version 3 of the License, or 9 | * (at your option) any later version. 10 | * 11 | * ocreader is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | * GNU General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU General Public License 17 | * along with Foobar. If not, see . 18 | */ 19 | 20 | package email.schaal.ocreader.view 21 | 22 | import androidx.lifecycle.MutableLiveData 23 | import androidx.lifecycle.ViewModel 24 | 25 | class LoginViewModel : ViewModel() { 26 | val credentialsLiveData: MutableLiveData?> by lazy { MutableLiveData?>() } 27 | } -------------------------------------------------------------------------------- /app/src/main/java/email/schaal/ocreader/view/ProgressFloatingActionButton.kt: -------------------------------------------------------------------------------- 1 | package email.schaal.ocreader.view 2 | 3 | import android.content.Context 4 | import android.content.res.ColorStateList 5 | import android.graphics.Canvas 6 | import android.graphics.Paint 7 | import android.util.AttributeSet 8 | import androidx.annotation.Keep 9 | import com.google.android.material.floatingactionbutton.FloatingActionButton 10 | import email.schaal.ocreader.R 11 | 12 | /** 13 | * FloatingActionButton with ability to show progress 14 | */ 15 | class ProgressFloatingActionButton(context: Context, attrs: AttributeSet?) : FloatingActionButton(context, attrs) { 16 | private val circlePaint = Paint() 17 | 18 | var progress = 0f 19 | @Keep 20 | set(value) { 21 | field = value 22 | invalidate() 23 | } 24 | 25 | private val diameter: Float = resources.getDimensionPixelSize(R.dimen.fab_size_normal).toFloat() 26 | 27 | @Keep 28 | fun setFabBackgroundColor(color: Int) { 29 | backgroundTintList = ColorStateList.valueOf(color) 30 | } 31 | 32 | override fun onDraw(canvas: Canvas) { 33 | val radius = diameter / 2 34 | val count = canvas.save() 35 | // draw progress circle fraction 36 | canvas.clipRect(0f, diameter * (1 - progress), diameter, diameter) 37 | canvas.drawCircle(radius, radius, radius, circlePaint) 38 | canvas.restoreToCount(count) 39 | super.onDraw(canvas) 40 | } 41 | 42 | init { 43 | context.obtainStyledAttributes(attrs, R.styleable.ProgressFloatingActionButton).also { 44 | circlePaint.color = it.getColor(R.styleable.ProgressFloatingActionButton_progressColor, 0) 45 | }.recycle() 46 | } 47 | } -------------------------------------------------------------------------------- /app/src/main/java/email/schaal/ocreader/view/ScrollAwareFABBehavior.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2015 Daniel Schaal 3 | * 4 | * This file is part of OCReader. 5 | * 6 | * OCReader is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU General Public License as published by 8 | * the Free Software Foundation, either version 3 of the License, or 9 | * (at your option) any later version. 10 | * 11 | * OCReader is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | * GNU General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU General Public License 17 | * along with OCReader. If not, see . 18 | * 19 | */ 20 | package email.schaal.ocreader.view 21 | 22 | import android.content.Context 23 | import android.util.AttributeSet 24 | import android.view.View 25 | import androidx.annotation.Keep 26 | import androidx.coordinatorlayout.widget.CoordinatorLayout 27 | import androidx.core.view.ViewCompat 28 | import com.google.android.material.floatingactionbutton.FloatingActionButton 29 | import com.google.android.material.floatingactionbutton.FloatingActionButton.OnVisibilityChangedListener 30 | 31 | @Keep 32 | class ScrollAwareFABBehavior() : FloatingActionButton.Behavior() { 33 | constructor(context: Context, attributeSet: AttributeSet) : this() 34 | 35 | override fun onStartNestedScroll(coordinatorLayout: CoordinatorLayout, child: FloatingActionButton, directTargetChild: View, target: View, axes: Int, type: Int): Boolean { 36 | return axes == ViewCompat.SCROLL_AXIS_VERTICAL || 37 | super.onStartNestedScroll(coordinatorLayout, child, directTargetChild, target, axes, type) 38 | } 39 | 40 | override fun onNestedScroll(coordinatorLayout: CoordinatorLayout, child: FloatingActionButton, target: View, dxConsumed: Int, dyConsumed: Int, dxUnconsumed: Int, dyUnconsumed: Int, type: Int, consumed: IntArray) { 41 | super.onNestedScroll(coordinatorLayout, child, target, dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, type, consumed) 42 | if (dyConsumed > 0 && child.visibility == View.VISIBLE) { // User scrolled down and the FAB is currently visible -> hide the FAB 43 | child.hide(object : OnVisibilityChangedListener() { 44 | override fun onHidden(fab: FloatingActionButton) { // Workaround bug in android support library 45 | // see http://b.android.com/230298 46 | fab.visibility = View.INVISIBLE 47 | } 48 | }) 49 | } else if (dyConsumed < 0 && child.visibility != View.VISIBLE) { // User scrolled up and the FAB is currently not visible -> show the FAB 50 | child.show() 51 | } 52 | } 53 | } -------------------------------------------------------------------------------- /app/src/main/res/drawable/account_circle.xml: -------------------------------------------------------------------------------- 1 | 19 | 20 | 25 | 28 | 29 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/favicon_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/fresh.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 15 | 17 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_add.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_app_shortcut_feed.xml: -------------------------------------------------------------------------------- 1 | 3 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_app_shortcut_fresh.xml: -------------------------------------------------------------------------------- 1 | 3 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_app_shortcut_star.xml: -------------------------------------------------------------------------------- 1 | 3 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_check_box.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_check_box_outline_blank.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_delete.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_done_above.xml: -------------------------------------------------------------------------------- 1 | 20 | 21 | 26 | 29 | 30 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_done_all.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_feed_icon.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 12 | 13 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_folder.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_info.xml: -------------------------------------------------------------------------------- 1 | 19 | 20 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 8 | 12 | 13 | 19 | 22 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 6 | 8 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_open_in_browser.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_play_arrow.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_play_circle_outline.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_refresh.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_settings.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_share.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_star.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_star_outline.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_warning.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/selectable_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /app/src/main/res/font/crimson.xml: -------------------------------------------------------------------------------- 1 | 19 | 20 | 22 | 29 | -------------------------------------------------------------------------------- /app/src/main/res/font/crimsontextregular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/schaal/ocreader/c0311a0ccbac01a930772a5d7d593b2367ae9ddc/app/src/main/res/font/crimsontextregular.ttf -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_item_pager.xml: -------------------------------------------------------------------------------- 1 | 2 | 21 | 22 | 25 | 26 | 32 | 33 | 35 | 36 | 41 | 42 | 51 | 52 | 63 | 64 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_list.xml: -------------------------------------------------------------------------------- 1 | 2 | 21 | 22 | 25 | 26 | 31 | 32 | 34 | 35 | 40 | 41 | 45 | 46 | 47 | 48 | 53 | 54 | 55 | 56 | 57 | 58 | 66 | 67 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_manage_feeds.xml: -------------------------------------------------------------------------------- 1 | 20 | 21 | 24 | 25 | 31 | 32 | 34 | 35 | 41 | 42 | 53 | 54 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 12 | 13 | 16 | 17 | 22 | 23 | -------------------------------------------------------------------------------- /app/src/main/res/layout/bottom_navigationview.xml: -------------------------------------------------------------------------------- 1 | 19 | 20 | 22 | 23 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /app/src/main/res/layout/dialog_folders.xml: -------------------------------------------------------------------------------- 1 | 2 | 20 | 21 | 24 | 28 | 29 | -------------------------------------------------------------------------------- /app/src/main/res/layout/fontpreference.xml: -------------------------------------------------------------------------------- 1 | 2 | 20 | 21 | 25 | 26 | 31 | 32 | 38 | -------------------------------------------------------------------------------- /app/src/main/res/layout/fragment_add_new_feed.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 10 | 13 | 14 | 21 | 22 | 23 | 24 | 32 | 33 | 37 | 38 | -------------------------------------------------------------------------------- /app/src/main/res/layout/fragment_item_pager.xml: -------------------------------------------------------------------------------- 1 | 20 | 21 | 23 | 24 | 31 | 32 | -------------------------------------------------------------------------------- /app/src/main/res/layout/fragment_login_flow_web_view.xml: -------------------------------------------------------------------------------- 1 | 19 | 20 | 21 | 22 | 26 | -------------------------------------------------------------------------------- /app/src/main/res/layout/list_divider.xml: -------------------------------------------------------------------------------- 1 | 2 | 20 | 21 | 23 | 29 | -------------------------------------------------------------------------------- /app/src/main/res/layout/list_empty.xml: -------------------------------------------------------------------------------- 1 | 2 | 21 | 22 | 28 | -------------------------------------------------------------------------------- /app/src/main/res/layout/list_error.xml: -------------------------------------------------------------------------------- 1 | 20 | 21 | 29 | 30 | 34 | 35 | 45 | 46 | 55 | 56 | 66 | 67 | 68 | 69 | -------------------------------------------------------------------------------- /app/src/main/res/layout/list_folder.xml: -------------------------------------------------------------------------------- 1 | 19 | 20 | 23 | 24 | 31 | 32 | 41 | 42 | 54 | 55 | -------------------------------------------------------------------------------- /app/src/main/res/layout/list_loadmore.xml: -------------------------------------------------------------------------------- 1 | 20 | 21 | 22 | 23 | 27 | 28 |