From ea9739a19a3799e2c811c57406d13ad396ecae33 Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Mon, 3 Aug 2020 23:23:31 +0200 Subject: [PATCH 001/187] Delete :api sample tests --- .../readrops/api/ExampleInstrumentedTest.java | 26 ------------------- .../com/readrops/api/ExampleUnitTest.java | 17 ------------ 2 files changed, 43 deletions(-) delete mode 100644 api/src/androidTest/java/com/readrops/api/ExampleInstrumentedTest.java delete mode 100644 api/src/test/java/com/readrops/api/ExampleUnitTest.java diff --git a/api/src/androidTest/java/com/readrops/api/ExampleInstrumentedTest.java b/api/src/androidTest/java/com/readrops/api/ExampleInstrumentedTest.java deleted file mode 100644 index 3c0ef7cf..00000000 --- a/api/src/androidTest/java/com/readrops/api/ExampleInstrumentedTest.java +++ /dev/null @@ -1,26 +0,0 @@ -package com.readrops.api; - -import android.content.Context; -import androidx.test.InstrumentationRegistry; -import androidx.test.runner.AndroidJUnit4; - -import org.junit.Test; -import org.junit.runner.RunWith; - -import static org.junit.Assert.*; - -/** - * Instrumented test, which will execute on an Android device. - * - * @see Testing documentation - */ -@RunWith(AndroidJUnit4.class) -public class ExampleInstrumentedTest { - @Test - public void useAppContext() { - // Context of the app under test. - Context appContext = InstrumentationRegistry.getTargetContext(); - - assertEquals("com.readrops.api.test", appContext.getPackageName()); - } -} diff --git a/api/src/test/java/com/readrops/api/ExampleUnitTest.java b/api/src/test/java/com/readrops/api/ExampleUnitTest.java deleted file mode 100644 index 79e5f257..00000000 --- a/api/src/test/java/com/readrops/api/ExampleUnitTest.java +++ /dev/null @@ -1,17 +0,0 @@ -package com.readrops.api; - -import org.junit.Test; - -import static org.junit.Assert.*; - -/** - * Example local unit test, which will execute on the development machine (host). - * - * @see Testing documentation - */ -public class ExampleUnitTest { - @Test - public void addition_isCorrect() { - assertEquals(4, 2 + 2); - } -} \ No newline at end of file From f6cfa52bc07600c38ea4682dc747c81dbc7cffa7 Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Mon, 3 Aug 2020 23:41:37 +0200 Subject: [PATCH 002/187] OPML head tag isn't required --- api/src/main/java/com/readrops/api/opml/model/OPML.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/src/main/java/com/readrops/api/opml/model/OPML.kt b/api/src/main/java/com/readrops/api/opml/model/OPML.kt index cc6de1f9..6cdc7087 100644 --- a/api/src/main/java/com/readrops/api/opml/model/OPML.kt +++ b/api/src/main/java/com/readrops/api/opml/model/OPML.kt @@ -8,7 +8,7 @@ import org.simpleframework.xml.Root @Order(elements = ["head", "body"]) @Root(name = "opml", strict = false) data class OPML(@field:Attribute(required = true) var version: String?, - @field:Element(required = true) var head: Head?, + @field:Element(required = false) var head: Head?, @field:Element(required = true) var body: Body?) { /** From b3dd60e1fbfaa5bd46c3f0931dd6946626d103e0 Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Mon, 3 Aug 2020 23:43:55 +0200 Subject: [PATCH 003/187] Add support for OPML sub outlines --- .../java/com/readrops/api/opml/OPMLParser.kt | 86 +++++++++++++++---- .../java/com/readrops/api/utils/LibUtils.java | 2 +- .../java/com/readrops/db/entities/Folder.java | 22 +++++ 3 files changed, 93 insertions(+), 17 deletions(-) diff --git a/api/src/main/java/com/readrops/api/opml/OPMLParser.kt b/api/src/main/java/com/readrops/api/opml/OPMLParser.kt index 92d7b883..5d744c00 100644 --- a/api/src/main/java/com/readrops/api/opml/OPMLParser.kt +++ b/api/src/main/java/com/readrops/api/opml/OPMLParser.kt @@ -11,16 +11,26 @@ import com.readrops.db.entities.Feed import com.readrops.db.entities.Folder import io.reactivex.Completable import io.reactivex.Single +import io.reactivex.SingleOnSubscribe import org.simpleframework.xml.Serializer import org.simpleframework.xml.core.Persister +import java.io.InputStream import java.io.OutputStream object OPMLParser { @JvmStatic - fun read(uri: Uri, context: Context): Single>> { + fun read(uri: Uri, context: Context): Single>> { + return Single.create(SingleOnSubscribe { + val stream = context.contentResolver.openInputStream(uri) + it.onSuccess(stream!!) + }).flatMap { stream -> read(stream) } + } + + @JvmStatic + fun read(stream: InputStream): Single>> { return Single.create { emitter -> - val fileString = LibUtils.fileToString(uri, context) + val fileString = LibUtils.inputStreamToString(stream) val serializer: Serializer = Persister() val opml: OPML = serializer.read(OPML::class.java, fileString) @@ -39,25 +49,52 @@ object OPMLParser { } } - private fun opmltoFoldersAndFeeds(opml: OPML): Map> { - val foldersAndFeeds: MutableMap> = HashMap() + private fun opmltoFoldersAndFeeds(opml: OPML): Map> { + val foldersAndFeeds: MutableMap> = HashMap() val body = opml.body!! body.outlines?.forEach { outline -> - val folder = Folder(outline.title) + val outlineParsing = parseOutline(outline) + associateOrphanFeedsToFolder(foldersAndFeeds, outlineParsing, null) - val feeds = arrayListOf() - outline.outlines?.forEach { feedOutline -> - val feed = Feed().apply { - name = feedOutline.title - url = feedOutline.xmlUrl - siteUrl = feedOutline.htmlUrl - } + foldersAndFeeds.putAll(outlineParsing) + } - feeds.add(feed) + return foldersAndFeeds + } + + /** + * Parse outline and its children recursively + * @param outline node to parse + */ + private fun parseOutline(outline: Outline): MutableMap> { + val foldersAndFeeds: MutableMap> = HashMap() + + // The outline is a folder/category + if ((outline.outlines != null && !outline.outlines?.isEmpty()!!) || outline.xmlUrl.isNullOrEmpty()) { + val folder = Folder(outline.text) + + outline.outlines?.forEach { + val recursiveFeedsFolders = parseOutline(it) + + // Treat feeds without folder, so belonging to the current folder + associateOrphanFeedsToFolder(foldersAndFeeds, recursiveFeedsFolders, folder) + foldersAndFeeds.putAll(recursiveFeedsFolders.toMap()) } - foldersAndFeeds[folder] = feeds + // empty outline + if (!foldersAndFeeds.containsKey(folder)) foldersAndFeeds[folder] = listOf() + + } else { // the outline is a feed + if (!outline.xmlUrl.isNullOrEmpty()) { + val feed = Feed().apply { + name = outline.title + url = outline.xmlUrl + siteUrl = outline.htmlUrl + } + // parsed feed is linked to null to be assigned to the previous level folder + foldersAndFeeds[null] = listOf(feed) + } } return foldersAndFeeds @@ -72,11 +109,11 @@ object OPMLParser { folderAndFeeds.value.forEach { feed -> val feedOutline = Outline(feed.name, feed.url, feed.siteUrl) - feedOutlines.add(feedOutline) + feedOutlines += feedOutline } outline.outlines = feedOutlines - outlines.add(outline) + outlines += outline } val head = Head("Subscriptions") @@ -85,4 +122,21 @@ object OPMLParser { return OPML("2.0", head, body) } + /** + * Associate parsed feeds without folder to the previous level folder. + * @param foldersAndFeeds final result + * @param parsingResult current level parsing + * @param folder the folder feeds will be associated to + * + */ + private fun associateOrphanFeedsToFolder(foldersAndFeeds: MutableMap>, + parsingResult: MutableMap>, folder: Folder?) { + val feeds = parsingResult[null] + if (feeds != null && feeds.isNotEmpty()) { + if (foldersAndFeeds[folder] == null) foldersAndFeeds[folder] = feeds + else foldersAndFeeds[folder] = foldersAndFeeds[folder]?.plus(feeds)!! + + parsingResult.remove(null) + } + } } \ No newline at end of file diff --git a/api/src/main/java/com/readrops/api/utils/LibUtils.java b/api/src/main/java/com/readrops/api/utils/LibUtils.java index d838e68e..bbc4c426 100644 --- a/api/src/main/java/com/readrops/api/utils/LibUtils.java +++ b/api/src/main/java/com/readrops/api/utils/LibUtils.java @@ -35,7 +35,7 @@ public final class LibUtils { } public static String fileToString(Uri uri, Context context) throws FileNotFoundException { - InputStream inputStream = context.getContentResolver().openInputStream(uri); + InputStream inputStream = context.getContentResolver().openInputStream(uri); return inputStreamToString(inputStream); } diff --git a/db/src/main/java/com/readrops/db/entities/Folder.java b/db/src/main/java/com/readrops/db/entities/Folder.java index 0889b6f7..f03c081e 100644 --- a/db/src/main/java/com/readrops/db/entities/Folder.java +++ b/db/src/main/java/com/readrops/db/entities/Folder.java @@ -11,6 +11,8 @@ import androidx.room.PrimaryKey; import com.readrops.db.entities.account.Account; +import java.util.Objects; + @Entity(foreignKeys = @ForeignKey(entity = Account.class, parentColumns = "id", childColumns = "account_id", onDelete = ForeignKey.CASCADE)) public class Folder implements Parcelable, Comparable { @@ -94,6 +96,26 @@ public class Folder implements Parcelable, Comparable { dest.writeString(name); } + @Override + public boolean equals(Object o) { + if (o == null) { + return false; + } else if (!(o instanceof Folder)) { + return false; + } else { + Folder folder = (Folder) o; + + return id == folder.id && Objects.equals(name, folder.name) && + Objects.equals(remoteId, folder.remoteId) && + accountId == folder.accountId; + } + } + + @Override + public int hashCode() { + return Objects.hash(id, name, remoteId, accountId); + } + @Override public int compareTo(Folder o) { return this.getName().compareTo(o.getName()); From e92367bbc1447cbae7c7abe42fd1c11db4b2f52a Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Mon, 3 Aug 2020 23:45:21 +0200 Subject: [PATCH 004/187] Add test for OPML read case --- api/build.gradle | 5 +++ api/src/androidTest/assets/subscriptions.opml | 27 ++++++++++++ .../java/com/readrops/api/OPMLParserTest.kt | 44 +++++++++++++++++++ 3 files changed, 76 insertions(+) create mode 100755 api/src/androidTest/assets/subscriptions.opml create mode 100644 api/src/androidTest/java/com/readrops/api/OPMLParserTest.kt diff --git a/api/build.gradle b/api/build.gradle index a254b5f5..87e5f49a 100644 --- a/api/build.gradle +++ b/api/build.gradle @@ -18,6 +18,10 @@ android { abortOnError false } + sourceSets { + androidTest.assets.srcDirs += files("$projectDir/androidTest/assets".toString()) + } + buildTypes { release { minifyEnabled true @@ -48,6 +52,7 @@ dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" testImplementation 'junit:junit:4.12' + androidTestImplementation 'androidx.test.ext:junit:1.1.1' androidTestImplementation 'androidx.test:runner:1.2.0' androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' diff --git a/api/src/androidTest/assets/subscriptions.opml b/api/src/androidTest/assets/subscriptions.opml new file mode 100755 index 00000000..f33900ca --- /dev/null +++ b/api/src/androidTest/assets/subscriptions.opml @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/api/src/androidTest/java/com/readrops/api/OPMLParserTest.kt b/api/src/androidTest/java/com/readrops/api/OPMLParserTest.kt new file mode 100644 index 00000000..27b5a232 --- /dev/null +++ b/api/src/androidTest/java/com/readrops/api/OPMLParserTest.kt @@ -0,0 +1,44 @@ +package com.readrops.api + +import android.content.Context +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry +import com.readrops.api.opml.OPMLParser +import com.readrops.db.entities.Feed +import com.readrops.db.entities.Folder +import io.reactivex.schedulers.Schedulers +import junit.framework.TestCase.assertEquals +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class OPMLParserTest { + + private val context: Context = InstrumentationRegistry.getInstrumentation().context + + @Test + fun readOpmlTest() { + val stream = context.resources.assets.open("subscriptions.opml") + + var foldersAndFeeds: Map>? = null + + OPMLParser.read(stream) + .observeOn(Schedulers.trampoline()) + .subscribeOn(Schedulers.trampoline()) + .subscribe { result -> foldersAndFeeds = result } + + assertEquals(foldersAndFeeds?.size, 6) + + assertEquals(foldersAndFeeds?.get(Folder("Folder 1"))?.size, 2) + assertEquals(foldersAndFeeds?.get(Folder("Subfolder 1"))?.size, 4) + assertEquals(foldersAndFeeds?.get(Folder("Subfolder 2"))?.size, 1) + assertEquals(foldersAndFeeds?.get(Folder("Sub subfolder 1"))?.size, 2) + assertEquals(foldersAndFeeds?.get(Folder("Sub subfolder 2"))?.size, 0) + assertEquals(foldersAndFeeds?.get(null)?.size, 2) + } + + @Test + fun writeOpmlTest() { + + } +} \ No newline at end of file From 7976140834d1e2289e46c1e1e3dfb8f4d68e5814 Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Tue, 4 Aug 2020 14:08:21 +0200 Subject: [PATCH 005/187] Test opml version before parsing --- api/src/androidTest/assets/wrong_version.opml | 6 ++++++ .../java/com/readrops/api/OPMLParserTest.kt | 10 ++++++++++ .../java/com/readrops/api/opml/OPMLParser.kt | 20 +++++++++++++++---- .../readrops/api/utils/ParseException.java | 7 +++++++ 4 files changed, 39 insertions(+), 4 deletions(-) create mode 100644 api/src/androidTest/assets/wrong_version.opml diff --git a/api/src/androidTest/assets/wrong_version.opml b/api/src/androidTest/assets/wrong_version.opml new file mode 100644 index 00000000..da6af325 --- /dev/null +++ b/api/src/androidTest/assets/wrong_version.opml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/api/src/androidTest/java/com/readrops/api/OPMLParserTest.kt b/api/src/androidTest/java/com/readrops/api/OPMLParserTest.kt index 27b5a232..03cac14e 100644 --- a/api/src/androidTest/java/com/readrops/api/OPMLParserTest.kt +++ b/api/src/androidTest/java/com/readrops/api/OPMLParserTest.kt @@ -4,6 +4,7 @@ import android.content.Context import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.platform.app.InstrumentationRegistry import com.readrops.api.opml.OPMLParser +import com.readrops.api.utils.ParseException import com.readrops.db.entities.Feed import com.readrops.db.entities.Folder import io.reactivex.schedulers.Schedulers @@ -37,6 +38,15 @@ class OPMLParserTest { assertEquals(foldersAndFeeds?.get(null)?.size, 2) } + @Test + fun opmlVersionTest() { + val stream = context.resources.assets.open("wrong_version.opml") + + OPMLParser.read(stream) + .test() + .assertError(ParseException::class.java) + } + @Test fun writeOpmlTest() { diff --git a/api/src/main/java/com/readrops/api/opml/OPMLParser.kt b/api/src/main/java/com/readrops/api/opml/OPMLParser.kt index 5d744c00..1c7b3f2f 100644 --- a/api/src/main/java/com/readrops/api/opml/OPMLParser.kt +++ b/api/src/main/java/com/readrops/api/opml/OPMLParser.kt @@ -2,11 +2,13 @@ package com.readrops.api.opml import android.content.Context import android.net.Uri +import android.util.Log import com.readrops.api.opml.model.Body import com.readrops.api.opml.model.Head import com.readrops.api.opml.model.OPML import com.readrops.api.opml.model.Outline import com.readrops.api.utils.LibUtils +import com.readrops.api.utils.ParseException import com.readrops.db.entities.Feed import com.readrops.db.entities.Folder import io.reactivex.Completable @@ -19,6 +21,8 @@ import java.io.OutputStream object OPMLParser { + val TAG = OPMLParser.javaClass.simpleName + @JvmStatic fun read(uri: Uri, context: Context): Single>> { return Single.create(SingleOnSubscribe { @@ -30,12 +34,17 @@ object OPMLParser { @JvmStatic fun read(stream: InputStream): Single>> { return Single.create { emitter -> - val fileString = LibUtils.inputStreamToString(stream) - val serializer: Serializer = Persister() + try { + val fileString = LibUtils.inputStreamToString(stream) + val serializer: Serializer = Persister() - val opml: OPML = serializer.read(OPML::class.java, fileString) + val opml: OPML = serializer.read(OPML::class.java, fileString) - emitter.onSuccess(opmltoFoldersAndFeeds(opml)) + emitter.onSuccess(opmltoFoldersAndFeeds(opml)) + } catch (e: Exception) { + Log.d(TAG, e.message, e) + emitter.onError(e) + } } } @@ -50,6 +59,9 @@ object OPMLParser { } private fun opmltoFoldersAndFeeds(opml: OPML): Map> { + if (opml.version != "2.0") + throw ParseException("Only 2.0 OPML specification is supported") + val foldersAndFeeds: MutableMap> = HashMap() val body = opml.body!! diff --git a/api/src/main/java/com/readrops/api/utils/ParseException.java b/api/src/main/java/com/readrops/api/utils/ParseException.java index 12db280c..46065da3 100644 --- a/api/src/main/java/com/readrops/api/utils/ParseException.java +++ b/api/src/main/java/com/readrops/api/utils/ParseException.java @@ -2,4 +2,11 @@ package com.readrops.api.utils; public class ParseException extends Exception { + public ParseException() { + super(); + } + + public ParseException(String message) { + super(message); + } } From 5fe10a848c7e6e3ebee2e0950a42865dfac59604 Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Tue, 4 Aug 2020 15:23:57 +0200 Subject: [PATCH 006/187] Close streams at end of tests --- api/src/androidTest/java/com/readrops/api/OPMLParserTest.kt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/api/src/androidTest/java/com/readrops/api/OPMLParserTest.kt b/api/src/androidTest/java/com/readrops/api/OPMLParserTest.kt index 03cac14e..0c40871e 100644 --- a/api/src/androidTest/java/com/readrops/api/OPMLParserTest.kt +++ b/api/src/androidTest/java/com/readrops/api/OPMLParserTest.kt @@ -36,6 +36,8 @@ class OPMLParserTest { assertEquals(foldersAndFeeds?.get(Folder("Sub subfolder 1"))?.size, 2) assertEquals(foldersAndFeeds?.get(Folder("Sub subfolder 2"))?.size, 0) assertEquals(foldersAndFeeds?.get(null)?.size, 2) + + stream.close() } @Test @@ -45,6 +47,8 @@ class OPMLParserTest { OPMLParser.read(stream) .test() .assertError(ParseException::class.java) + + stream.close() } @Test From ab86bcbcc29ff41c46d219c4a3f9b7682ec7b154 Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Sat, 8 Aug 2020 22:40:23 +0200 Subject: [PATCH 007/187] Add write opml test --- api/build.gradle | 1 + api/src/androidTest/assets/subscriptions.opml | 2 +- .../java/com/readrops/api/OPMLParserTest.kt | 42 +++++++++++++++++++ api/src/debug/AndroidManifest.xml | 9 ++++ .../java/com/readrops/api/opml/OPMLParser.kt | 37 +++++++++------- .../com/readrops/api/opml/model/Outline.kt | 11 +++-- 6 files changed, 83 insertions(+), 19 deletions(-) create mode 100644 api/src/debug/AndroidManifest.xml diff --git a/api/build.gradle b/api/build.gradle index 87e5f49a..05f25476 100644 --- a/api/build.gradle +++ b/api/build.gradle @@ -54,6 +54,7 @@ dependencies { testImplementation 'junit:junit:4.12' androidTestImplementation 'androidx.test.ext:junit:1.1.1' androidTestImplementation 'androidx.test:runner:1.2.0' + androidTestImplementation 'androidx.test:rules:1.2.0' androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' implementation 'com.squareup.retrofit2:retrofit:2.7.1' diff --git a/api/src/androidTest/assets/subscriptions.opml b/api/src/androidTest/assets/subscriptions.opml index f33900ca..c2998c87 100755 --- a/api/src/androidTest/assets/subscriptions.opml +++ b/api/src/androidTest/assets/subscriptions.opml @@ -1,7 +1,7 @@ - + diff --git a/api/src/androidTest/java/com/readrops/api/OPMLParserTest.kt b/api/src/androidTest/java/com/readrops/api/OPMLParserTest.kt index 0c40871e..5edc4ed3 100644 --- a/api/src/androidTest/java/com/readrops/api/OPMLParserTest.kt +++ b/api/src/androidTest/java/com/readrops/api/OPMLParserTest.kt @@ -1,22 +1,36 @@ package com.readrops.api +import android.Manifest import android.content.Context +import android.os.Environment +import android.util.Log import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.rule.GrantPermissionRule import com.readrops.api.opml.OPMLParser import com.readrops.api.utils.ParseException import com.readrops.db.entities.Feed import com.readrops.db.entities.Folder +import io.reactivex.CompletableObserver +import io.reactivex.disposables.Disposable import io.reactivex.schedulers.Schedulers import junit.framework.TestCase.assertEquals +import junit.framework.TestCase.assertTrue +import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith +import java.io.File +import java.io.FileOutputStream +import java.io.OutputStream @RunWith(AndroidJUnit4::class) class OPMLParserTest { private val context: Context = InstrumentationRegistry.getInstrumentation().context + @get:Rule + val permissionRule: GrantPermissionRule = GrantPermissionRule.grant(Manifest.permission.WRITE_EXTERNAL_STORAGE) + @Test fun readOpmlTest() { val stream = context.resources.assets.open("subscriptions.opml") @@ -53,6 +67,34 @@ class OPMLParserTest { @Test fun writeOpmlTest() { + val filePath = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).absolutePath + val file = File(filePath, "subscriptions.opml") + val outputStream: OutputStream = FileOutputStream(file) + val foldersAndFeeds: Map> = HashMap>().apply { + put(null, listOf(Feed("Feed1", "", "https://feed1.com"), + Feed("Feed2", "", "https://feed2.com"))) + put(Folder("Folder1"), listOf()) + put(Folder("Folder2"), listOf(Feed("Feed3", "", "https://feed3.com"), + Feed("Feed4", "", "https://feed4.com"))) + } + + OPMLParser.write(foldersAndFeeds, outputStream) + .subscribeOn(Schedulers.trampoline()) + .subscribe() + + outputStream.flush() + outputStream.close() + + val inputStream = file.inputStream() + var foldersAndFeeds2: Map>? = null + OPMLParser.read(inputStream).subscribe { result -> foldersAndFeeds2 = result } + + assertEquals(foldersAndFeeds.size, foldersAndFeeds2?.size) + assertEquals(foldersAndFeeds[Folder("Folder1")]?.size, foldersAndFeeds2?.get(Folder("Folder1"))?.size) + assertEquals(foldersAndFeeds[Folder("Folder2")]?.size, foldersAndFeeds2?.get(Folder("Folder2"))?.size) + assertEquals(foldersAndFeeds[null]?.size, foldersAndFeeds2?.get(null)?.size) + + inputStream.close() } } \ No newline at end of file diff --git a/api/src/debug/AndroidManifest.xml b/api/src/debug/AndroidManifest.xml new file mode 100644 index 00000000..7c249c25 --- /dev/null +++ b/api/src/debug/AndroidManifest.xml @@ -0,0 +1,9 @@ + + + + + + + + \ No newline at end of file diff --git a/api/src/main/java/com/readrops/api/opml/OPMLParser.kt b/api/src/main/java/com/readrops/api/opml/OPMLParser.kt index 1c7b3f2f..094751d8 100644 --- a/api/src/main/java/com/readrops/api/opml/OPMLParser.kt +++ b/api/src/main/java/com/readrops/api/opml/OPMLParser.kt @@ -40,7 +40,7 @@ object OPMLParser { val opml: OPML = serializer.read(OPML::class.java, fileString) - emitter.onSuccess(opmltoFoldersAndFeeds(opml)) + emitter.onSuccess(opmlToFoldersAndFeeds(opml)) } catch (e: Exception) { Log.d(TAG, e.message, e) emitter.onError(e) @@ -49,7 +49,7 @@ object OPMLParser { } @JvmStatic - fun write(foldersAndFeeds: Map>, outputStream: OutputStream): Completable { + fun write(foldersAndFeeds: Map>, outputStream: OutputStream): Completable { return Completable.create { emitter -> val serializer: Serializer = Persister() serializer.write(foldersAndFeedsToOPML(foldersAndFeeds), outputStream) @@ -58,7 +58,7 @@ object OPMLParser { } } - private fun opmltoFoldersAndFeeds(opml: OPML): Map> { + private fun opmlToFoldersAndFeeds(opml: OPML): Map> { if (opml.version != "2.0") throw ParseException("Only 2.0 OPML specification is supported") @@ -84,7 +84,9 @@ object OPMLParser { // The outline is a folder/category if ((outline.outlines != null && !outline.outlines?.isEmpty()!!) || outline.xmlUrl.isNullOrEmpty()) { - val folder = Folder(outline.text) + // if the outline doesn't have text or title value but contains sub outlines, + // those sub outlines will be considered as not belonging to any folder and join the others at the top level of the hierarchy + val folder = if (outline.name != null) Folder(outline.name) else null outline.outlines?.forEach { val recursiveFeedsFolders = parseOutline(it) @@ -100,7 +102,7 @@ object OPMLParser { } else { // the outline is a feed if (!outline.xmlUrl.isNullOrEmpty()) { val feed = Feed().apply { - name = outline.title + name = outline.name url = outline.xmlUrl siteUrl = outline.htmlUrl } @@ -112,20 +114,27 @@ object OPMLParser { return foldersAndFeeds } - private fun foldersAndFeedsToOPML(foldersAndFeeds: Map>): OPML { + private fun foldersAndFeedsToOPML(foldersAndFeeds: Map>): OPML { val outlines = arrayListOf() + for (folderAndFeeds in foldersAndFeeds) { - val outline = Outline(folderAndFeeds.key.name) + if (folderAndFeeds.key != null) { + val outline = Outline(folderAndFeeds.key?.name) - val feedOutlines = arrayListOf() - folderAndFeeds.value.forEach { feed -> - val feedOutline = Outline(feed.name, feed.url, feed.siteUrl) + val feedOutlines = arrayListOf() + for (feed in folderAndFeeds.value) { + val feedOutline = Outline(feed.name, feed.url, feed.siteUrl) - feedOutlines += feedOutline + feedOutlines += feedOutline + } + + outline.outlines = feedOutlines + outlines += outline + } else { + for (feed in folderAndFeeds.value) { + outlines += Outline(feed.name, feed.url, feed.siteUrl) + } } - - outline.outlines = feedOutlines - outlines += outline } val head = Head("Subscriptions") diff --git a/api/src/main/java/com/readrops/api/opml/model/Outline.kt b/api/src/main/java/com/readrops/api/opml/model/Outline.kt index 9a261069..2b0635ef 100644 --- a/api/src/main/java/com/readrops/api/opml/model/Outline.kt +++ b/api/src/main/java/com/readrops/api/opml/model/Outline.kt @@ -5,8 +5,8 @@ import org.simpleframework.xml.ElementList import org.simpleframework.xml.Root @Root(name = "outline", strict = false) -data class Outline(@field:Attribute(required = false) var title: String?, - @field:Attribute(required = false) var text: String?, +data class Outline(@field:Attribute(required = false) private var title: String?, + @field:Attribute(required = false) private var text: String?, @field:Attribute(required = false) var type: String?, @field:Attribute(required = false) var xmlUrl: String?, @field:Attribute(required = false) var htmlUrl: String?, @@ -23,7 +23,10 @@ data class Outline(@field:Attribute(required = false) var title: String?, null, null) - constructor(title: String) : this(title, null, null, null, null, null) + constructor(title: String?) : this(title, title, null, null, null, null) - constructor(title: String, xmlUrl: String, htmlUrl: String) : this(title, title, "rss", xmlUrl, htmlUrl, null) + constructor(title: String?, xmlUrl: String, htmlUrl: String?) : this(title, title, "rss", xmlUrl, htmlUrl, null) + + val name: String? + get() = title ?: text } \ No newline at end of file From 74447b1671d13527392297f0b46f3c0df0652a33 Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Mon, 10 Aug 2020 22:01:51 +0200 Subject: [PATCH 008/187] Use a null key for feeds without folders --- app/build.gradle | 4 ++++ .../java/com/readrops/app/repositories/ARepository.java | 8 ++++---- .../main/java/com/readrops/app/utils/DrawerManager.java | 2 +- .../com/readrops/app/viewmodels/AccountViewModel.java | 1 + .../java/com/readrops/app/viewmodels/MainViewModel.java | 1 + 5 files changed, 11 insertions(+), 5 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 4655cf7b..c65168d7 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -39,6 +39,8 @@ android { } } compileOptions { + coreLibraryDesugaringEnabled true + sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } @@ -56,6 +58,8 @@ dependencies { implementation project(':api') implementation project(':db') + coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.0.10' + implementation 'com.google.android.material:material:1.1.0' implementation 'androidx.cardview:cardview:1.0.0' implementation 'androidx.palette:palette:1.0.0' diff --git a/app/src/main/java/com/readrops/app/repositories/ARepository.java b/app/src/main/java/com/readrops/app/repositories/ARepository.java index 56b07c2e..faa55acb 100644 --- a/app/src/main/java/com/readrops/app/repositories/ARepository.java +++ b/app/src/main/java/com/readrops/app/repositories/ARepository.java @@ -19,6 +19,7 @@ import com.readrops.db.entities.account.AccountType; import com.readrops.api.services.SyncResult; import java.util.ArrayList; +import java.util.Comparator; import java.util.List; import java.util.Map; import java.util.TreeMap; @@ -132,7 +133,7 @@ public abstract class ARepository { public Single>> getFoldersWithFeeds() { return Single.create(emitter -> { List folders = database.folderDao().getFolders(account.getId()); - Map> foldersWithFeeds = new TreeMap<>(Folder::compareTo); + Map> foldersWithFeeds = new TreeMap<>(Comparator.nullsLast(Folder::compareTo)); for (Folder folder : folders) { List feeds = database.feedDao().getFeedsByFolder(folder.getId()); @@ -145,14 +146,13 @@ public abstract class ARepository { foldersWithFeeds.put(folder, feeds); } - Folder noFolder = new Folder("no folder"); - + // feeds without folder List feedsWithoutFolder = database.feedDao().getFeedsWithoutFolder(account.getId()); for (Feed feed : feedsWithoutFolder) { feed.setUnreadCount(database.itemDao().getUnreadCount(feed.getId())); } - foldersWithFeeds.put(noFolder, feedsWithoutFolder); + foldersWithFeeds.put(null, feedsWithoutFolder); emitter.onSuccess(foldersWithFeeds); }); diff --git a/app/src/main/java/com/readrops/app/utils/DrawerManager.java b/app/src/main/java/com/readrops/app/utils/DrawerManager.java index 570acee8..a1354f62 100644 --- a/app/src/main/java/com/readrops/app/utils/DrawerManager.java +++ b/app/src/main/java/com/readrops/app/utils/DrawerManager.java @@ -90,7 +90,7 @@ public class DrawerManager { for (Map.Entry> entry : folderListMap.entrySet()) { Folder folder = entry.getKey(); - if (folder.getId() != 0) { + if (folder != null) { // no identifier for badge items, but if needed, be aware of not getting conflicts // with secondary item identifiers (folder and feed ids can be the same) ExpandableBadgeDrawerItem badgeDrawerItem = new ExpandableBadgeDrawerItem() diff --git a/app/src/main/java/com/readrops/app/viewmodels/AccountViewModel.java b/app/src/main/java/com/readrops/app/viewmodels/AccountViewModel.java index b84b4340..b34aa46c 100644 --- a/app/src/main/java/com/readrops/app/viewmodels/AccountViewModel.java +++ b/app/src/main/java/com/readrops/app/viewmodels/AccountViewModel.java @@ -66,6 +66,7 @@ public class AccountViewModel extends AndroidViewModel { return database.accountDao().getAccountCount(); } + @SuppressWarnings("unchecked") public Single>> getFoldersWithFeeds() { return repository.getFoldersWithFeeds(); } diff --git a/app/src/main/java/com/readrops/app/viewmodels/MainViewModel.java b/app/src/main/java/com/readrops/app/viewmodels/MainViewModel.java index 621f94b0..39ce3359 100644 --- a/app/src/main/java/com/readrops/app/viewmodels/MainViewModel.java +++ b/app/src/main/java/com/readrops/app/viewmodels/MainViewModel.java @@ -124,6 +124,7 @@ public class MainViewModel extends AndroidViewModel { return repository.getFeedCount(currentAccount.getId()); } + @SuppressWarnings("unchecked") public Single>> getFoldersWithFeeds() { return repository.getFoldersWithFeeds(); } From 3657f740dffcf90b248124d361917e3936a0ddf7 Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Mon, 10 Aug 2020 23:31:10 +0200 Subject: [PATCH 009/187] Add another read OPML test --- .../assets/lite_subscriptions.opml | 9 +++++++++ .../java/com/readrops/api/OPMLParserTest.kt | 20 +++++++++++++++---- 2 files changed, 25 insertions(+), 4 deletions(-) create mode 100644 api/src/androidTest/assets/lite_subscriptions.opml diff --git a/api/src/androidTest/assets/lite_subscriptions.opml b/api/src/androidTest/assets/lite_subscriptions.opml new file mode 100644 index 00000000..cd4d40ba --- /dev/null +++ b/api/src/androidTest/assets/lite_subscriptions.opml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/api/src/androidTest/java/com/readrops/api/OPMLParserTest.kt b/api/src/androidTest/java/com/readrops/api/OPMLParserTest.kt index 5edc4ed3..b3e4e091 100644 --- a/api/src/androidTest/java/com/readrops/api/OPMLParserTest.kt +++ b/api/src/androidTest/java/com/readrops/api/OPMLParserTest.kt @@ -3,7 +3,6 @@ package com.readrops.api import android.Manifest import android.content.Context import android.os.Environment -import android.util.Log import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.platform.app.InstrumentationRegistry import androidx.test.rule.GrantPermissionRule @@ -11,11 +10,8 @@ import com.readrops.api.opml.OPMLParser import com.readrops.api.utils.ParseException import com.readrops.db.entities.Feed import com.readrops.db.entities.Folder -import io.reactivex.CompletableObserver -import io.reactivex.disposables.Disposable import io.reactivex.schedulers.Schedulers import junit.framework.TestCase.assertEquals -import junit.framework.TestCase.assertTrue import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith @@ -54,6 +50,22 @@ class OPMLParserTest { stream.close() } + @Test + fun readLiteSubscriptionsTest() { + val stream = context.resources.assets.open("lite_subscriptions.opml") + + var foldersAndFeeds: Map>? = null + + OPMLParser.read(stream) + .subscribe { result -> foldersAndFeeds = result } + + assertEquals(foldersAndFeeds?.values?.first()?.size, 2) + assertEquals(foldersAndFeeds?.values?.first()?.first()?.url, "http://www.theverge.com/rss/index.xml") + assertEquals(foldersAndFeeds?.values?.first()?.get(1)?.url, "https://techcrunch.com/feed/") + + stream.close() + } + @Test fun opmlVersionTest() { val stream = context.resources.assets.open("wrong_version.opml") From f41f430a52b91025b8796926f46ea9b5edf8b25d Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Tue, 11 Aug 2020 21:27:31 +0200 Subject: [PATCH 010/187] Migrate MainActivity to viewBinding --- .../readrops/app/activities/MainActivity.java | 128 +++++++----------- 1 file changed, 51 insertions(+), 77 deletions(-) diff --git a/app/src/main/java/com/readrops/app/activities/MainActivity.java b/app/src/main/java/com/readrops/app/activities/MainActivity.java index d419425e..0cb7faf9 100644 --- a/app/src/main/java/com/readrops/app/activities/MainActivity.java +++ b/app/src/main/java/com/readrops/app/activities/MainActivity.java @@ -9,16 +9,10 @@ import android.view.ActionMode; import android.view.Menu; import android.view.MenuItem; import android.view.View; -import android.widget.LinearLayout; -import android.widget.ProgressBar; -import android.widget.RelativeLayout; -import android.widget.TextView; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.app.AppCompatActivity; -import androidx.appcompat.widget.Toolbar; -import androidx.constraintlayout.widget.ConstraintLayout; import androidx.core.content.ContextCompat; import androidx.core.graphics.drawable.DrawableCompat; import androidx.drawerlayout.widget.DrawerLayout; @@ -34,7 +28,6 @@ import com.afollestad.materialdialogs.MaterialDialog; import com.bumptech.glide.Glide; import com.bumptech.glide.integration.recyclerview.RecyclerViewPreloader; import com.bumptech.glide.util.ViewPreloadSizeProvider; -import com.google.android.material.floatingactionbutton.FloatingActionButton; import com.mikepenz.aboutlibraries.Libs; import com.mikepenz.aboutlibraries.LibsBuilder; import com.mikepenz.materialdrawer.Drawer; @@ -43,6 +36,7 @@ import com.mikepenz.materialdrawer.model.SecondaryDrawerItem; import com.mikepenz.materialdrawer.model.interfaces.IDrawerItem; import com.readrops.app.R; import com.readrops.app.adapters.MainItemListAdapter; +import com.readrops.app.databinding.ActivityMainBinding; import com.readrops.app.utils.DrawerManager; import com.readrops.app.utils.GlideApp; import com.readrops.app.utils.ReadropsItemTouchCallback; @@ -88,12 +82,9 @@ public class MainActivity extends AppCompatActivity implements SwipeRefreshLayou public static final int ITEM_REQUEST = 3; public static final int ADD_ACCOUNT_REQUEST = 4; - private RecyclerView recyclerView; + private ActivityMainBinding binding; private MainItemListAdapter adapter; - private SwipeRefreshLayout refreshLayout; - private ConstraintLayout rootLayout; - private Toolbar toolbar; private Drawer drawer; private PagedList allItems; @@ -101,12 +92,6 @@ public class MainActivity extends AppCompatActivity implements SwipeRefreshLayou private MainViewModel viewModel; private DrawerManager drawerManager; - private LinearLayout emptyListLayout; - private RelativeLayout syncProgressLayout; - private TextView syncProgress; - private ProgressBar syncProgressBar; - private FloatingActionButton actionButton; - private int feedCount; private int feedNb; private boolean scrollToTop; @@ -121,20 +106,12 @@ public class MainActivity extends AppCompatActivity implements SwipeRefreshLayou @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - setContentView(R.layout.activity_main); + binding = ActivityMainBinding.inflate(getLayoutInflater()); - toolbar = findViewById(R.id.toolbar_main); - setSupportActionBar(toolbar); + setContentView(binding.getRoot()); + setSupportActionBar(binding.toolbarMain); - emptyListLayout = findViewById(R.id.empty_list_layout); - refreshLayout = findViewById(R.id.swipe_refresh_layout); - refreshLayout.setOnRefreshListener(this); - rootLayout = findViewById(R.id.main_root); - - syncProgressLayout = findViewById(R.id.sync_progress_layout); - syncProgress = findViewById(R.id.sync_progress_text_view); - syncProgressBar = findViewById(R.id.sync_progress_bar); - actionButton = findViewById(R.id.add_feed_fab); + binding.swipeRefreshLayout.setOnRefreshListener(this); feedCount = 0; initRecyclerView(); @@ -148,15 +125,15 @@ public class MainActivity extends AppCompatActivity implements SwipeRefreshLayou allItems = itemWithFeeds; if (!itemWithFeeds.isEmpty()) - emptyListLayout.setVisibility(View.GONE); + binding.emptyListLayout.setVisibility(View.GONE); else - emptyListLayout.setVisibility(View.VISIBLE); + binding.emptyListLayout.setVisibility(View.VISIBLE); - if (!refreshLayout.isRefreshing()) + if (!binding.swipeRefreshLayout.isRefreshing()) adapter.submitList(itemWithFeeds); }); - drawerManager = new DrawerManager(this, toolbar, (view, position, drawerItem) -> { + drawerManager = new DrawerManager(this, binding.toolbarMain, (view, position, drawerItem) -> { handleDrawerClick(drawerItem); return true; @@ -227,11 +204,11 @@ public class MainActivity extends AppCompatActivity implements SwipeRefreshLayou } if (accountWeakReference.get() != null && !accountWeakReference.get().isLocal()) { - refreshLayout.setRefreshing(true); + binding.swipeRefreshLayout.setRefreshing(true); onRefresh(); accountWeakReference.clear(); } else if (currentAccount == null && savedInstanceState != null && savedInstanceState.getBoolean(SYNCING)) { - refreshLayout.setRefreshing(true); + binding.swipeRefreshLayout.setRefreshing(true); onRefresh(); savedInstanceState.clear(); } @@ -257,7 +234,7 @@ public class MainActivity extends AppCompatActivity implements SwipeRefreshLayou viewModel.setItemReadState(intent.getIntExtra(ITEM_ID, 0), true, true) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) - .doOnError(throwable -> Utils.showSnackbar(rootLayout, throwable.getMessage())) + .doOnError(throwable -> Utils.showSnackbar(binding.mainRoot, throwable.getMessage())) .subscribe(); } } @@ -308,7 +285,7 @@ public class MainActivity extends AppCompatActivity implements SwipeRefreshLayou @Override public void onError(Throwable e) { - Utils.showSnackbar(rootLayout, e.getMessage()); + Utils.showSnackbar(binding.mainRoot, e.getMessage()); } }); } @@ -322,8 +299,6 @@ public class MainActivity extends AppCompatActivity implements SwipeRefreshLayou } private void initRecyclerView() { - recyclerView = findViewById(R.id.items_recycler_view); - ViewPreloadSizeProvider preloadSizeProvider = new ViewPreloadSizeProvider(); adapter = new MainItemListAdapter(GlideApp.with(this), preloadSizeProvider); adapter.setOnItemClickListener(new MainItemListAdapter.OnItemClickListener() { @@ -339,7 +314,7 @@ public class MainActivity extends AppCompatActivity implements SwipeRefreshLayou viewModel.setItemReadState(itemWithFeed, true) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) - .doOnError(throwable -> Utils.showSnackbar(rootLayout, throwable.getMessage())) + .doOnError(throwable -> Utils.showSnackbar(binding.mainRoot, throwable.getMessage())) .subscribe(); itemWithFeed.getItem().setRead(true); @@ -358,7 +333,7 @@ public class MainActivity extends AppCompatActivity implements SwipeRefreshLayou @Override public void onItemLongClick(ItemWithFeed itemWithFeed, int position) { - if (actionMode != null || refreshLayout.isRefreshing()) + if (actionMode != null || binding.swipeRefreshLayout.isRefreshing()) return; selectedItemWithFeed = itemWithFeed; @@ -370,20 +345,20 @@ public class MainActivity extends AppCompatActivity implements SwipeRefreshLayou }); RecyclerViewPreloader preloader = new RecyclerViewPreloader(Glide.with(this), adapter, preloadSizeProvider, 10); - recyclerView.addOnScrollListener(preloader); + binding.itemsRecyclerView.addOnScrollListener(preloader); - recyclerView.setRecyclerListener(viewHolder -> { + binding.itemsRecyclerView.setRecyclerListener(viewHolder -> { MainItemListAdapter.ItemViewHolder vh = (MainItemListAdapter.ItemViewHolder) viewHolder; GlideApp.with(this).clear(vh.getItemImage()); }); LinearLayoutManager layoutManager = new LinearLayoutManager(this); - recyclerView.setLayoutManager(layoutManager); + binding.itemsRecyclerView.setLayoutManager(layoutManager); DividerItemDecoration decoration = new DividerItemDecoration(this, layoutManager.getOrientation()); - recyclerView.addItemDecoration(decoration); + binding.itemsRecyclerView.addItemDecoration(decoration); - recyclerView.setAdapter(adapter); + binding.itemsRecyclerView.setAdapter(adapter); Drawable readLater = ContextCompat.getDrawable(this, R.drawable.ic_read_later).mutate(); @@ -396,34 +371,34 @@ public class MainActivity extends AppCompatActivity implements SwipeRefreshLayou .leftDraw(ContextCompat.getColor(this, R.color.colorAccent), R.drawable.ic_read_later, readLater) .rightDraw(ContextCompat.getColor(this, R.color.colorAccent), R.drawable.ic_read, null) .build())) - .attachToRecyclerView(recyclerView); + .attachToRecyclerView(binding.itemsRecyclerView); adapter.registerAdapterDataObserver(new RecyclerView.AdapterDataObserver() { @Override public void onItemRangeInserted(int positionStart, int itemCount) { if (scrollToTop) { - recyclerView.scrollToPosition(0); + binding.itemsRecyclerView.scrollToPosition(0); scrollToTop = false; } } @Override public void onItemRangeMoved(int fromPosition, int toPosition, int itemCount) { - ;if (scrollToTop) { - recyclerView.scrollToPosition(0); + if (scrollToTop) { + binding.itemsRecyclerView.scrollToPosition(0); scrollToTop = false; } else super.onItemRangeMoved(fromPosition, toPosition, itemCount); } }); - recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() { + binding.itemsRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() { @Override public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) { if (dy > 0) { - actionButton.hide(); + binding.addFeedFab.hide(); } else { - actionButton.show(); + binding.addFeedFab.show(); } } }); @@ -437,7 +412,7 @@ public class MainActivity extends AppCompatActivity implements SwipeRefreshLayou viewModel.setItemReadState(itemWithFeed, !itemWithFeed.getItem().isRead()) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) - .doOnError(throwable -> Utils.showSnackbar(rootLayout, throwable.getMessage())) + .doOnError(throwable -> Utils.showSnackbar(binding.mainRoot, throwable.getMessage())) .subscribe(); itemWithFeed.getItem().setRead(!itemWithFeed.getItem().isRead()); @@ -447,7 +422,7 @@ public class MainActivity extends AppCompatActivity implements SwipeRefreshLayou viewModel.setItemReadItLater((int) adapter.getItemId(viewHolder.getAdapterPosition())) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) - .doOnError(throwable -> Utils.showSnackbar(rootLayout, throwable.getMessage())) + .doOnError(throwable -> Utils.showSnackbar(binding.mainRoot, throwable.getMessage())) .subscribe(); if (viewModel.getFilterType() == FilterType.READ_IT_LATER_FILTER) @@ -458,7 +433,7 @@ public class MainActivity extends AppCompatActivity implements SwipeRefreshLayou @Override public boolean onCreateActionMode(ActionMode actionMode, Menu menu) { drawer.getDrawerLayout().setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED); - refreshLayout.setEnabled(false); + binding.swipeRefreshLayout.setEnabled(false); actionMode.getMenuInflater().inflate(R.menu.item_list_contextual_menu, menu); getWindow().setStatusBarColor(ContextCompat.getColor(this, R.color.primary_dark)); @@ -504,7 +479,7 @@ public class MainActivity extends AppCompatActivity implements SwipeRefreshLayou actionMode = null; drawer.getDrawerLayout().setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED); - refreshLayout.setEnabled(true); + binding.swipeRefreshLayout.setEnabled(true); adapter.clearSelection(); } @@ -514,7 +489,7 @@ public class MainActivity extends AppCompatActivity implements SwipeRefreshLayou viewModel.setAllItemsReadState(read) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) - .doOnError(throwable -> Utils.showSnackbar(rootLayout, throwable.getMessage())) + .doOnError(throwable -> Utils.showSnackbar(binding.mainRoot, throwable.getMessage())) .subscribe(); allItemsSelected = false; @@ -522,7 +497,7 @@ public class MainActivity extends AppCompatActivity implements SwipeRefreshLayou viewModel.setItemsReadState(adapter.getSelectedItems(), read) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) - .doOnError(throwable -> Utils.showSnackbar(rootLayout, throwable.getMessage())) + .doOnError(throwable -> Utils.showSnackbar(binding.mainRoot, throwable.getMessage())) .subscribe(); } @@ -550,7 +525,7 @@ public class MainActivity extends AppCompatActivity implements SwipeRefreshLayou @Override public void onError(Throwable e) { - Utils.showSnackbar(rootLayout, e.getMessage()); + Utils.showSnackbar(binding.mainRoot, e.getMessage()); } }); } else { @@ -570,7 +545,7 @@ public class MainActivity extends AppCompatActivity implements SwipeRefreshLayou List feeds = data.getParcelableArrayListExtra(FEEDS); if (feeds != null && !feeds.isEmpty() && viewModel.isAccountLocal()) { - refreshLayout.setRefreshing(true); + binding.swipeRefreshLayout.setRefreshing(true); feedNb = feeds.size(); sync(feeds); } @@ -592,7 +567,7 @@ public class MainActivity extends AppCompatActivity implements SwipeRefreshLayou // start syncing only if the account is not local if (!viewModel.isAccountLocal()) { - refreshLayout.setRefreshing(true); + binding.swipeRefreshLayout.setRefreshing(true); onRefresh(); } @@ -615,19 +590,19 @@ public class MainActivity extends AppCompatActivity implements SwipeRefreshLayou syncDisposable = d; if (viewModel.isAccountLocal() && feedNb > 0) { - syncProgressLayout.setVisibility(View.VISIBLE); - syncProgressBar.setProgress(0); + binding.syncProgressLayout.setVisibility(View.VISIBLE); + binding.syncProgressBar.setProgress(0); } } @Override public void onNext(Feed feed) { if (viewModel.isAccountLocal() && feedNb > 0) { - syncProgress.setText(getString(R.string.updating_feed, feed.getName())); + binding.syncProgressTextView.setText(getString(R.string.updating_feed, feed.getName())); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - syncProgressBar.setProgress((feedCount * 100) / feedNb, true); + binding.syncProgressBar.setProgress((feedCount * 100) / feedNb, true); } else - syncProgressBar.setProgress((feedCount * 100) / feedNb); + binding.syncProgressBar.setProgress((feedCount * 100) / feedNb); } feedCount++; @@ -636,10 +611,10 @@ public class MainActivity extends AppCompatActivity implements SwipeRefreshLayou @Override public void onError(Throwable e) { e.printStackTrace(); - refreshLayout.setRefreshing(false); - syncProgressLayout.setVisibility(View.GONE); + binding.swipeRefreshLayout.setRefreshing(false); + binding.syncProgressLayout.setVisibility(View.GONE); - Utils.showSnackbar(rootLayout, e.getMessage()); + Utils.showSnackbar(binding.mainRoot, e.getMessage()); drawerManager.enableAccountSelection(); } @@ -647,14 +622,14 @@ public class MainActivity extends AppCompatActivity implements SwipeRefreshLayou public void onComplete() { if (viewModel.isAccountLocal() && feedNb > 0) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) - syncProgressBar.setProgress(100, true); + binding.syncProgressBar.setProgress(100, true); else - syncProgressBar.setProgress(100); + binding.syncProgressBar.setProgress(100); - syncProgressLayout.setVisibility(View.GONE); + binding.syncProgressLayout.setVisibility(View.GONE); } - refreshLayout.setRefreshing(false); + binding.swipeRefreshLayout.setRefreshing(false); scrollToTop = true; adapter.submitList(allItems); @@ -700,7 +675,7 @@ public class MainActivity extends AppCompatActivity implements SwipeRefreshLayou return true; case R.id.start_sync: if (!viewModel.isAccountLocal()) { - refreshLayout.setRefreshing(true); + binding.swipeRefreshLayout.setRefreshing(true); } onRefresh(); break; @@ -770,10 +745,9 @@ public class MainActivity extends AppCompatActivity implements SwipeRefreshLayou @Override protected void onSaveInstanceState(Bundle outState) { - if (refreshLayout.isRefreshing()) + if (binding.swipeRefreshLayout.isRefreshing()) outState.putBoolean(SYNCING, true); super.onSaveInstanceState(outState); } - } From 93be8b92b9f89206338746eb94c04645a152d2d1 Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Tue, 11 Aug 2020 21:58:38 +0200 Subject: [PATCH 011/187] Migrate ItemActivity to viewBinding --- .../readrops/app/activities/ItemActivity.java | 135 +++++++----------- 1 file changed, 48 insertions(+), 87 deletions(-) diff --git a/app/src/main/java/com/readrops/app/activities/ItemActivity.java b/app/src/main/java/com/readrops/app/activities/ItemActivity.java index 59b2add3..86462056 100644 --- a/app/src/main/java/com/readrops/app/activities/ItemActivity.java +++ b/app/src/main/java/com/readrops/app/activities/ItemActivity.java @@ -18,15 +18,10 @@ import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.webkit.WebView; -import android.widget.ImageView; -import android.widget.RelativeLayout; -import android.widget.TextView; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.app.AppCompatActivity; -import androidx.appcompat.widget.Toolbar; -import androidx.coordinatorlayout.widget.CoordinatorLayout; import androidx.core.app.ActivityCompat; import androidx.core.app.ShareCompat; import androidx.lifecycle.ViewModelProvider; @@ -35,14 +30,11 @@ import com.afollestad.materialdialogs.MaterialDialog; import com.bumptech.glide.load.engine.DiskCacheStrategy; import com.bumptech.glide.request.target.CustomTarget; import com.bumptech.glide.request.transition.Transition; -import com.google.android.material.appbar.AppBarLayout; -import com.google.android.material.appbar.CollapsingToolbarLayout; -import com.google.android.material.floatingactionbutton.FloatingActionButton; import com.readrops.app.R; +import com.readrops.app.databinding.ActivityItemBinding; import com.readrops.app.utils.DateUtils; import com.readrops.app.utils.GlideApp; import com.readrops.app.utils.PermissionManager; -import com.readrops.app.utils.ReadropsWebView; import com.readrops.app.utils.SharedPreferencesManager; import com.readrops.app.utils.Utils; import com.readrops.app.viewmodels.ItemViewModel; @@ -62,72 +54,47 @@ public class ItemActivity extends AppCompatActivity { private static final String TAG = ItemActivity.class.getSimpleName(); private static final int WRITE_EXTERNAL_STORAGE_REQUEST = 1; + private ActivityItemBinding binding; private ItemViewModel viewModel; - private TextView date; - private TextView title; - private TextView author; - private TextView readTime; - - private RelativeLayout readTimeLayout; - RelativeLayout dateLayout; - - private CollapsingToolbarLayout toolbarLayout; - private Toolbar toolbar; - private FloatingActionButton actionButton; - private ReadropsWebView webView; private ItemWithFeed itemWithFeed; private boolean appBarCollapsed; - private CoordinatorLayout rootLayout; private String urlToDownload; private String imageTitle; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - setContentView(R.layout.activity_item); + + binding = ActivityItemBinding.inflate(getLayoutInflater()); + setContentView(binding.getRoot()); Intent intent = getIntent(); int itemId = intent.getIntExtra(ITEM_ID, 0); String imageUrl = intent.getStringExtra(IMAGE_URL); - toolbar = findViewById(R.id.collapsing_layout_toolbar); - setSupportActionBar(toolbar); + setSupportActionBar(binding.collapsingLayoutToolbar); - if (getSupportActionBar() != null) + if (getSupportActionBar() != null) { getSupportActionBar().setDisplayHomeAsUpEnabled(true); + } - toolbarLayout = findViewById(R.id.collapsing_layout); - AppBarLayout appBarLayout = findViewById(R.id.app_bar_layout); - - ImageView imageView = findViewById(R.id.collapsing_layout_image); - View scrim = findViewById(R.id.collapsing_layout_scrim); - actionButton = findViewById(R.id.activity_item_fab); - webView = findViewById(R.id.item_webview); - date = findViewById(R.id.activity_item_date); - title = findViewById(R.id.activity_item_title); - author = findViewById(R.id.activity_item_author); - readTime = findViewById(R.id.activity_item_readtime); - readTimeLayout = findViewById(R.id.activity_item_readtime_layout); - dateLayout = findViewById(R.id.activity_item_date_layout); - rootLayout = findViewById(R.id.item_root); - - registerForContextMenu(webView); + registerForContextMenu(binding.itemWebview); if (imageUrl == null) { getSupportActionBar().setDisplayShowTitleEnabled(false); - toolbarLayout.setTitleEnabled(false); - scrim.setVisibility(View.GONE); + binding.collapsingLayout.setTitleEnabled(false); + binding.collapsingLayoutScrim.setVisibility(View.GONE); } else { - appBarLayout.setExpanded(true); - toolbarLayout.setTitleEnabled(true); + binding.appBarLayout.setExpanded(true); + binding.collapsingLayout.setTitleEnabled(true); GlideApp.with(this) .load(imageUrl) .diskCacheStrategy(DiskCacheStrategy.ALL) - .into(imageView); + .into(binding.collapsingLayoutImage); } final TypedArray styledAttributes = getTheme().obtainStyledAttributes( @@ -135,83 +102,77 @@ public class ItemActivity extends AppCompatActivity { int actionBarSize = (int) styledAttributes.getDimension(0, 0); styledAttributes.recycle(); - appBarLayout.addOnOffsetChangedListener(((appBarLayout1, i) -> { + binding.appBarLayout.addOnOffsetChangedListener(((appBarLayout1, i) -> { + appBarCollapsed = Math.abs(i) >= (binding.appBarLayout.getTotalScrollRange() - + actionBarSize - ((8 * binding.appBarLayout.getTotalScrollRange()) / 100)); - if (Math.abs(i) >= (appBarLayout.getTotalScrollRange() - actionBarSize - ((8 * appBarLayout.getTotalScrollRange()) / 100))) { - appBarCollapsed = true; - invalidateOptionsMenu(); - } else { - appBarCollapsed = false; - invalidateOptionsMenu(); - } + invalidateOptionsMenu(); })); viewModel = new ViewModelProvider(this).get(ItemViewModel.class); viewModel.getItemById(itemId).observe(this, this::bindUI); - actionButton.setOnClickListener(v -> openInNavigator()); + binding.activityItemFab.setOnClickListener(v -> openInNavigator()); } private void bindUI(ItemWithFeed itemWithFeed) { this.itemWithFeed = itemWithFeed; Item item = itemWithFeed.getItem(); - date.setText(DateUtils.formattedDateTimeByLocal(item.getPubDate())); + binding.activityItemDate.setText(DateUtils.formattedDateTimeByLocal(item.getPubDate())); if (item.getImageLink() == null) - toolbar.setTitle(itemWithFeed.getFeedName()); + binding.collapsingLayoutToolbar.setTitle(itemWithFeed.getFeedName()); else - toolbarLayout.setTitle(itemWithFeed.getFeedName()); + binding.collapsingLayout.setTitle(itemWithFeed.getFeedName()); if (itemWithFeed.getFolder() != null) { - toolbar.setSubtitle(itemWithFeed.getFolder().getName()); + binding.collapsingLayoutToolbar.setSubtitle(itemWithFeed.getFolder().getName()); } - title.setText(item.getTitle()); + binding.activityItemTitle.setText(item.getTitle()); if (itemWithFeed.getBgColor() != 0) { - title.setTextColor(itemWithFeed.getBgColor()); - Utils.setDrawableColor(dateLayout.getBackground(), itemWithFeed.getBgColor()); + binding.activityItemTitle.setTextColor(itemWithFeed.getBgColor()); + Utils.setDrawableColor(binding.activityItemDateLayout.getBackground(), itemWithFeed.getBgColor()); } else if (itemWithFeed.getColor() != 0) { - title.setTextColor(itemWithFeed.getColor()); - Utils.setDrawableColor(dateLayout.getBackground(), itemWithFeed.getColor()); + binding.activityItemTitle.setTextColor(itemWithFeed.getColor()); + Utils.setDrawableColor(binding.activityItemDateLayout.getBackground(), itemWithFeed.getColor()); } if (item.getAuthor() != null && !item.getAuthor().isEmpty()) { - author.setText(getString(R.string.by_author, item.getAuthor())); - author.setVisibility(View.VISIBLE); + binding.activityItemAuthor.setText(getString(R.string.by_author, item.getAuthor())); + binding.activityItemAuthor.setVisibility(View.VISIBLE); } if (item.getReadTime() > 0) { int minutes = (int) Math.round(item.getReadTime()); if (minutes < 1) - readTime.setText(getResources().getString(R.string.read_time_lower_than_1)); + binding.activityItemReadtime.setText(getResources().getString(R.string.read_time_lower_than_1)); else if (minutes > 1) - readTime.setText(getResources().getString(R.string.read_time, String.valueOf(minutes))); + binding.activityItemReadtime.setText(getResources().getString(R.string.read_time, String.valueOf(minutes))); else - readTime.setText(getResources().getString(R.string.read_time_one_minute)); + binding.activityItemReadtime.setText(getResources().getString(R.string.read_time_one_minute)); - readTimeLayout.setVisibility(View.VISIBLE); + binding.activityItemReadtimeLayout.setVisibility(View.VISIBLE); } if (itemWithFeed.getBgColor() != 0) { - toolbarLayout.setBackgroundColor(itemWithFeed.getBgColor()); - toolbarLayout.setContentScrimColor(itemWithFeed.getBgColor()); - toolbarLayout.setStatusBarScrimColor(itemWithFeed.getBgColor()); + binding.collapsingLayout.setBackgroundColor(itemWithFeed.getBgColor()); + binding.collapsingLayout.setContentScrimColor(itemWithFeed.getBgColor()); + binding.collapsingLayout.setStatusBarScrimColor(itemWithFeed.getBgColor()); getWindow().setStatusBarColor(itemWithFeed.getBgColor()); - - actionButton.setBackgroundTintList(ColorStateList.valueOf(itemWithFeed.getBgColor())); + binding.activityItemFab.setBackgroundTintList(ColorStateList.valueOf(itemWithFeed.getBgColor())); } else if (itemWithFeed.getColor() != 0) { - toolbarLayout.setBackgroundColor(itemWithFeed.getColor()); - toolbarLayout.setContentScrimColor(itemWithFeed.getColor()); - toolbarLayout.setStatusBarScrimColor(itemWithFeed.getColor()); + binding.collapsingLayout.setBackgroundColor(itemWithFeed.getColor()); + binding.collapsingLayout.setContentScrimColor(itemWithFeed.getColor()); + binding.collapsingLayout.setStatusBarScrimColor(itemWithFeed.getColor()); getWindow().setStatusBarColor(itemWithFeed.getColor()); - - actionButton.setBackgroundTintList(ColorStateList.valueOf(itemWithFeed.getColor())); + binding.activityItemFab.setBackgroundTintList(ColorStateList.valueOf(itemWithFeed.getColor())); } - webView.setItem(itemWithFeed); + binding.itemWebview.setItem(itemWithFeed); } @Override @@ -238,7 +199,7 @@ public class ItemActivity extends AppCompatActivity { shareArticle(); return true; case R.id.item_open: - int value = Integer.valueOf(SharedPreferencesManager.readString(this, + int value = Integer.parseInt(SharedPreferencesManager.readString(this, SharedPreferencesManager.SharedPrefKey.OPEN_ITEMS_IN)); if (value == 0) openInNavigator(); @@ -278,7 +239,7 @@ public class ItemActivity extends AppCompatActivity { @Override public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) { - WebView.HitTestResult hitTestResult = webView.getHitTestResult(); + WebView.HitTestResult hitTestResult = binding.itemWebview.getHitTestResult(); if (hitTestResult.getType() == WebView.HitTestResult.IMAGE_TYPE || hitTestResult.getType() == WebView.HitTestResult.SRC_IMAGE_ANCHOR_TYPE) { @@ -300,7 +261,7 @@ public class ItemActivity extends AppCompatActivity { break; case 2: urlToDownload = hitTestResult.getExtra(); - String content = webView.getItemContent(); + String content = binding.itemWebview.getItemContent(); Pattern p = Pattern.compile("()"); Matcher m = p.matcher(content); @@ -334,12 +295,12 @@ public class ItemActivity extends AppCompatActivity { downloadImage(urlToDownload); } else { if (ActivityCompat.shouldShowRequestPermissionRationale(this, permissions[0])) { - Utils.showSnackBarWithAction(rootLayout, getString(R.string.download_image_permission), + Utils.showSnackBarWithAction(binding.itemRoot, getString(R.string.download_image_permission), getString(R.string.try_again), v -> PermissionManager.requestPermissions(this, WRITE_EXTERNAL_STORAGE_REQUEST, Manifest.permission.WRITE_EXTERNAL_STORAGE)); } else { - Utils.showSnackBarWithAction(rootLayout, getString(R.string.download_image_permission), + Utils.showSnackBarWithAction(binding.itemRoot, getString(R.string.download_image_permission), getString(R.string.permissions), v -> { Intent intent = new Intent(); intent.setAction(Settings.ACTION_APPLICATION_DETAILS_SETTINGS); From f7a78df8155ebe3e0863eebd17010978990a91f2 Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Tue, 11 Aug 2020 22:41:57 +0200 Subject: [PATCH 012/187] Migrate FeedsAdapter to viewBinding --- .../readrops/app/adapters/FeedsAdapter.java | 60 ++++++++----------- 1 file changed, 25 insertions(+), 35 deletions(-) diff --git a/app/src/main/java/com/readrops/app/adapters/FeedsAdapter.java b/app/src/main/java/com/readrops/app/adapters/FeedsAdapter.java index 6050ea3b..385813e4 100644 --- a/app/src/main/java/com/readrops/app/adapters/FeedsAdapter.java +++ b/app/src/main/java/com/readrops/app/adapters/FeedsAdapter.java @@ -3,8 +3,6 @@ package com.readrops.app.adapters; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; -import android.widget.ImageView; -import android.widget.TextView; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -14,12 +12,13 @@ import androidx.recyclerview.widget.RecyclerView; import com.bumptech.glide.load.engine.DiskCacheStrategy; import com.readrops.app.R; -import com.readrops.db.pojo.FeedWithFolder; +import com.readrops.app.databinding.FeedLayoutBinding; import com.readrops.app.utils.GlideApp; +import com.readrops.db.pojo.FeedWithFolder; import java.util.List; -public class FeedsAdapter extends ListAdapter { +public class FeedsAdapter extends ListAdapter { private ManageFeedsListener listener; @@ -52,20 +51,16 @@ public class FeedsAdapter extends ListAdapter listener.onEdit(feedWithFolder)); viewHolder.itemView.setOnLongClickListener(v -> { @@ -98,16 +93,16 @@ public class FeedsAdapter extends ListAdapter payloads) { + public void onBindViewHolder(@NonNull FeedViewHolder holder, int position, @NonNull List payloads) { if (!payloads.isEmpty()) { FeedWithFolder feedWithFolder = (FeedWithFolder) payloads.get(0); - holder.feedName.setText(feedWithFolder.getFeed().getName()); + holder.binding.feedLayoutName.setText(feedWithFolder.getFeed().getName()); if (feedWithFolder.getFolder() != null) - holder.folderName.setText(feedWithFolder.getFolder().getName()); + holder.binding.feedLayoutName.setText(feedWithFolder.getFolder().getName()); else - holder.folderName.setText(R.string.no_folder); + holder.binding.feedLayoutName.setText(R.string.no_folder); } else onBindViewHolder(holder, position); @@ -115,24 +110,19 @@ public class FeedsAdapter extends ListAdapter Date: Sun, 16 Aug 2020 23:04:26 +0200 Subject: [PATCH 013/187] Use more mime types to open opml files --- .../java/com/readrops/api/opml/OPMLHelper.kt | 27 +++++++++++++++++++ .../activities/AccountTypeListActivity.java | 9 +++---- .../settings/AccountSettingsFragment.java | 15 +++-------- .../readrops/app/utils/PermissionManager.kt | 22 +++++++-------- 4 files changed, 44 insertions(+), 29 deletions(-) create mode 100644 api/src/main/java/com/readrops/api/opml/OPMLHelper.kt diff --git a/api/src/main/java/com/readrops/api/opml/OPMLHelper.kt b/api/src/main/java/com/readrops/api/opml/OPMLHelper.kt new file mode 100644 index 00000000..60d6f750 --- /dev/null +++ b/api/src/main/java/com/readrops/api/opml/OPMLHelper.kt @@ -0,0 +1,27 @@ +package com.readrops.api.opml + +import android.app.Activity +import android.content.Intent +import androidx.fragment.app.Fragment + +object OPMLHelper { + + const val OPEN_OPML_FILE_REQUEST = 1 + + @JvmStatic + fun openFileIntent(activity: Activity) = + activity.startActivityForResult(createIntent(), OPEN_OPML_FILE_REQUEST) + + @JvmStatic + fun openFileIntent(fragment: Fragment) = + fragment.startActivityForResult(createIntent(), OPEN_OPML_FILE_REQUEST) + + + private fun createIntent(): Intent { + return Intent(Intent.ACTION_OPEN_DOCUMENT).apply { + addCategory(Intent.CATEGORY_OPENABLE) + type = "*/*" + putExtra(Intent.EXTRA_MIME_TYPES, arrayOf("application/*", "text/*")) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/readrops/app/activities/AccountTypeListActivity.java b/app/src/main/java/com/readrops/app/activities/AccountTypeListActivity.java index 5f971bed..3af60e23 100644 --- a/app/src/main/java/com/readrops/app/activities/AccountTypeListActivity.java +++ b/app/src/main/java/com/readrops/app/activities/AccountTypeListActivity.java @@ -16,6 +16,7 @@ import androidx.recyclerview.widget.DividerItemDecoration; import androidx.recyclerview.widget.LinearLayoutManager; import com.afollestad.materialdialogs.MaterialDialog; +import com.readrops.api.opml.OPMLHelper; import com.readrops.app.R; import com.readrops.app.adapters.AccountTypeListAdapter; import com.readrops.app.databinding.ActivityAccountTypeListBinding; @@ -32,7 +33,7 @@ import io.reactivex.observers.DisposableCompletableObserver; import io.reactivex.observers.DisposableSingleObserver; import io.reactivex.schedulers.Schedulers; -import static com.readrops.app.fragments.settings.AccountSettingsFragment.OPEN_OPML_FILE_REQUEST; +import static com.readrops.api.opml.OPMLHelper.OPEN_OPML_FILE_REQUEST; import static com.readrops.app.utils.ReadropsKeys.ACCOUNT; import static com.readrops.app.utils.ReadropsKeys.ACCOUNT_TYPE; import static com.readrops.app.utils.ReadropsKeys.FROM_MAIN_ACTIVITY; @@ -127,11 +128,7 @@ public class AccountTypeListActivity extends AppCompatActivity { } public void openOPMLFile(View view) { - Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT); - intent.addCategory(Intent.CATEGORY_OPENABLE); - intent.setType("application/*"); - - startActivityForResult(intent, OPEN_OPML_FILE_REQUEST); + OPMLHelper.openFileIntent(this); } @Override diff --git a/app/src/main/java/com/readrops/app/fragments/settings/AccountSettingsFragment.java b/app/src/main/java/com/readrops/app/fragments/settings/AccountSettingsFragment.java index a2221f56..2df601a6 100644 --- a/app/src/main/java/com/readrops/app/fragments/settings/AccountSettingsFragment.java +++ b/app/src/main/java/com/readrops/app/fragments/settings/AccountSettingsFragment.java @@ -22,6 +22,7 @@ import androidx.preference.Preference; import androidx.preference.PreferenceFragmentCompat; import com.afollestad.materialdialogs.MaterialDialog; +import com.readrops.api.opml.OPMLHelper; import com.readrops.api.opml.OPMLParser; import com.readrops.app.R; import com.readrops.app.ReadropsApp; @@ -45,6 +46,7 @@ import io.reactivex.observers.DisposableCompletableObserver; import io.reactivex.schedulers.Schedulers; import static android.app.Activity.RESULT_OK; +import static com.readrops.api.opml.OPMLHelper.OPEN_OPML_FILE_REQUEST; import static com.readrops.app.utils.ReadropsKeys.ACCOUNT; import static com.readrops.app.utils.ReadropsKeys.ACCOUNT_ID; import static com.readrops.app.utils.ReadropsKeys.EDIT_ACCOUNT; @@ -56,7 +58,6 @@ public class AccountSettingsFragment extends PreferenceFragmentCompat { private static final String TAG = AccountSettingsFragment.class.getSimpleName(); - public static final int OPEN_OPML_FILE_REQUEST = 1; private static final int WRITE_EXTERNAL_STORAGE_REQUEST = 1; private Account account; @@ -122,7 +123,7 @@ public class AccountSettingsFragment extends PreferenceFragmentCompat { .items(R.array.opml_import_export) .itemsCallback(((dialog, itemView, position, text) -> { if (position == 0) { - openOPMLFile(); + OPMLHelper.openFileIntent(this); } else { if (PermissionManager.isPermissionGranted(getContext(), Manifest.permission.WRITE_EXTERNAL_STORAGE)) exportAsOPMLFile(); @@ -179,15 +180,7 @@ public class AccountSettingsFragment extends PreferenceFragmentCompat { } // region opml import - - private void openOPMLFile() { - Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT); - intent.addCategory(Intent.CATEGORY_OPENABLE); - intent.setType("application/*"); - - startActivityForResult(intent, OPEN_OPML_FILE_REQUEST); - } - + @Override public void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) { if (requestCode == OPEN_OPML_FILE_REQUEST && resultCode == RESULT_OK && data != null) { diff --git a/app/src/main/java/com/readrops/app/utils/PermissionManager.kt b/app/src/main/java/com/readrops/app/utils/PermissionManager.kt index 55242ef9..6d72c145 100644 --- a/app/src/main/java/com/readrops/app/utils/PermissionManager.kt +++ b/app/src/main/java/com/readrops/app/utils/PermissionManager.kt @@ -7,20 +7,18 @@ import androidx.core.app.ActivityCompat import androidx.core.content.ContextCompat import androidx.fragment.app.Fragment -class PermissionManager { +object PermissionManager { - companion object { - @JvmStatic - fun isPermissionGranted(context: Context, permission: String): Boolean = - ContextCompat.checkSelfPermission(context, permission) == PackageManager.PERMISSION_GRANTED + @JvmStatic + fun isPermissionGranted(context: Context, permission: String): Boolean = + ContextCompat.checkSelfPermission(context, permission) == PackageManager.PERMISSION_GRANTED - @JvmStatic - fun requestPermissions(activity: Activity, requestCode: Int, vararg permissions: String) = - ActivityCompat.requestPermissions(activity, permissions, requestCode) + @JvmStatic + fun requestPermissions(activity: Activity, requestCode: Int, vararg permissions: String) = + ActivityCompat.requestPermissions(activity, permissions, requestCode) - @JvmStatic - fun requestPermissions(fragment: Fragment, requestCode: Int, vararg permissions: String) = - fragment.requestPermissions(permissions, requestCode) - } + @JvmStatic + fun requestPermissions(fragment: Fragment, requestCode: Int, vararg permissions: String) = + fragment.requestPermissions(permissions, requestCode) } \ No newline at end of file From 76d5e295893990d67e6b91e1b60779c13e771bc9 Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Sun, 23 Aug 2020 21:51:09 +0200 Subject: [PATCH 014/187] Use scope storage with API >= 29 for OPML export --- .../settings/AccountSettingsFragment.java | 56 +++++---------- .../java/com/readrops/app/utils/FileUtils.kt | 69 +++++++++++++++++++ 2 files changed, 87 insertions(+), 38 deletions(-) create mode 100644 app/src/main/java/com/readrops/app/utils/FileUtils.kt diff --git a/app/src/main/java/com/readrops/app/fragments/settings/AccountSettingsFragment.java b/app/src/main/java/com/readrops/app/fragments/settings/AccountSettingsFragment.java index 2df601a6..99bce58c 100644 --- a/app/src/main/java/com/readrops/app/fragments/settings/AccountSettingsFragment.java +++ b/app/src/main/java/com/readrops/app/fragments/settings/AccountSettingsFragment.java @@ -8,7 +8,6 @@ import android.content.Intent; import android.content.pm.PackageManager; import android.net.Uri; import android.os.Bundle; -import android.os.Environment; import android.provider.Settings; import android.util.Log; @@ -29,6 +28,7 @@ import com.readrops.app.ReadropsApp; import com.readrops.app.activities.AddAccountActivity; import com.readrops.app.activities.ManageFeedsFoldersActivity; import com.readrops.app.activities.NotificationPermissionActivity; +import com.readrops.app.utils.FileUtils; import com.readrops.app.utils.PermissionManager; import com.readrops.app.utils.SharedPreferencesManager; import com.readrops.app.utils.Utils; @@ -36,14 +36,10 @@ import com.readrops.app.viewmodels.AccountViewModel; import com.readrops.db.entities.account.Account; import com.readrops.db.entities.account.AccountType; -import java.io.File; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.OutputStream; - import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.observers.DisposableCompletableObserver; import io.reactivex.schedulers.Schedulers; +import kotlin.Unit; import static android.app.Activity.RESULT_OK; import static com.readrops.api.opml.OPMLHelper.OPEN_OPML_FILE_REQUEST; @@ -180,7 +176,7 @@ public class AccountSettingsFragment extends PreferenceFragmentCompat { } // region opml import - + @Override public void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) { if (requestCode == OPEN_OPML_FILE_REQUEST && resultCode == RESULT_OK && data != null) { @@ -231,37 +227,21 @@ public class AccountSettingsFragment extends PreferenceFragmentCompat { //region opml export private void exportAsOPMLFile() { + String fileName = "subscriptions.opml"; + try { - String filePath = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).getAbsolutePath(); - File file = new File(filePath, "subscriptions.opml"); + String path = FileUtils.writeDownloadFile(getContext(), fileName, "text/xml", outputStream -> { + viewModel.getFoldersWithFeeds() + .flatMapCompletable(folderListMap -> OPMLParser.write(folderListMap, outputStream)) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .doOnError(e -> Utils.showSnackbar(getView(), e.getMessage())) + .subscribe(); - final OutputStream outputStream = new FileOutputStream(file); + return Unit.INSTANCE; + }); - viewModel.getFoldersWithFeeds() - .flatMapCompletable(folderListMap -> OPMLParser.write(folderListMap, outputStream)) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .doAfterTerminate(() -> { - try { - outputStream.flush(); - outputStream.close(); - - } catch (IOException e) { - Log.e(TAG, e.getMessage()); - Utils.showSnackbar(getView(), e.getMessage()); - } - }) - .subscribe(new DisposableCompletableObserver() { - @Override - public void onComplete() { - displayNotification(file); - } - - @Override - public void onError(Throwable e) { - Utils.showSnackbar(getView(), e.getMessage()); - } - }); + displayNotification(fileName, path); } catch (Exception e) { Log.e(TAG, e.getMessage()); Utils.showSnackbar(getView(), e.getMessage()); @@ -269,13 +249,13 @@ public class AccountSettingsFragment extends PreferenceFragmentCompat { } - private void displayNotification(File file) { + private void displayNotification(String name, String absolutePath) { Intent intent = new Intent(Intent.ACTION_VIEW); - intent.setDataAndType(Uri.parse(file.getAbsolutePath()), "text/plain"); + intent.setDataAndType(Uri.parse(absolutePath), "text/plain"); Notification notification = new NotificationCompat.Builder(getContext(), ReadropsApp.OPML_EXPORT_CHANNEL_ID) .setContentTitle(getString(R.string.opml_export)) - .setContentText(file.getName()) + .setContentText(name) .setSmallIcon(R.drawable.ic_notif) .setContentIntent(PendingIntent.getActivity(getContext(), 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)) .setAutoCancel(true) diff --git a/app/src/main/java/com/readrops/app/utils/FileUtils.kt b/app/src/main/java/com/readrops/app/utils/FileUtils.kt new file mode 100644 index 00000000..9ae24dff --- /dev/null +++ b/app/src/main/java/com/readrops/app/utils/FileUtils.kt @@ -0,0 +1,69 @@ +package com.readrops.app.utils + +import android.content.ContentValues +import android.content.Context +import android.os.Build +import android.os.Environment +import android.provider.MediaStore +import androidx.annotation.RequiresApi +import java.io.File +import java.io.FileOutputStream +import java.io.OutputStream + +object FileUtils { + + @JvmStatic + fun writeDownloadFile(context: Context, fileName: String, mimeType: String, listener: (OutputStream) -> Unit): String { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) + writeFileApi29(context, fileName, mimeType, listener) + else + writeFileApi28(fileName, listener) + } + + @RequiresApi(Build.VERSION_CODES.Q) + private fun writeFileApi29(context: Context, fileName: String, mimeType: String, listener: (OutputStream) -> Unit): String { + val resolver = context.contentResolver + val downloadsUri = MediaStore.Downloads + .getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY) + + val fileDetails = ContentValues().apply { + put(MediaStore.Downloads.DISPLAY_NAME, fileName) + put(MediaStore.Downloads.IS_PENDING, 1) + put(MediaStore.Downloads.MIME_TYPE, mimeType) + } + + val contentUri = resolver.insert(downloadsUri, fileDetails) + + resolver.openFileDescriptor(contentUri!!, "w", null).use { pfd -> + val outputStream = FileOutputStream(pfd?.fileDescriptor!!) + + try { + listener(outputStream) + } catch (e: Exception) { + throw e + } finally { + outputStream.flush() + outputStream.close() + } + } + + fileDetails.clear() + fileDetails.put(MediaStore.Downloads.IS_PENDING, 0) + resolver.update(contentUri, fileDetails, null, null) + + return contentUri.path!! + } + + private fun writeFileApi28(fileName: String, listener: (OutputStream) -> Unit): String { + val filePath = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).absolutePath + val file = File(filePath, fileName) + + val outputStream = FileOutputStream(file) + listener(outputStream) + + outputStream.flush() + outputStream.close() + + return file.absolutePath + } +} \ No newline at end of file From e370abbb434f6eed44f8dcfc804cb075c73f11b7 Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Sun, 23 Aug 2020 22:09:13 +0200 Subject: [PATCH 015/187] Request storage permission only with APIs <= 28 --- app/src/main/AndroidManifest.xml | 4 ++- .../settings/AccountSettingsFragment.java | 33 +++++++++++-------- 2 files changed, 23 insertions(+), 14 deletions(-) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 13390561..9d47cb49 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -6,7 +6,9 @@ - + { new MaterialDialog.Builder(getActivity()) .items(R.array.opml_import_export) - .itemsCallback(((dialog, itemView, position, text) -> { - if (position == 0) { - OPMLHelper.openFileIntent(this); - } else { - if (PermissionManager.isPermissionGranted(getContext(), Manifest.permission.WRITE_EXTERNAL_STORAGE)) - exportAsOPMLFile(); - else - requestExternalStoragePermission(); - } - })) + .itemsCallback(((dialog, itemView, position, text) -> openOPMLMode(position))) .show(); return true; }); @@ -175,6 +167,22 @@ public class AccountSettingsFragment extends PreferenceFragmentCompat { .show(); } + private void openOPMLMode(int position) { + if (position == 0) { + OPMLHelper.openFileIntent(this); + } else { + if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.P) { + if (PermissionManager.isPermissionGranted(getContext(), Manifest.permission.WRITE_EXTERNAL_STORAGE)) { + exportAsOPMLFile(); + } else { + requestExternalStoragePermission(); + } + } else { + exportAsOPMLFile(); + } + } + } + // region opml import @Override @@ -243,8 +251,7 @@ public class AccountSettingsFragment extends PreferenceFragmentCompat { displayNotification(fileName, path); } catch (Exception e) { - Log.e(TAG, e.getMessage()); - Utils.showSnackbar(getView(), e.getMessage()); + displayErrorMessage(); } } From 6c6653e44303dbf1bbc1e8514f368f765812a28e Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Sun, 23 Aug 2020 22:12:37 +0200 Subject: [PATCH 016/187] Deactivate niddler by default --- app/src/debug/java/com/readrops/app/ReadropsDebugApp.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/debug/java/com/readrops/app/ReadropsDebugApp.java b/app/src/debug/java/com/readrops/app/ReadropsDebugApp.java index 9e09bd2e..244f9afd 100644 --- a/app/src/debug/java/com/readrops/app/ReadropsDebugApp.java +++ b/app/src/debug/java/com/readrops/app/ReadropsDebugApp.java @@ -29,7 +29,7 @@ public class ReadropsDebugApp extends ReadropsApp implements Configuration.Provi SoLoader.init(this, false); initFlipper(); - initNiddler(); + //initNiddler(); } private void initFlipper() { From 355b5a4375a11591f98725df67c225e762590df8 Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Mon, 24 Aug 2020 21:38:46 +0200 Subject: [PATCH 017/187] Add new helper class to get RSS feed type, with tests --- .../readrops/api/localfeed/LocalRSSHelper.kt | 68 +++++++++++++++++++ .../com/readrops/api/LocalRSSHelperTest.kt | 60 ++++++++++++++++ 2 files changed, 128 insertions(+) create mode 100644 api/src/main/java/com/readrops/api/localfeed/LocalRSSHelper.kt create mode 100644 api/src/test/java/com/readrops/api/LocalRSSHelperTest.kt diff --git a/api/src/main/java/com/readrops/api/localfeed/LocalRSSHelper.kt b/api/src/main/java/com/readrops/api/localfeed/LocalRSSHelper.kt new file mode 100644 index 00000000..f3cc57f3 --- /dev/null +++ b/api/src/main/java/com/readrops/api/localfeed/LocalRSSHelper.kt @@ -0,0 +1,68 @@ +package com.readrops.api.localfeed + +import com.readrops.api.utils.ParseException +import java.io.InputStream +import java.util.regex.Pattern + +object LocalRSSHelper { + + private const val RSS_DEFAULT_CONTENT_TYPE = "application/rss+xml" + private const val RSS_TEXT_CONTENT_TYPE = "text/xml" + private const val RSS_APPLICATION_CONTENT_TYPE = "application/xml" + private const val ATOM_CONTENT_TYPE = "application/atom+xml" + private const val JSON_CONTENT_TYPE = "application/json" + private const val HTML_CONTENT_TYPE = "text/html" + + private const val RSS_2_REGEX = "rss.*version=\"2.0\"" + + private const val ATOM_REGEX = " RSSType.RSS_2 + ATOM_CONTENT_TYPE -> RSSType.ATOM + JSON_CONTENT_TYPE -> RSSType.JSONFEED + RSS_TEXT_CONTENT_TYPE, RSS_APPLICATION_CONTENT_TYPE, HTML_CONTENT_TYPE -> RSSType.UNKNOWN + else -> throw ParseException("Unknown content type") + } + } + + /** + * Guess RSS type based on xml content + */ + fun getRSSContentType(content: InputStream): RSSType { + val stringBuffer = StringBuffer() + val reader = content.bufferedReader() + + var currentLine = reader.readLine() + while (currentLine != null) { + stringBuffer.append(currentLine) + + if (Pattern.compile(RSS_2_REGEX).matcher(stringBuffer.toString()).find()) { + reader.close() + content.close() + + return RSSType.RSS_2 + } else if (Pattern.compile(ATOM_REGEX).matcher(stringBuffer.toString()).find()) { + reader.close() + content.close() + + return RSSType.ATOM + } + + currentLine = reader.readLine() + } + + return RSSType.UNKNOWN + } + + enum class RSSType { + RSS_2, + ATOM, + JSONFEED, + UNKNOWN + } +} \ No newline at end of file diff --git a/api/src/test/java/com/readrops/api/LocalRSSHelperTest.kt b/api/src/test/java/com/readrops/api/LocalRSSHelperTest.kt new file mode 100644 index 00000000..681352d7 --- /dev/null +++ b/api/src/test/java/com/readrops/api/LocalRSSHelperTest.kt @@ -0,0 +1,60 @@ +package com.readrops.api + +import com.readrops.api.localfeed.LocalRSSHelper +import com.readrops.api.utils.ParseException +import junit.framework.TestCase.assertEquals +import org.junit.Test +import java.io.ByteArrayInputStream + +class LocalRSSHelperTest { + + @Test + fun standardContentTypesTest() { + assertEquals(LocalRSSHelper.getRSSType("application/rss+xml"), + LocalRSSHelper.RSSType.RSS_2) + assertEquals(LocalRSSHelper.getRSSType("application/atom+xml"), + LocalRSSHelper.RSSType.ATOM) + assertEquals(LocalRSSHelper.getRSSType("application/json"), + LocalRSSHelper.RSSType.JSONFEED) + } + + @Test + fun nonStandardContentTypesTest() { + assertEquals(LocalRSSHelper.getRSSType("application/xml"), + LocalRSSHelper.RSSType.UNKNOWN) + assertEquals(LocalRSSHelper.getRSSType("text/xml"), + LocalRSSHelper.RSSType.UNKNOWN) + assertEquals(LocalRSSHelper.getRSSType("text/html"), + LocalRSSHelper.RSSType.UNKNOWN) + } + + @Test(expected = ParseException::class) + fun nonSupportedContentTypeTest() { + LocalRSSHelper.getRSSType("image/jpeg") + } + + @Test + fun rssContentTest() { + assertEquals(LocalRSSHelper.getRSSContentType(ByteArrayInputStream( + """ + """.toByteArray() + )), LocalRSSHelper.RSSType.RSS_2) + } + + @Test + fun atomContentTest() { + assertEquals(LocalRSSHelper.getRSSContentType(ByteArrayInputStream( + """ + + """.toByteArray() + )), LocalRSSHelper.RSSType.ATOM) + + } +} \ No newline at end of file From 51cea7c4c28de0a9d57633041a3f0a50b7ac8721 Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Tue, 25 Aug 2020 23:35:19 +0200 Subject: [PATCH 018/187] Add data source for local rss, with tests --- api/build.gradle | 7 +- .../androidTest/assets/localfeed/rss_feed.xml | 57 +++++++++ .../assets/{ => opml}/lite_subscriptions.opml | 0 .../assets/{ => opml}/subscriptions.opml | 0 .../assets/{ => opml}/wrong_version.opml | 0 .../api/localfeed/LocalRSSDataSourceTest.kt | 116 ++++++++++++++++++ api/src/debug/AndroidManifest.xml | 5 +- .../api/localfeed/LocalRSSDataSource.kt | 88 +++++++++++++ .../java/com/readrops/api/utils/LibUtils.java | 17 +++ .../api/{ => localfeed}/LocalRSSHelperTest.kt | 0 10 files changed, 288 insertions(+), 2 deletions(-) create mode 100644 api/src/androidTest/assets/localfeed/rss_feed.xml rename api/src/androidTest/assets/{ => opml}/lite_subscriptions.opml (100%) rename api/src/androidTest/assets/{ => opml}/subscriptions.opml (100%) rename api/src/androidTest/assets/{ => opml}/wrong_version.opml (100%) create mode 100644 api/src/androidTest/java/com/readrops/api/localfeed/LocalRSSDataSourceTest.kt create mode 100644 api/src/main/java/com/readrops/api/localfeed/LocalRSSDataSource.kt rename api/src/test/java/com/readrops/api/{ => localfeed}/LocalRSSHelperTest.kt (100%) diff --git a/api/build.gradle b/api/build.gradle index 05f25476..ab07a8eb 100644 --- a/api/build.gradle +++ b/api/build.gradle @@ -56,8 +56,13 @@ dependencies { androidTestImplementation 'androidx.test:runner:1.2.0' androidTestImplementation 'androidx.test:rules:1.2.0' androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' + androidTestImplementation 'com.squareup.okhttp3:mockwebserver:4.8.1' - implementation 'com.squareup.retrofit2:retrofit:2.7.1' + implementation 'com.squareup.okhttp3:okhttp:4.8.1' + + implementation('com.squareup.retrofit2:retrofit:2.7.1') { + exclude group: 'okhttp3', module: 'okhttp3' + } implementation('com.squareup.retrofit2:converter-moshi:2.7.1') { exclude group: 'moshi', module: 'moshi' // moshi converter uses moshi 1.8.0 which breaks codegen 1.9.2 } diff --git a/api/src/androidTest/assets/localfeed/rss_feed.xml b/api/src/androidTest/assets/localfeed/rss_feed.xml new file mode 100644 index 00000000..8c012917 --- /dev/null +++ b/api/src/androidTest/assets/localfeed/rss_feed.xml @@ -0,0 +1,57 @@ + + + + Hacker News + https://news.ycombinator.com/ + Links for the intellectually curious, ranked by readers. + + Africa declared free of wild polio + https://www.bbc.com/news/world-africa-53887947 + Tue, 25 Aug 2020 17:15:49 +0000 + https://news.ycombinator.com/item?id=24273602 + Comments]]> + + + Palantir S-1 + https://www.sec.gov/Archives/edgar/data/1321655/000119312520230013/d904406ds1.htm + Tue, 25 Aug 2020 21:03:42 +0000 + https://news.ycombinator.com/item?id=24276086 + Comments]]> + + + Openwifi: Linux mac80211 compatible full-stack 802.11/Wi-Fi design based on SDR + https://github.com/open-sdr/openwifi + Tue, 25 Aug 2020 17:45:19 +0000 + https://news.ycombinator.com/item?id=24273919 + Comments]]> + + + Syllabus for Eric's PhD Students + https://docs.google.com/document/d/11D3kHElzS2HQxTwPqcaTnU5HCJ8WGE5brTXI4KLf4dM/edit + Tue, 25 Aug 2020 18:55:12 +0000 + https://news.ycombinator.com/item?id=24274699 + Comments]]> + + + WebBundles harmful to content blocking, security tools, and the open web + https://brave.com/webbundles-harmful-to-content-blocking-security-tools-and-the-open-web/ + Tue, 25 Aug 2020 19:18:50 +0000 + https://news.ycombinator.com/item?id=24274968 + Comments]]> + + + Zappos CEO Tony Hsieh is stepping down after 21 years + https://footwearnews.com/2020/business/executive-moves/zappos-ceo-tony-hsieh-steps-down-1203045974/ + Tue, 25 Aug 2020 06:11:42 +0000 + https://news.ycombinator.com/item?id=24268522 + Comments]]> + + + Evgeny Kuznetsov practices with Bauer stick that has hole in the blade + https://russianmachineneverbreaks.com/2020/07/17/evgeny-kuznetsov-practices-with-bauer-stick-that-has-hole-in-the-blade/ + Tue, 25 Aug 2020 19:38:09 +0000 + https://news.ycombinator.com/item?id=24275159 + Comments]]> + + + \ No newline at end of file diff --git a/api/src/androidTest/assets/lite_subscriptions.opml b/api/src/androidTest/assets/opml/lite_subscriptions.opml similarity index 100% rename from api/src/androidTest/assets/lite_subscriptions.opml rename to api/src/androidTest/assets/opml/lite_subscriptions.opml diff --git a/api/src/androidTest/assets/subscriptions.opml b/api/src/androidTest/assets/opml/subscriptions.opml similarity index 100% rename from api/src/androidTest/assets/subscriptions.opml rename to api/src/androidTest/assets/opml/subscriptions.opml diff --git a/api/src/androidTest/assets/wrong_version.opml b/api/src/androidTest/assets/opml/wrong_version.opml similarity index 100% rename from api/src/androidTest/assets/wrong_version.opml rename to api/src/androidTest/assets/opml/wrong_version.opml diff --git a/api/src/androidTest/java/com/readrops/api/localfeed/LocalRSSDataSourceTest.kt b/api/src/androidTest/java/com/readrops/api/localfeed/LocalRSSDataSourceTest.kt new file mode 100644 index 00000000..2d0b3db7 --- /dev/null +++ b/api/src/androidTest/java/com/readrops/api/localfeed/LocalRSSDataSourceTest.kt @@ -0,0 +1,116 @@ +package com.readrops.api.localfeed + +import android.accounts.NetworkErrorException +import android.content.Context +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry +import com.readrops.api.utils.HttpManager +import com.readrops.api.utils.ParseException +import junit.framework.TestCase.* +import okhttp3.HttpUrl +import okhttp3.mockwebserver.MockResponse +import okhttp3.mockwebserver.MockWebServer +import org.junit.After +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import java.net.HttpURLConnection + + +@RunWith(AndroidJUnit4::class) +class LocalRSSDataSourceTest { + + private val context: Context = InstrumentationRegistry.getInstrumentation().context + private lateinit var url: HttpUrl + + private val mockServer: MockWebServer = MockWebServer() + private val localRSSDataSource = LocalRSSDataSource(HttpManager.getInstance().okHttpClient) + + @Before + fun before() { + mockServer.start() + url = mockServer.url("/rss") + } + + @After + fun tearDown() { + mockServer.close() + } + + @Test + fun successfulQueryTest() { + mockServer.enqueue(MockResponse().setResponseCode(HttpURLConnection.HTTP_OK) + .addHeader("Content-Type", "application/rss+xml; charset=UTF-8") + .setBody(context.resources.assets.open("localfeed/rss_feed.xml").toString())) + + + val pair = localRSSDataSource.queryRSSResource(url.toString(), null, false) + + assertNotNull(pair?.first) + assertNotNull(pair?.second) + } + + @Test + fun response304Test() { + mockServer.enqueue(MockResponse().setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED)) + + val pair = localRSSDataSource.queryRSSResource(url.toString(), null, false) + + assertNull(pair) + } + + @Test(expected = NetworkErrorException::class) + fun response404Test() { + mockServer.enqueue(MockResponse().setResponseCode(HttpURLConnection.HTTP_NOT_FOUND)) + + localRSSDataSource.queryRSSResource(url.toString(), null, false) + } + + @Test(expected = ParseException::class) + fun noContentTypeTest() { + mockServer.enqueue(MockResponse().setResponseCode(HttpURLConnection.HTTP_OK)) + + localRSSDataSource.queryRSSResource(url.toString(), null, false) + } + + @Test(expected = ParseException::class) + fun badContentTypeTest() { + mockServer.enqueue(MockResponse().setResponseCode(HttpURLConnection.HTTP_OK) + .addHeader("Content-Type", "")) + + localRSSDataSource.queryRSSResource(url.toString(), null, false) + } + + @Test(expected = ParseException::class) + fun badContentTest() { + mockServer.enqueue(MockResponse().setResponseCode(HttpURLConnection.HTTP_OK) + .addHeader("Content-Type", "application/xml") + .setBody(" ")) + + localRSSDataSource.queryRSSResource(url.toString(), null, false) + } + + @Test + fun isUrlResourceSuccessfulTest() { + mockServer.enqueue(MockResponse().setResponseCode(HttpURLConnection.HTTP_OK) + .addHeader("Content-Type", "application/atom+xml")) + + assertTrue(localRSSDataSource.isUrlRSSResource(url.toString())) + } + + @Test + fun isUrlRSSResourceFailureTest() { + mockServer.enqueue(MockResponse().setResponseCode(HttpURLConnection.HTTP_NOT_FOUND)) + + assertFalse(localRSSDataSource.isUrlRSSResource(url.toString())) + } + + @Test + fun isUrlRSSResourceBadContentTypeTest() { + mockServer.enqueue(MockResponse().setResponseCode(HttpURLConnection.HTTP_OK) + .addHeader("Content-Type", "application/xml") + .setBody(" ")) + + assertFalse(localRSSDataSource.isUrlRSSResource(url.toString())) + } +} \ No newline at end of file diff --git a/api/src/debug/AndroidManifest.xml b/api/src/debug/AndroidManifest.xml index 7c249c25..bc80eafd 100644 --- a/api/src/debug/AndroidManifest.xml +++ b/api/src/debug/AndroidManifest.xml @@ -3,7 +3,10 @@ + - + \ No newline at end of file diff --git a/api/src/main/java/com/readrops/api/localfeed/LocalRSSDataSource.kt b/api/src/main/java/com/readrops/api/localfeed/LocalRSSDataSource.kt new file mode 100644 index 00000000..a6f2a007 --- /dev/null +++ b/api/src/main/java/com/readrops/api/localfeed/LocalRSSDataSource.kt @@ -0,0 +1,88 @@ +package com.readrops.api.localfeed + +import android.accounts.NetworkErrorException +import androidx.annotation.WorkerThread +import com.readrops.api.utils.LibUtils +import com.readrops.api.utils.ParseException +import com.readrops.db.entities.Feed +import com.readrops.db.entities.Item +import okhttp3.Headers +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.Response +import java.io.IOException +import java.io.InputStream +import java.net.HttpURLConnection + +class LocalRSSDataSource(private val httpClient: OkHttpClient) { + + /** + * Query RSS url + * @param url url to query + * @param headers request headers + * @param withItems parse items with their feed + * @return a Feed object with its items if specified by [withItems] + */ + @WorkerThread + fun queryRSSResource(url: String, headers: Headers?, withItems: Boolean): Pair>? { + val response = queryUrl(url, headers) + + return when { + response.isSuccessful -> { + val header = response.header(LibUtils.CONTENT_TYPE_HEADER) + ?: throw ParseException("Unable to get $url content-type") + + val contentType = LibUtils.parseContentType(header) + ?: throw ParseException("Unable to get $url content-type") + + var type = LocalRSSHelper.getRSSType(contentType) + + // if we can't guess type based on content-type header, we use the content + if (type == LocalRSSHelper.RSSType.UNKNOWN) type = LocalRSSHelper.getRSSContentType(response.body?.byteStream()!!) + // if we can't guess type even with the content, we are unable to go further + if (type == LocalRSSHelper.RSSType.UNKNOWN) throw ParseException("Unable to guess $url RSS type") + + val feed = parseFeed(response, type) + val items = if (withItems) parseItems(response.body?.byteStream()!!, type) else listOf() + + return Pair(feed, items) + } + response.code == HttpURLConnection.HTTP_NOT_MODIFIED -> null + else -> throw NetworkErrorException("$url returned ${response.code} code : ${response.message}") + } + } + + @WorkerThread + fun isUrlRSSResource(url: String): Boolean { + val response = queryUrl(url, null) + + return if (response.isSuccessful) { + val contentType = response.header(LibUtils.CONTENT_TYPE_HEADER) + ?: return false + + var type = LocalRSSHelper.getRSSType(contentType) + + if (type == LocalRSSHelper.RSSType.UNKNOWN) + type = LocalRSSHelper.getRSSContentType(response.body?.byteStream()!!) // stream is closed in helper method + + type != LocalRSSHelper.RSSType.UNKNOWN + } else false + } + + @Throws(IOException::class) + private fun queryUrl(url: String, headers: Headers?): Response { + val requestBuilder = Request.Builder().url(url) + headers?.let { requestBuilder.headers(it) } + + return httpClient.newCall(requestBuilder.build()).execute() + } + + private fun parseFeed(response: Response, type: LocalRSSHelper.RSSType): Feed { + response.body?.close() + return Feed() + } + + private fun parseItems(inputStream: InputStream, type: LocalRSSHelper.RSSType): List { + return listOf() + } +} \ No newline at end of file diff --git a/api/src/main/java/com/readrops/api/utils/LibUtils.java b/api/src/main/java/com/readrops/api/utils/LibUtils.java index bbc4c426..88c238d2 100644 --- a/api/src/main/java/com/readrops/api/utils/LibUtils.java +++ b/api/src/main/java/com/readrops/api/utils/LibUtils.java @@ -4,10 +4,13 @@ import android.content.Context; import android.net.Uri; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import java.io.FileNotFoundException; import java.io.InputStream; import java.util.Scanner; +import java.util.regex.Matcher; +import java.util.regex.Pattern; public final class LibUtils { @@ -28,6 +31,8 @@ public final class LibUtils { public static final int HTTP_NOT_FOUND = 404; public static final int HTTP_CONFLICT = 409; + private static final String RSS_CONTENT_TYPE_REGEX = "([^;]+)"; + public static String inputStreamToString(InputStream input) { Scanner scanner = new Scanner(input).useDelimiter("\\A"); @@ -44,4 +49,16 @@ public final class LibUtils { return type.equals("image") || type.equals("image/jpeg") || type.equals("image/jpg") || type.equals("image/png"); } + + @Nullable + public static String parseContentType(String header) { + Matcher matcher = Pattern.compile(RSS_CONTENT_TYPE_REGEX) + .matcher(header); + + if (matcher.find()) { + return matcher.group(0); + } else { + return null; + } + } } diff --git a/api/src/test/java/com/readrops/api/LocalRSSHelperTest.kt b/api/src/test/java/com/readrops/api/localfeed/LocalRSSHelperTest.kt similarity index 100% rename from api/src/test/java/com/readrops/api/LocalRSSHelperTest.kt rename to api/src/test/java/com/readrops/api/localfeed/LocalRSSHelperTest.kt From 4e27a0b86f3a64be4f3657bc3e0c9c2f462352c2 Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Fri, 28 Aug 2020 22:50:53 +0200 Subject: [PATCH 019/187] Add some unit tests for RSS type and content guess methods --- .../api/localfeed/LocalRSSHelperTest.kt | 13 +++++++++++-- .../com/readrops/api/utils/LibUtilsTest.kt | 19 +++++++++++++++++++ 2 files changed, 30 insertions(+), 2 deletions(-) create mode 100644 api/src/test/java/com/readrops/api/utils/LibUtilsTest.kt diff --git a/api/src/test/java/com/readrops/api/localfeed/LocalRSSHelperTest.kt b/api/src/test/java/com/readrops/api/localfeed/LocalRSSHelperTest.kt index 681352d7..a5678bbc 100644 --- a/api/src/test/java/com/readrops/api/localfeed/LocalRSSHelperTest.kt +++ b/api/src/test/java/com/readrops/api/localfeed/LocalRSSHelperTest.kt @@ -1,6 +1,5 @@ -package com.readrops.api +package com.readrops.api.localfeed -import com.readrops.api.localfeed.LocalRSSHelper import com.readrops.api.utils.ParseException import junit.framework.TestCase.assertEquals import org.junit.Test @@ -55,6 +54,16 @@ class LocalRSSHelperTest { """.toByteArray() )), LocalRSSHelper.RSSType.ATOM) + } + + @Test + fun unknownContentTest() { + assertEquals(LocalRSSHelper.getRSSContentType(ByteArrayInputStream( + """ + + + """.trimMargin().toByteArray() + )), LocalRSSHelper.RSSType.UNKNOWN) } } \ No newline at end of file diff --git a/api/src/test/java/com/readrops/api/utils/LibUtilsTest.kt b/api/src/test/java/com/readrops/api/utils/LibUtilsTest.kt new file mode 100644 index 00000000..a6801cae --- /dev/null +++ b/api/src/test/java/com/readrops/api/utils/LibUtilsTest.kt @@ -0,0 +1,19 @@ +package com.readrops.api.utils + +import junit.framework.TestCase.assertEquals +import org.junit.Test + +class LibUtilsTest { + + @Test + fun contentTypeWithCharsetTest() { + assertEquals(LibUtils.parseContentType("application/rss+xml; charset=UTF-8"), + "application/rss+xml") + } + + @Test + fun contentTypeWithoutCharsetText() { + assertEquals(LibUtils.parseContentType("text/xml"), + "text/xml") + } +} \ No newline at end of file From 752135621d771ac968a57f0aef976335f6ab9088 Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Sun, 30 Aug 2020 16:39:08 +0200 Subject: [PATCH 020/187] Move DateUtils in api package --- .../src/main/java/com/readrops/api}/utils/DateUtils.java | 2 +- .../src/test/java/com/readrops/api/utils}/DateUtilsTest.java | 4 +--- .../main/java/com/readrops/app/activities/ItemActivity.java | 2 +- .../java/com/readrops/app/adapters/MainItemListAdapter.java | 2 +- .../java/com/readrops/app/utils/matchers/ItemMatcher.java | 2 +- 5 files changed, 5 insertions(+), 7 deletions(-) rename {app/src/main/java/com/readrops/app => api/src/main/java/com/readrops/api}/utils/DateUtils.java (98%) rename {app/src/test/java/com/readrops/app => api/src/test/java/com/readrops/api/utils}/DateUtilsTest.java (96%) diff --git a/app/src/main/java/com/readrops/app/utils/DateUtils.java b/api/src/main/java/com/readrops/api/utils/DateUtils.java similarity index 98% rename from app/src/main/java/com/readrops/app/utils/DateUtils.java rename to api/src/main/java/com/readrops/api/utils/DateUtils.java index f7ea1e36..68133f3a 100644 --- a/app/src/main/java/com/readrops/app/utils/DateUtils.java +++ b/api/src/main/java/com/readrops/api/utils/DateUtils.java @@ -1,4 +1,4 @@ -package com.readrops.app.utils; +package com.readrops.api.utils; import org.joda.time.LocalDateTime; import org.joda.time.format.DateTimeFormat; diff --git a/app/src/test/java/com/readrops/app/DateUtilsTest.java b/api/src/test/java/com/readrops/api/utils/DateUtilsTest.java similarity index 96% rename from app/src/test/java/com/readrops/app/DateUtilsTest.java rename to api/src/test/java/com/readrops/api/utils/DateUtilsTest.java index 95b9058f..7e36f383 100644 --- a/app/src/test/java/com/readrops/app/DateUtilsTest.java +++ b/api/src/test/java/com/readrops/api/utils/DateUtilsTest.java @@ -1,6 +1,4 @@ -package com.readrops.app; - -import com.readrops.app.utils.DateUtils; +package com.readrops.api.utils; import org.joda.time.LocalDateTime; import org.junit.Test; diff --git a/app/src/main/java/com/readrops/app/activities/ItemActivity.java b/app/src/main/java/com/readrops/app/activities/ItemActivity.java index 86462056..a1493f25 100644 --- a/app/src/main/java/com/readrops/app/activities/ItemActivity.java +++ b/app/src/main/java/com/readrops/app/activities/ItemActivity.java @@ -32,7 +32,7 @@ import com.bumptech.glide.request.target.CustomTarget; import com.bumptech.glide.request.transition.Transition; import com.readrops.app.R; import com.readrops.app.databinding.ActivityItemBinding; -import com.readrops.app.utils.DateUtils; +import com.readrops.api.utils.DateUtils; import com.readrops.app.utils.GlideApp; import com.readrops.app.utils.PermissionManager; import com.readrops.app.utils.SharedPreferencesManager; diff --git a/app/src/main/java/com/readrops/app/adapters/MainItemListAdapter.java b/app/src/main/java/com/readrops/app/adapters/MainItemListAdapter.java index a818387c..30520174 100644 --- a/app/src/main/java/com/readrops/app/adapters/MainItemListAdapter.java +++ b/app/src/main/java/com/readrops/app/adapters/MainItemListAdapter.java @@ -29,7 +29,7 @@ import com.readrops.app.R; import com.readrops.db.entities.Item; import com.readrops.db.pojo.ItemWithFeed; import com.readrops.app.databinding.ListItemBinding; -import com.readrops.app.utils.DateUtils; +import com.readrops.api.utils.DateUtils; import com.readrops.app.utils.GlideRequests; import com.readrops.app.utils.Utils; diff --git a/app/src/main/java/com/readrops/app/utils/matchers/ItemMatcher.java b/app/src/main/java/com/readrops/app/utils/matchers/ItemMatcher.java index 096134c6..651fa211 100644 --- a/app/src/main/java/com/readrops/app/utils/matchers/ItemMatcher.java +++ b/app/src/main/java/com/readrops/app/utils/matchers/ItemMatcher.java @@ -1,6 +1,6 @@ package com.readrops.app.utils.matchers; -import com.readrops.app.utils.DateUtils; +import com.readrops.api.utils.DateUtils; import com.readrops.app.utils.Utils; import com.readrops.db.entities.Feed; import com.readrops.db.entities.Item; From d59e38ee9d11da186131b602425231eff0896956 Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Sat, 5 Sep 2020 14:05:16 +0200 Subject: [PATCH 021/187] Use project level okhttp client with glide --- .../com/readrops/app/utils/GlideModule.java | 10 --------- .../readrops/app/utils/ReadropsGlideModule.kt | 21 +++++++++++++++++++ 2 files changed, 21 insertions(+), 10 deletions(-) delete mode 100644 app/src/main/java/com/readrops/app/utils/GlideModule.java create mode 100644 app/src/main/java/com/readrops/app/utils/ReadropsGlideModule.kt diff --git a/app/src/main/java/com/readrops/app/utils/GlideModule.java b/app/src/main/java/com/readrops/app/utils/GlideModule.java deleted file mode 100644 index 7d765a40..00000000 --- a/app/src/main/java/com/readrops/app/utils/GlideModule.java +++ /dev/null @@ -1,10 +0,0 @@ -package com.readrops.app.utils; - -import androidx.annotation.NonNull; - -import com.bumptech.glide.module.AppGlideModule; - -@com.bumptech.glide.annotation.GlideModule -public class GlideModule extends AppGlideModule { - -} diff --git a/app/src/main/java/com/readrops/app/utils/ReadropsGlideModule.kt b/app/src/main/java/com/readrops/app/utils/ReadropsGlideModule.kt new file mode 100644 index 00000000..3138c7dd --- /dev/null +++ b/app/src/main/java/com/readrops/app/utils/ReadropsGlideModule.kt @@ -0,0 +1,21 @@ +package com.readrops.app.utils + +import android.content.Context +import com.bumptech.glide.Glide +import com.bumptech.glide.Registry +import com.bumptech.glide.annotation.GlideModule +import com.bumptech.glide.integration.okhttp3.OkHttpUrlLoader +import com.bumptech.glide.load.model.GlideUrl +import com.bumptech.glide.module.AppGlideModule +import com.readrops.api.utils.HttpManager +import java.io.InputStream + +@GlideModule +class ReadropsGlideModule : AppGlideModule() { + + override fun registerComponents(context: Context, glide: Glide, registry: Registry) { + val factory = OkHttpUrlLoader.Factory(HttpManager.getInstance().okHttpClient) + + glide.registry.replace(GlideUrl::class.java, InputStream::class.java, factory) + } +} \ No newline at end of file From 85fcf03e64d8b482e4d2af8c2bcd1509d946944f Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Sat, 5 Sep 2020 14:23:48 +0200 Subject: [PATCH 022/187] Use clear text mode for the feed url text input in AddFeedActivity --- app/build.gradle | 2 +- .../app/activities/AddFeedActivity.java | 25 +++---------------- app/src/main/res/layout/activity_add_feed.xml | 2 +- 3 files changed, 5 insertions(+), 24 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index c65168d7..96c07aa3 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -60,7 +60,7 @@ dependencies { coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.0.10' - implementation 'com.google.android.material:material:1.1.0' + implementation 'com.google.android.material:material:1.2.0' implementation 'androidx.cardview:cardview:1.0.0' implementation 'androidx.palette:palette:1.0.0' implementation 'androidx.recyclerview:recyclerview:1.1.0' diff --git a/app/src/main/java/com/readrops/app/activities/AddFeedActivity.java b/app/src/main/java/com/readrops/app/activities/AddFeedActivity.java index b8dc7acf..9e231ff6 100644 --- a/app/src/main/java/com/readrops/app/activities/AddFeedActivity.java +++ b/app/src/main/java/com/readrops/app/activities/AddFeedActivity.java @@ -7,7 +7,6 @@ import android.os.Bundle; import android.util.Patterns; import android.view.KeyEvent; import android.view.MenuItem; -import android.view.MotionEvent; import android.view.View; import androidx.annotation.Nullable; @@ -70,33 +69,15 @@ public class AddFeedActivity extends AppCompatActivity implements View.OnClickLi binding.addFeedOk.setOnClickListener(this); binding.addFeedOk.setEnabled(false); - binding.addFeedTextInput.setOnTouchListener((v, event) -> { - final int DRAWABLE_RIGHT = 2; - - int drawablePos = (binding.addFeedTextInput.getRight() - - binding.addFeedTextInput.getCompoundDrawables()[DRAWABLE_RIGHT].getBounds().width()); - if (event.getAction() == MotionEvent.ACTION_UP && event.getRawX() >= drawablePos) { - binding.addFeedTextInput.setText(""); - return true; - } - - return false; - }); - viewModel = new ViewModelProvider(this).get(AddFeedsViewModel.class); parseItemsAdapter = new ItemAdapter<>(); fastAdapter = FastAdapter.with(parseItemsAdapter); fastAdapter.withSelectable(true); fastAdapter.withOnClickListener((v, adapter, item, position) -> { - if (item.isChecked()) { - item.setChecked(false); - fastAdapter.notifyAdapterItemChanged(position); - } else { - item.setChecked(true); - fastAdapter.notifyAdapterItemChanged(position); - } - + item.setChecked(!item.isChecked()); + + fastAdapter.notifyAdapterItemChanged(position); binding.addFeedOk.setEnabled(recyclerViewHasCheckedItems()); return true; diff --git a/app/src/main/res/layout/activity_add_feed.xml b/app/src/main/res/layout/activity_add_feed.xml index 123511a4..cd53baf3 100644 --- a/app/src/main/res/layout/activity_add_feed.xml +++ b/app/src/main/res/layout/activity_add_feed.xml @@ -36,6 +36,7 @@ android:id="@+id/add_feed_input_layout" android:layout_width="0dp" android:layout_height="wrap_content" + app:endIconMode="clear_text" app:layout_constraintEnd_toStartOf="@id/add_feed_load" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent"> @@ -44,7 +45,6 @@ android:id="@+id/add_feed_text_input" android:layout_width="match_parent" android:layout_height="wrap_content" - android:drawableEnd="@drawable/ic_cancel_grey" android:hint="@string/feed_url" android:inputType="text" /> From e0945823eecf269e5beea646ac5d7e630e08afbf Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Sat, 5 Sep 2020 15:28:23 +0200 Subject: [PATCH 023/187] Use gradle parallel builds --- gradle.properties | 1 + 1 file changed, 1 insertion(+) diff --git a/gradle.properties b/gradle.properties index 09457c85..3313ccbc 100644 --- a/gradle.properties +++ b/gradle.properties @@ -11,6 +11,7 @@ android.useAndroidX=true org.gradle.jvmargs=-Xmx1536m android.databinding.incremental=true kapt.incremental.apt=true +org.gradle.parallel=true # When configured, Gradle will run in incubating parallel mode. # This option should only be used with decoupled projects. More details, visit # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects From c15f093a1bc4211e85f8d1817c9073e307afe5ac Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Sun, 6 Sep 2020 23:09:59 +0200 Subject: [PATCH 024/187] Add an option to open item url in custom tab --- app/build.gradle | 2 +- .../readrops/app/activities/ItemActivity.java | 42 +++++++++++++++---- app/src/main/res/values-fr/strings.xml | 1 + app/src/main/res/values/arrays.xml | 2 + app/src/main/res/values/strings.xml | 1 + app/src/main/res/xml/preferences.xml | 2 +- 6 files changed, 41 insertions(+), 9 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 96c07aa3..0cd879c0 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -71,7 +71,7 @@ dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" implementation "androidx.work:work-runtime-ktx:2.4.0" implementation "androidx.fragment:fragment-ktx:1.2.3" - + implementation "androidx.browser:browser:1.2.0" testImplementation 'junit:junit:4.12' androidTestImplementation 'androidx.test.ext:junit:1.1.1' diff --git a/app/src/main/java/com/readrops/app/activities/ItemActivity.java b/app/src/main/java/com/readrops/app/activities/ItemActivity.java index 86462056..b995d9cc 100644 --- a/app/src/main/java/com/readrops/app/activities/ItemActivity.java +++ b/app/src/main/java/com/readrops/app/activities/ItemActivity.java @@ -22,6 +22,7 @@ import android.webkit.WebView; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.app.AppCompatActivity; +import androidx.browser.customtabs.CustomTabsIntent; import androidx.core.app.ActivityCompat; import androidx.core.app.ShareCompat; import androidx.lifecycle.ViewModelProvider; @@ -199,12 +200,7 @@ public class ItemActivity extends AppCompatActivity { shareArticle(); return true; case R.id.item_open: - int value = Integer.parseInt(SharedPreferencesManager.readString(this, - SharedPreferencesManager.SharedPrefKey.OPEN_ITEMS_IN)); - if (value == 0) - openInNavigator(); - else - openInWebView(); + openUrl(); return true; default: return super.onOptionsItemSelected(item); @@ -217,6 +213,22 @@ public class ItemActivity extends AppCompatActivity { super.onBackPressed(); } + private void openUrl() { + int value = Integer.parseInt(SharedPreferencesManager.readString(this, + SharedPreferencesManager.SharedPrefKey.OPEN_ITEMS_IN)); + switch (value) { + case 0: + openInNavigator(); + break; + case 1: + openInWebView(); + break; + default: + openInCustomTab(); + break; + } + } + private void openInNavigator() { Intent urlIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(itemWithFeed.getItem().getLink())); startActivity(urlIntent); @@ -225,11 +237,27 @@ public class ItemActivity extends AppCompatActivity { private void openInWebView() { Intent intent = new Intent(this, WebViewActivity.class); intent.putExtra(WEB_URL, itemWithFeed.getItem().getLink()); - intent.putExtra(ACTION_BAR_COLOR, itemWithFeed.getColor() != 0 ? itemWithFeed.getColor() : itemWithFeed.getBgColor()); + intent.putExtra(ACTION_BAR_COLOR, itemWithFeed.getBgColor() != 0 ? itemWithFeed.getBgColor() : itemWithFeed.getColor()); startActivity(intent); } + private void openInCustomTab() { + boolean darkTheme = Boolean.parseBoolean(SharedPreferencesManager.readString(this, SharedPreferencesManager.SharedPrefKey.DARK_THEME)); + int color = itemWithFeed.getBgColor() != 0 ? itemWithFeed.getBgColor() : itemWithFeed.getColor(); + + CustomTabsIntent customTabsIntent = new CustomTabsIntent.Builder() + .addDefaultShareMenuItem() + .setToolbarColor(color) + .setSecondaryToolbarColor(color) + .setColorScheme(darkTheme ? CustomTabsIntent.COLOR_SCHEME_DARK : CustomTabsIntent.COLOR_SCHEME_LIGHT) + .enableUrlBarHiding() + .setShowTitle(true) + .build(); + + customTabsIntent.launchUrl(this, Uri.parse(itemWithFeed.getItem().getLink())); + } + private void shareArticle() { Intent shareIntent = new Intent(Intent.ACTION_SEND); shareIntent.setType("text/plain"); diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index fe4f52b0..cc89f85b 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -132,5 +132,6 @@ Afficher la légende Votre mot de passe d\'API (Configuration > Profil) Synchroniser + Vue navigateur \ No newline at end of file diff --git a/app/src/main/res/values/arrays.xml b/app/src/main/res/values/arrays.xml index 824cd35c..a603ebf8 100644 --- a/app/src/main/res/values/arrays.xml +++ b/app/src/main/res/values/arrays.xml @@ -33,11 +33,13 @@ @string/external_navigator @string/webview + @string/navigator_view 0 1 + 2 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 077eaec1..8021181f 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -138,4 +138,5 @@ Back Show caption Synchronize + Navigator view diff --git a/app/src/main/res/xml/preferences.xml b/app/src/main/res/xml/preferences.xml index 9a14e305..625fc43a 100644 --- a/app/src/main/res/xml/preferences.xml +++ b/app/src/main/res/xml/preferences.xml @@ -16,7 +16,7 @@ android:title="@string/reload_feeds_colors" /> Date: Fri, 11 Sep 2020 22:53:50 +0200 Subject: [PATCH 025/187] Start replacing simplexml with konsume-xml by parsing RSS feed channel with it --- api/build.gradle | 14 +++--- .../localfeed/rss/rss_feed_special_cases.xml | 9 ++++ .../assets/localfeed/rss/rss_full_feed.xml | 9 ++++ .../api/localfeed/rss/RSSFeedAdapterTest.kt | 36 +++++++++++++++ .../api/localfeed/LocalRSSDataSource.kt | 24 ++++++++-- .../com/readrops/api/localfeed/XmlAdapter.kt | 20 ++++++++ .../api/localfeed/rss/RSSFeedAdapter.kt | 46 +++++++++++++++++++ .../readrops/api/utils/KonsumerExtensions.kt | 13 ++++++ 8 files changed, 160 insertions(+), 11 deletions(-) create mode 100644 api/src/androidTest/assets/localfeed/rss/rss_feed_special_cases.xml create mode 100644 api/src/androidTest/assets/localfeed/rss/rss_full_feed.xml create mode 100644 api/src/androidTest/java/com/readrops/api/localfeed/rss/RSSFeedAdapterTest.kt create mode 100644 api/src/main/java/com/readrops/api/localfeed/XmlAdapter.kt create mode 100644 api/src/main/java/com/readrops/api/localfeed/rss/RSSFeedAdapter.kt create mode 100644 api/src/main/java/com/readrops/api/utils/KonsumerExtensions.kt diff --git a/api/build.gradle b/api/build.gradle index ab07a8eb..6d0d0bca 100644 --- a/api/build.gradle +++ b/api/build.gradle @@ -43,11 +43,6 @@ dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) implementation project(':db') - // xpp3 has a conflict with kxml when running connectedCheck task - configurations { - all*.exclude group: 'xpp3', module: 'xpp3' - } - implementation "androidx.core:core-ktx:1.2.0" implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" @@ -58,6 +53,8 @@ dependencies { androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' androidTestImplementation 'com.squareup.okhttp3:mockwebserver:4.8.1' + implementation 'com.gitlab.mvysny.konsume-xml:konsume-xml:0.11' + implementation 'com.squareup.okhttp3:okhttp:4.8.1' implementation('com.squareup.retrofit2:retrofit:2.7.1') { @@ -67,7 +64,12 @@ dependencies { exclude group: 'moshi', module: 'moshi' // moshi converter uses moshi 1.8.0 which breaks codegen 1.9.2 } - implementation 'com.squareup.retrofit2:converter-simplexml:2.7.1' + implementation ('com.squareup.retrofit2:converter-simplexml:2.7.1') { + exclude module: 'stax' + exclude module: 'stax-api' + exclude module: 'xpp3' + } + implementation 'com.squareup.retrofit2:adapter-rxjava2:2.7.1' implementation 'com.squareup.moshi:moshi:1.9.2' diff --git a/api/src/androidTest/assets/localfeed/rss/rss_feed_special_cases.xml b/api/src/androidTest/assets/localfeed/rss/rss_feed_special_cases.xml new file mode 100644 index 00000000..4ec78959 --- /dev/null +++ b/api/src/androidTest/assets/localfeed/rss/rss_feed_special_cases.xml @@ -0,0 +1,9 @@ + + + + + + https://news.ycombinator.com/ + Links for the intellectually curious, ranked by readers. + + \ No newline at end of file diff --git a/api/src/androidTest/assets/localfeed/rss/rss_full_feed.xml b/api/src/androidTest/assets/localfeed/rss/rss_full_feed.xml new file mode 100644 index 00000000..671e4066 --- /dev/null +++ b/api/src/androidTest/assets/localfeed/rss/rss_full_feed.xml @@ -0,0 +1,9 @@ + + + + Hacker News + + https://news.ycombinator.com/ + Links for the intellectually curious, ranked by readers. + + \ No newline at end of file diff --git a/api/src/androidTest/java/com/readrops/api/localfeed/rss/RSSFeedAdapterTest.kt b/api/src/androidTest/java/com/readrops/api/localfeed/rss/RSSFeedAdapterTest.kt new file mode 100644 index 00000000..79fbcafb --- /dev/null +++ b/api/src/androidTest/java/com/readrops/api/localfeed/rss/RSSFeedAdapterTest.kt @@ -0,0 +1,36 @@ +package com.readrops.api.localfeed.rss + +import android.content.Context +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry +import com.readrops.api.utils.ParseException +import junit.framework.TestCase.assertEquals +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class RSSFeedAdapterTest { + + private val context: Context = InstrumentationRegistry.getInstrumentation().context + + private val adapter = RSSFeedAdapter() + + @Test + fun normalCasesTest() { + val stream = context.resources.assets.open("localfeed/rss/rss_full_feed.xml") + + val feed = adapter.fromXml(stream) + + assertEquals(feed.name, "Hacker News") + assertEquals(feed.url, "https://news.ycombinator.com/feed/") + assertEquals(feed.siteUrl, "https://news.ycombinator.com/") + assertEquals(feed.description, "Links for the intellectually curious, ranked by readers.") + } + + + @Test(expected = ParseException::class) + fun nullTitleTest() { + val stream = context.resources.assets.open("localfeed/rss/rss_feed_special_cases.xml") + adapter.fromXml(stream) + } +} \ No newline at end of file diff --git a/api/src/main/java/com/readrops/api/localfeed/LocalRSSDataSource.kt b/api/src/main/java/com/readrops/api/localfeed/LocalRSSDataSource.kt index a6f2a007..896213fe 100644 --- a/api/src/main/java/com/readrops/api/localfeed/LocalRSSDataSource.kt +++ b/api/src/main/java/com/readrops/api/localfeed/LocalRSSDataSource.kt @@ -4,6 +4,7 @@ import android.accounts.NetworkErrorException import androidx.annotation.WorkerThread import com.readrops.api.utils.LibUtils import com.readrops.api.utils.ParseException +import com.readrops.api.utils.UnknownFormatException import com.readrops.db.entities.Feed import com.readrops.db.entities.Item import okhttp3.Headers @@ -33,19 +34,21 @@ class LocalRSSDataSource(private val httpClient: OkHttpClient) { ?: throw ParseException("Unable to get $url content-type") val contentType = LibUtils.parseContentType(header) - ?: throw ParseException("Unable to get $url content-type") + ?: throw ParseException("Unable to parse $url content-type") var type = LocalRSSHelper.getRSSType(contentType) // if we can't guess type based on content-type header, we use the content - if (type == LocalRSSHelper.RSSType.UNKNOWN) type = LocalRSSHelper.getRSSContentType(response.body?.byteStream()!!) + if (type == LocalRSSHelper.RSSType.UNKNOWN) + type = LocalRSSHelper.getRSSContentType(response.body?.byteStream()!!) // if we can't guess type even with the content, we are unable to go further if (type == LocalRSSHelper.RSSType.UNKNOWN) throw ParseException("Unable to guess $url RSS type") val feed = parseFeed(response, type) val items = if (withItems) parseItems(response.body?.byteStream()!!, type) else listOf() - return Pair(feed, items) + response.body?.close() + Pair(feed, items) } response.code == HttpURLConnection.HTTP_NOT_MODIFIED -> null else -> throw NetworkErrorException("$url returned ${response.code} code : ${response.message}") @@ -78,8 +81,19 @@ class LocalRSSDataSource(private val httpClient: OkHttpClient) { } private fun parseFeed(response: Response, type: LocalRSSHelper.RSSType): Feed { - response.body?.close() - return Feed() + val feed = if (type != LocalRSSHelper.RSSType.JSONFEED) { + val adapter = XmlAdapter.xmlFeedAdapterFactory(type) + + //adapter.fromXml(response.body?.byteStream()!!) + Feed() + } else { + Feed() + } + + feed.etag = response.header(LibUtils.ETAG_HEADER) + feed.lastModified = response.header(LibUtils.IF_MODIFIED_HEADER) + + return feed } private fun parseItems(inputStream: InputStream, type: LocalRSSHelper.RSSType): List { diff --git a/api/src/main/java/com/readrops/api/localfeed/XmlAdapter.kt b/api/src/main/java/com/readrops/api/localfeed/XmlAdapter.kt new file mode 100644 index 00000000..824d8005 --- /dev/null +++ b/api/src/main/java/com/readrops/api/localfeed/XmlAdapter.kt @@ -0,0 +1,20 @@ +package com.readrops.api.localfeed + +import com.readrops.api.localfeed.rss.RSSFeedAdapter +import com.readrops.db.entities.Feed +import java.io.InputStream + +interface XmlAdapter { + + fun fromXml(inputStream: InputStream): T + + companion object { + fun xmlFeedAdapterFactory(type: LocalRSSHelper.RSSType): XmlAdapter { + return when (type) { + LocalRSSHelper.RSSType.RSS_2 -> RSSFeedAdapter() + else -> throw Exception("Unknown RSS type : $type") + } + } + } +} + diff --git a/api/src/main/java/com/readrops/api/localfeed/rss/RSSFeedAdapter.kt b/api/src/main/java/com/readrops/api/localfeed/rss/RSSFeedAdapter.kt new file mode 100644 index 00000000..52c6fcff --- /dev/null +++ b/api/src/main/java/com/readrops/api/localfeed/rss/RSSFeedAdapter.kt @@ -0,0 +1,46 @@ +package com.readrops.api.localfeed.rss + +import com.gitlab.mvysny.konsumexml.Names +import com.gitlab.mvysny.konsumexml.allChildrenAutoIgnore +import com.gitlab.mvysny.konsumexml.konsumeXml +import com.readrops.api.localfeed.XmlAdapter +import com.readrops.api.utils.ParseException +import com.readrops.api.utils.nonNullText +import com.readrops.api.utils.nullableText +import com.readrops.db.entities.Feed +import org.jsoup.Jsoup +import java.io.InputStream + +class RSSFeedAdapter : XmlAdapter { + + override fun fromXml(inputStream: InputStream): Feed { + val konsume = inputStream.konsumeXml() + val feed = Feed() + + return try { + konsume.child("rss") { + child("channel") { + allChildrenAutoIgnore(names) { + with(feed) { + when (tagName) { + "title" -> name = Jsoup.parse(nonNullText()).text() + "description" -> description = nullableText(failOnElement = false) + "link" -> siteUrl = nullableText() + "atom:link" -> url = attributes.getValueOpt("href") + } + } + } + } + } + + konsume.close() + feed + } catch (e: Exception) { + throw ParseException(e.message) + } + } + + companion object { + val names = Names.of("title", "description", "link") + } +} \ No newline at end of file diff --git a/api/src/main/java/com/readrops/api/utils/KonsumerExtensions.kt b/api/src/main/java/com/readrops/api/utils/KonsumerExtensions.kt new file mode 100644 index 00000000..adef66de --- /dev/null +++ b/api/src/main/java/com/readrops/api/utils/KonsumerExtensions.kt @@ -0,0 +1,13 @@ +package com.readrops.api.utils + +import com.gitlab.mvysny.konsumexml.Konsumer + +fun Konsumer.nonNullText(failOnElement: Boolean = true): String { + val text = text(failOnElement = failOnElement) + return if (text.isNotEmpty()) text else throw ParseException("Xml field $name can't be null") +} + +fun Konsumer.nullableText(failOnElement: Boolean = true): String? { + val text = text(failOnElement = failOnElement) + return if (text.isNotEmpty()) text else null +} \ No newline at end of file From 4bf56b8f7f1c2502a8525cd964f27c7bc0fbac46 Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Sat, 12 Sep 2020 12:02:03 +0200 Subject: [PATCH 026/187] Use UnknownFormatException instead of ParseException for some cases --- .../com/readrops/api/localfeed/LocalRSSDataSourceTest.kt | 3 ++- .../java/com/readrops/api/localfeed/LocalRSSDataSource.kt | 2 +- .../main/java/com/readrops/api/localfeed/LocalRSSHelper.kt | 6 +++--- .../java/com/readrops/api/localfeed/LocalRSSHelperTest.kt | 4 ++-- 4 files changed, 8 insertions(+), 7 deletions(-) diff --git a/api/src/androidTest/java/com/readrops/api/localfeed/LocalRSSDataSourceTest.kt b/api/src/androidTest/java/com/readrops/api/localfeed/LocalRSSDataSourceTest.kt index 2d0b3db7..aa6b7ee2 100644 --- a/api/src/androidTest/java/com/readrops/api/localfeed/LocalRSSDataSourceTest.kt +++ b/api/src/androidTest/java/com/readrops/api/localfeed/LocalRSSDataSourceTest.kt @@ -6,6 +6,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.platform.app.InstrumentationRegistry import com.readrops.api.utils.HttpManager import com.readrops.api.utils.ParseException +import com.readrops.api.utils.UnknownFormatException import junit.framework.TestCase.* import okhttp3.HttpUrl import okhttp3.mockwebserver.MockResponse @@ -81,7 +82,7 @@ class LocalRSSDataSourceTest { localRSSDataSource.queryRSSResource(url.toString(), null, false) } - @Test(expected = ParseException::class) + @Test(expected = UnknownFormatException::class) fun badContentTest() { mockServer.enqueue(MockResponse().setResponseCode(HttpURLConnection.HTTP_OK) .addHeader("Content-Type", "application/xml") diff --git a/api/src/main/java/com/readrops/api/localfeed/LocalRSSDataSource.kt b/api/src/main/java/com/readrops/api/localfeed/LocalRSSDataSource.kt index 896213fe..af72392d 100644 --- a/api/src/main/java/com/readrops/api/localfeed/LocalRSSDataSource.kt +++ b/api/src/main/java/com/readrops/api/localfeed/LocalRSSDataSource.kt @@ -42,7 +42,7 @@ class LocalRSSDataSource(private val httpClient: OkHttpClient) { if (type == LocalRSSHelper.RSSType.UNKNOWN) type = LocalRSSHelper.getRSSContentType(response.body?.byteStream()!!) // if we can't guess type even with the content, we are unable to go further - if (type == LocalRSSHelper.RSSType.UNKNOWN) throw ParseException("Unable to guess $url RSS type") + if (type == LocalRSSHelper.RSSType.UNKNOWN) throw UnknownFormatException("Unable to guess $url RSS type") val feed = parseFeed(response, type) val items = if (withItems) parseItems(response.body?.byteStream()!!, type) else listOf() diff --git a/api/src/main/java/com/readrops/api/localfeed/LocalRSSHelper.kt b/api/src/main/java/com/readrops/api/localfeed/LocalRSSHelper.kt index f3cc57f3..3d4a42b5 100644 --- a/api/src/main/java/com/readrops/api/localfeed/LocalRSSHelper.kt +++ b/api/src/main/java/com/readrops/api/localfeed/LocalRSSHelper.kt @@ -1,6 +1,6 @@ package com.readrops.api.localfeed -import com.readrops.api.utils.ParseException +import com.readrops.api.utils.UnknownFormatException import java.io.InputStream import java.util.regex.Pattern @@ -12,7 +12,7 @@ object LocalRSSHelper { private const val ATOM_CONTENT_TYPE = "application/atom+xml" private const val JSON_CONTENT_TYPE = "application/json" private const val HTML_CONTENT_TYPE = "text/html" - + private const val RSS_2_REGEX = "rss.*version=\"2.0\"" private const val ATOM_REGEX = " RSSType.ATOM JSON_CONTENT_TYPE -> RSSType.JSONFEED RSS_TEXT_CONTENT_TYPE, RSS_APPLICATION_CONTENT_TYPE, HTML_CONTENT_TYPE -> RSSType.UNKNOWN - else -> throw ParseException("Unknown content type") + else -> throw UnknownFormatException("Unknown content type : $contentType") } } diff --git a/api/src/test/java/com/readrops/api/localfeed/LocalRSSHelperTest.kt b/api/src/test/java/com/readrops/api/localfeed/LocalRSSHelperTest.kt index a5678bbc..c35c9e5e 100644 --- a/api/src/test/java/com/readrops/api/localfeed/LocalRSSHelperTest.kt +++ b/api/src/test/java/com/readrops/api/localfeed/LocalRSSHelperTest.kt @@ -1,6 +1,6 @@ package com.readrops.api.localfeed -import com.readrops.api.utils.ParseException +import com.readrops.api.utils.UnknownFormatException import junit.framework.TestCase.assertEquals import org.junit.Test import java.io.ByteArrayInputStream @@ -27,7 +27,7 @@ class LocalRSSHelperTest { LocalRSSHelper.RSSType.UNKNOWN) } - @Test(expected = ParseException::class) + @Test(expected = UnknownFormatException::class) fun nonSupportedContentTypeTest() { LocalRSSHelper.getRSSType("image/jpeg") } From 20e814f36d764d7b3ebe090e23baad38e6521dc0 Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Sat, 12 Sep 2020 19:34:03 +0200 Subject: [PATCH 027/187] Add adapter for RSS item parsing --- .../localfeed/rss/rss_items_enclosure.xml | 25 ++++++ .../localfeed/rss/rss_items_media_content.xml | 24 ++++++ .../localfeed/rss/rss_items_no_date.xml | 21 +++++ .../localfeed/rss/rss_items_no_link.xml | 21 +++++ .../localfeed/rss/rss_items_no_title.xml | 21 +++++ .../rss/rss_items_other_namespaces.xml | 23 ++++++ .../androidTest/assets/localfeed/rss_feed.xml | 1 + .../api/localfeed/rss/RSSItemsAdapterTest.kt | 82 +++++++++++++++++++ .../api/localfeed/LocalRSSDataSource.kt | 9 +- .../com/readrops/api/localfeed/XmlAdapter.kt | 9 ++ .../api/localfeed/rss/RSSItemsAdapter.kt | 82 +++++++++++++++++++ 11 files changed, 317 insertions(+), 1 deletion(-) create mode 100644 api/src/androidTest/assets/localfeed/rss/rss_items_enclosure.xml create mode 100644 api/src/androidTest/assets/localfeed/rss/rss_items_media_content.xml create mode 100644 api/src/androidTest/assets/localfeed/rss/rss_items_no_date.xml create mode 100644 api/src/androidTest/assets/localfeed/rss/rss_items_no_link.xml create mode 100644 api/src/androidTest/assets/localfeed/rss/rss_items_no_title.xml create mode 100644 api/src/androidTest/assets/localfeed/rss/rss_items_other_namespaces.xml create mode 100644 api/src/androidTest/java/com/readrops/api/localfeed/rss/RSSItemsAdapterTest.kt create mode 100644 api/src/main/java/com/readrops/api/localfeed/rss/RSSItemsAdapter.kt diff --git a/api/src/androidTest/assets/localfeed/rss/rss_items_enclosure.xml b/api/src/androidTest/assets/localfeed/rss/rss_items_enclosure.xml new file mode 100644 index 00000000..58601828 --- /dev/null +++ b/api/src/androidTest/assets/localfeed/rss/rss_items_enclosure.xml @@ -0,0 +1,25 @@ + + + + + title + link + + 2020-08-05T14:03:48Z + + + + + + + guid + + + + + + + + \ No newline at end of file diff --git a/api/src/androidTest/assets/localfeed/rss/rss_items_media_content.xml b/api/src/androidTest/assets/localfeed/rss/rss_items_media_content.xml new file mode 100644 index 00000000..323246c7 --- /dev/null +++ b/api/src/androidTest/assets/localfeed/rss/rss_items_media_content.xml @@ -0,0 +1,24 @@ + + + + + title + link + + 2020-08-05T14:03:48Z + + + + + + + guid + + + + + + + \ No newline at end of file diff --git a/api/src/androidTest/assets/localfeed/rss/rss_items_no_date.xml b/api/src/androidTest/assets/localfeed/rss/rss_items_no_date.xml new file mode 100644 index 00000000..3ee2e090 --- /dev/null +++ b/api/src/androidTest/assets/localfeed/rss/rss_items_no_date.xml @@ -0,0 +1,21 @@ + + + + + title + link + + + + + + + + guid + + + + + + \ No newline at end of file diff --git a/api/src/androidTest/assets/localfeed/rss/rss_items_no_link.xml b/api/src/androidTest/assets/localfeed/rss/rss_items_no_link.xml new file mode 100644 index 00000000..69c76fcd --- /dev/null +++ b/api/src/androidTest/assets/localfeed/rss/rss_items_no_link.xml @@ -0,0 +1,21 @@ + + + + + title + + 2020-08-05T14:03:48Z + + + + + + + guid + + + + + + \ No newline at end of file diff --git a/api/src/androidTest/assets/localfeed/rss/rss_items_no_title.xml b/api/src/androidTest/assets/localfeed/rss/rss_items_no_title.xml new file mode 100644 index 00000000..a602c8d5 --- /dev/null +++ b/api/src/androidTest/assets/localfeed/rss/rss_items_no_title.xml @@ -0,0 +1,21 @@ + + + + + link + + 2020-08-05T14:03:48Z + + + + + + + guid + + + + + + \ No newline at end of file diff --git a/api/src/androidTest/assets/localfeed/rss/rss_items_other_namespaces.xml b/api/src/androidTest/assets/localfeed/rss/rss_items_other_namespaces.xml new file mode 100644 index 00000000..cde19568 --- /dev/null +++ b/api/src/androidTest/assets/localfeed/rss/rss_items_other_namespaces.xml @@ -0,0 +1,23 @@ + + + + + title + link + guid + + 2020-08-05T14:03:48Z + + + + + + + guid + + + + + + \ No newline at end of file diff --git a/api/src/androidTest/assets/localfeed/rss_feed.xml b/api/src/androidTest/assets/localfeed/rss_feed.xml index 8c012917..fbd76101 100644 --- a/api/src/androidTest/assets/localfeed/rss_feed.xml +++ b/api/src/androidTest/assets/localfeed/rss_feed.xml @@ -9,6 +9,7 @@ https://www.bbc.com/news/world-africa-53887947 Tue, 25 Aug 2020 17:15:49 +0000 https://news.ycombinator.com/item?id=24273602 + Author 1 Comments]]> diff --git a/api/src/androidTest/java/com/readrops/api/localfeed/rss/RSSItemsAdapterTest.kt b/api/src/androidTest/java/com/readrops/api/localfeed/rss/RSSItemsAdapterTest.kt new file mode 100644 index 00000000..2358d872 --- /dev/null +++ b/api/src/androidTest/java/com/readrops/api/localfeed/rss/RSSItemsAdapterTest.kt @@ -0,0 +1,82 @@ +package com.readrops.api.localfeed.rss + +import android.content.Context +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry +import com.readrops.api.utils.DateUtils +import com.readrops.api.utils.ParseException +import junit.framework.TestCase.assertEquals +import junit.framework.TestCase.assertNotNull +import org.junit.Assert +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class RSSItemsAdapterTest { + + private val context: Context = InstrumentationRegistry.getInstrumentation().context + + private val adapter = RSSItemsAdapter() + + @Test + fun normalCasesTest() { + val stream = context.resources.assets.open("localfeed/rss_feed.xml") + + val items = adapter.fromXml(stream) + assertEquals(items.size, 7) + + val item = items[0] + + assertEquals(item.title, "Africa declared free of wild polio") + assertEquals(item.link, "https://www.bbc.com/news/world-africa-53887947") + assertEquals(item.pubDate, DateUtils.stringToLocalDateTime("Tue, 25 Aug 2020 17:15:49 +0000")) + assertEquals(item.author, "Author 1") + assertNotNull(item.description) + assertEquals(item.guid, "https://www.bbc.com/news/world-africa-53887947") + } + + @Test + fun otherNamespacesTest() { + val stream = context.resources.assets.open("localfeed/rss/rss_items_other_namespaces.xml") + val item = adapter.fromXml(stream)[0] + + assertEquals(item.guid, "guid") + assertEquals(item.author, "creator") + assertEquals(item.pubDate, DateUtils.stringToLocalDateTime("2020-08-05T14:03:48Z")) + assertEquals(item.content, "content:encoded") + } + + @Test + fun noTitleTest() { + val stream = context.resources.assets.open("localfeed/rss/rss_items_no_title.xml") + Assert.assertThrows("Item title can't be null", ParseException::class.java) { adapter.fromXml(stream) } + } + + @Test + fun noLinkTest() { + val stream = context.resources.assets.open("localfeed/rss/rss_items_no_link.xml") + Assert.assertThrows("Item link can't be null", ParseException::class.java) { adapter.fromXml(stream) } + } + + @Test + fun noDateTest() { + val stream = context.resources.assets.open("localfeed/rss/rss_items_no_date.xml") + Assert.assertThrows("Item date can't be null", ParseException::class.java) { adapter.fromXml(stream) } + } + + @Test + fun enclosureTest() { + val stream = context.resources.assets.open("localfeed/rss/rss_items_enclosure.xml") + val item = adapter.fromXml(stream)[0] + + assertEquals(item.imageLink, "https://image1.jpg") + } + + @Test + fun mediaContentTest() { + val stream = context.resources.assets.open("localfeed/rss/rss_items_media_content.xml") + val item = adapter.fromXml(stream)[0] + + assertEquals(item.imageLink, "https://image2.jpg") + } +} \ No newline at end of file diff --git a/api/src/main/java/com/readrops/api/localfeed/LocalRSSDataSource.kt b/api/src/main/java/com/readrops/api/localfeed/LocalRSSDataSource.kt index af72392d..32ce19c4 100644 --- a/api/src/main/java/com/readrops/api/localfeed/LocalRSSDataSource.kt +++ b/api/src/main/java/com/readrops/api/localfeed/LocalRSSDataSource.kt @@ -97,6 +97,13 @@ class LocalRSSDataSource(private val httpClient: OkHttpClient) { } private fun parseItems(inputStream: InputStream, type: LocalRSSHelper.RSSType): List { - return listOf() + return if (type != LocalRSSHelper.RSSType.JSONFEED) { + val adapter = XmlAdapter.xmlItemsAdapterFactory(type) + + //adapter.fromXml(inputStream) + listOf() + } else { + listOf() + } } } \ No newline at end of file diff --git a/api/src/main/java/com/readrops/api/localfeed/XmlAdapter.kt b/api/src/main/java/com/readrops/api/localfeed/XmlAdapter.kt index 824d8005..874d2944 100644 --- a/api/src/main/java/com/readrops/api/localfeed/XmlAdapter.kt +++ b/api/src/main/java/com/readrops/api/localfeed/XmlAdapter.kt @@ -1,7 +1,9 @@ package com.readrops.api.localfeed import com.readrops.api.localfeed.rss.RSSFeedAdapter +import com.readrops.api.localfeed.rss.RSSItemsAdapter import com.readrops.db.entities.Feed +import com.readrops.db.entities.Item import java.io.InputStream interface XmlAdapter { @@ -15,6 +17,13 @@ interface XmlAdapter { else -> throw Exception("Unknown RSS type : $type") } } + + fun xmlItemsAdapterFactory(type: LocalRSSHelper.RSSType): XmlAdapter> { + return when (type) { + LocalRSSHelper.RSSType.RSS_2 -> RSSItemsAdapter() + else -> throw Exception("Unknown RSS type : $type") + } + } } } diff --git a/api/src/main/java/com/readrops/api/localfeed/rss/RSSItemsAdapter.kt b/api/src/main/java/com/readrops/api/localfeed/rss/RSSItemsAdapter.kt new file mode 100644 index 00000000..3a069b19 --- /dev/null +++ b/api/src/main/java/com/readrops/api/localfeed/rss/RSSItemsAdapter.kt @@ -0,0 +1,82 @@ +package com.readrops.api.localfeed.rss + +import com.gitlab.mvysny.konsumexml.* +import com.readrops.api.localfeed.XmlAdapter +import com.readrops.api.utils.* +import com.readrops.db.entities.Item +import java.io.InputStream + +class RSSItemsAdapter : XmlAdapter> { + + override fun fromXml(inputStream: InputStream): List { + val konsume = inputStream.konsumeXml() + val items = mutableListOf() + + return try { + konsume.child("rss") { + child("channel") { + allChildrenAutoIgnore("item") { + val enclosures = arrayListOf() + val mediaContents = arrayListOf() + + val item = Item().apply { + allChildrenAutoIgnore(names) { + when (tagName) { + "title" -> title = nonNullText() + "link" -> link = nonNullText() + "author" -> author = nullableText() + "dc:creator" -> author = nullableText() + "pubDate" -> pubDate = DateUtils.stringToLocalDateTime(nonNullText()) + "dc:date" -> pubDate = DateUtils.stringToLocalDateTime(nonNullText()) + "guid" -> guid = nullableText() + "description" -> description = nullableText() + "content:encoded" -> content = nullableText() + "enclosure" -> parseEnclosure(this, enclosures) + "media:content" -> parseMediaContent(this, mediaContents) + } + } + } + + validateItem(item) + if (item.guid == null) item.guid = item.link + + if (enclosures.isNotEmpty()) item.imageLink = enclosures.first() + else if (mediaContents.isNotEmpty()) item.imageLink = mediaContents.first() + + items += item + } + } + } + + konsume.close() + items + } catch (e: KonsumerException) { + throw ParseException(e.message) + } + } + + private fun parseEnclosure(konsume: Konsumer, enclosures: MutableList) { + if (konsume.attributes.getValueOpt("type") != null + && LibUtils.isMimeImage(konsume.attributes["type"])) + enclosures += konsume.attributes["url"] + } + + private fun parseMediaContent(konsume: Konsumer, mediaContents: MutableList) { + if (konsume.attributes.getValueOpt("medium") != null + && LibUtils.isMimeImage(konsume.attributes["medium"])) + mediaContents += konsume.attributes["url"] + } + + private fun validateItem(item: Item) { + when { + item.title == null -> throw ParseException("Item title can't be null") + item.link == null -> throw ParseException("Item link can't be null") + item.pubDate == null -> throw ParseException("Item date can't be null") + } + } + + companion object { + val names = Names.of("title", "link", "author", "creator", "pubDate", "date", + "guid", "description", "encoded", "enclosure", "content") + } +} \ No newline at end of file From 8e5900833bfa75a6d61cde9eef53ca2fc637deb0 Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Sun, 13 Sep 2020 12:28:33 +0200 Subject: [PATCH 028/187] Support multiple creator elements in RSS items --- .../assets/localfeed/rss/rss_items_other_namespaces.xml | 7 ++++++- .../com/readrops/api/localfeed/rss/RSSItemsAdapterTest.kt | 2 +- .../java/com/readrops/api/localfeed/rss/RSSItemsAdapter.kt | 5 ++++- 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/api/src/androidTest/assets/localfeed/rss/rss_items_other_namespaces.xml b/api/src/androidTest/assets/localfeed/rss/rss_items_other_namespaces.xml index cde19568..05bb2187 100644 --- a/api/src/androidTest/assets/localfeed/rss/rss_items_other_namespaces.xml +++ b/api/src/androidTest/assets/localfeed/rss/rss_items_other_namespaces.xml @@ -6,7 +6,12 @@ title link guid - + + + + + + 2020-08-05T14:03:48Z diff --git a/api/src/androidTest/java/com/readrops/api/localfeed/rss/RSSItemsAdapterTest.kt b/api/src/androidTest/java/com/readrops/api/localfeed/rss/RSSItemsAdapterTest.kt index 2358d872..7644cc24 100644 --- a/api/src/androidTest/java/com/readrops/api/localfeed/rss/RSSItemsAdapterTest.kt +++ b/api/src/androidTest/java/com/readrops/api/localfeed/rss/RSSItemsAdapterTest.kt @@ -41,7 +41,7 @@ class RSSItemsAdapterTest { val item = adapter.fromXml(stream)[0] assertEquals(item.guid, "guid") - assertEquals(item.author, "creator") + assertEquals(item.author, "creator 1") assertEquals(item.pubDate, DateUtils.stringToLocalDateTime("2020-08-05T14:03:48Z")) assertEquals(item.content, "content:encoded") } diff --git a/api/src/main/java/com/readrops/api/localfeed/rss/RSSItemsAdapter.kt b/api/src/main/java/com/readrops/api/localfeed/rss/RSSItemsAdapter.kt index 3a069b19..ea1736dd 100644 --- a/api/src/main/java/com/readrops/api/localfeed/rss/RSSItemsAdapter.kt +++ b/api/src/main/java/com/readrops/api/localfeed/rss/RSSItemsAdapter.kt @@ -18,6 +18,7 @@ class RSSItemsAdapter : XmlAdapter> { allChildrenAutoIgnore("item") { val enclosures = arrayListOf() val mediaContents = arrayListOf() + val creators = arrayListOf() val item = Item().apply { allChildrenAutoIgnore(names) { @@ -25,7 +26,7 @@ class RSSItemsAdapter : XmlAdapter> { "title" -> title = nonNullText() "link" -> link = nonNullText() "author" -> author = nullableText() - "dc:creator" -> author = nullableText() + "dc:creator" -> creators += nullableText() "pubDate" -> pubDate = DateUtils.stringToLocalDateTime(nonNullText()) "dc:date" -> pubDate = DateUtils.stringToLocalDateTime(nonNullText()) "guid" -> guid = nullableText() @@ -39,6 +40,8 @@ class RSSItemsAdapter : XmlAdapter> { validateItem(item) if (item.guid == null) item.guid = item.link + if (item.author == null && creators.filterNotNull().isNotEmpty()) + item.author = creators.filterNotNull().first() if (enclosures.isNotEmpty()) item.imageLink = enclosures.first() else if (mediaContents.isNotEmpty()) item.imageLink = mediaContents.first() From 15e1723893ef8ccf016f0166c5eea756877e3f6b Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Sun, 13 Sep 2020 12:38:51 +0200 Subject: [PATCH 029/187] Move cleanText method to api package --- .../readrops/api/localfeed/rss/RSSItemsAdapter.kt | 2 +- .../java/com/readrops/api/utils/LibUtils.java | 12 ++++++++++++ .../java/com/readrops/api/utils/LibUtilsTest.kt | 6 ++++++ .../main/java/com/readrops/app/utils/Utils.java | 12 ------------ .../readrops/app/utils/matchers/ItemMatcher.java | 15 ++++++++------- app/src/test/java/com/readrops/app/UtilsTest.java | 8 -------- 6 files changed, 27 insertions(+), 28 deletions(-) diff --git a/api/src/main/java/com/readrops/api/localfeed/rss/RSSItemsAdapter.kt b/api/src/main/java/com/readrops/api/localfeed/rss/RSSItemsAdapter.kt index ea1736dd..e2344061 100644 --- a/api/src/main/java/com/readrops/api/localfeed/rss/RSSItemsAdapter.kt +++ b/api/src/main/java/com/readrops/api/localfeed/rss/RSSItemsAdapter.kt @@ -23,7 +23,7 @@ class RSSItemsAdapter : XmlAdapter> { val item = Item().apply { allChildrenAutoIgnore(names) { when (tagName) { - "title" -> title = nonNullText() + "title" -> title = LibUtils.cleanText(nonNullText()) "link" -> link = nonNullText() "author" -> author = nullableText() "dc:creator" -> creators += nullableText() diff --git a/api/src/main/java/com/readrops/api/utils/LibUtils.java b/api/src/main/java/com/readrops/api/utils/LibUtils.java index 88c238d2..cf7a6813 100644 --- a/api/src/main/java/com/readrops/api/utils/LibUtils.java +++ b/api/src/main/java/com/readrops/api/utils/LibUtils.java @@ -6,6 +6,8 @@ import android.net.Uri; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import org.jsoup.Jsoup; + import java.io.FileNotFoundException; import java.io.InputStream; import java.util.Scanner; @@ -61,4 +63,14 @@ public final class LibUtils { return null; } } + + /** + * Remove html tags and trim the text + * + * @param text string to clean + * @return cleaned text + */ + public static String cleanText(String text) { + return Jsoup.parse(text).text().trim(); + } } diff --git a/api/src/test/java/com/readrops/api/utils/LibUtilsTest.kt b/api/src/test/java/com/readrops/api/utils/LibUtilsTest.kt index a6801cae..ba6b8bc4 100644 --- a/api/src/test/java/com/readrops/api/utils/LibUtilsTest.kt +++ b/api/src/test/java/com/readrops/api/utils/LibUtilsTest.kt @@ -16,4 +16,10 @@ class LibUtilsTest { assertEquals(LibUtils.parseContentType("text/xml"), "text/xml") } + + @Test + fun cleanTextTest() { + val text = "

This is a text
to

clean " + assertEquals("This is a text to clean", LibUtils.cleanText(text)) + } } \ No newline at end of file diff --git a/app/src/main/java/com/readrops/app/utils/Utils.java b/app/src/main/java/com/readrops/app/utils/Utils.java index ea220f4e..7da6514a 100644 --- a/app/src/main/java/com/readrops/app/utils/Utils.java +++ b/app/src/main/java/com/readrops/app/utils/Utils.java @@ -17,8 +17,6 @@ import androidx.annotation.NonNull; import com.google.android.material.snackbar.Snackbar; import com.readrops.api.utils.HttpManager; -import org.jsoup.Jsoup; - import java.io.InputStream; import java.util.Locale; @@ -97,16 +95,6 @@ public final class Utils { snackbar.show(); } - /** - * Remove html tags and trim the text - * - * @param text string to clean - * @return cleaned text - */ - public static String cleanText(String text) { - return Jsoup.parse(text).text().trim(); - } - public static Bitmap getBitmapFromDrawable(Drawable drawable) { Bitmap bitmap = Bitmap.createBitmap(drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight(), Bitmap.Config.ARGB_8888); diff --git a/app/src/main/java/com/readrops/app/utils/matchers/ItemMatcher.java b/app/src/main/java/com/readrops/app/utils/matchers/ItemMatcher.java index 651fa211..58803743 100644 --- a/app/src/main/java/com/readrops/app/utils/matchers/ItemMatcher.java +++ b/app/src/main/java/com/readrops/app/utils/matchers/ItemMatcher.java @@ -1,15 +1,16 @@ package com.readrops.app.utils.matchers; -import com.readrops.api.utils.DateUtils; -import com.readrops.app.utils.Utils; -import com.readrops.db.entities.Feed; -import com.readrops.db.entities.Item; import com.readrops.api.localfeed.atom.ATOMEntry; import com.readrops.api.localfeed.json.JSONItem; import com.readrops.api.localfeed.rss.RSSEnclosure; import com.readrops.api.localfeed.rss.RSSItem; import com.readrops.api.localfeed.rss.RSSMediaContent; +import com.readrops.api.utils.DateUtils; +import com.readrops.api.utils.LibUtils; import com.readrops.api.utils.ParseException; +import com.readrops.app.utils.Utils; +import com.readrops.db.entities.Feed; +import com.readrops.db.entities.Item; import java.util.ArrayList; import java.util.List; @@ -26,7 +27,7 @@ public final class ItemMatcher { newItem.setContent(item.getContent()); // Jsoup.clean(item.getContent(), Whitelist.relaxed()) newItem.setDescription(item.getDescription()); newItem.setGuid(item.getGuid() != null ? item.getGuid() : item.getLink()); - newItem.setTitle(Utils.cleanText(item.getTitle())); + newItem.setTitle(LibUtils.cleanText(item.getTitle())); try { newItem.setPubDate(DateUtils.stringToLocalDateTime(item.getDate())); @@ -72,7 +73,7 @@ public final class ItemMatcher { dbItem.setContent(item.getContent()); // Jsoup.clean(item.getContent(), Whitelist.relaxed()) dbItem.setDescription(item.getSummary()); dbItem.setGuid(item.getId()); - dbItem.setTitle(Utils.cleanText(item.getTitle())); + dbItem.setTitle(LibUtils.cleanText(item.getTitle())); try { dbItem.setPubDate(DateUtils.stringToLocalDateTime(item.getUpdated())); @@ -102,7 +103,7 @@ public final class ItemMatcher { dbItem.setContent(item.getContent()); // Jsoup.clean(item.getContent(), Whitelist.relaxed()) dbItem.setDescription(item.getSummary()); dbItem.setGuid(item.getId()); - dbItem.setTitle(Utils.cleanText(item.getTitle())); + dbItem.setTitle(LibUtils.cleanText(item.getTitle())); try { dbItem.setPubDate(DateUtils.stringToLocalDateTime(item.getPubDate())); diff --git a/app/src/test/java/com/readrops/app/UtilsTest.java b/app/src/test/java/com/readrops/app/UtilsTest.java index b15e86a8..f9c4eb22 100644 --- a/app/src/test/java/com/readrops/app/UtilsTest.java +++ b/app/src/test/java/com/readrops/app/UtilsTest.java @@ -7,17 +7,9 @@ import com.readrops.app.utils.Utils; import org.junit.Test; import static junit.framework.Assert.assertTrue; -import static junit.framework.TestCase.assertEquals; public class UtilsTest { - @Test - public void cleanTextTest() { - String text = "

This is a text
to

clean "; - - assertEquals("This is a text to clean", Utils.cleanText(text)); - } - @Test public void colorTooBrightTest() { assertTrue(Utils.isColorTooBright(-986896)); From 156601e0c5b1ababdf87b013ce5b5867f485e768 Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Sun, 13 Sep 2020 15:15:42 +0200 Subject: [PATCH 030/187] Use multiple streams based on a byteArray for parsing the body response --- .../api/localfeed/LocalRSSDataSourceTest.kt | 38 +++++++++++++++++-- .../api/localfeed/LocalRSSDataSource.kt | 19 +++++----- 2 files changed, 44 insertions(+), 13 deletions(-) diff --git a/api/src/androidTest/java/com/readrops/api/localfeed/LocalRSSDataSourceTest.kt b/api/src/androidTest/java/com/readrops/api/localfeed/LocalRSSDataSourceTest.kt index aa6b7ee2..60c76b80 100644 --- a/api/src/androidTest/java/com/readrops/api/localfeed/LocalRSSDataSourceTest.kt +++ b/api/src/androidTest/java/com/readrops/api/localfeed/LocalRSSDataSourceTest.kt @@ -5,12 +5,15 @@ import android.content.Context import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.platform.app.InstrumentationRegistry import com.readrops.api.utils.HttpManager +import com.readrops.api.utils.LibUtils import com.readrops.api.utils.ParseException import com.readrops.api.utils.UnknownFormatException import junit.framework.TestCase.* +import okhttp3.Headers import okhttp3.HttpUrl import okhttp3.mockwebserver.MockResponse import okhttp3.mockwebserver.MockWebServer +import okio.Buffer import org.junit.After import org.junit.Before import org.junit.Test @@ -40,15 +43,42 @@ class LocalRSSDataSourceTest { @Test fun successfulQueryTest() { + val stream = context.resources.assets.open("localfeed/rss_feed.xml") + + mockServer.enqueue(MockResponse().setResponseCode(HttpURLConnection.HTTP_OK) + .addHeader(LibUtils.CONTENT_TYPE_HEADER, "application/xml; charset=UTF-8") + .addHeader(LibUtils.ETAG_HEADER, "ETag-value") + .addHeader(LibUtils.LAST_MODIFIED_HEADER, "Last-Modified") + .setBody(Buffer().readFrom(stream))) + + val pair = localRSSDataSource.queryRSSResource(url.toString(), null, true) + val feed = pair?.first!! + + assertEquals(feed.name, "Hacker News") + assertEquals(feed.siteUrl, "https://news.ycombinator.com/") + assertEquals(feed.description, "Links for the intellectually curious, ranked by readers.") + + assertEquals(feed.etag, "ETag-value") + assertEquals(feed.lastModified, "Last-Modified") + + assertEquals(pair.second.size, 7) + } + + @Test + fun headersTest() { + val stream = context.resources.assets.open("localfeed/rss_feed.xml") + mockServer.enqueue(MockResponse().setResponseCode(HttpURLConnection.HTTP_OK) .addHeader("Content-Type", "application/rss+xml; charset=UTF-8") - .setBody(context.resources.assets.open("localfeed/rss_feed.xml").toString())) + .setBody(Buffer().readFrom(stream))) + val headers = Headers.headersOf(LibUtils.ETAG_HEADER, "ETag", LibUtils.LAST_MODIFIED_HEADER, "Last-Modified") + localRSSDataSource.queryRSSResource(url.toString(), headers, false) - val pair = localRSSDataSource.queryRSSResource(url.toString(), null, false) + val request = mockServer.takeRequest() - assertNotNull(pair?.first) - assertNotNull(pair?.second) + assertEquals(request.headers[LibUtils.ETAG_HEADER], "ETag") + assertEquals(request.headers[LibUtils.LAST_MODIFIED_HEADER], "Last-Modified") } @Test diff --git a/api/src/main/java/com/readrops/api/localfeed/LocalRSSDataSource.kt b/api/src/main/java/com/readrops/api/localfeed/LocalRSSDataSource.kt index 32ce19c4..817bc126 100644 --- a/api/src/main/java/com/readrops/api/localfeed/LocalRSSDataSource.kt +++ b/api/src/main/java/com/readrops/api/localfeed/LocalRSSDataSource.kt @@ -11,6 +11,7 @@ import okhttp3.Headers import okhttp3.OkHttpClient import okhttp3.Request import okhttp3.Response +import java.io.ByteArrayInputStream import java.io.IOException import java.io.InputStream import java.net.HttpURLConnection @@ -38,14 +39,16 @@ class LocalRSSDataSource(private val httpClient: OkHttpClient) { var type = LocalRSSHelper.getRSSType(contentType) + val bodyArray = response.peekBody(Long.MAX_VALUE).bytes() + // if we can't guess type based on content-type header, we use the content if (type == LocalRSSHelper.RSSType.UNKNOWN) - type = LocalRSSHelper.getRSSContentType(response.body?.byteStream()!!) + type = LocalRSSHelper.getRSSContentType(ByteArrayInputStream(bodyArray)) // if we can't guess type even with the content, we are unable to go further if (type == LocalRSSHelper.RSSType.UNKNOWN) throw UnknownFormatException("Unable to guess $url RSS type") - val feed = parseFeed(response, type) - val items = if (withItems) parseItems(response.body?.byteStream()!!, type) else listOf() + val feed = parseFeed(ByteArrayInputStream(bodyArray), type, response) + val items = if (withItems) parseItems(ByteArrayInputStream(bodyArray), type) else listOf() response.body?.close() Pair(feed, items) @@ -80,18 +83,17 @@ class LocalRSSDataSource(private val httpClient: OkHttpClient) { return httpClient.newCall(requestBuilder.build()).execute() } - private fun parseFeed(response: Response, type: LocalRSSHelper.RSSType): Feed { + private fun parseFeed(stream: InputStream, type: LocalRSSHelper.RSSType, response: Response): Feed { val feed = if (type != LocalRSSHelper.RSSType.JSONFEED) { val adapter = XmlAdapter.xmlFeedAdapterFactory(type) - //adapter.fromXml(response.body?.byteStream()!!) - Feed() + adapter.fromXml(stream) } else { Feed() } feed.etag = response.header(LibUtils.ETAG_HEADER) - feed.lastModified = response.header(LibUtils.IF_MODIFIED_HEADER) + feed.lastModified = response.header(LibUtils.LAST_MODIFIED_HEADER) return feed } @@ -100,8 +102,7 @@ class LocalRSSDataSource(private val httpClient: OkHttpClient) { return if (type != LocalRSSHelper.RSSType.JSONFEED) { val adapter = XmlAdapter.xmlItemsAdapterFactory(type) - //adapter.fromXml(inputStream) - listOf() + adapter.fromXml(inputStream) } else { listOf() } From f1bf65d629d1209939f045e48f72a6d4c04fbf88 Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Sun, 13 Sep 2020 15:19:41 +0200 Subject: [PATCH 031/187] Trigger workflow for every branch --- .github/workflows/android.yml | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml index a00b8662..a1070e53 100644 --- a/.github/workflows/android.yml +++ b/.github/workflows/android.yml @@ -3,12 +3,10 @@ name: Android CI on: push: branches: - - master - - develop + - '**' pull_request: branches: - - master - - develop + - '**' jobs: build: From 8b4b7b0f1c4464bb30b3a5310fc94cd0927b90b3 Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Sun, 13 Sep 2020 15:33:06 +0200 Subject: [PATCH 032/187] Fix opml parser tests --- api/src/androidTest/java/com/readrops/api/OPMLParserTest.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/api/src/androidTest/java/com/readrops/api/OPMLParserTest.kt b/api/src/androidTest/java/com/readrops/api/OPMLParserTest.kt index b3e4e091..33da88f0 100644 --- a/api/src/androidTest/java/com/readrops/api/OPMLParserTest.kt +++ b/api/src/androidTest/java/com/readrops/api/OPMLParserTest.kt @@ -29,7 +29,7 @@ class OPMLParserTest { @Test fun readOpmlTest() { - val stream = context.resources.assets.open("subscriptions.opml") + val stream = context.resources.assets.open("opml/subscriptions.opml") var foldersAndFeeds: Map>? = null @@ -52,7 +52,7 @@ class OPMLParserTest { @Test fun readLiteSubscriptionsTest() { - val stream = context.resources.assets.open("lite_subscriptions.opml") + val stream = context.resources.assets.open("opml/lite_subscriptions.opml") var foldersAndFeeds: Map>? = null @@ -68,7 +68,7 @@ class OPMLParserTest { @Test fun opmlVersionTest() { - val stream = context.resources.assets.open("wrong_version.opml") + val stream = context.resources.assets.open("opml/wrong_version.opml") OPMLParser.read(stream) .test() From 5c342a45b6ba25975c34c101740363594a400884 Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Mon, 14 Sep 2020 17:56:25 +0200 Subject: [PATCH 033/187] Add adapter for parsing atom feed --- .../assets/localfeed/atom/atom_feed.xml | 9 ++++ .../api/localfeed/atom/ATOMFeedAdapterTest.kt | 28 ++++++++++ .../com/readrops/api/localfeed/XmlAdapter.kt | 2 + .../api/localfeed/atom/ATOMFeedAdapter.kt | 52 +++++++++++++++++++ 4 files changed, 91 insertions(+) create mode 100644 api/src/androidTest/assets/localfeed/atom/atom_feed.xml create mode 100644 api/src/androidTest/java/com/readrops/api/localfeed/atom/ATOMFeedAdapterTest.kt create mode 100644 api/src/main/java/com/readrops/api/localfeed/atom/ATOMFeedAdapter.kt diff --git a/api/src/androidTest/assets/localfeed/atom/atom_feed.xml b/api/src/androidTest/assets/localfeed/atom/atom_feed.xml new file mode 100644 index 00000000..797a7917 --- /dev/null +++ b/api/src/androidTest/assets/localfeed/atom/atom_feed.xml @@ -0,0 +1,9 @@ + + + tag:github.com,2008:/readrops/Readrops/commits/develop + + + Recent Commits to Readrops:develop + 2020-09-06T21:09:59Z + Here is a subtitle + \ No newline at end of file diff --git a/api/src/androidTest/java/com/readrops/api/localfeed/atom/ATOMFeedAdapterTest.kt b/api/src/androidTest/java/com/readrops/api/localfeed/atom/ATOMFeedAdapterTest.kt new file mode 100644 index 00000000..57711b22 --- /dev/null +++ b/api/src/androidTest/java/com/readrops/api/localfeed/atom/ATOMFeedAdapterTest.kt @@ -0,0 +1,28 @@ +package com.readrops.api.localfeed.atom + +import android.content.Context +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry +import junit.framework.TestCase.assertEquals +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class ATOMFeedAdapterTest { + + private val context: Context = InstrumentationRegistry.getInstrumentation().context + + private val adapter = ATOMFeedAdapter() + + @Test + fun normalCasesTest() { + val stream = context.assets.open("localfeed/atom/atom_feed.xml") + + val feed = adapter.fromXml(stream) + + assertEquals(feed.name, "Recent Commits to Readrops:develop") + assertEquals(feed.url, "https://github.com/readrops/Readrops/commits/develop.atom") + assertEquals(feed.siteUrl, "https://github.com/readrops/Readrops/commits/develop") + assertEquals(feed.description, "Here is a subtitle") + } +} \ No newline at end of file diff --git a/api/src/main/java/com/readrops/api/localfeed/XmlAdapter.kt b/api/src/main/java/com/readrops/api/localfeed/XmlAdapter.kt index 874d2944..c41a4478 100644 --- a/api/src/main/java/com/readrops/api/localfeed/XmlAdapter.kt +++ b/api/src/main/java/com/readrops/api/localfeed/XmlAdapter.kt @@ -1,5 +1,6 @@ package com.readrops.api.localfeed +import com.readrops.api.localfeed.atom.ATOMFeedAdapter import com.readrops.api.localfeed.rss.RSSFeedAdapter import com.readrops.api.localfeed.rss.RSSItemsAdapter import com.readrops.db.entities.Feed @@ -14,6 +15,7 @@ interface XmlAdapter { fun xmlFeedAdapterFactory(type: LocalRSSHelper.RSSType): XmlAdapter { return when (type) { LocalRSSHelper.RSSType.RSS_2 -> RSSFeedAdapter() + LocalRSSHelper.RSSType.ATOM -> ATOMFeedAdapter() else -> throw Exception("Unknown RSS type : $type") } } diff --git a/api/src/main/java/com/readrops/api/localfeed/atom/ATOMFeedAdapter.kt b/api/src/main/java/com/readrops/api/localfeed/atom/ATOMFeedAdapter.kt new file mode 100644 index 00000000..e53a4bf2 --- /dev/null +++ b/api/src/main/java/com/readrops/api/localfeed/atom/ATOMFeedAdapter.kt @@ -0,0 +1,52 @@ +package com.readrops.api.localfeed.atom + +import com.gitlab.mvysny.konsumexml.Konsumer +import com.gitlab.mvysny.konsumexml.Names +import com.gitlab.mvysny.konsumexml.allChildrenAutoIgnore +import com.gitlab.mvysny.konsumexml.konsumeXml +import com.readrops.api.localfeed.XmlAdapter +import com.readrops.api.utils.ParseException +import com.readrops.api.utils.nonNullText +import com.readrops.api.utils.nullableText +import com.readrops.db.entities.Feed +import java.io.InputStream + +class ATOMFeedAdapter : XmlAdapter { + + override fun fromXml(inputStream: InputStream): Feed { + val konsume = inputStream.konsumeXml() + val feed = Feed() + + return try { + konsume.child("feed") { + allChildrenAutoIgnore(names) { + with(feed) { + when (tagName) { + "title" -> name = nonNullText() + "link" -> parseLink(this@allChildrenAutoIgnore, feed) + "subtitle" -> description = nullableText() + } + } + } + } + + konsume.close() + feed + } catch (e: Exception) { + throw ParseException(e.message) + } + } + + private fun parseLink(konsume: Konsumer, feed: Feed) { + val rel = konsume.attributes["rel"] + + if (rel == "self") + feed.url = konsume.attributes["href"] + else if (rel == "alternate") + feed.siteUrl = konsume.attributes["href"] + } + + companion object { + val names = Names.of("title", "link", "subtitle") + } +} \ No newline at end of file From 0c48bd2adb1f0587a1112ac1cae2c70ecb7c56e1 Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Mon, 14 Sep 2020 19:41:10 +0200 Subject: [PATCH 034/187] Add adapter for parsing atom entries --- .../assets/localfeed/atom/atom_items.xml | 71 +++++++++++++++++++ .../localfeed/atom/atom_items_no_date.xml | 22 ++++++ .../localfeed/atom/atom_items_no_link.xml | 22 ++++++ .../localfeed/atom/atom_items_no_title.xml | 22 ++++++ .../localfeed/atom/ATOMItemsAdapterTest.kt | 58 +++++++++++++++ .../com/readrops/api/localfeed/XmlAdapter.kt | 2 + .../api/localfeed/atom/ATOMItemsAdapter.kt | 62 ++++++++++++++++ 7 files changed, 259 insertions(+) create mode 100644 api/src/androidTest/assets/localfeed/atom/atom_items.xml create mode 100644 api/src/androidTest/assets/localfeed/atom/atom_items_no_date.xml create mode 100644 api/src/androidTest/assets/localfeed/atom/atom_items_no_link.xml create mode 100644 api/src/androidTest/assets/localfeed/atom/atom_items_no_title.xml create mode 100644 api/src/androidTest/java/com/readrops/api/localfeed/atom/ATOMItemsAdapterTest.kt create mode 100644 api/src/main/java/com/readrops/api/localfeed/atom/ATOMItemsAdapter.kt diff --git a/api/src/androidTest/assets/localfeed/atom/atom_items.xml b/api/src/androidTest/assets/localfeed/atom/atom_items.xml new file mode 100644 index 00000000..107a0950 --- /dev/null +++ b/api/src/androidTest/assets/localfeed/atom/atom_items.xml @@ -0,0 +1,71 @@ + + + tag:github.com,2008:/readrops/Readrops/commits/develop + + + Recent Commits to Readrops:develop + 2020-09-06T21:09:59Z + + tag:github.com,2008:Grit::Commit/c15f093a1bc4211e85f8d1817c9073e307afe5ac + + Add an option to open item url in custom tab + 2020-09-06T21:09:59Z + + + Shinokuni + https://github.com/Shinokuni + + Summary + + <pre style='white-space:pre-wrap;width:81ex'>Add an option to open item url in custom tab</pre> + + + + tag:github.com,2008:Grit::Commit/e0945823eecf269e5beea646ac5d7e630e08afbf + + + Use gradle parallel builds + + 2020-09-05T13:28:23Z + + + Shinokuni + https://github.com/Shinokuni + + + <pre style='white-space:pre-wrap;width:81ex'>Use gradle parallel builds</pre> + + + + tag:github.com,2008:Grit::Commit/85fcf03e64d8b482e4d2af8c2bcd1509d946944f + + + Use clear text mode for the feed url text input in AddFeedActivity + + 2020-09-05T12:23:48Z + + + Shinokuni + https://github.com/Shinokuni + + + <pre style='white-space:pre-wrap;width:81ex'>Use clear text mode for the feed url text input in AddFeedActivity</pre> + + + + tag:github.com,2008:Grit::Commit/d59e38ee9d11da186131b602425231eff0896956 + + + Use project level okhttp client with glide + + 2020-09-05T12:05:16Z + + + Shinokuni + https://github.com/Shinokuni + + + <pre style='white-space:pre-wrap;width:81ex'>Use project level okhttp client with glide</pre> + + + \ No newline at end of file diff --git a/api/src/androidTest/assets/localfeed/atom/atom_items_no_date.xml b/api/src/androidTest/assets/localfeed/atom/atom_items_no_date.xml new file mode 100644 index 00000000..08ae5692 --- /dev/null +++ b/api/src/androidTest/assets/localfeed/atom/atom_items_no_date.xml @@ -0,0 +1,22 @@ + + + tag:github.com,2008:/readrops/Readrops/commits/develop + + + Recent Commits to Readrops:develop + 2020-09-06T21:09:59Z + + tag:github.com,2008:Grit::Commit/c15f093a1bc4211e85f8d1817c9073e307afe5ac + Add an option to open item url in custom tab + + + + Shinokuni + https://github.com/Shinokuni + + Summary + + <pre style='white-space:pre-wrap;width:81ex'>Add an option to open item url in custom tab</pre> + + + \ No newline at end of file diff --git a/api/src/androidTest/assets/localfeed/atom/atom_items_no_link.xml b/api/src/androidTest/assets/localfeed/atom/atom_items_no_link.xml new file mode 100644 index 00000000..c0d817d8 --- /dev/null +++ b/api/src/androidTest/assets/localfeed/atom/atom_items_no_link.xml @@ -0,0 +1,22 @@ + + + tag:github.com,2008:/readrops/Readrops/commits/develop + + + Recent Commits to Readrops:develop + 2020-09-06T21:09:59Z + + tag:github.com,2008:Grit::Commit/c15f093a1bc4211e85f8d1817c9073e307afe5ac + Add an option to open item url in custom tab + 2020-09-06T21:09:59Z + + + Shinokuni + https://github.com/Shinokuni + + Summary + + <pre style='white-space:pre-wrap;width:81ex'>Add an option to open item url in custom tab</pre> + + + \ No newline at end of file diff --git a/api/src/androidTest/assets/localfeed/atom/atom_items_no_title.xml b/api/src/androidTest/assets/localfeed/atom/atom_items_no_title.xml new file mode 100644 index 00000000..8bf4adbd --- /dev/null +++ b/api/src/androidTest/assets/localfeed/atom/atom_items_no_title.xml @@ -0,0 +1,22 @@ + + + tag:github.com,2008:/readrops/Readrops/commits/develop + + + Recent Commits to Readrops:develop + 2020-09-06T21:09:59Z + + tag:github.com,2008:Grit::Commit/c15f093a1bc4211e85f8d1817c9073e307afe5ac + + 2020-09-06T21:09:59Z + + + Shinokuni + https://github.com/Shinokuni + + Summary + + <pre style='white-space:pre-wrap;width:81ex'>Add an option to open item url in custom tab</pre> + + + \ No newline at end of file diff --git a/api/src/androidTest/java/com/readrops/api/localfeed/atom/ATOMItemsAdapterTest.kt b/api/src/androidTest/java/com/readrops/api/localfeed/atom/ATOMItemsAdapterTest.kt new file mode 100644 index 00000000..f6095a97 --- /dev/null +++ b/api/src/androidTest/java/com/readrops/api/localfeed/atom/ATOMItemsAdapterTest.kt @@ -0,0 +1,58 @@ +package com.readrops.api.localfeed.atom + +import android.content.Context +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry +import com.readrops.api.utils.DateUtils +import com.readrops.api.utils.ParseException +import junit.framework.TestCase.assertEquals +import junit.framework.TestCase.assertNotNull +import org.junit.Assert +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class ATOMItemsAdapterTest { + + private val context: Context = InstrumentationRegistry.getInstrumentation().context + + private val adapter = ATOMItemsAdapter() + + @Test + fun normalCasesTest() { + val stream = context.resources.assets.open("localfeed/atom/atom_items.xml") + + val items = adapter.fromXml(stream) + val item = items[0] + + assertEquals(items.size, 4) + assertEquals(item.title, "Add an option to open item url in custom tab") + assertEquals(item.link, "https://github.com/readrops/Readrops/commit/c15f093a1bc4211e85f8d1817c9073e307afe5ac") + assertEquals(item.pubDate, DateUtils.stringToLocalDateTime("2020-09-06T21:09:59Z")) + assertEquals(item.author, "Shinokuni") + assertEquals(item.description, "Summary") + assertEquals(item.guid, "tag:github.com,2008:Grit::Commit/c15f093a1bc4211e85f8d1817c9073e307afe5ac") + assertNotNull(item.content) + } + + @Test + fun noTitleTest() { + val stream = context.resources.assets.open("localfeed/atom/atom_items_no_title.xml") + + Assert.assertThrows("Item title is required", ParseException::class.java) { adapter.fromXml(stream) } + } + + @Test + fun noLinkTest() { + val stream = context.resources.assets.open("localfeed/atom/atom_items_no_link.xml") + + Assert.assertThrows("Item link is required", ParseException::class.java) { adapter.fromXml(stream) } + } + + @Test + fun noDateTest() { + val stream = context.resources.assets.open("localfeed/atom/atom_items_no_date.xml") + + Assert.assertThrows("Item date is required", ParseException::class.java) { adapter.fromXml(stream) } + } +} \ No newline at end of file diff --git a/api/src/main/java/com/readrops/api/localfeed/XmlAdapter.kt b/api/src/main/java/com/readrops/api/localfeed/XmlAdapter.kt index c41a4478..b7f39d64 100644 --- a/api/src/main/java/com/readrops/api/localfeed/XmlAdapter.kt +++ b/api/src/main/java/com/readrops/api/localfeed/XmlAdapter.kt @@ -1,6 +1,7 @@ package com.readrops.api.localfeed import com.readrops.api.localfeed.atom.ATOMFeedAdapter +import com.readrops.api.localfeed.atom.ATOMItemsAdapter import com.readrops.api.localfeed.rss.RSSFeedAdapter import com.readrops.api.localfeed.rss.RSSItemsAdapter import com.readrops.db.entities.Feed @@ -23,6 +24,7 @@ interface XmlAdapter { fun xmlItemsAdapterFactory(type: LocalRSSHelper.RSSType): XmlAdapter> { return when (type) { LocalRSSHelper.RSSType.RSS_2 -> RSSItemsAdapter() + LocalRSSHelper.RSSType.ATOM -> ATOMItemsAdapter() else -> throw Exception("Unknown RSS type : $type") } } diff --git a/api/src/main/java/com/readrops/api/localfeed/atom/ATOMItemsAdapter.kt b/api/src/main/java/com/readrops/api/localfeed/atom/ATOMItemsAdapter.kt new file mode 100644 index 00000000..db7fe178 --- /dev/null +++ b/api/src/main/java/com/readrops/api/localfeed/atom/ATOMItemsAdapter.kt @@ -0,0 +1,62 @@ +package com.readrops.api.localfeed.atom + +import com.gitlab.mvysny.konsumexml.Names +import com.gitlab.mvysny.konsumexml.allChildrenAutoIgnore +import com.gitlab.mvysny.konsumexml.konsumeXml +import com.readrops.api.localfeed.XmlAdapter +import com.readrops.api.utils.DateUtils +import com.readrops.api.utils.ParseException +import com.readrops.api.utils.nonNullText +import com.readrops.api.utils.nullableText +import com.readrops.db.entities.Item +import java.io.InputStream + +class ATOMItemsAdapter : XmlAdapter> { + + override fun fromXml(inputStream: InputStream): List { + val konsume = inputStream.konsumeXml() + val items = arrayListOf() + + return try { + konsume.child("feed") { + allChildrenAutoIgnore("entry") { + val item = Item().apply { + allChildrenAutoIgnore(names) { + when (tagName) { + "title" -> title = nonNullText() + "id" -> guid = nullableText() + "updated" -> pubDate = DateUtils.stringToLocalDateTime(nonNullText()) + "link" -> if (attributes["rel"] == "alternate") link = attributes["href"] + "author" -> allChildrenAutoIgnore("name") { author = text() } + "summary" -> description = nullableText() + "content" -> content = nullableText() + } + } + } + + validateItem(item) + if (item.guid == null) item.guid = item.link + + items += item + } + } + + konsume.close() + items + } catch (e: Exception) { + throw ParseException(e.message) + } + } + + private fun validateItem(item: Item) { + when { + item.title == null -> throw ParseException("Item title is required") + item.link == null -> throw ParseException("Item link is required") + item.pubDate == null -> throw ParseException("Item date id required") + } + } + + companion object { + val names = Names.of("title", "id", "updated", "link", "author", "summary", "content") + } +} \ No newline at end of file From c9c316b5795b01a66558b13431408766365a10e6 Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Tue, 15 Sep 2020 13:48:52 +0200 Subject: [PATCH 035/187] Add adapter for parsing jsonfeed feed --- .../assets/localfeed/json/json_feed.json | 82 +++++++++++++++++++ .../api/localfeed/json/JSONFeedAdapterTest.kt | 35 ++++++++ .../api/localfeed/json/JSONFeedAdapter.kt | 39 +++++++++ 3 files changed, 156 insertions(+) create mode 100644 api/src/androidTest/assets/localfeed/json/json_feed.json create mode 100644 api/src/androidTest/java/com/readrops/api/localfeed/json/JSONFeedAdapterTest.kt create mode 100644 api/src/main/java/com/readrops/api/localfeed/json/JSONFeedAdapter.kt diff --git a/api/src/androidTest/assets/localfeed/json/json_feed.json b/api/src/androidTest/assets/localfeed/json/json_feed.json new file mode 100644 index 00000000..35ca0e3b --- /dev/null +++ b/api/src/androidTest/assets/localfeed/json/json_feed.json @@ -0,0 +1,82 @@ +{ + "version": "https://jsonfeed.org/version/1", + "title": "News from Flying Meat", + "home_page_url": "http://flyingmeat.com/blog/", + "feed_url": "http://flyingmeat.com/blog/feed.json", + "description": "News from your friends at Flying Meat.", + "author": { + "name": "Gus Mueller" + }, + "items": [ + { + "id": "http://flyingmeat.com/blog/archives/2017/9/acorn_and_10.13.html", + "title": "Acorn and 10.13", + "content_html": "

Happy Mac OS High Sierra release day everyone.

\n

I'm happy to say that there are no known issues with Acorn 6.0.3 or Acorn 5.6.6 when running on Mac OS 10.13 High Sierra. In fact, you might even notice that some things are actually faster and it can now open HEIF images. How awesome is that?

\n

I'm also working on some 10.13 goodies for Acorn 6 folks later this year. I can't wait to share that with you, but you'll have to wait just a little bit.

\n", + "date_published": "2017-09-25T14:27:27-07:00", + "url": "http://flyingmeat.com/blog/archives/2017/9/acorn_and_10.13.html" + }, + { + "id": "http://flyingmeat.com/blog/archives/2018/2/acorn_6.1_is_out.html", + "title": "Acorn 6.1 Is Out", + "content_html": "

Acorn 6.1 has been released.

\n

You can read a longer post about it over on Gus's blog, but the short of it is: Better, faster, smoother, stronger. And now with Metal 2 support.

\n", + "date_published": "2018-02-16T09:59:11-08:00", + "url": "http://flyingmeat.com/blog/archives/2018/2/acorn_6.1_is_out.html" + }, + { + "id": "http://flyingmeat.com/blog/archives/2018/6/a_pair_of_updates.html", + "title": "A Pair of Updates", + "content_html": "

Happy summer solstice everybody! (at least for folks in the northern hemisphere, and for folks in the south… sorry. It's going to start getting brighter for you though).

\n

Today I've got a pair of minor app updates to annouce for you.

\n

First up is Acorn 6.1.3, which fixes a number of bugs including one that stemmed from trying to use QuickLook on a file that was created with Acorn 1.0. For the one or two of you that this was affecting, hurray!

\n

Next up is Retrobatch, which also includes some bug fixes, the beginnings of Voice Over support, performance improvements, and more.

\n

What's next for these apps? Work on Acorn 6.2 will begin shortly, as will Retrobatch 1.1. WWDC introduced some great new APIs that I want to take advantage of (cool new machine learning things), so that'll be a focus- as well as Dark Mode for Acorn and one other major thing I've got planned. Retrobatch will probably also get the Dark Mode treatment, but not until I've done it for Acorn first.

\n

So it's going to be a busy summer, but I'm looking forward to it.

\n", + "date_published": "2018-06-21T10:18:46-07:00", + "url": "http://flyingmeat.com/blog/archives/2018/6/a_pair_of_updates.html" + }, + { + "id": "http://flyingmeat.com/blog/archives/2018/9/retrobatch_1.1_is_out.html", + "title": "Retrobatch 1.1 Is Out", + "content_html": "

Here's something new for your lazy August September* morning: Retrobatch 1.1 is out.

\n

What's new and awesome? Well, Retrobatch now has some great scripting goodness in the form of a new Automator action which will run a workflow for you (and create Automator droplets), a new JavaScript node*, and the ability to run Retrobatch workflows from the terminal.

\n

We've added a handful of new nodes such as Dither, Auto Enhance, Instant Alpha, and Color Posterize. New options to existing nodes have also shown up, such as "Only scale smaller" for the Scale node.

\n

And an interesting idea that I've had folks ask about a number of times- it's now possible to run an image through a machine learning classifier, and then have the classification written to metadata such as the image title, or keywords. This was done by adding token support to the Set Specific Metadata node. This also means you can use other tokens such as the Current Year in metadata fields. Awesome? We think so.

\n

The full release notes are available, and if you have ideas or questions- make sure to poke around on the forums or write us: support@flyingmeat.com. We've got lots of ideas for future releases, but if you'd like something specific in there make sure to let us know.

\n
\n\n
\n* Whoa, it's September already?

\n\n

**I'm calling the JavaScript node a "preview". It works very well, but I'm not 100% sold on the API that I've provided to folks. So this is a disclaimer that it might change a little bit in the future.

\n", + "date_published": "2018-09-07T09:43:10-07:00", + "url": "http://flyingmeat.com/blog/archives/2018/9/retrobatch_1.1_is_out.html" + }, + { + "id": "http://flyingmeat.com/blog/archives/2018/9/acorn_6.2_with_mojave_dark_mode_is_out.html", + "title": "Acorn 6.2 With Mojave Dark Mode Is Out", + "content_html": "

On Monday I flipped some switches on the FM servers and Acorn 6.2 was released to the universe. You might also remember that Monday a little known operating system from Apple was updated, which includes a neat new feature known as Dark Mode.

\n
\n\n

I think Acorn looks pretty good in Dark Aqua, especially the icon refresh from Matthew Skiles.

\n

To celebrate the new release, we've put Acorn on sale for 50% off. So go grab it at the insanely low price of $14.99. If you haven't already upgraded from previous versions of Acorn, now is a good time to do so.

\n

We've also packed a bunch of little changes, bug fixes, and compatibility with Mojave in there. And of course, there's more to come in the future as always.

\n", + "date_published": "2018-09-27T14:37:21-07:00", + "url": "http://flyingmeat.com/blog/archives/2018/9/acorn_6.2_with_mojave_dark_mode_is_out.html" + }, + { + "id": "http://flyingmeat.com/blog/archives/2019/1/acorn_6.3_is_out.html", + "title": "Acorn 6.3 Is Out", + "content_html": "

Acorn 6.3 is available, and the full release notes are up as well.

\n

Here's what I think is awesome in this release:

\n

Portrait Mask Support. If you have an iPhone running iOS 12 (and can take Portrait photos), Acorn will now detect the Portrait Matte from those images and turn it into a layer mask. The Portrait Matte is the image data which enables blurring in the background, or other fancy camera tricks. This means you can use this matte to erase and add fancy backgrounds or custom blurs for your image, all within Acorn.

\n

Other Mask Features. You can now drag and drop masks from the layers list into another layer, or copy it out as a new layer. When exporting layers you now have an option to apply the mask on export, or just write it as an additional image along with everything else. There are a number of new shortcuts when dealing with layer masks as well.

\n

Brush Stuff. If you're running MacOS 10.13 or later, you get a performance boost when brushing (painting, smudging, cloning, etc…). This is especially noticeable when brusing on deep color images.

\n

I've also added options to the brush palette for adjusting flow, softness and blending. In addition to all this, there's a bunch of new brushes under the "Basic Round" category which are designed for the new brush engine.

\n

Other Stuff. There's other good things including improved PDF export, various MacOS Mojave UI fixes, additional speed improvements with with deep images, and more. And as always, it's a free upgrade for anyone who has already purchased Acorn 6.

\n", + "date_published": "2019-01-09T13:00:07-08:00", + "url": "http://flyingmeat.com/blog/archives/2019/1/acorn_6.3_is_out.html" + }, + { + "id": "http://flyingmeat.com/blog/archives/2019/4/retrobatch_1.2_released.html", + "title": "Retrobatch 1.2 Released", + "content_html": "

We're happy to announce that Retrobatch 1.2 has now been released, which is a free update for all owners of Retrobatch. Highlights of this release include:

\n
    \n
  • Create animated GIF and PNG images with the Animated Image node. When using Retrobatch you can load in a folder of images and produce an optimized animated image with options for setting the frame rate, format, as well as letting the image loop or not.

    \n
  • \n
  • New nodes including "Round Corner", "Image Grid", and "Limit". We've also added improvements to the Write node allowing you to write back to the original processed image.

    \n
  • \n
  • Droplet support (Retrobatch Pro). Turn your workflow into an an application which you can drag and drop images onto. The droplet can work anywhere an application normally would, even in the Dock.

    \n
  • \n
  • Write Plug-Ins using JavaScript (Retrobatch Pro). Using the combined power of JavaScript and the native to MacOS Cocoa APIs, you can make and distribute new plugins for Retrobatch. Got an idea for a plug-in and you want to use Core Image to make it? Or maybe you want to use Core Graphics to add some funky text to your images? Now you can do this with JavaScript and Cocoa.

    \n
  • \n
\n

The full release notes are available, as well as information on bug fixes we delivered in this update.

\n

As always, we're always listening for feedback and feature requests. And don't forget to head over to the Retrobatch community formus to chat with us and other Retrobatch users.

\n", + "date_published": "2019-04-01T13:38:21-07:00", + "url": "http://flyingmeat.com/blog/archives/2019/4/retrobatch_1.2_released.html" + }, + { + "id": "http://flyingmeat.com/blog/archives/2019/10/catalina_ready.html", + "title": "Catalina Ready", + "content_html": "

MacOS 10.15 Catalina was just released, and we're happy to let you know that both Acorn 6.5.1 and Retrobatch 1.2 are compatible with it.

\n

And to celebrate the release of Catalina, we're discounting Acorn by 50% for a limited time. So if you haven't upgraded yet, now is a good time.

\n", + "date_published": "2019-10-07T10:48:03-07:00", + "url": "http://flyingmeat.com/blog/archives/2019/10/catalina_ready.html" + }, + { + "id": "http://flyingmeat.com/blog/archives/2020/3/retrobatch_1.4_is_out.html", + "title": "Retrobatch 1.4 Is Out", + "content_html": "

I've just typed the magic commands* and let the servers do their thing and now Retrobatch 1.4 is loose on the world.

\n

There's a couple of interesting new features in this update I'd like to call out. First up is JavaScript expressions in Retrobatch Pro. Various nodes in Retrobatch which allow you to set the size or length of a value (such as the Crop, Border, Gradient, Adjust Margin nodes) now have an option of running a little snippet of JavaScript code to figure out the value. This is a super powerful feature, which you can read about in our JavaScript Expressions documentation

\n

Let's say you have some images of varying sizes, which are all at 480 x 380 or smaller, and you want them to expand to meet that size. But- you only want it to grow evenly on either side of the image, but you want to keep a baseline so only transparent area is added to the top of the image, and the bottom stays in the same spot. This little picture of the new Adjust Margins node shows how this can be done:

\n
\n\n

Yes, this is an oddball (and very real) case- but there's a billion of these little oddball cases out there. With the new JavaScript expressions support, these small but hard to do scenarios are now super easy.

\n

And yes, all of the JavaScript support in Retrobatch now sits atop FMJS, which any developer can use to build similar support into their apps.

\n

What else is new?

\n

File numbers with leading zeros for the Write node. You can add (and it's case sensitive) $FileNumber04$ in the File name: field of the Write node to have the file number of your image written out as part of the name, with a padding of up to 4 zeros. If you'd like to pad that number to 6, you would enter $FileNumber06$, and so on.

\n

The Mask to Alpha node got a new "invert colors" option. Normally Mask to Alpha will convert the black areas of your image to transparent, and the white to opaque (with gray somewhere inbetween). With the new Invert Colors option, Mask to Alpha will now convert the white areas of your image to transparent, and keep the black opaque. This is great if you are scanning in line drawings from your own artwork, and want to make the backgrounds transparent.

\n

This request comes up a lot in Acorn as well. Previously you'd have to add an Invert Colors node (or filter for Acorn), then the Mask to Alpha, and then Invert Colors again. Now it's just a checkbox in Mask to Alpha, which is super easy. I've also added an update to the same filter in Acorn for the next release. You can grab a preview of it from here.

\n

And finally for my short list, you can now make a droplet which doesn't take any files. Why is this useful? Well, imagine you have a workflow that reads an image from the clipboard, resizes it to a specific width, and then writes it back to the clipboard. Now you can make a little droplet to do just this. Just a double click from the Finder (or a single click from the Dock) and your workflow is run.

\n

The full release notes for Retrobatch 1.4 are available in the usual place.

\n

* ./bin/otbuild.sh -e 1.4

\n", + "date_published": "2020-03-31T14:37:15-07:00", + "url": "http://flyingmeat.com/blog/archives/2020/3/retrobatch_1.4_is_out.html" + }, + { + "id": "http://flyingmeat.com/blog/archives/2020/5/acorn_6.6_released.html", + "title": "Acorn 6.6 is out with new Shape Processors and more", + "content_html": "

Acorn 6.6 is out. You can update to this release via the App Store as or the Acorn ▸ Check for Updates… menu if you bought it directly from us.

\n

Originally this was going to be a bug fix release but I kept on adding useful things and it snowballed into a feature release. As usual, the full release notes have all the details about what was updated.

\n

The main new features are with the Shape Processor. If you're not already familiar with the shape processor, it's a neat ability Acorn has to take shapes on vector layers and pipe them through a series of actions, similar to how Automator or Acorn's bitmap filters work. Only instead of working on pixels, the processors will alter the shapes by scaling them or moving them around, or changing colors or blend modes. There's even a processor which will generate shapes for you- so if you want your canvas to fill up with hundreds of stars, you can do that.

\n

Acorn 6.6 adds new processors which let you set the stroke, fill, and blend mode of your processed shapes. You can now also flip your shapes and even shift colors.

\n\n\n

Chaining these processors together can get you some neat looking images. You can make interesting desktop backgrounds, as well as textures for your photos. Or if you just need a bunch of hexagons arranged in a circle, that's just two processors stacked together.

\n

Have you made something interesting with the Shape Processor? I'd love to see it either via Twitter (I'm @ccgus) or via email.

\n

There are of course the usual bug fixes and other minor details. And if you don't already have Acorn, a no-strings attached free trial is available on our website. Try it out, and we're always looking to hear from you about feature requests, thoughts, and anything else.

\n", + "date_published": "2020-05-28T12:17:57-07:00", + "url": "http://flyingmeat.com/blog/archives/2020/5/acorn_6.6_released.html" + } + ] +} \ No newline at end of file diff --git a/api/src/androidTest/java/com/readrops/api/localfeed/json/JSONFeedAdapterTest.kt b/api/src/androidTest/java/com/readrops/api/localfeed/json/JSONFeedAdapterTest.kt new file mode 100644 index 00000000..def7cd0c --- /dev/null +++ b/api/src/androidTest/java/com/readrops/api/localfeed/json/JSONFeedAdapterTest.kt @@ -0,0 +1,35 @@ +package com.readrops.api.localfeed.json + +import android.content.Context +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry +import com.readrops.db.entities.Feed +import com.squareup.moshi.Moshi +import junit.framework.TestCase.assertEquals +import okio.Buffer +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class JSONFeedAdapterTest { + + private val context: Context = InstrumentationRegistry.getInstrumentation().context + + private val adapter = Moshi.Builder() + .add(JSONFeedAdapter()) + .build() + .adapter(Feed::class.java) + + @Test + fun normalCasesTest() { + val stream = context.assets.open("localfeed/json/json_feed.json") + + val feed = adapter.fromJson(Buffer().readFrom(stream))!! + + assertEquals(feed.name, "News from Flying Meat") + assertEquals(feed.url, "http://flyingmeat.com/blog/feed.json") + assertEquals(feed.siteUrl, "http://flyingmeat.com/blog/") + assertEquals(feed.description, "News from your friends at Flying Meat.") + } + +} \ No newline at end of file diff --git a/api/src/main/java/com/readrops/api/localfeed/json/JSONFeedAdapter.kt b/api/src/main/java/com/readrops/api/localfeed/json/JSONFeedAdapter.kt new file mode 100644 index 00000000..4358f315 --- /dev/null +++ b/api/src/main/java/com/readrops/api/localfeed/json/JSONFeedAdapter.kt @@ -0,0 +1,39 @@ +package com.readrops.api.localfeed.json + +import com.readrops.api.utils.nextNullableString +import com.readrops.db.entities.Feed +import com.squareup.moshi.FromJson +import com.squareup.moshi.JsonReader +import com.squareup.moshi.ToJson + +class JSONFeedAdapter { + + @ToJson + fun toJson(feed: Feed) = "" + + @FromJson + fun fromJson(reader: JsonReader): Feed { + val feed = Feed() + reader.beginObject() + + while (reader.hasNext()) { + with(feed) { + when (reader.selectName(names)) { + 0 -> name = reader.nextString() + 1 -> siteUrl = reader.nextNullableString() + 2 -> url = reader.nextNullableString() + 3 -> description = reader.nextNullableString() + else -> reader.skipValue() + } + } + } + + reader.endObject() + return feed + } + + companion object { + val names: JsonReader.Options = JsonReader.Options.of("title", "home_page_url", + "feed_url", "description") + } +} \ No newline at end of file From 592fa4603aa79bd75bf9013545bc273ade11dc77 Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Tue, 15 Sep 2020 13:51:17 +0200 Subject: [PATCH 036/187] Add support for jsonfeed new mime-type : application/feed+json --- api/src/main/java/com/readrops/api/localfeed/LocalRSSHelper.kt | 3 ++- .../test/java/com/readrops/api/localfeed/LocalRSSHelperTest.kt | 2 ++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/api/src/main/java/com/readrops/api/localfeed/LocalRSSHelper.kt b/api/src/main/java/com/readrops/api/localfeed/LocalRSSHelper.kt index 3d4a42b5..13894e6a 100644 --- a/api/src/main/java/com/readrops/api/localfeed/LocalRSSHelper.kt +++ b/api/src/main/java/com/readrops/api/localfeed/LocalRSSHelper.kt @@ -10,6 +10,7 @@ object LocalRSSHelper { private const val RSS_TEXT_CONTENT_TYPE = "text/xml" private const val RSS_APPLICATION_CONTENT_TYPE = "application/xml" private const val ATOM_CONTENT_TYPE = "application/atom+xml" + private const val JSONFEED_CONTENT_TYPE = "application/feed+json" private const val JSON_CONTENT_TYPE = "application/json" private const val HTML_CONTENT_TYPE = "text/html" @@ -24,7 +25,7 @@ object LocalRSSHelper { return when (contentType) { RSS_DEFAULT_CONTENT_TYPE -> RSSType.RSS_2 ATOM_CONTENT_TYPE -> RSSType.ATOM - JSON_CONTENT_TYPE -> RSSType.JSONFEED + JSON_CONTENT_TYPE, JSONFEED_CONTENT_TYPE -> RSSType.JSONFEED RSS_TEXT_CONTENT_TYPE, RSS_APPLICATION_CONTENT_TYPE, HTML_CONTENT_TYPE -> RSSType.UNKNOWN else -> throw UnknownFormatException("Unknown content type : $contentType") } diff --git a/api/src/test/java/com/readrops/api/localfeed/LocalRSSHelperTest.kt b/api/src/test/java/com/readrops/api/localfeed/LocalRSSHelperTest.kt index c35c9e5e..bec3cbf1 100644 --- a/api/src/test/java/com/readrops/api/localfeed/LocalRSSHelperTest.kt +++ b/api/src/test/java/com/readrops/api/localfeed/LocalRSSHelperTest.kt @@ -15,6 +15,8 @@ class LocalRSSHelperTest { LocalRSSHelper.RSSType.ATOM) assertEquals(LocalRSSHelper.getRSSType("application/json"), LocalRSSHelper.RSSType.JSONFEED) + assertEquals(LocalRSSHelper.getRSSType("application/feed+json"), + LocalRSSHelper.RSSType.JSONFEED) } @Test From ca29c4acb06feb91caa7c4b9abbbdfedad3cbf57 Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Tue, 15 Sep 2020 23:30:56 +0200 Subject: [PATCH 037/187] Add adapter for jsonfeed items --- .../assets/localfeed/json/json_feed.json | 6 +- .../json/json_items_other_cases.json | 28 +++++ .../json/json_items_required_elements.json | 22 ++++ .../localfeed/json/JSONItemsAdapterTest.kt | 77 ++++++++++++ .../api/localfeed/json/JSONItemsAdapter.kt | 116 ++++++++++++++++++ 5 files changed, 248 insertions(+), 1 deletion(-) create mode 100644 api/src/androidTest/assets/localfeed/json/json_items_other_cases.json create mode 100644 api/src/androidTest/assets/localfeed/json/json_items_required_elements.json create mode 100644 api/src/androidTest/java/com/readrops/api/localfeed/json/JSONItemsAdapterTest.kt create mode 100644 api/src/main/java/com/readrops/api/localfeed/json/JSONItemsAdapter.kt diff --git a/api/src/androidTest/assets/localfeed/json/json_feed.json b/api/src/androidTest/assets/localfeed/json/json_feed.json index 35ca0e3b..4e5b032f 100644 --- a/api/src/androidTest/assets/localfeed/json/json_feed.json +++ b/api/src/androidTest/assets/localfeed/json/json_feed.json @@ -13,7 +13,11 @@ "title": "Acorn and 10.13", "content_html": "

Happy Mac OS High Sierra release day everyone.

\n

I'm happy to say that there are no known issues with Acorn 6.0.3 or Acorn 5.6.6 when running on Mac OS 10.13 High Sierra. In fact, you might even notice that some things are actually faster and it can now open HEIF images. How awesome is that?

\n

I'm also working on some 10.13 goodies for Acorn 6 folks later this year. I can't wait to share that with you, but you'll have to wait just a little bit.

\n", "date_published": "2017-09-25T14:27:27-07:00", - "url": "http://flyingmeat.com/blog/archives/2017/9/acorn_and_10.13.html" + "url": "http://flyingmeat.com/blog/archives/2017/9/acorn_and_10.13.html", + "author": { + "url": "this is an url", + "name": "Author 1" + } }, { "id": "http://flyingmeat.com/blog/archives/2018/2/acorn_6.1_is_out.html", diff --git a/api/src/androidTest/assets/localfeed/json/json_items_other_cases.json b/api/src/androidTest/assets/localfeed/json/json_items_other_cases.json new file mode 100644 index 00000000..e91ee941 --- /dev/null +++ b/api/src/androidTest/assets/localfeed/json/json_items_other_cases.json @@ -0,0 +1,28 @@ +{ + "items": [ + { + "id": "http://flyingmeat.com/blog/archives/2017/9/acorn_and_10.13.html", + "title": "Acorn and 10.13", + "summary": "This is a summary", + "content_html": "content_html", + "content_text": "content_text", + "date_published": "2017-09-25T14:27:27-07:00", + "url": "http://flyingmeat.com/blog/archives/2017/9/acorn_and_10.13.html", + "image": "https://image.com", + "authors": [ + { + "url": "url 1", + "name": "Author 1" + }, + { + "url": "url 2", + "name": "Author 2" + }, + { + "url": "url 3", + "name": "Author 3" + } + ] + } + ] +} \ No newline at end of file diff --git a/api/src/androidTest/assets/localfeed/json/json_items_required_elements.json b/api/src/androidTest/assets/localfeed/json/json_items_required_elements.json new file mode 100644 index 00000000..f46aa9a1 --- /dev/null +++ b/api/src/androidTest/assets/localfeed/json/json_items_required_elements.json @@ -0,0 +1,22 @@ +{ + "items": [ + { + "id": "http://flyingmeat.com/blog/archives/2017/9/acorn_and_10.13.html", + "content_html": "

Happy Mac OS High Sierra release day everyone.

\n

I'm happy to say that there are no known issues with Acorn 6.0.3 or Acorn 5.6.6 when running on Mac OS 10.13 High Sierra. In fact, you might even notice that some things are actually faster and it can now open HEIF images. How awesome is that?

\n

I'm also working on some 10.13 goodies for Acorn 6 folks later this year. I can't wait to share that with you, but you'll have to wait just a little bit.

\n", + "date_published": "2017-09-25T14:27:27-07:00", + "url": "http://flyingmeat.com/blog/archives/2017/9/acorn_and_10.13.html" + }, + { + "id": "http://flyingmeat.com/blog/archives/2018/2/acorn_6.1_is_out.html", + "title": "Acorn 6.1 Is Out", + "content_html": "

Acorn 6.1 has been released.

\n

You can read a longer post about it over on Gus's blog, but the short of it is: Better, faster, smoother, stronger. And now with Metal 2 support.

\n", + "date_published": "2018-02-16T09:59:11-08:00", + }, + { + "id": "http://flyingmeat.com/blog/archives/2018/6/a_pair_of_updates.html", + "title": "A Pair of Updates", + "content_html": "

Happy summer solstice everybody! (at least for folks in the northern hemisphere, and for folks in the south… sorry. It's going to start getting brighter for you though).

\n

Today I've got a pair of minor app updates to annouce for you.

\n

First up is Acorn 6.1.3, which fixes a number of bugs including one that stemmed from trying to use QuickLook on a file that was created with Acorn 1.0. For the one or two of you that this was affecting, hurray!

\n

Next up is Retrobatch, which also includes some bug fixes, the beginnings of Voice Over support, performance improvements, and more.

\n

What's next for these apps? Work on Acorn 6.2 will begin shortly, as will Retrobatch 1.1. WWDC introduced some great new APIs that I want to take advantage of (cool new machine learning things), so that'll be a focus- as well as Dark Mode for Acorn and one other major thing I've got planned. Retrobatch will probably also get the Dark Mode treatment, but not until I've done it for Acorn first.

\n

So it's going to be a busy summer, but I'm looking forward to it.

\n", + "url": "http://flyingmeat.com/blog/archives/2018/6/a_pair_of_updates.html" + } + ] +} \ No newline at end of file diff --git a/api/src/androidTest/java/com/readrops/api/localfeed/json/JSONItemsAdapterTest.kt b/api/src/androidTest/java/com/readrops/api/localfeed/json/JSONItemsAdapterTest.kt new file mode 100644 index 00000000..c45e3e34 --- /dev/null +++ b/api/src/androidTest/java/com/readrops/api/localfeed/json/JSONItemsAdapterTest.kt @@ -0,0 +1,77 @@ +package com.readrops.api.localfeed.json + +import android.content.Context +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry +import com.readrops.api.utils.DateUtils +import com.readrops.api.utils.ParseException +import com.readrops.db.entities.Item +import com.squareup.moshi.Moshi +import com.squareup.moshi.Types +import junit.framework.TestCase.assertEquals +import junit.framework.TestCase.assertNotNull +import okio.Buffer +import org.junit.Assert +import org.junit.Test +import org.junit.runner.RunWith + + +@RunWith(AndroidJUnit4::class) +class JSONItemsAdapterTest { + + private val context: Context = InstrumentationRegistry.getInstrumentation().context + + private val adapter = Moshi.Builder() + .add(Types.newParameterizedType(List::class.java, Item::class.java), JSONItemsAdapter()) + .build() + .adapter>(Types.newParameterizedType(List::class.java, Item::class.java)) + + @Test + fun normalCasesTest() { + val stream = context.resources.assets.open("localfeed/json/json_feed.json") + + val items = adapter.fromJson(Buffer().readFrom(stream))!! + val item = items[0] + + assertEquals(items.size, 10) + assertEquals(item.guid, "http://flyingmeat.com/blog/archives/2017/9/acorn_and_10.13.html") + assertEquals(item.title, "Acorn and 10.13") + assertEquals(item.link, "http://flyingmeat.com/blog/archives/2017/9/acorn_and_10.13.html") + assertEquals(item.pubDate, DateUtils.stringToLocalDateTime("2017-09-25T14:27:27-07:00")) + assertEquals(item.author, "Author 1") + assertNotNull(item.content) + } + + @Test + fun otherCasesTest() { + val stream = context.resources.assets.open("localfeed/json/json_items_other_cases.json") + + val item = adapter.fromJson(Buffer().readFrom(stream))!![0] + + assertEquals(item.description, "This is a summary") + assertEquals(item.content, "content_html") + assertEquals(item.imageLink, "https://image.com") + assertEquals(item.author, "Author 1") + } + + @Test + fun nullTitleTest() { + val stream = context.resources.assets.open("localfeed/json/json_items_required_elements.json") + + Assert.assertThrows("Item title is required", ParseException::class.java) { adapter.fromJson(Buffer().readFrom(stream))!![0] } + } + + @Test + fun nullLinkTest() { + val stream = context.resources.assets.open("localfeed/json/json_items_required_elements.json") + + Assert.assertThrows("Item link is required", ParseException::class.java) { adapter.fromJson(Buffer().readFrom(stream))!![1] } + } + + @Test + fun nullDateTest() { + val stream = context.resources.assets.open("localfeed/json/json_items_required_elements.json") + + Assert.assertThrows("Item date is required", ParseException::class.java) { adapter.fromJson(Buffer().readFrom(stream))!![2] } + } +} \ No newline at end of file diff --git a/api/src/main/java/com/readrops/api/localfeed/json/JSONItemsAdapter.kt b/api/src/main/java/com/readrops/api/localfeed/json/JSONItemsAdapter.kt new file mode 100644 index 00000000..b2fbb4f8 --- /dev/null +++ b/api/src/main/java/com/readrops/api/localfeed/json/JSONItemsAdapter.kt @@ -0,0 +1,116 @@ +package com.readrops.api.localfeed.json + +import com.readrops.api.utils.DateUtils +import com.readrops.api.utils.ParseException +import com.readrops.api.utils.nextNullableString +import com.readrops.db.entities.Item +import com.squareup.moshi.FromJson +import com.squareup.moshi.JsonAdapter +import com.squareup.moshi.JsonReader +import com.squareup.moshi.JsonWriter + +class JSONItemsAdapter : JsonAdapter>() { + + override fun toJson(writer: JsonWriter, value: List?) { + // not useful + } + + @FromJson + override fun fromJson(reader: JsonReader): List { + try { + val items = arrayListOf() + reader.beginObject() + + while (reader.hasNext()) { + when (reader.nextName()) { + "items" -> parseItems(reader, items) + else -> reader.skipValue() + } + } + + return items + } catch (e: Exception) { + throw ParseException(e.message) + } + } + + private fun parseItems(reader: JsonReader, items: MutableList) { + reader.beginArray() + + while (reader.hasNext()) { + reader.beginObject() + val item = Item() + + var contentText: String? = null + var contentHtml: String? = null + + while (reader.hasNext()) { + with(item) { + when (reader.selectName(names)) { + 0 -> guid = reader.nextString() + 1 -> link = reader.nextString() + 2 -> title = reader.nextString() + 3 -> contentHtml = reader.nextNullableString() + 4 -> contentText = reader.nextNullableString() + 5 -> description = reader.nextNullableString() + 6 -> imageLink = reader.nextNullableString() + 7 -> pubDate = DateUtils.stringToLocalDateTime(reader.nextString()) + 8 -> author = parseAuthor(reader) // jsonfeed 1.0 + 9 -> author = parseAuthors(reader) // jsonfeed 1.1 + } + } + } + + validateItem(item) + item.content = if (contentHtml != null) contentHtml else contentText + + reader.endObject() + items += item + } + + reader.endArray() + } + + private fun parseAuthor(reader: JsonReader): String? { + var author: String? = null + reader.beginObject() + + while (reader.hasNext()) { + when (reader.nextName()) { + "name" -> author = reader.nextNullableString() + else -> reader.skipValue() + } + } + + reader.endObject() + return author + } + + /** + * Returns the first author of the array + */ + private fun parseAuthors(reader: JsonReader): String? { + val authors = arrayListOf() + reader.beginArray() + + while (reader.hasNext()) { + authors.add(parseAuthor(reader)) + } + + reader.endArray() + return if (authors.filterNotNull().isNotEmpty()) authors.filterNotNull().first() else null + } + + private fun validateItem(item: Item) { + when { + item.title == null -> throw ParseException("Item title is required") + item.link == null -> throw ParseException("Item link is required") + item.pubDate == null -> throw ParseException("Item date id required") + } + } + + companion object { + val names: JsonReader.Options = JsonReader.Options.of("id", "url", "title", "content_html", "content_text", + "summary", "image", "date_published", "author", "authors") + } +} \ No newline at end of file From 4b999e9fd65aa4bf39059af1663dcfa068791f33 Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Wed, 16 Sep 2020 13:57:45 +0200 Subject: [PATCH 038/187] Add jsonfeed parsing in LocalRSSDataSource --- .../api/localfeed/LocalRSSDataSourceTest.kt | 14 +++++++++++ .../api/localfeed/LocalRSSDataSource.kt | 23 +++++++++++++++---- 2 files changed, 33 insertions(+), 4 deletions(-) diff --git a/api/src/androidTest/java/com/readrops/api/localfeed/LocalRSSDataSourceTest.kt b/api/src/androidTest/java/com/readrops/api/localfeed/LocalRSSDataSourceTest.kt index 60c76b80..6960c9f5 100644 --- a/api/src/androidTest/java/com/readrops/api/localfeed/LocalRSSDataSourceTest.kt +++ b/api/src/androidTest/java/com/readrops/api/localfeed/LocalRSSDataSourceTest.kt @@ -81,6 +81,20 @@ class LocalRSSDataSourceTest { assertEquals(request.headers[LibUtils.LAST_MODIFIED_HEADER], "Last-Modified") } + @Test + fun jsonFeedTest() { + val stream = context.resources.assets.open("localfeed/json/json_feed.json") + + mockServer.enqueue(MockResponse().setResponseCode(HttpURLConnection.HTTP_OK) + .addHeader(LibUtils.CONTENT_TYPE_HEADER, "application/feed+json") + .setBody(Buffer().readFrom(stream))) + + val pair = localRSSDataSource.queryRSSResource(url.toString(), null, true)!! + + assertEquals(pair.first.name, "News from Flying Meat") + assertEquals(pair.second.size, 10) + } + @Test fun response304Test() { mockServer.enqueue(MockResponse().setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED)) diff --git a/api/src/main/java/com/readrops/api/localfeed/LocalRSSDataSource.kt b/api/src/main/java/com/readrops/api/localfeed/LocalRSSDataSource.kt index 817bc126..3541ff1d 100644 --- a/api/src/main/java/com/readrops/api/localfeed/LocalRSSDataSource.kt +++ b/api/src/main/java/com/readrops/api/localfeed/LocalRSSDataSource.kt @@ -2,15 +2,20 @@ package com.readrops.api.localfeed import android.accounts.NetworkErrorException import androidx.annotation.WorkerThread +import com.readrops.api.localfeed.json.JSONFeedAdapter +import com.readrops.api.localfeed.json.JSONItemsAdapter import com.readrops.api.utils.LibUtils import com.readrops.api.utils.ParseException import com.readrops.api.utils.UnknownFormatException import com.readrops.db.entities.Feed import com.readrops.db.entities.Item +import com.squareup.moshi.Moshi +import com.squareup.moshi.Types import okhttp3.Headers import okhttp3.OkHttpClient import okhttp3.Request import okhttp3.Response +import okio.Buffer import java.io.ByteArrayInputStream import java.io.IOException import java.io.InputStream @@ -89,7 +94,12 @@ class LocalRSSDataSource(private val httpClient: OkHttpClient) { adapter.fromXml(stream) } else { - Feed() + val adapter = Moshi.Builder() + .add(JSONFeedAdapter()) + .build() + .adapter(Feed::class.java) + + adapter.fromJson(Buffer().readFrom(stream))!! } feed.etag = response.header(LibUtils.ETAG_HEADER) @@ -98,13 +108,18 @@ class LocalRSSDataSource(private val httpClient: OkHttpClient) { return feed } - private fun parseItems(inputStream: InputStream, type: LocalRSSHelper.RSSType): List { + private fun parseItems(stream: InputStream, type: LocalRSSHelper.RSSType): List { return if (type != LocalRSSHelper.RSSType.JSONFEED) { val adapter = XmlAdapter.xmlItemsAdapterFactory(type) - adapter.fromXml(inputStream) + adapter.fromXml(stream) } else { - listOf() + val adapter = Moshi.Builder() + .add(Types.newParameterizedType(MutableList::class.java, Item::class.java), JSONItemsAdapter()) + .build() + .adapter>(Types.newParameterizedType(MutableList::class.java, Item::class.java)) + + adapter.fromJson(Buffer().readFrom(stream))!! } } } \ No newline at end of file From 838768800a641aa6c549b6da9636c01b47acea71 Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Thu, 17 Sep 2020 22:11:05 +0200 Subject: [PATCH 039/187] Handle the absence of some tags in RSS2 and ATOM feeds --- .../readrops/api/localfeed/LocalRSSDataSource.kt | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/api/src/main/java/com/readrops/api/localfeed/LocalRSSDataSource.kt b/api/src/main/java/com/readrops/api/localfeed/LocalRSSDataSource.kt index 3541ff1d..5b2726c5 100644 --- a/api/src/main/java/com/readrops/api/localfeed/LocalRSSDataSource.kt +++ b/api/src/main/java/com/readrops/api/localfeed/LocalRSSDataSource.kt @@ -102,6 +102,8 @@ class LocalRSSDataSource(private val httpClient: OkHttpClient) { adapter.fromJson(Buffer().readFrom(stream))!! } + handleSpecialCases(feed, type, response) + feed.etag = response.header(LibUtils.ETAG_HEADER) feed.lastModified = response.header(LibUtils.LAST_MODIFIED_HEADER) @@ -122,4 +124,15 @@ class LocalRSSDataSource(private val httpClient: OkHttpClient) { adapter.fromJson(Buffer().readFrom(stream))!! } } + + private fun handleSpecialCases(feed: Feed, type: LocalRSSHelper.RSSType, response: Response) { + with(feed) { + if (type == LocalRSSHelper.RSSType.RSS_2) { + if (url == null) url = response.request.url.toString() + } else if (type == LocalRSSHelper.RSSType.ATOM) { + if (url == null) url = response.request.url.toString() + if (siteUrl == null) siteUrl = response.request.url.scheme + "://" + response.request.url.host + } + } + } } \ No newline at end of file From 82c29d073215e3158a1783b230d4620a96bb4ce3 Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Thu, 17 Sep 2020 22:15:33 +0200 Subject: [PATCH 040/187] Use new LocalRSSDataSource in LocalFeedRepository --- .../api/localfeed/LocalRSSDataSource.kt | 1 + .../app/repositories/LocalFeedRepository.java | 181 ++++++------------ .../java/com/readrops/db/dao/FeedDao.java | 3 + 3 files changed, 64 insertions(+), 121 deletions(-) diff --git a/api/src/main/java/com/readrops/api/localfeed/LocalRSSDataSource.kt b/api/src/main/java/com/readrops/api/localfeed/LocalRSSDataSource.kt index 5b2726c5..15074d79 100644 --- a/api/src/main/java/com/readrops/api/localfeed/LocalRSSDataSource.kt +++ b/api/src/main/java/com/readrops/api/localfeed/LocalRSSDataSource.kt @@ -30,6 +30,7 @@ class LocalRSSDataSource(private val httpClient: OkHttpClient) { * @param withItems parse items with their feed * @return a Feed object with its items if specified by [withItems] */ + @Throws(ParseException::class, UnknownFormatException::class, NetworkErrorException::class, IOException::class) @WorkerThread fun queryRSSResource(url: String, headers: Headers?, withItems: Boolean): Pair>? { val response = queryUrl(url, headers) diff --git a/app/src/main/java/com/readrops/app/repositories/LocalFeedRepository.java b/app/src/main/java/com/readrops/app/repositories/LocalFeedRepository.java index e3d9651d..dc21cb85 100644 --- a/app/src/main/java/com/readrops/app/repositories/LocalFeedRepository.java +++ b/app/src/main/java/com/readrops/app/repositories/LocalFeedRepository.java @@ -2,30 +2,25 @@ package com.readrops.app.repositories; import android.accounts.NetworkErrorException; import android.content.Context; +import android.util.Log; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import com.readrops.api.localfeed.LocalRSSDataSource; +import com.readrops.api.services.SyncResult; +import com.readrops.api.utils.HttpManager; +import com.readrops.api.utils.LibUtils; +import com.readrops.api.utils.ParseException; +import com.readrops.api.utils.UnknownFormatException; import com.readrops.app.utils.FeedInsertionResult; import com.readrops.app.utils.HtmlParser; import com.readrops.app.utils.ParsingResult; import com.readrops.app.utils.SharedPreferencesManager; import com.readrops.app.utils.Utils; -import com.readrops.app.utils.matchers.FeedMatcher; -import com.readrops.app.utils.matchers.ItemMatcher; import com.readrops.db.entities.Feed; import com.readrops.db.entities.Item; import com.readrops.db.entities.account.Account; -import com.readrops.api.localfeed.AFeed; -import com.readrops.api.localfeed.RSSQuery; -import com.readrops.api.localfeed.RSSQueryResult; -import com.readrops.api.localfeed.atom.ATOMFeed; -import com.readrops.api.localfeed.json.JSONFeed; -import com.readrops.api.localfeed.rss.RSSFeed; -import com.readrops.api.services.SyncResult; -import com.readrops.api.utils.LibUtils; -import com.readrops.api.utils.ParseException; -import com.readrops.api.utils.UnknownFormatException; import org.jsoup.Jsoup; @@ -33,20 +28,24 @@ import java.io.IOException; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; -import java.util.HashMap; import java.util.List; import io.reactivex.Observable; import io.reactivex.Single; +import kotlin.Pair; +import okhttp3.Headers; public class LocalFeedRepository extends ARepository { private static final String TAG = LocalFeedRepository.class.getSimpleName(); + private LocalRSSDataSource dataSource; + public LocalFeedRepository(@NonNull Context context, @Nullable Account account) { super(context, account); syncResult = new SyncResult(); + dataSource = new LocalRSSDataSource(HttpManager.getInstance().getOkHttpClient()); } @Override @@ -64,47 +63,31 @@ public class LocalFeedRepository extends ARepository { return Observable.create(emitter -> { List feedList; - if (feeds == null || feeds.size() == 0) + if (feeds == null || feeds.isEmpty()) { feedList = database.feedDao().getFeeds(account.getId()); - else - feedList = new ArrayList<>(feeds); - - RSSQuery rssQuery = new RSSQuery(); - List syncErrors = new ArrayList<>(); + } else { + feedList = feeds; + } for (Feed feed : feedList) { emitter.onNext(feed); - FeedInsertionResult syncError = new FeedInsertionResult(); try { - HashMap headers = new HashMap<>(); - if (feed.getEtag() != null) - headers.put(LibUtils.IF_NONE_MATCH_HEADER, feed.getEtag()); - if (feed.getLastModified() != null) - headers.put(LibUtils.IF_MODIFIED_HEADER, feed.getLastModified()); + Headers.Builder headers = new Headers.Builder(); + if (feed.getEtag() != null) { + headers.add(LibUtils.IF_NONE_MATCH_HEADER, feed.getEtag()); + } + if (feed.getLastModified() != null) { + headers.add(LibUtils.IF_MODIFIED_HEADER, feed.getLastModified()); + } - RSSQueryResult queryResult = rssQuery.queryUrl(feed.getUrl(), headers); - if (queryResult != null && queryResult.getException() == null) - insertNewItems(queryResult.getFeed(), queryResult.getRssType()); - else if (queryResult != null && queryResult.getException() != null) { - Exception e = queryResult.getException(); + Pair> pair = dataSource.queryRSSResource(feed.getUrl(), headers.build(), true); - if (e instanceof UnknownFormatException) - syncError.setInsertionError(FeedInsertionResult.FeedInsertionError.FORMAT_ERROR); - else if (e instanceof NetworkErrorException) - syncError.setInsertionError(FeedInsertionResult.FeedInsertionError.NETWORK_ERROR); - - syncError.setFeed(feed); - syncErrors.add(syncError); + if (pair != null) { + insertNewItems(feed, pair.getSecond()); } } catch (Exception e) { - if (e instanceof IOException) - syncError.setInsertionError(FeedInsertionResult.FeedInsertionError.NETWORK_ERROR); - else - syncError.setInsertionError(FeedInsertionResult.FeedInsertionError.PARSE_ERROR); - - syncError.setFeed(feed); - syncErrors.add(syncError); + Log.d(TAG, "sync: " + e.getMessage()); } } @@ -121,28 +104,22 @@ public class LocalFeedRepository extends ARepository { FeedInsertionResult insertionResult = new FeedInsertionResult(); try { - RSSQuery rssNet = new RSSQuery(); - RSSQueryResult queryResult = rssNet.queryUrl(parsingResult.getUrl(), new HashMap<>()); + Pair> pair = dataSource.queryRSSResource(parsingResult.getUrl(), + null, false); + Feed feed = insertFeed(pair.getFirst(), parsingResult); - if (queryResult != null && queryResult.getException() == null) { - Feed feed = insertFeed(queryResult.getFeed(), queryResult.getRssType(), parsingResult); - if (feed != null) { - insertionResult.setFeed(feed); - insertionResult.setParsingResult(parsingResult); - insertionResults.add(insertionResult); - } - } else if (queryResult != null && queryResult.getException() != null) { - insertionResult.setParsingResult(parsingResult); - insertionResult.setInsertionError(getErrorFromException(queryResult.getException())); - - insertionResults.add(insertionResult); + if (feed != null) { + insertionResult.setFeed(feed); } + } catch (ParseException e) { + insertionResult.setInsertionError(FeedInsertionResult.FeedInsertionError.PARSE_ERROR); + } catch (UnknownFormatException e) { + insertionResult.setInsertionError(FeedInsertionResult.FeedInsertionError.FORMAT_ERROR); + } catch (NetworkErrorException | IOException e) { + insertionResult.setInsertionError(FeedInsertionResult.FeedInsertionError.NETWORK_ERROR); } catch (Exception e) { - if (e instanceof IOException) - insertionResult.setInsertionError(FeedInsertionResult.FeedInsertionError.NETWORK_ERROR); - else - insertionResult.setInsertionError(FeedInsertionResult.FeedInsertionError.PARSE_ERROR); - + insertionResult.setInsertionError(FeedInsertionResult.FeedInsertionError.UNKNOWN_ERROR); + } finally { insertionResult.setParsingResult(parsingResult); insertionResults.add(insertionResult); } @@ -152,67 +129,38 @@ public class LocalFeedRepository extends ARepository { }); } - private void insertNewItems(AFeed feed, RSSQuery.RSSType type) throws ParseException { - Feed dbFeed; - List items; + @SuppressWarnings("SimplifyStreamApiCallChains") + private void insertNewItems(Feed feed, List items) { + database.feedDao().updateHeaders(feed.getEtag(), feed.getLastModified(), feed.getId()); - switch (type) { - case RSS_2: - dbFeed = database.feedDao().getFeedByUrl(((RSSFeed) feed).getChannel().getFeedUrl(), account.getId()); - items = ItemMatcher.itemsFromRSS(((RSSFeed) feed).getChannel().getItems(), dbFeed); - break; - case RSS_ATOM: - dbFeed = database.feedDao().getFeedByUrl(((ATOMFeed) feed).getUrl(), account.getId()); - items = ItemMatcher.itemsFromATOM(((ATOMFeed) feed).getEntries(), dbFeed); - break; - case RSS_JSON: - dbFeed = database.feedDao().getFeedByUrl(((JSONFeed) feed).getFeedUrl(), account.getId()); - items = ItemMatcher.itemsFromJSON(((JSONFeed) feed).getItems(), dbFeed); - break; - default: - throw new IllegalArgumentException("Unknown RSS type"); - } - - database.feedDao().updateHeaders(dbFeed.getEtag(), dbFeed.getLastModified(), dbFeed.getId()); Collections.sort(items, Item::compareTo); - int maxItems = Integer.parseInt(SharedPreferencesManager.readString(context, SharedPreferencesManager.SharedPrefKey.ITEMS_TO_PARSE_MAX_NB)); - if (maxItems > 0 && items.size() > maxItems) + int maxItems = Integer.parseInt(SharedPreferencesManager.readString(context, + SharedPreferencesManager.SharedPrefKey.ITEMS_TO_PARSE_MAX_NB)); + if (maxItems > 0 && items.size() > maxItems) { items = items.subList(items.size() - maxItems, items.size()); - - insertItems(items, dbFeed); - } - - private Feed insertFeed(AFeed feed, RSSQuery.RSSType type, ParsingResult parsingResult) { - Feed dbFeed; - switch (type) { - case RSS_2: - dbFeed = FeedMatcher.feedFromRSS((RSSFeed) feed); - break; - case RSS_ATOM: - dbFeed = FeedMatcher.feedFromATOM((ATOMFeed) feed); - break; - case RSS_JSON: - dbFeed = FeedMatcher.feedFromJSON((JSONFeed) feed); - break; - default: - throw new IllegalArgumentException("Unknown RSS type"); } - dbFeed.setFolderId(parsingResult.getFolderId()); + items.stream().forEach(item -> item.setFeedId(feed.getId())); + insertItems(items, feed); + } - if (database.feedDao().feedExists(dbFeed.getUrl(), account.getId())) + private Feed insertFeed(Feed feed, ParsingResult parsingResult) { + feed.setFolderId(parsingResult.getFolderId()); + + if (database.feedDao().feedExists(feed.getUrl(), account.getId())) { return null; // feed already inserted + } - setFeedColors(dbFeed); - dbFeed.setAccountId(account.getId()); + setFeedColors(feed); + feed.setAccountId(account.getId()); // we need empty headers to query the feed just after, without any 304 result - dbFeed.setEtag(null); - dbFeed.setLastModified(null); + feed.setEtag(null); + feed.setLastModified(null); - dbFeed.setId((int) (database.feedDao().compatInsert(dbFeed))); - return dbFeed; + feed.setId((int) (database.feedDao().compatInsert(feed))); + return feed; } private void insertItems(Collection items, Feed feed) { @@ -253,13 +201,4 @@ public class LocalFeedRepository extends ARepository { syncResult.getItems().addAll(itemsToInsert); database.itemDao().insert(itemsToInsert); } - - private FeedInsertionResult.FeedInsertionError getErrorFromException(Exception e) { - if (e instanceof UnknownFormatException) - return FeedInsertionResult.FeedInsertionError.FORMAT_ERROR; - else if (e instanceof NetworkErrorException) - return FeedInsertionResult.FeedInsertionError.NETWORK_ERROR; - else - return FeedInsertionResult.FeedInsertionError.UNKNOWN_ERROR; - } } diff --git a/db/src/main/java/com/readrops/db/dao/FeedDao.java b/db/src/main/java/com/readrops/db/dao/FeedDao.java index 9ff6a5b7..f1a70691 100644 --- a/db/src/main/java/com/readrops/db/dao/FeedDao.java +++ b/db/src/main/java/com/readrops/db/dao/FeedDao.java @@ -29,6 +29,9 @@ public abstract class FeedDao implements BaseDao { @Query("Select * from Feed Where id = :feedId") public abstract Feed getFeedById(int feedId); + @Query("Select id From Feed Where url = :url and account_id = :accountId") + public abstract int getFeedIdByUrl(String url, int accountId); + @Query("Select case When :feedUrl In (Select url from Feed Where account_id = :accountId) Then 1 else 0 end") public abstract boolean feedExists(String feedUrl, int accountId); From f6f8807b3a3562ed1b690e7c642ca7dd9c40a14e Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Thu, 17 Sep 2020 22:17:58 +0200 Subject: [PATCH 041/187] Preserve feeds elements whitespaces --- .../main/java/com/readrops/api/utils/KonsumerExtensions.kt | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/api/src/main/java/com/readrops/api/utils/KonsumerExtensions.kt b/api/src/main/java/com/readrops/api/utils/KonsumerExtensions.kt index adef66de..f1760e85 100644 --- a/api/src/main/java/com/readrops/api/utils/KonsumerExtensions.kt +++ b/api/src/main/java/com/readrops/api/utils/KonsumerExtensions.kt @@ -1,13 +1,14 @@ package com.readrops.api.utils import com.gitlab.mvysny.konsumexml.Konsumer +import com.gitlab.mvysny.konsumexml.Whitespace fun Konsumer.nonNullText(failOnElement: Boolean = true): String { - val text = text(failOnElement = failOnElement) + val text = text(failOnElement = failOnElement, whitespace = Whitespace.preserve) return if (text.isNotEmpty()) text else throw ParseException("Xml field $name can't be null") } fun Konsumer.nullableText(failOnElement: Boolean = true): String? { - val text = text(failOnElement = failOnElement) + val text = text(failOnElement = failOnElement, whitespace = Whitespace.preserve) return if (text.isNotEmpty()) text else null } \ No newline at end of file From ccce0a810da8b492ac1dee65a52093988c97c1cc Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Fri, 18 Sep 2020 14:12:07 +0200 Subject: [PATCH 042/187] Fix some RSS2 parsing issues --- .../assets/localfeed/rss/rss_feed_special_cases.xml | 2 +- .../androidTest/assets/localfeed/rss/rss_full_feed.xml | 3 ++- api/src/androidTest/assets/localfeed/rss_feed.xml | 3 ++- .../com/readrops/api/localfeed/rss/RSSItemsAdapterTest.kt | 2 +- .../java/com/readrops/api/localfeed/rss/RSSFeedAdapter.kt | 8 ++++++-- .../com/readrops/api/localfeed/rss/RSSItemsAdapter.kt | 1 + 6 files changed, 13 insertions(+), 6 deletions(-) diff --git a/api/src/androidTest/assets/localfeed/rss/rss_feed_special_cases.xml b/api/src/androidTest/assets/localfeed/rss/rss_feed_special_cases.xml index 4ec78959..55f74777 100644 --- a/api/src/androidTest/assets/localfeed/rss/rss_feed_special_cases.xml +++ b/api/src/androidTest/assets/localfeed/rss/rss_feed_special_cases.xml @@ -2,7 +2,7 @@ - + https://news.ycombinator.com/ Links for the intellectually curious, ranked by readers. diff --git a/api/src/androidTest/assets/localfeed/rss/rss_full_feed.xml b/api/src/androidTest/assets/localfeed/rss/rss_full_feed.xml index 671e4066..e56ad91b 100644 --- a/api/src/androidTest/assets/localfeed/rss/rss_full_feed.xml +++ b/api/src/androidTest/assets/localfeed/rss/rss_full_feed.xml @@ -2,7 +2,8 @@ Hacker News - + + https://news.ycombinator.com/ Links for the intellectually curious, ranked by readers. diff --git a/api/src/androidTest/assets/localfeed/rss_feed.xml b/api/src/androidTest/assets/localfeed/rss_feed.xml index fbd76101..79d3b7a1 100644 --- a/api/src/androidTest/assets/localfeed/rss_feed.xml +++ b/api/src/androidTest/assets/localfeed/rss_feed.xml @@ -1,5 +1,5 @@ - + Hacker News https://news.ycombinator.com/ @@ -11,6 +11,7 @@ https://news.ycombinator.com/item?id=24273602 Author 1 Comments]]> + media description Palantir S-1 diff --git a/api/src/androidTest/java/com/readrops/api/localfeed/rss/RSSItemsAdapterTest.kt b/api/src/androidTest/java/com/readrops/api/localfeed/rss/RSSItemsAdapterTest.kt index 7644cc24..992a2cdb 100644 --- a/api/src/androidTest/java/com/readrops/api/localfeed/rss/RSSItemsAdapterTest.kt +++ b/api/src/androidTest/java/com/readrops/api/localfeed/rss/RSSItemsAdapterTest.kt @@ -31,7 +31,7 @@ class RSSItemsAdapterTest { assertEquals(item.link, "https://www.bbc.com/news/world-africa-53887947") assertEquals(item.pubDate, DateUtils.stringToLocalDateTime("Tue, 25 Aug 2020 17:15:49 +0000")) assertEquals(item.author, "Author 1") - assertNotNull(item.description) + assertEquals(item.description, "Comments") assertEquals(item.guid, "https://www.bbc.com/news/world-africa-53887947") } diff --git a/api/src/main/java/com/readrops/api/localfeed/rss/RSSFeedAdapter.kt b/api/src/main/java/com/readrops/api/localfeed/rss/RSSFeedAdapter.kt index 52c6fcff..35f805a0 100644 --- a/api/src/main/java/com/readrops/api/localfeed/rss/RSSFeedAdapter.kt +++ b/api/src/main/java/com/readrops/api/localfeed/rss/RSSFeedAdapter.kt @@ -24,9 +24,13 @@ class RSSFeedAdapter : XmlAdapter { with(feed) { when (tagName) { "title" -> name = Jsoup.parse(nonNullText()).text() - "description" -> description = nullableText(failOnElement = false) + "description" -> description = nullableText() "link" -> siteUrl = nullableText() - "atom:link" -> url = attributes.getValueOpt("href") + "atom:link" -> { + if (attributes.getValueOpt("rel") == "self") + url = attributes.getValueOpt("href") + } + else -> skipContents() } } } diff --git a/api/src/main/java/com/readrops/api/localfeed/rss/RSSItemsAdapter.kt b/api/src/main/java/com/readrops/api/localfeed/rss/RSSItemsAdapter.kt index e2344061..cae98764 100644 --- a/api/src/main/java/com/readrops/api/localfeed/rss/RSSItemsAdapter.kt +++ b/api/src/main/java/com/readrops/api/localfeed/rss/RSSItemsAdapter.kt @@ -34,6 +34,7 @@ class RSSItemsAdapter : XmlAdapter> { "content:encoded" -> content = nullableText() "enclosure" -> parseEnclosure(this, enclosures) "media:content" -> parseMediaContent(this, mediaContents) + else -> skipContents() // for example media:description } } } From 0eb518d69227456029bc9fb89064f3c36b3c1d36 Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Sat, 19 Sep 2020 17:25:36 +0200 Subject: [PATCH 043/187] Ignore media content sub elements --- .../localfeed/rss/rss_items_media_content.xml | 2 +- .../readrops/api/localfeed/rss/RSSItemsAdapter.kt | 13 ++++++++++--- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/api/src/androidTest/assets/localfeed/rss/rss_items_media_content.xml b/api/src/androidTest/assets/localfeed/rss/rss_items_media_content.xml index 323246c7..74355f7b 100644 --- a/api/src/androidTest/assets/localfeed/rss/rss_items_media_content.xml +++ b/api/src/androidTest/assets/localfeed/rss/rss_items_media_content.xml @@ -18,7 +18,7 @@ - + image2 title \ No newline at end of file diff --git a/api/src/main/java/com/readrops/api/localfeed/rss/RSSItemsAdapter.kt b/api/src/main/java/com/readrops/api/localfeed/rss/RSSItemsAdapter.kt index cae98764..5f074af3 100644 --- a/api/src/main/java/com/readrops/api/localfeed/rss/RSSItemsAdapter.kt +++ b/api/src/main/java/com/readrops/api/localfeed/rss/RSSItemsAdapter.kt @@ -30,10 +30,15 @@ class RSSItemsAdapter : XmlAdapter> { "pubDate" -> pubDate = DateUtils.stringToLocalDateTime(nonNullText()) "dc:date" -> pubDate = DateUtils.stringToLocalDateTime(nonNullText()) "guid" -> guid = nullableText() - "description" -> description = nullableText() - "content:encoded" -> content = nullableText() + "description" -> description = text(failOnElement = false) + "content:encoded" -> content = nullableText(failOnElement = false) "enclosure" -> parseEnclosure(this, enclosures) "media:content" -> parseMediaContent(this, mediaContents) + "media:group" -> allChildrenAutoIgnore("content") { + when (tagName) { + "media:content" -> parseMediaContent(this, mediaContents) + } + } else -> skipContents() // for example media:description } } @@ -69,6 +74,8 @@ class RSSItemsAdapter : XmlAdapter> { if (konsume.attributes.getValueOpt("medium") != null && LibUtils.isMimeImage(konsume.attributes["medium"])) mediaContents += konsume.attributes["url"] + + konsume.skipContents() // ignore media content sub elements } private fun validateItem(item: Item) { @@ -81,6 +88,6 @@ class RSSItemsAdapter : XmlAdapter> { companion object { val names = Names.of("title", "link", "author", "creator", "pubDate", "date", - "guid", "description", "encoded", "enclosure", "content") + "guid", "description", "encoded", "enclosure", "content", "group") } } \ No newline at end of file From 581de2e1dd09baa39585088d1180a45d32581a2b Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Sat, 19 Sep 2020 18:50:23 +0200 Subject: [PATCH 044/187] Use nullable attributes in ATOM adapters --- .../java/com/readrops/api/localfeed/atom/ATOMFeedAdapter.kt | 2 +- .../com/readrops/api/localfeed/atom/ATOMItemsAdapter.kt | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/api/src/main/java/com/readrops/api/localfeed/atom/ATOMFeedAdapter.kt b/api/src/main/java/com/readrops/api/localfeed/atom/ATOMFeedAdapter.kt index e53a4bf2..43446073 100644 --- a/api/src/main/java/com/readrops/api/localfeed/atom/ATOMFeedAdapter.kt +++ b/api/src/main/java/com/readrops/api/localfeed/atom/ATOMFeedAdapter.kt @@ -38,7 +38,7 @@ class ATOMFeedAdapter : XmlAdapter { } private fun parseLink(konsume: Konsumer, feed: Feed) { - val rel = konsume.attributes["rel"] + val rel = konsume.attributes.getValueOpt("rel") if (rel == "self") feed.url = konsume.attributes["href"] diff --git a/api/src/main/java/com/readrops/api/localfeed/atom/ATOMItemsAdapter.kt b/api/src/main/java/com/readrops/api/localfeed/atom/ATOMItemsAdapter.kt index db7fe178..3d2ccf8f 100644 --- a/api/src/main/java/com/readrops/api/localfeed/atom/ATOMItemsAdapter.kt +++ b/api/src/main/java/com/readrops/api/localfeed/atom/ATOMItemsAdapter.kt @@ -26,7 +26,11 @@ class ATOMItemsAdapter : XmlAdapter> { "title" -> title = nonNullText() "id" -> guid = nullableText() "updated" -> pubDate = DateUtils.stringToLocalDateTime(nonNullText()) - "link" -> if (attributes["rel"] == "alternate") link = attributes["href"] + "link" -> { + if (attributes.getValueOpt("rel") == null || + attributes["rel"] == "alternate") + link = attributes["href"] + } "author" -> allChildrenAutoIgnore("name") { author = text() } "summary" -> description = nullableText() "content" -> content = nullableText() From 6a1ddaeabb63ac5325037ef2a8bfe1dcc9a7fa40 Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Sun, 20 Sep 2020 18:12:05 +0200 Subject: [PATCH 045/187] Use LocalRSSDataSource isUrlRSSResource --- .../readrops/api/localfeed/LocalRSSHelper.kt | 35 +++++++------------ .../app/viewmodels/AddFeedsViewModel.java | 15 ++++---- 2 files changed, 20 insertions(+), 30 deletions(-) diff --git a/api/src/main/java/com/readrops/api/localfeed/LocalRSSHelper.kt b/api/src/main/java/com/readrops/api/localfeed/LocalRSSHelper.kt index 13894e6a..313d564a 100644 --- a/api/src/main/java/com/readrops/api/localfeed/LocalRSSHelper.kt +++ b/api/src/main/java/com/readrops/api/localfeed/LocalRSSHelper.kt @@ -1,18 +1,14 @@ package com.readrops.api.localfeed -import com.readrops.api.utils.UnknownFormatException import java.io.InputStream import java.util.regex.Pattern object LocalRSSHelper { private const val RSS_DEFAULT_CONTENT_TYPE = "application/rss+xml" - private const val RSS_TEXT_CONTENT_TYPE = "text/xml" - private const val RSS_APPLICATION_CONTENT_TYPE = "application/xml" private const val ATOM_CONTENT_TYPE = "application/atom+xml" private const val JSONFEED_CONTENT_TYPE = "application/feed+json" private const val JSON_CONTENT_TYPE = "application/json" - private const val HTML_CONTENT_TYPE = "text/html" private const val RSS_2_REGEX = "rss.*version=\"2.0\"" @@ -26,8 +22,7 @@ object LocalRSSHelper { RSS_DEFAULT_CONTENT_TYPE -> RSSType.RSS_2 ATOM_CONTENT_TYPE -> RSSType.ATOM JSON_CONTENT_TYPE, JSONFEED_CONTENT_TYPE -> RSSType.JSONFEED - RSS_TEXT_CONTENT_TYPE, RSS_APPLICATION_CONTENT_TYPE, HTML_CONTENT_TYPE -> RSSType.UNKNOWN - else -> throw UnknownFormatException("Unknown content type : $contentType") + else -> RSSType.UNKNOWN } } @@ -37,27 +32,21 @@ object LocalRSSHelper { fun getRSSContentType(content: InputStream): RSSType { val stringBuffer = StringBuffer() val reader = content.bufferedReader() + var type = RSSType.UNKNOWN - var currentLine = reader.readLine() - while (currentLine != null) { - stringBuffer.append(currentLine) + // we get the first 10 lines which should be sufficient to get the type, + // otherwise iterating over the whole file could be too slow + for (i in 0..9) stringBuffer.append(reader.readLine()) - if (Pattern.compile(RSS_2_REGEX).matcher(stringBuffer.toString()).find()) { - reader.close() - content.close() - - return RSSType.RSS_2 - } else if (Pattern.compile(ATOM_REGEX).matcher(stringBuffer.toString()).find()) { - reader.close() - content.close() - - return RSSType.ATOM - } - - currentLine = reader.readLine() + if (Pattern.compile(RSS_2_REGEX).matcher(stringBuffer.toString()).find()) { + type = RSSType.RSS_2 + } else if (Pattern.compile(ATOM_REGEX).matcher(stringBuffer.toString()).find()) { + type = RSSType.ATOM } - return RSSType.UNKNOWN + reader.close() + content.close() + return type } enum class RSSType { diff --git a/app/src/main/java/com/readrops/app/viewmodels/AddFeedsViewModel.java b/app/src/main/java/com/readrops/app/viewmodels/AddFeedsViewModel.java index 804ae2eb..303fdb0e 100644 --- a/app/src/main/java/com/readrops/app/viewmodels/AddFeedsViewModel.java +++ b/app/src/main/java/com/readrops/app/viewmodels/AddFeedsViewModel.java @@ -7,13 +7,14 @@ import androidx.annotation.NonNull; import androidx.lifecycle.AndroidViewModel; import androidx.lifecycle.LiveData; -import com.readrops.db.Database; -import com.readrops.db.entities.account.Account; +import com.readrops.api.localfeed.LocalRSSDataSource; +import com.readrops.api.utils.HttpManager; import com.readrops.app.repositories.ARepository; import com.readrops.app.utils.FeedInsertionResult; import com.readrops.app.utils.HtmlParser; import com.readrops.app.utils.ParsingResult; -import com.readrops.api.localfeed.RSSQuery; +import com.readrops.db.Database; +import com.readrops.db.entities.account.Account; import java.util.ArrayList; import java.util.List; @@ -47,15 +48,15 @@ public class AddFeedsViewModel extends AndroidViewModel { public Single> parseUrl(String url) { return Single.create(emitter -> { - RSSQuery rssApi = new RSSQuery(); + LocalRSSDataSource dataSource = new LocalRSSDataSource(HttpManager.getInstance().getOkHttpClient()); List results = new ArrayList<>(); - if (rssApi.isUrlFeedLink(url)) { + if (dataSource.isUrlRSSResource(url)) { ParsingResult parsingResult = new ParsingResult(url, null); results.add(parsingResult); - - } else + } else { results.addAll(HtmlParser.getFeedLink(url)); + } emitter.onSuccess(results); }); From 5998fa9126f975b21c2b82144c75eb529d293430 Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Sun, 20 Sep 2020 18:17:43 +0200 Subject: [PATCH 046/187] Delete unused code --- .../java/com/readrops/api/localfeed/AFeed.kt | 9 - .../com/readrops/api/localfeed/RSSQuery.java | 198 ------------------ .../api/localfeed/RSSQueryResult.java | 42 ---- .../api/localfeed/atom/ATOMAuthor.java | 22 -- .../api/localfeed/atom/ATOMEntry.java | 98 --------- .../readrops/api/localfeed/atom/ATOMFeed.java | 128 ----------- .../readrops/api/localfeed/atom/ATOMLink.java | 30 --- .../readrops/api/localfeed/json/JSONAuthor.kt | 9 - .../readrops/api/localfeed/json/JSONFeed.kt | 15 -- .../readrops/api/localfeed/json/JSONItem.kt | 21 -- .../api/localfeed/rss/RSSChannel.java | 87 -------- .../api/localfeed/rss/RSSEnclosure.java | 30 --- .../readrops/api/localfeed/rss/RSSFeed.java | 21 -- .../readrops/api/localfeed/rss/RSSItem.java | 154 -------------- .../readrops/api/localfeed/rss/RSSLink.java | 40 ---- .../api/localfeed/rss/RSSMediaContent.java | 30 --- .../app/utils/matchers/FeedMatcher.java | 65 ------ .../app/utils/matchers/ItemMatcher.java | 123 ----------- 18 files changed, 1122 deletions(-) delete mode 100644 api/src/main/java/com/readrops/api/localfeed/AFeed.kt delete mode 100644 api/src/main/java/com/readrops/api/localfeed/RSSQuery.java delete mode 100644 api/src/main/java/com/readrops/api/localfeed/RSSQueryResult.java delete mode 100644 api/src/main/java/com/readrops/api/localfeed/atom/ATOMAuthor.java delete mode 100644 api/src/main/java/com/readrops/api/localfeed/atom/ATOMEntry.java delete mode 100644 api/src/main/java/com/readrops/api/localfeed/atom/ATOMFeed.java delete mode 100644 api/src/main/java/com/readrops/api/localfeed/atom/ATOMLink.java delete mode 100644 api/src/main/java/com/readrops/api/localfeed/json/JSONAuthor.kt delete mode 100644 api/src/main/java/com/readrops/api/localfeed/json/JSONFeed.kt delete mode 100644 api/src/main/java/com/readrops/api/localfeed/json/JSONItem.kt delete mode 100644 api/src/main/java/com/readrops/api/localfeed/rss/RSSChannel.java delete mode 100644 api/src/main/java/com/readrops/api/localfeed/rss/RSSEnclosure.java delete mode 100644 api/src/main/java/com/readrops/api/localfeed/rss/RSSFeed.java delete mode 100644 api/src/main/java/com/readrops/api/localfeed/rss/RSSItem.java delete mode 100644 api/src/main/java/com/readrops/api/localfeed/rss/RSSLink.java delete mode 100644 api/src/main/java/com/readrops/api/localfeed/rss/RSSMediaContent.java delete mode 100644 app/src/main/java/com/readrops/app/utils/matchers/FeedMatcher.java delete mode 100644 app/src/main/java/com/readrops/app/utils/matchers/ItemMatcher.java diff --git a/api/src/main/java/com/readrops/api/localfeed/AFeed.kt b/api/src/main/java/com/readrops/api/localfeed/AFeed.kt deleted file mode 100644 index 3cbd7653..00000000 --- a/api/src/main/java/com/readrops/api/localfeed/AFeed.kt +++ /dev/null @@ -1,9 +0,0 @@ -package com.readrops.api.localfeed - -/* - A simple class to give an abstract level to rss/atom/json feed classes - */ -abstract class AFeed { - var etag: String? = null - var lastModified: String? = null -} \ No newline at end of file diff --git a/api/src/main/java/com/readrops/api/localfeed/RSSQuery.java b/api/src/main/java/com/readrops/api/localfeed/RSSQuery.java deleted file mode 100644 index 75b8c8fc..00000000 --- a/api/src/main/java/com/readrops/api/localfeed/RSSQuery.java +++ /dev/null @@ -1,198 +0,0 @@ -package com.readrops.api.localfeed; - -import android.accounts.NetworkErrorException; -import android.util.Log; - -import com.readrops.api.localfeed.atom.ATOMFeed; -import com.readrops.api.localfeed.json.JSONFeed; -import com.readrops.api.localfeed.rss.RSSFeed; -import com.readrops.api.localfeed.rss.RSSLink; -import com.readrops.api.utils.HttpManager; -import com.readrops.api.utils.LibUtils; -import com.readrops.api.utils.UnknownFormatException; -import com.squareup.moshi.JsonAdapter; -import com.squareup.moshi.Moshi; - -import org.simpleframework.xml.Serializer; -import org.simpleframework.xml.core.Persister; - -import java.io.IOException; -import java.io.InputStream; -import java.util.HashMap; -import java.util.Map; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -import okhttp3.OkHttpClient; -import okhttp3.Request; -import okhttp3.Response; - -public class RSSQuery { - - private static final String TAG = RSSQuery.class.getSimpleName(); - - private static final String RSS_CONTENT_TYPE_REGEX = "([^;]+)"; - - private static final String RSS_2_REGEX = "rss.*version=\"2.0\""; - - private static final String ATOM_REGEX = "has to be called from another thread than the main one. - * - * @param url url to request - * @throws Exception - */ - public RSSQueryResult queryUrl(String url, Map headers) throws Exception { - Response response = query(url, headers); - - if (response.isSuccessful()) { - String header = response.header(LibUtils.CONTENT_TYPE_HEADER); - RSSType type = getRSSType(header); - - if (type == null) - return new RSSQueryResult(new UnknownFormatException("bad content type : " + header + "for " + url)); - - return parseFeed(response.body().byteStream(), type, response); - } else if (response.code() == 304) - return null; - else - return new RSSQueryResult(new NetworkErrorException("Error " + response.code() + " when requesting url " + url)); - } - - public boolean isUrlFeedLink(String url) throws IOException { - Response response = query(url, new HashMap<>()); - - if (response.isSuccessful()) { - String header = response.header(LibUtils.CONTENT_TYPE_HEADER); - RSSType type = getRSSType(header); - - if (type == RSSType.RSS_UNKNOWN) { - RSSType contentType = getContentRSSType(response.body().string()); - return contentType != RSSType.RSS_UNKNOWN; - } else return type != null; - } else - return false; - } - - private Response query(String url, Map headers) throws IOException { - OkHttpClient okHttpClient = HttpManager.getInstance().getOkHttpClient(); - HttpManager.getInstance().setCredentials(null); - - Request.Builder builder = new Request.Builder().url(url); - for (String header : headers.keySet()) { - String value = headers.get(header); - builder.addHeader(header, value); - } - - Request request = builder.build(); - return okHttpClient.newCall(request).execute(); - } - - private RSSType getRSSType(String contentType) { - Pattern pattern = Pattern.compile(RSS_CONTENT_TYPE_REGEX); - Matcher matcher = pattern.matcher(contentType); - - String header; - if (matcher.find()) - header = matcher.group(0); - else - header = contentType; - - switch (header) { - case LibUtils.RSS_DEFAULT_CONTENT_TYPE: - return RSSType.RSS_2; - case LibUtils.RSS_TEXT_CONTENT_TYPE: - case LibUtils.HTML_CONTENT_TYPE: - case LibUtils.RSS_APPLICATION_CONTENT_TYPE: - return RSSType.RSS_UNKNOWN; - case LibUtils.ATOM_CONTENT_TYPE: - return RSSType.RSS_ATOM; - case LibUtils.JSON_CONTENT_TYPE: - return RSSType.RSS_JSON; - default: - Log.d(TAG, "bad content type : " + contentType); - return null; - } - } - - /** - * Parse input feed - * - * @param stream source to parse - * @param type rss type, important to know the feed format - * @param response query response - * @throws Exception - */ - private RSSQueryResult parseFeed(InputStream stream, RSSType type, Response response) throws Exception { - String xml = LibUtils.inputStreamToString(stream); - Serializer serializer = new Persister(); - - if (type == RSSType.RSS_UNKNOWN) { - RSSType contentType = getContentRSSType(xml); - if (contentType == RSSType.RSS_UNKNOWN) { - return new RSSQueryResult(new UnknownFormatException("Unknown content format")); - } else - type = contentType; - } - - String etag = response.header(LibUtils.ETAG_HEADER); - String lastModified = response.header(LibUtils.LAST_MODIFIED_HEADER); - AFeed feed = null; - RSSQueryResult queryResult = new RSSQueryResult(); - - switch (type) { - case RSS_2: - feed = serializer.read(RSSFeed.class, xml); - - // workaround if the channel does not have any atom:link tag - if (((RSSFeed) feed).getChannel().getFeedUrl() == null) { - ((RSSFeed) feed).getChannel().getLinks().add(new RSSLink(null, response.request().url().toString())); - } - break; - case RSS_ATOM: - feed = serializer.read(ATOMFeed.class, xml); - ((ATOMFeed) feed).setWebsiteUrl(response.request().url().scheme() + "://" + response.request().url().host()); - ((ATOMFeed) feed).setUrl(response.request().url().toString()); - break; - case RSS_JSON: - Moshi moshi = new Moshi.Builder() - .build(); - - JsonAdapter jsonFeedAdapter = moshi.adapter(JSONFeed.class); - feed = jsonFeedAdapter.fromJson(xml); - break; - } - - queryResult.setFeed(feed); - queryResult.setRssType(type); - - feed.setEtag(etag); - feed.setLastModified(lastModified); - - return queryResult; - } - - private RSSType getContentRSSType(String content) { - RSSType type; - - if (Pattern.compile(RSS_2_REGEX).matcher(content).find()) - type = RSSType.RSS_2; - else if (Pattern.compile(ATOM_REGEX).matcher(content).find()) - type = RSSType.RSS_ATOM; - else - type = RSSType.RSS_UNKNOWN; - - return type; - } - - public enum RSSType { - RSS_2, - RSS_ATOM, - RSS_JSON, - RSS_UNKNOWN - } - - -} diff --git a/api/src/main/java/com/readrops/api/localfeed/RSSQueryResult.java b/api/src/main/java/com/readrops/api/localfeed/RSSQueryResult.java deleted file mode 100644 index 449e162a..00000000 --- a/api/src/main/java/com/readrops/api/localfeed/RSSQueryResult.java +++ /dev/null @@ -1,42 +0,0 @@ -package com.readrops.api.localfeed; - -public class RSSQueryResult { - - private AFeed feed; - - private RSSQuery.RSSType rssType; - - private Exception exception; - - public RSSQueryResult(Exception exception) { - this.exception = exception; - } - - public RSSQueryResult() { - - } - - public AFeed getFeed() { - return feed; - } - - public void setFeed(AFeed feed) { - this.feed = feed; - } - - public RSSQuery.RSSType getRssType() { - return rssType; - } - - public void setRssType(RSSQuery.RSSType rssType) { - this.rssType = rssType; - } - - public void setException(Exception exception) { - this.exception = exception; - } - - public Exception getException() { - return exception; - } -} diff --git a/api/src/main/java/com/readrops/api/localfeed/atom/ATOMAuthor.java b/api/src/main/java/com/readrops/api/localfeed/atom/ATOMAuthor.java deleted file mode 100644 index 302d06c3..00000000 --- a/api/src/main/java/com/readrops/api/localfeed/atom/ATOMAuthor.java +++ /dev/null @@ -1,22 +0,0 @@ -package com.readrops.api.localfeed.atom; - -import org.simpleframework.xml.Element; -import org.simpleframework.xml.Root; - -@Root(name = "author", strict = false) -public class ATOMAuthor { - - @Element(required = false) - private String name; - - @Element(required = false) - private String email; - - public String getName() { - return name; - } - - public String getEmail() { - return email; - } -} diff --git a/api/src/main/java/com/readrops/api/localfeed/atom/ATOMEntry.java b/api/src/main/java/com/readrops/api/localfeed/atom/ATOMEntry.java deleted file mode 100644 index 08671f1e..00000000 --- a/api/src/main/java/com/readrops/api/localfeed/atom/ATOMEntry.java +++ /dev/null @@ -1,98 +0,0 @@ -package com.readrops.api.localfeed.atom; - -import org.simpleframework.xml.Attribute; -import org.simpleframework.xml.Element; -import org.simpleframework.xml.ElementList; -import org.simpleframework.xml.Root; - -import java.util.List; - -@Root(name = "entry", strict = false) -public class ATOMEntry { - - @Element(required = false) - private String title; - - @ElementList(name = "link", inline = true, required = false) - private List links; - - @Element(required = false) - private String updated; - - @Element(required = false) - private String summary; - - @Element(required = false) - private String id; - - @Element(required = false) - private String content; - - @Attribute(name = "type", required = false) - private String contentType; - - public String getTitle() { - return title; - } - - public void setTitle(String title) { - this.title = title; - } - - public List getLinks() { - return links; - } - - public void setLinks(List links) { - this.links = links; - } - - public String getUpdated() { - return updated; - } - - public void setUpdated(String updated) { - this.updated = updated; - } - - public String getSummary() { - return summary; - } - - public void setSummary(String summary) { - this.summary = summary; - } - - public String getId() { - return id; - } - - public void setId(String id) { - this.id = id; - } - - public String getContent() { - return content; - } - - public void setContent(String content) { - this.content = content; - } - - public String getContentType() { - return contentType; - } - - public void setContentType(String contentType) { - this.contentType = contentType; - } - - public String getUrl() { - for (ATOMLink link : links) { - if (link.getRel() == null || link.getRel().equals("self") || link.getRel().equals("alternate")) - return link.getHref(); - } - - return null; - } -} diff --git a/api/src/main/java/com/readrops/api/localfeed/atom/ATOMFeed.java b/api/src/main/java/com/readrops/api/localfeed/atom/ATOMFeed.java deleted file mode 100644 index e83e3c1f..00000000 --- a/api/src/main/java/com/readrops/api/localfeed/atom/ATOMFeed.java +++ /dev/null @@ -1,128 +0,0 @@ -package com.readrops.api.localfeed.atom; - -import com.readrops.api.localfeed.AFeed; - -import org.simpleframework.xml.Element; -import org.simpleframework.xml.ElementList; -import org.simpleframework.xml.Root; - -import java.util.List; - -@Root(name = "feed", strict = false) -public class ATOMFeed extends AFeed { - - @Element(required = false) - private String title; - - @ElementList(name = "link", inline = true, required = false) - private List links; - - private String url; - - private String websiteUrl; - - @Element(required = false) - private String id; - - @Element(required = false) - private String subtitle; - - @Element(required = false) - private String updated; - - @Element(required = false) - private ATOMAuthor author; - - @ElementList(inline = true, required = false) - private List entries; - - public String getTitle() { - return title; - } - - public void setTitle(String title) { - this.title = title; - } - - public List getLinks() { - return links; - } - - public void setLinks(List links) { - this.links = links; - } - - public String getSubtitle() { - return subtitle; - } - - public void setSubtitle(String subtitle) { - this.subtitle = subtitle; - } - - public String getUpdated() { - return updated; - } - - public void setUpdated(String updated) { - this.updated = updated; - } - - public ATOMAuthor getAuthor() { - return author; - } - - public void setAuthor(ATOMAuthor author) { - this.author = author; - } - - public List getEntries() { - return entries; - } - - public void setEntries(List entries) { - this.entries = entries; - } - - public String getId() { - return id; - } - - public void setId(String id) { - this.id = id; - } - - public String getUrl() { - return url; - } - - public void setUrl(String url) { - this.url = url; - } - - public String getWebsiteUrl() { - return websiteUrl; - } - - public void setWebsiteUrl(String websiteUrl) { - this.websiteUrl = websiteUrl; - } - - /*public String getWebSiteUrl() { - return id; - } - - public String getUrl() { - if (links.size() > 0) { - if (links.get(0).getRel() != null) - return links.get(0).getHref(); - else { - if (links.size() > 1) - return links.get(1).getHref(); - else - return null; - } - } else - return null; - }*/ -} diff --git a/api/src/main/java/com/readrops/api/localfeed/atom/ATOMLink.java b/api/src/main/java/com/readrops/api/localfeed/atom/ATOMLink.java deleted file mode 100644 index 024a137f..00000000 --- a/api/src/main/java/com/readrops/api/localfeed/atom/ATOMLink.java +++ /dev/null @@ -1,30 +0,0 @@ -package com.readrops.api.localfeed.atom; - -import org.simpleframework.xml.Attribute; -import org.simpleframework.xml.Root; - -@Root(name = "link", strict = false) -public class ATOMLink { - - @Attribute(name = "href", required = false) - private String href; - - @Attribute(name = "rel", required = false) - private String rel; - - public String getHref() { - return href; - } - - public void setHref(String href) { - this.href = href; - } - - public String getRel() { - return rel; - } - - public void setRel(String rel) { - this.rel = rel; - } -} diff --git a/api/src/main/java/com/readrops/api/localfeed/json/JSONAuthor.kt b/api/src/main/java/com/readrops/api/localfeed/json/JSONAuthor.kt deleted file mode 100644 index 125e69a1..00000000 --- a/api/src/main/java/com/readrops/api/localfeed/json/JSONAuthor.kt +++ /dev/null @@ -1,9 +0,0 @@ -package com.readrops.api.localfeed.json - -import com.squareup.moshi.Json -import com.squareup.moshi.JsonClass - -@JsonClass(generateAdapter = true) -data class JSONAuthor(val name: String, - val url: String, - @Json(name = "avatar") val avatarUrl: String?) \ No newline at end of file diff --git a/api/src/main/java/com/readrops/api/localfeed/json/JSONFeed.kt b/api/src/main/java/com/readrops/api/localfeed/json/JSONFeed.kt deleted file mode 100644 index 7d83d834..00000000 --- a/api/src/main/java/com/readrops/api/localfeed/json/JSONFeed.kt +++ /dev/null @@ -1,15 +0,0 @@ -package com.readrops.api.localfeed.json - -import com.readrops.api.localfeed.AFeed -import com.squareup.moshi.Json -import com.squareup.moshi.JsonClass - -@JsonClass(generateAdapter = true) -data class JSONFeed(val version: String, - val title: String, - @Json(name = "home_page_url") val homePageUrl: String?, - @Json(name = "feed_url") val feedUrl: String?, - val description: String?, - @Json(name = "icon") val iconUrl: String?, - @Json(name = "favicon") val faviconUrl: String?, - val items: List) : AFeed() \ No newline at end of file diff --git a/api/src/main/java/com/readrops/api/localfeed/json/JSONItem.kt b/api/src/main/java/com/readrops/api/localfeed/json/JSONItem.kt deleted file mode 100644 index 94e50b51..00000000 --- a/api/src/main/java/com/readrops/api/localfeed/json/JSONItem.kt +++ /dev/null @@ -1,21 +0,0 @@ -package com.readrops.api.localfeed.json - -import com.squareup.moshi.Json -import com.squareup.moshi.JsonClass - -@JsonClass(generateAdapter = true) -data class JSONItem(val id: String, - val title: String?, - val summary: String?, - @Json(name = "content_text") val contentText: String?, - @Json(name = "content_html") val contentHtml: String?, - val url: String?, - @Json(name = "image") val imageUrl: String?, - @Json(name = "date_published") val pubDate: String, - @Json(name = "date_modified") val modDate: String?, - val author: JSONAuthor?) { - - fun getContent(): String? { - return contentHtml ?: contentText - } -} \ No newline at end of file diff --git a/api/src/main/java/com/readrops/api/localfeed/rss/RSSChannel.java b/api/src/main/java/com/readrops/api/localfeed/rss/RSSChannel.java deleted file mode 100644 index 32c88be3..00000000 --- a/api/src/main/java/com/readrops/api/localfeed/rss/RSSChannel.java +++ /dev/null @@ -1,87 +0,0 @@ -package com.readrops.api.localfeed.rss; - -import org.simpleframework.xml.Element; -import org.simpleframework.xml.ElementList; -import org.simpleframework.xml.Root; - -import java.util.List; - -@Root(name = "channel", strict = false) -public class RSSChannel { - - @Element(name = "title", required = false) - private String title; - - @Element(name = "description", required = false) - private String description; - - // workaround to get the two links (feed and regular) - @ElementList(name = "link", inline = true, required = false) - private List links; - - @Element(name = "lastBuildDate", required = false) - private String lastUpdated; - - @ElementList(inline = true, required = false) - private List items; - - public String getTitle() { - return title; - } - - public void setTitle(String title) { - this.title = title; - } - - public String getDescription() { - return description; - } - - public void setDescription(String description) { - this.description = description; - } - - public List getLinks() { - return links; - } - - public void setLinks(List links) { - this.links = links; - } - - public List getItems() { - return items; - } - - public void setItems(List items) { - this.items = items; - } - - public String getLastUpdated() { - return lastUpdated; - } - - public void setLastUpdated(String lastUpdated) { - this.lastUpdated = lastUpdated; - } - - public String getFeedUrl() { - if (links.size() > 1) { - if (links.get(0).getHref() != null) - return links.get(0).getHref(); - else - return links.get(1).getHref(); - } else - return null; - } - - public String getUrl() { - if (links.size() > 1) { - if (links.get(1).getText() != null) - return links.get(1).getText(); - else - return links.get(0).getText(); - } else - return null; - } -} diff --git a/api/src/main/java/com/readrops/api/localfeed/rss/RSSEnclosure.java b/api/src/main/java/com/readrops/api/localfeed/rss/RSSEnclosure.java deleted file mode 100644 index 45a25dbd..00000000 --- a/api/src/main/java/com/readrops/api/localfeed/rss/RSSEnclosure.java +++ /dev/null @@ -1,30 +0,0 @@ -package com.readrops.api.localfeed.rss; - -import org.simpleframework.xml.Attribute; -import org.simpleframework.xml.Root; - -@Root(name = "enclosure", strict = false) -public class RSSEnclosure { - - @Attribute(required = false) - private String type; - - @Attribute(required = false) - private String url; - - public String getType() { - return type; - } - - public void setType(String type) { - this.type = type; - } - - public String getUrl() { - return url; - } - - public void setUrl(String url) { - this.url = url; - } -} diff --git a/api/src/main/java/com/readrops/api/localfeed/rss/RSSFeed.java b/api/src/main/java/com/readrops/api/localfeed/rss/RSSFeed.java deleted file mode 100644 index e52ab253..00000000 --- a/api/src/main/java/com/readrops/api/localfeed/rss/RSSFeed.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.readrops.api.localfeed.rss; - -import com.readrops.api.localfeed.AFeed; - -import org.simpleframework.xml.Element; -import org.simpleframework.xml.Root; - -@Root(name = "rss", strict = false) -public class RSSFeed extends AFeed { - - @Element(name = "channel", required = false) - private RSSChannel channel; - - public RSSChannel getChannel() { - return channel; - } - - public void setChannel(RSSChannel channel) { - this.channel = channel; - } -} diff --git a/api/src/main/java/com/readrops/api/localfeed/rss/RSSItem.java b/api/src/main/java/com/readrops/api/localfeed/rss/RSSItem.java deleted file mode 100644 index 7cd15406..00000000 --- a/api/src/main/java/com/readrops/api/localfeed/rss/RSSItem.java +++ /dev/null @@ -1,154 +0,0 @@ -package com.readrops.api.localfeed.rss; - -import org.simpleframework.xml.Element; -import org.simpleframework.xml.ElementList; -import org.simpleframework.xml.Namespace; -import org.simpleframework.xml.Root; - -import java.util.List; - -@Root(name = "item", strict = false) -public class RSSItem { - - @Element - private String title; - - @Element(name = "link", required = false) - private String link; - - @Element(name = "imageLink", required = false) - private String imageLink; - - @ElementList(name = "content", inline = true, required = false) - @Namespace(prefix = "media") - private List mediaContents; - - @ElementList(name = "enclosure", inline = true, required = false) - private List enclosures; - - @ElementList(name = "creator", inline = true, required = false) - @Namespace(prefix = "dc", reference = "http://purl.org/dc/elements/1.1/") - private List creator; - - @Element(required = false) - private String author; - - @Element(name = "pubDate", required = false) - private String pubDate; - - @Element(name = "date", required = false) - @Namespace(prefix = "dc") - private String date; - - @Element(name = "description", required = false) - private String description; - - @Element(name = "encoded", required = false) - @Namespace(prefix = "content") - private String content; - - @Element(required = false) - private String guid; - - public String getTitle() { - return title; - } - - public void setTitle(String title) { - this.title = title; - } - - public String getDescription() { - return description; - } - - public void setDescription(String description) { - this.description = description; - } - - public String getLink() { - return link; - } - - public void setLink(String link) { - this.link = link; - } - - public String getImageLink() { - return imageLink; - } - - public void setImageLink(String imageLink) { - this.imageLink = imageLink; - } - - public List getCreator() { - return creator; - } - - public void setCreator(List creator) { - this.creator = creator; - } - - public String getPubDate() { - return pubDate; - } - - public void setPubDate(String pubDate) { - this.pubDate = pubDate; - } - - public String getContent() { - return content; - } - - public void setContent(String content) { - this.content = content; - } - - public String getGuid() { - return guid; - } - - public void setGuid(String guid) { - this.guid = guid; - } - - public List getMediaContents() { - return mediaContents; - } - - public void setMediaContents(List mediaContents) { - this.mediaContents = mediaContents; - } - - public List getEnclosures() { - return enclosures; - } - - public void setEnclosures(List enclosures) { - this.enclosures = enclosures; - } - - public String getDate() { - if (pubDate != null) - return pubDate; - else - return date; - } - - public void setDate(String date) { - this.date = date; - } - - public String getAuthor() { - if (creator != null && !creator.isEmpty()) - return creator.get(0); - else - return author; - } - - public void setAuthor(String author) { - this.author = author; - } -} diff --git a/api/src/main/java/com/readrops/api/localfeed/rss/RSSLink.java b/api/src/main/java/com/readrops/api/localfeed/rss/RSSLink.java deleted file mode 100644 index e6511c33..00000000 --- a/api/src/main/java/com/readrops/api/localfeed/rss/RSSLink.java +++ /dev/null @@ -1,40 +0,0 @@ -package com.readrops.api.localfeed.rss; - -import org.simpleframework.xml.Attribute; -import org.simpleframework.xml.Root; -import org.simpleframework.xml.Text; - -@Root(name = "link", strict = false) -public class RSSLink { - - @Text(required = false) - private String text; - - @Attribute(name = "href", required = false) - private String href; - - public RSSLink() { - - } - - public RSSLink(String text, String href) { - this.text = text; - this.href = href; - } - - public String getHref() { - return href; - } - - public void setHref(String href) { - this.href = href; - } - - public String getText() { - return text; - } - - public void setText(String text) { - this.text = text; - } -} diff --git a/api/src/main/java/com/readrops/api/localfeed/rss/RSSMediaContent.java b/api/src/main/java/com/readrops/api/localfeed/rss/RSSMediaContent.java deleted file mode 100644 index 4e7c38e1..00000000 --- a/api/src/main/java/com/readrops/api/localfeed/rss/RSSMediaContent.java +++ /dev/null @@ -1,30 +0,0 @@ -package com.readrops.api.localfeed.rss; - -import org.simpleframework.xml.Attribute; -import org.simpleframework.xml.Root; - -@Root(name = "content", strict = false) -public class RSSMediaContent { - - @Attribute(required = false) - private String url; - - @Attribute(required = false) - private String medium; - - public String getUrl() { - return url; - } - - public void setUrl(String url) { - this.url = url; - } - - public String getMedium() { - return medium; - } - - public void setMedium(String medium) { - this.medium = medium; - } -} diff --git a/app/src/main/java/com/readrops/app/utils/matchers/FeedMatcher.java b/app/src/main/java/com/readrops/app/utils/matchers/FeedMatcher.java deleted file mode 100644 index bb07ae91..00000000 --- a/app/src/main/java/com/readrops/app/utils/matchers/FeedMatcher.java +++ /dev/null @@ -1,65 +0,0 @@ -package com.readrops.app.utils.matchers; - -import com.readrops.db.entities.Feed; -import com.readrops.api.localfeed.atom.ATOMFeed; -import com.readrops.api.localfeed.json.JSONFeed; -import com.readrops.api.localfeed.rss.RSSChannel; -import com.readrops.api.localfeed.rss.RSSFeed; - -import org.jsoup.Jsoup; - -public final class FeedMatcher { - - public static Feed feedFromRSS(RSSFeed rssFeed) { - Feed feed = new Feed(); - RSSChannel channel = rssFeed.getChannel(); - - feed.setName(Jsoup.parse(channel.getTitle()).text()); - feed.setUrl(channel.getFeedUrl()); - feed.setSiteUrl(channel.getUrl()); - feed.setDescription(channel.getDescription()); - feed.setLastUpdated(channel.getLastUpdated()); - - feed.setEtag(rssFeed.getEtag()); - feed.setLastModified(rssFeed.getLastModified()); - - feed.setFolderId(null); - - return feed; - } - - public static Feed feedFromATOM(ATOMFeed atomFeed) { - Feed feed = new Feed(); - - feed.setName(atomFeed.getTitle()); - feed.setDescription(atomFeed.getSubtitle()); - feed.setUrl(atomFeed.getUrl()); - feed.setSiteUrl(atomFeed.getWebsiteUrl()); - feed.setDescription(atomFeed.getSubtitle()); - feed.setLastUpdated(atomFeed.getUpdated()); - - feed.setEtag(atomFeed.getEtag()); - feed.setLastModified(atomFeed.getLastModified()); - - feed.setFolderId(null); - - return feed; - } - - public static Feed feedFromJSON(JSONFeed jsonFeed) { - Feed feed = new Feed(); - - feed.setName(jsonFeed.getTitle()); - feed.setUrl(jsonFeed.getFeedUrl()); - feed.setSiteUrl(jsonFeed.getHomePageUrl()); - feed.setDescription(jsonFeed.getDescription()); - - feed.setEtag(jsonFeed.getEtag()); - feed.setLastModified(jsonFeed.getLastModified()); - feed.setIconUrl(jsonFeed.getFaviconUrl()); - - feed.setFolderId(null); - - return feed; - } -} diff --git a/app/src/main/java/com/readrops/app/utils/matchers/ItemMatcher.java b/app/src/main/java/com/readrops/app/utils/matchers/ItemMatcher.java deleted file mode 100644 index 58803743..00000000 --- a/app/src/main/java/com/readrops/app/utils/matchers/ItemMatcher.java +++ /dev/null @@ -1,123 +0,0 @@ -package com.readrops.app.utils.matchers; - -import com.readrops.api.localfeed.atom.ATOMEntry; -import com.readrops.api.localfeed.json.JSONItem; -import com.readrops.api.localfeed.rss.RSSEnclosure; -import com.readrops.api.localfeed.rss.RSSItem; -import com.readrops.api.localfeed.rss.RSSMediaContent; -import com.readrops.api.utils.DateUtils; -import com.readrops.api.utils.LibUtils; -import com.readrops.api.utils.ParseException; -import com.readrops.app.utils.Utils; -import com.readrops.db.entities.Feed; -import com.readrops.db.entities.Item; - -import java.util.ArrayList; -import java.util.List; - -public final class ItemMatcher { - - public static List itemsFromRSS(List items, Feed feed) throws ParseException { - List dbItems = new ArrayList<>(); - - for (RSSItem item : items) { - Item newItem = new Item(); - - newItem.setAuthor(item.getAuthor()); - newItem.setContent(item.getContent()); // Jsoup.clean(item.getContent(), Whitelist.relaxed()) - newItem.setDescription(item.getDescription()); - newItem.setGuid(item.getGuid() != null ? item.getGuid() : item.getLink()); - newItem.setTitle(LibUtils.cleanText(item.getTitle())); - - try { - newItem.setPubDate(DateUtils.stringToLocalDateTime(item.getDate())); - } catch (Exception e) { - throw new ParseException(); - } - - newItem.setLink(item.getLink()); - newItem.setFeedId(feed.getId()); - - if (item.getMediaContents() != null && !item.getMediaContents().isEmpty()) { - for (RSSMediaContent mediaContent : item.getMediaContents()) { - if (mediaContent.getMedium() != null && Utils.isTypeImage(mediaContent.getMedium())) { - newItem.setImageLink(mediaContent.getUrl()); - break; - } - } - } else { - if (item.getEnclosures() != null) { - for (RSSEnclosure enclosure : item.getEnclosures()) { - if (enclosure.getType() != null && Utils.isTypeImage(enclosure.getType()) - && enclosure.getUrl() != null) { - newItem.setImageLink(enclosure.getUrl()); - break; - } - } - - } - } - - dbItems.add(newItem); - } - - return dbItems; - } - - public static List itemsFromATOM(List items, Feed feed) throws ParseException { - List dbItems = new ArrayList<>(); - - for (ATOMEntry item : items) { - Item dbItem = new Item(); - - dbItem.setContent(item.getContent()); // Jsoup.clean(item.getContent(), Whitelist.relaxed()) - dbItem.setDescription(item.getSummary()); - dbItem.setGuid(item.getId()); - dbItem.setTitle(LibUtils.cleanText(item.getTitle())); - - try { - dbItem.setPubDate(DateUtils.stringToLocalDateTime(item.getUpdated())); - } catch (Exception e) { - throw new ParseException(); - } - - dbItem.setLink(item.getUrl()); - - dbItem.setFeedId(feed.getId()); - - dbItems.add(dbItem); - } - - return dbItems; - } - - public static List itemsFromJSON(List items, Feed feed) throws ParseException { - List dbItems = new ArrayList<>(); - - for (JSONItem item : items) { - Item dbItem = new Item(); - - if (item.getAuthor() != null) - dbItem.setAuthor(item.getAuthor().getName()); - - dbItem.setContent(item.getContent()); // Jsoup.clean(item.getContent(), Whitelist.relaxed()) - dbItem.setDescription(item.getSummary()); - dbItem.setGuid(item.getId()); - dbItem.setTitle(LibUtils.cleanText(item.getTitle())); - - try { - dbItem.setPubDate(DateUtils.stringToLocalDateTime(item.getPubDate())); - } catch (Exception e) { - throw new ParseException(); - } - - dbItem.setLink(item.getUrl()); - - dbItem.setFeedId(feed.getId()); - - dbItems.add(dbItem); - } - - return dbItems; - } -} From de21a308b6dae64f661d6d8d3bfa8e1373eccf7b Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Sun, 20 Sep 2020 18:22:53 +0200 Subject: [PATCH 047/187] Fix LocalRSSHelper unit tests --- .../java/com/readrops/api/localfeed/LocalRSSHelperTest.kt | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/api/src/test/java/com/readrops/api/localfeed/LocalRSSHelperTest.kt b/api/src/test/java/com/readrops/api/localfeed/LocalRSSHelperTest.kt index bec3cbf1..c039d38d 100644 --- a/api/src/test/java/com/readrops/api/localfeed/LocalRSSHelperTest.kt +++ b/api/src/test/java/com/readrops/api/localfeed/LocalRSSHelperTest.kt @@ -1,6 +1,5 @@ package com.readrops.api.localfeed -import com.readrops.api.utils.UnknownFormatException import junit.framework.TestCase.assertEquals import org.junit.Test import java.io.ByteArrayInputStream @@ -20,7 +19,7 @@ class LocalRSSHelperTest { } @Test - fun nonStandardContentTypesTest() { + fun nonSupportedContentTypesTest() { assertEquals(LocalRSSHelper.getRSSType("application/xml"), LocalRSSHelper.RSSType.UNKNOWN) assertEquals(LocalRSSHelper.getRSSType("text/xml"), @@ -29,10 +28,6 @@ class LocalRSSHelperTest { LocalRSSHelper.RSSType.UNKNOWN) } - @Test(expected = UnknownFormatException::class) - fun nonSupportedContentTypeTest() { - LocalRSSHelper.getRSSType("image/jpeg") - } @Test fun rssContentTest() { From 694ff6331e4abc28979834a8a66b551c2c482c8b Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Sun, 20 Sep 2020 18:44:55 +0200 Subject: [PATCH 048/187] Add tests for XmlAdapter --- api/build.gradle | 10 +++---- .../com/readrops/api/localfeed/XmlAdapter.kt | 5 ++-- .../readrops/api/localfeed/XmlAdapterTest.kt | 28 +++++++++++++++++++ 3 files changed, 36 insertions(+), 7 deletions(-) create mode 100644 api/src/test/java/com/readrops/api/localfeed/XmlAdapterTest.kt diff --git a/api/build.gradle b/api/build.gradle index 6d0d0bca..c7fa49aa 100644 --- a/api/build.gradle +++ b/api/build.gradle @@ -46,11 +46,11 @@ dependencies { implementation "androidx.core:core-ktx:1.2.0" implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" - testImplementation 'junit:junit:4.12' - androidTestImplementation 'androidx.test.ext:junit:1.1.1' - androidTestImplementation 'androidx.test:runner:1.2.0' - androidTestImplementation 'androidx.test:rules:1.2.0' - androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' + testImplementation 'junit:junit:4.13' + androidTestImplementation 'androidx.test.ext:junit:1.1.2' + androidTestImplementation 'androidx.test:runner:1.3.0' + androidTestImplementation 'androidx.test:rules:1.3.0' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0' androidTestImplementation 'com.squareup.okhttp3:mockwebserver:4.8.1' implementation 'com.gitlab.mvysny.konsume-xml:konsume-xml:0.11' diff --git a/api/src/main/java/com/readrops/api/localfeed/XmlAdapter.kt b/api/src/main/java/com/readrops/api/localfeed/XmlAdapter.kt index b7f39d64..bbc05bda 100644 --- a/api/src/main/java/com/readrops/api/localfeed/XmlAdapter.kt +++ b/api/src/main/java/com/readrops/api/localfeed/XmlAdapter.kt @@ -7,6 +7,7 @@ import com.readrops.api.localfeed.rss.RSSItemsAdapter import com.readrops.db.entities.Feed import com.readrops.db.entities.Item import java.io.InputStream +import java.lang.IllegalArgumentException interface XmlAdapter { @@ -17,7 +18,7 @@ interface XmlAdapter { return when (type) { LocalRSSHelper.RSSType.RSS_2 -> RSSFeedAdapter() LocalRSSHelper.RSSType.ATOM -> ATOMFeedAdapter() - else -> throw Exception("Unknown RSS type : $type") + else -> throw IllegalArgumentException("Unknown RSS type : $type") } } @@ -25,7 +26,7 @@ interface XmlAdapter { return when (type) { LocalRSSHelper.RSSType.RSS_2 -> RSSItemsAdapter() LocalRSSHelper.RSSType.ATOM -> ATOMItemsAdapter() - else -> throw Exception("Unknown RSS type : $type") + else -> throw IllegalArgumentException("Unknown RSS type : $type") } } } diff --git a/api/src/test/java/com/readrops/api/localfeed/XmlAdapterTest.kt b/api/src/test/java/com/readrops/api/localfeed/XmlAdapterTest.kt new file mode 100644 index 00000000..5da0b20c --- /dev/null +++ b/api/src/test/java/com/readrops/api/localfeed/XmlAdapterTest.kt @@ -0,0 +1,28 @@ +package com.readrops.api.localfeed + +import com.readrops.api.localfeed.atom.ATOMFeedAdapter +import com.readrops.api.localfeed.atom.ATOMItemsAdapter +import com.readrops.api.localfeed.rss.RSSFeedAdapter +import com.readrops.api.localfeed.rss.RSSItemsAdapter +import junit.framework.TestCase.assertTrue +import org.junit.Assert +import org.junit.Test + +class XmlAdapterTest { + + @Test + fun xmlFeedAdapterFactoryTest() { + assertTrue(XmlAdapter.xmlFeedAdapterFactory(LocalRSSHelper.RSSType.RSS_2) is RSSFeedAdapter) + assertTrue(XmlAdapter.xmlFeedAdapterFactory(LocalRSSHelper.RSSType.ATOM) is ATOMFeedAdapter) + + Assert.assertThrows(IllegalArgumentException::class.java) { XmlAdapter.xmlFeedAdapterFactory(LocalRSSHelper.RSSType.UNKNOWN) } + } + + @Test + fun xmlItemsAdapterFactoryTest() { + assertTrue(XmlAdapter.xmlItemsAdapterFactory(LocalRSSHelper.RSSType.RSS_2) is RSSItemsAdapter) + assertTrue(XmlAdapter.xmlItemsAdapterFactory(LocalRSSHelper.RSSType.ATOM) is ATOMItemsAdapter) + + Assert.assertThrows(IllegalArgumentException::class.java) { XmlAdapter.xmlItemsAdapterFactory(LocalRSSHelper.RSSType.UNKNOWN) } + } +} \ No newline at end of file From cb41f3c7ac030fbe2b17adb29ddd1e2c6c625e03 Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Sun, 20 Sep 2020 18:48:43 +0200 Subject: [PATCH 049/187] Catch JSONFeedAdapter exceptions --- .../api/localfeed/json/JSONFeedAdapter.kt | 31 +++++++++++-------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/api/src/main/java/com/readrops/api/localfeed/json/JSONFeedAdapter.kt b/api/src/main/java/com/readrops/api/localfeed/json/JSONFeedAdapter.kt index 4358f315..a67a6a16 100644 --- a/api/src/main/java/com/readrops/api/localfeed/json/JSONFeedAdapter.kt +++ b/api/src/main/java/com/readrops/api/localfeed/json/JSONFeedAdapter.kt @@ -1,5 +1,6 @@ package com.readrops.api.localfeed.json +import com.readrops.api.utils.ParseException import com.readrops.api.utils.nextNullableString import com.readrops.db.entities.Feed import com.squareup.moshi.FromJson @@ -13,23 +14,27 @@ class JSONFeedAdapter { @FromJson fun fromJson(reader: JsonReader): Feed { - val feed = Feed() - reader.beginObject() + try { + val feed = Feed() + reader.beginObject() - while (reader.hasNext()) { - with(feed) { - when (reader.selectName(names)) { - 0 -> name = reader.nextString() - 1 -> siteUrl = reader.nextNullableString() - 2 -> url = reader.nextNullableString() - 3 -> description = reader.nextNullableString() - else -> reader.skipValue() + while (reader.hasNext()) { + with(feed) { + when (reader.selectName(names)) { + 0 -> name = reader.nextString() + 1 -> siteUrl = reader.nextNullableString() + 2 -> url = reader.nextNullableString() + 3 -> description = reader.nextNullableString() + else -> reader.skipValue() + } } } - } - reader.endObject() - return feed + reader.endObject() + return feed + } catch (e: Exception) { + throw ParseException(e.message) + } } companion object { From 963350d1dd686ba2730f443075a15be050a19050 Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Sun, 20 Sep 2020 19:00:49 +0200 Subject: [PATCH 050/187] Prepare files for the addition of RSS1 adapters --- .../RSSFeedAdapterTest.kt => rss2/RSS2FeedAdapterTest.kt} | 6 +++--- .../RSS2ItemsAdapterTest.kt} | 7 +++---- .../main/java/com/readrops/api/localfeed/XmlAdapter.kt | 8 ++++---- .../{rss/RSSFeedAdapter.kt => rss2/RSS2FeedAdapter.kt} | 4 ++-- .../{rss/RSSItemsAdapter.kt => rss2/RSS2ItemsAdapter.kt} | 4 ++-- .../java/com/readrops/api/localfeed/XmlAdapterTest.kt | 8 ++++---- 6 files changed, 18 insertions(+), 19 deletions(-) rename api/src/androidTest/java/com/readrops/api/localfeed/{rss/RSSFeedAdapterTest.kt => rss2/RSS2FeedAdapterTest.kt} (90%) rename api/src/androidTest/java/com/readrops/api/localfeed/{rss/RSSItemsAdapterTest.kt => rss2/RSS2ItemsAdapterTest.kt} (94%) rename api/src/main/java/com/readrops/api/localfeed/{rss/RSSFeedAdapter.kt => rss2/RSS2FeedAdapter.kt} (95%) rename api/src/main/java/com/readrops/api/localfeed/{rss/RSSItemsAdapter.kt => rss2/RSS2ItemsAdapter.kt} (97%) diff --git a/api/src/androidTest/java/com/readrops/api/localfeed/rss/RSSFeedAdapterTest.kt b/api/src/androidTest/java/com/readrops/api/localfeed/rss2/RSS2FeedAdapterTest.kt similarity index 90% rename from api/src/androidTest/java/com/readrops/api/localfeed/rss/RSSFeedAdapterTest.kt rename to api/src/androidTest/java/com/readrops/api/localfeed/rss2/RSS2FeedAdapterTest.kt index 79fbcafb..343cdae7 100644 --- a/api/src/androidTest/java/com/readrops/api/localfeed/rss/RSSFeedAdapterTest.kt +++ b/api/src/androidTest/java/com/readrops/api/localfeed/rss2/RSS2FeedAdapterTest.kt @@ -1,4 +1,4 @@ -package com.readrops.api.localfeed.rss +package com.readrops.api.localfeed.rss2 import android.content.Context import androidx.test.ext.junit.runners.AndroidJUnit4 @@ -9,11 +9,11 @@ import org.junit.Test import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) -class RSSFeedAdapterTest { +class RSS2FeedAdapterTest { private val context: Context = InstrumentationRegistry.getInstrumentation().context - private val adapter = RSSFeedAdapter() + private val adapter = RSS2FeedAdapter() @Test fun normalCasesTest() { diff --git a/api/src/androidTest/java/com/readrops/api/localfeed/rss/RSSItemsAdapterTest.kt b/api/src/androidTest/java/com/readrops/api/localfeed/rss2/RSS2ItemsAdapterTest.kt similarity index 94% rename from api/src/androidTest/java/com/readrops/api/localfeed/rss/RSSItemsAdapterTest.kt rename to api/src/androidTest/java/com/readrops/api/localfeed/rss2/RSS2ItemsAdapterTest.kt index 992a2cdb..b5d75e08 100644 --- a/api/src/androidTest/java/com/readrops/api/localfeed/rss/RSSItemsAdapterTest.kt +++ b/api/src/androidTest/java/com/readrops/api/localfeed/rss2/RSS2ItemsAdapterTest.kt @@ -1,4 +1,4 @@ -package com.readrops.api.localfeed.rss +package com.readrops.api.localfeed.rss2 import android.content.Context import androidx.test.ext.junit.runners.AndroidJUnit4 @@ -6,17 +6,16 @@ import androidx.test.platform.app.InstrumentationRegistry import com.readrops.api.utils.DateUtils import com.readrops.api.utils.ParseException import junit.framework.TestCase.assertEquals -import junit.framework.TestCase.assertNotNull import org.junit.Assert import org.junit.Test import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) -class RSSItemsAdapterTest { +class RSS2ItemsAdapterTest { private val context: Context = InstrumentationRegistry.getInstrumentation().context - private val adapter = RSSItemsAdapter() + private val adapter = RSS2ItemsAdapter() @Test fun normalCasesTest() { diff --git a/api/src/main/java/com/readrops/api/localfeed/XmlAdapter.kt b/api/src/main/java/com/readrops/api/localfeed/XmlAdapter.kt index bbc05bda..8f21a9e5 100644 --- a/api/src/main/java/com/readrops/api/localfeed/XmlAdapter.kt +++ b/api/src/main/java/com/readrops/api/localfeed/XmlAdapter.kt @@ -2,8 +2,8 @@ package com.readrops.api.localfeed import com.readrops.api.localfeed.atom.ATOMFeedAdapter import com.readrops.api.localfeed.atom.ATOMItemsAdapter -import com.readrops.api.localfeed.rss.RSSFeedAdapter -import com.readrops.api.localfeed.rss.RSSItemsAdapter +import com.readrops.api.localfeed.rss2.RSS2FeedAdapter +import com.readrops.api.localfeed.rss2.RSS2ItemsAdapter import com.readrops.db.entities.Feed import com.readrops.db.entities.Item import java.io.InputStream @@ -16,7 +16,7 @@ interface XmlAdapter { companion object { fun xmlFeedAdapterFactory(type: LocalRSSHelper.RSSType): XmlAdapter { return when (type) { - LocalRSSHelper.RSSType.RSS_2 -> RSSFeedAdapter() + LocalRSSHelper.RSSType.RSS_2 -> RSS2FeedAdapter() LocalRSSHelper.RSSType.ATOM -> ATOMFeedAdapter() else -> throw IllegalArgumentException("Unknown RSS type : $type") } @@ -24,7 +24,7 @@ interface XmlAdapter { fun xmlItemsAdapterFactory(type: LocalRSSHelper.RSSType): XmlAdapter> { return when (type) { - LocalRSSHelper.RSSType.RSS_2 -> RSSItemsAdapter() + LocalRSSHelper.RSSType.RSS_2 -> RSS2ItemsAdapter() LocalRSSHelper.RSSType.ATOM -> ATOMItemsAdapter() else -> throw IllegalArgumentException("Unknown RSS type : $type") } diff --git a/api/src/main/java/com/readrops/api/localfeed/rss/RSSFeedAdapter.kt b/api/src/main/java/com/readrops/api/localfeed/rss2/RSS2FeedAdapter.kt similarity index 95% rename from api/src/main/java/com/readrops/api/localfeed/rss/RSSFeedAdapter.kt rename to api/src/main/java/com/readrops/api/localfeed/rss2/RSS2FeedAdapter.kt index 35f805a0..2751a428 100644 --- a/api/src/main/java/com/readrops/api/localfeed/rss/RSSFeedAdapter.kt +++ b/api/src/main/java/com/readrops/api/localfeed/rss2/RSS2FeedAdapter.kt @@ -1,4 +1,4 @@ -package com.readrops.api.localfeed.rss +package com.readrops.api.localfeed.rss2 import com.gitlab.mvysny.konsumexml.Names import com.gitlab.mvysny.konsumexml.allChildrenAutoIgnore @@ -11,7 +11,7 @@ import com.readrops.db.entities.Feed import org.jsoup.Jsoup import java.io.InputStream -class RSSFeedAdapter : XmlAdapter { +class RSS2FeedAdapter : XmlAdapter { override fun fromXml(inputStream: InputStream): Feed { val konsume = inputStream.konsumeXml() diff --git a/api/src/main/java/com/readrops/api/localfeed/rss/RSSItemsAdapter.kt b/api/src/main/java/com/readrops/api/localfeed/rss2/RSS2ItemsAdapter.kt similarity index 97% rename from api/src/main/java/com/readrops/api/localfeed/rss/RSSItemsAdapter.kt rename to api/src/main/java/com/readrops/api/localfeed/rss2/RSS2ItemsAdapter.kt index 5f074af3..fde85f49 100644 --- a/api/src/main/java/com/readrops/api/localfeed/rss/RSSItemsAdapter.kt +++ b/api/src/main/java/com/readrops/api/localfeed/rss2/RSS2ItemsAdapter.kt @@ -1,4 +1,4 @@ -package com.readrops.api.localfeed.rss +package com.readrops.api.localfeed.rss2 import com.gitlab.mvysny.konsumexml.* import com.readrops.api.localfeed.XmlAdapter @@ -6,7 +6,7 @@ import com.readrops.api.utils.* import com.readrops.db.entities.Item import java.io.InputStream -class RSSItemsAdapter : XmlAdapter> { +class RSS2ItemsAdapter : XmlAdapter> { override fun fromXml(inputStream: InputStream): List { val konsume = inputStream.konsumeXml() diff --git a/api/src/test/java/com/readrops/api/localfeed/XmlAdapterTest.kt b/api/src/test/java/com/readrops/api/localfeed/XmlAdapterTest.kt index 5da0b20c..23d74683 100644 --- a/api/src/test/java/com/readrops/api/localfeed/XmlAdapterTest.kt +++ b/api/src/test/java/com/readrops/api/localfeed/XmlAdapterTest.kt @@ -2,8 +2,8 @@ package com.readrops.api.localfeed import com.readrops.api.localfeed.atom.ATOMFeedAdapter import com.readrops.api.localfeed.atom.ATOMItemsAdapter -import com.readrops.api.localfeed.rss.RSSFeedAdapter -import com.readrops.api.localfeed.rss.RSSItemsAdapter +import com.readrops.api.localfeed.rss2.RSS2FeedAdapter +import com.readrops.api.localfeed.rss2.RSS2ItemsAdapter import junit.framework.TestCase.assertTrue import org.junit.Assert import org.junit.Test @@ -12,7 +12,7 @@ class XmlAdapterTest { @Test fun xmlFeedAdapterFactoryTest() { - assertTrue(XmlAdapter.xmlFeedAdapterFactory(LocalRSSHelper.RSSType.RSS_2) is RSSFeedAdapter) + assertTrue(XmlAdapter.xmlFeedAdapterFactory(LocalRSSHelper.RSSType.RSS_2) is RSS2FeedAdapter) assertTrue(XmlAdapter.xmlFeedAdapterFactory(LocalRSSHelper.RSSType.ATOM) is ATOMFeedAdapter) Assert.assertThrows(IllegalArgumentException::class.java) { XmlAdapter.xmlFeedAdapterFactory(LocalRSSHelper.RSSType.UNKNOWN) } @@ -20,7 +20,7 @@ class XmlAdapterTest { @Test fun xmlItemsAdapterFactoryTest() { - assertTrue(XmlAdapter.xmlItemsAdapterFactory(LocalRSSHelper.RSSType.RSS_2) is RSSItemsAdapter) + assertTrue(XmlAdapter.xmlItemsAdapterFactory(LocalRSSHelper.RSSType.RSS_2) is RSS2ItemsAdapter) assertTrue(XmlAdapter.xmlItemsAdapterFactory(LocalRSSHelper.RSSType.ATOM) is ATOMItemsAdapter) Assert.assertThrows(IllegalArgumentException::class.java) { XmlAdapter.xmlItemsAdapterFactory(LocalRSSHelper.RSSType.UNKNOWN) } From 694d6842232924dec2aceca7b1b476e45570d065 Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Wed, 23 Sep 2020 18:57:53 +0200 Subject: [PATCH 051/187] Add adapter for RSS1 feed --- .../assets/localfeed/rss1/rss1_feed.xml | 268 ++++++++++++++++++ .../api/localfeed/rss1/RSS1FeedAdapterTest.kt | 28 ++ .../readrops/api/localfeed/LocalRSSHelper.kt | 7 +- .../com/readrops/api/localfeed/XmlAdapter.kt | 3 +- .../api/localfeed/rss1/RSS1FeedAdapter.kt | 48 ++++ .../api/localfeed/LocalRSSHelperTest.kt | 2 + .../readrops/api/localfeed/XmlAdapterTest.kt | 2 + 7 files changed, 355 insertions(+), 3 deletions(-) create mode 100644 api/src/androidTest/assets/localfeed/rss1/rss1_feed.xml create mode 100644 api/src/androidTest/java/com/readrops/api/localfeed/rss1/RSS1FeedAdapterTest.kt create mode 100644 api/src/main/java/com/readrops/api/localfeed/rss1/RSS1FeedAdapter.kt diff --git a/api/src/androidTest/assets/localfeed/rss1/rss1_feed.xml b/api/src/androidTest/assets/localfeed/rss1/rss1_feed.xml new file mode 100644 index 00000000..2dae7a51 --- /dev/null +++ b/api/src/androidTest/assets/localfeed/rss1/rss1_feed.xml @@ -0,0 +1,268 @@ + + + + Slashdot + https://slashdot.org/ + News for nerds, stuff that matters + en-us + Copyright 1997-2016, SlashdotMedia. All Rights Reserved. + 2020-09-23T16:20:20+00:00 + Dice + help@slashdot.org + Technology + 1970-01-01T00:00+00:00 + 1 + hourly + + + + + + + + + + + + + + + + + + + + + + + + + + + Slashdot + https://a.fsdn.com/sd/topics/topicslashdot.gif + https://slashdot.org/ + + + Google Expands its Flutter Development Kit To Windows Apps + + https://developers.slashdot.org/story/20/09/23/1616231/google-expands-its-flutter-development-kit-to-windows-apps?utm_source=rss1.0mainlinkanon&utm_medium=feed + + Google has announced that Flutter, its open source UI development kit for + building cross-platform software from the same codebase, is finally available for + Windows apps in alpha. From a report:For the world's leading desktop operating system + with some 1 billion installations of Windows 10 alone, this has been a long time coming. + Flutter's alpha incarnation was initially launched at Google's I/O developer conference + back in 2017, before arriving in beta less than a year later. In its original guise, + Flutter was designed for Android and iOS app development, but it has since expanded to + cover the web, MacOS, and Linux, which are currently available in various alpha or beta + iterations. Developers have had to consider unique platform-specific factors when + designing for the desktop or mobile phones, such as different screen sizes and how + people interact with their devices. On smartphones, people typically use touch and + swipe-based gestures, while keyboards and mice are commonly used on PCs and laptops. + This means Flutter has had to expand its support to cover the additional inputs.<p><div + class="share_submission" style="position:relative;"> <a class="slashpop" + href="http://twitter.com/home?status=Google+Expands+its+Flutter+Development+Kit+To+Windows+Apps%3A+https%3A%2F%2Fbit.ly%2F32X36MW"><img + src="https://a.fsdn.com/sd/twitter_icon_large.png"></a> <a class="slashpop" + href="http://www.facebook.com/sharer.php?u=https%3A%2F%2Fdevelopers.slashdot.org%2Fstory%2F20%2F09%2F23%2F1616231%2Fgoogle-expands-its-flutter-development-kit-to-windows-apps%3Futm_source%3Dslashdot%26utm_medium%3Dfacebook"><img + src="https://a.fsdn.com/sd/facebook_icon_large.png"></a> + + + </div></p><p><a + href="https://developers.slashdot.org/story/20/09/23/1616231/google-expands-its-flutter-development-kit-to-windows-apps?utm_source=rss1.0moreanon&amp;utm_medium=feed">Read + more of this story</a> at Slashdot.</p><iframe + src="https://slashdot.org/slashdot-it.pl?op=discuss&amp;id=17251868&amp;smallembed=1" + style="height: 300px; width: 100%; border: none;"></iframe> + + msmash + 2020-09-23T16:15:00+00:00 + programming + how-about-that + developers + 1 + 1,1,1,1,0,0,0 + + + Firefox Usage is Down 85% Despite Mozilla's Top Exec Pay Going Up 400% + + https://news.slashdot.org/story/20/09/23/1528219/firefox-usage-is-down-85-despite-mozillas-top-exec-pay-going-up-400?utm_source=rss1.0mainlinkanon&utm_medium=feed + + Software engineer Cal Paterson writes: Mozilla recently announced that they + would be dismissing 250 people. That's a quarter of their workforce so there are some + deep cuts to their work too. The victims include: the MDN docs (those are the web + standards docs everyone likes better than w3schools), the Rust compiler and even some + cuts to Firefox development. Like most people I want to see Mozilla do well but those + three projects comprise pretty much what I think of as the whole point of Mozilla, so + this news is a a big let down. The stated reason for the cuts is falling income. Mozilla + largely relies on "royalties" for funding. In return for payment, Mozilla allows big + technology companies to choose the default search engine in Firefox - the technology + companies are ultimately paying to increase the number of searches Firefox users make + with them. Mozilla haven't been particularly transparent about why these royalties are + being reduced, except to blame the coronavirus. I'm sure the coronavirus is not a great + help but I suspect the bigger problem is that Firefox's market share is now a tiny + fraction of its previous size and so the royalties will be smaller too - fewer users, so + fewer searches and therefore less money for Mozilla. + + The real problem is not the royalty cuts, though. Mozilla has already received more than + enough money to set themselves up for financial independence. Mozilla received up to + half a billion dollars a year (each year!) for many years. The real problem is that + Mozilla didn't use that money to achieve financial independence and instead just spent + it each year, doing the organisational equivalent of living hand-to-mouth. Despite their + slightly contrived legal structure as a non-profit that owns a for-profit, Mozilla are + an NGO just like any other. In this article I want to apply the traditional measures + that are applied to other NGOs to Mozilla in order to show what's wrong. These three + measures are: overheads, ethics and results.<p><div class="share_submission" + style="position:relative;"> <a class="slashpop" + href="http://twitter.com/home?status=Firefox+Usage+is+Down+85%25+Despite+Mozilla's+Top+Exec+Pay+Going+Up+400%25%3A+https%3A%2F%2Fbit.ly%2F33M9FB2"><img + src="https://a.fsdn.com/sd/twitter_icon_large.png"></a> <a class="slashpop" + href="http://www.facebook.com/sharer.php?u=https%3A%2F%2Fnews.slashdot.org%2Fstory%2F20%2F09%2F23%2F1528219%2Ffirefox-usage-is-down-85-despite-mozillas-top-exec-pay-going-up-400%3Futm_source%3Dslashdot%26utm_medium%3Dfacebook"><img + src="https://a.fsdn.com/sd/facebook_icon_large.png"></a> + + + </div></p><p><a + href="https://news.slashdot.org/story/20/09/23/1528219/firefox-usage-is-down-85-despite-mozillas-top-exec-pay-going-up-400?utm_source=rss1.0moreanon&amp;utm_medium=feed">Read + more of this story</a> at Slashdot.</p><iframe + src="https://slashdot.org/slashdot-it.pl?op=discuss&amp;id=17251650&amp;smallembed=1" + style="height: 300px; width: 100%; border: none;"></iframe> + + msmash + 2020-09-23T15:27:00+00:00 + firefox + closer-look + news + 31 + 31,29,24,21,3,1,0 + + + Climate Disruption Is Now Locked In. The Next Moves Will Be Crucial. + + https://news.slashdot.org/story/20/09/23/1451213/climate-disruption-is-now-locked-in-the-next-moves-will-be-crucial?utm_source=rss1.0mainlinkanon&utm_medium=feed + + America is now under siege by climate change in ways that scientists have + warned about for years. But there is a second part to their admonition: Decades of + growing crisis are already locked into the global ecosystem and cannot be reversed. From + a report: This means the kinds of cascading disasters occurring today -- drought in the + West fueling historic wildfires that send smoke all the way to the East Coast, or + parades of tropical storms lining up across the Atlantic to march destructively toward + North America -- are no longer features of some dystopian future. They are the here and + now, worsening for the next generation and perhaps longer, depending on humanity's + willingness to take action. "I've been labeled an alarmist," said Peter Kalmus, a + climate scientist in Los Angeles, where he and millions of others have inhaled + dangerously high levels of smoke for weeks. "And I think it's a lot harder for people to + say that I'm being alarmist now." Last month, before the skies over San Francisco turned + a surreal orange, Death Valley reached 130 degrees Fahrenheit, the highest temperature + ever measured on the planet. Dozens of people have perished from the heat in Phoenix, + which in July suffered its hottest month on record, only to surpass that milestone in + August. + + Conversations about climate change have broken into everyday life, to the top of the + headlines and to center stage in the presidential campaign. The questions are profound + and urgent. Can this be reversed? What can be done to minimize the looming dangers for + the decades ahead? Will the destruction of recent weeks become a moment of reckoning, or + just a blip in the news cycle? The Times spoke with two dozen climate experts, including + scientists, economists, sociologists and policymakers, and their answers were by turns + alarming, cynical and hopeful. "It's as if we've been smoking a pack of cigarettes a day + for decades" and the world is now feeling the effects, said Katharine Hayhoe, a climate + scientist at Texas Tech University. But, she said, "we're not dead yet." Their most + sobering message was that the world still hasn't seen the worst of it. Gone is the + climate of yesteryear, and there's no going back. The effects of climate change evident + today are the results of choices that countries made decades ago to keep pumping + heat-trapping greenhouse gases into the atmosphere at ever-increasing rates despite + warnings from scientists about the price to be paid.<p><div + class="share_submission" style="position:relative;"> <a class="slashpop" + href="http://twitter.com/home?status=Climate+Disruption+Is+Now+Locked+In.+The+Next+Moves+Will+Be+Crucial.%3A+https%3A%2F%2Fbit.ly%2F32TsNxO"><img + src="https://a.fsdn.com/sd/twitter_icon_large.png"></a> <a class="slashpop" + href="http://www.facebook.com/sharer.php?u=https%3A%2F%2Fnews.slashdot.org%2Fstory%2F20%2F09%2F23%2F1451213%2Fclimate-disruption-is-now-locked-in-the-next-moves-will-be-crucial%3Futm_source%3Dslashdot%26utm_medium%3Dfacebook"><img + src="https://a.fsdn.com/sd/facebook_icon_large.png"></a> + + + </div></p><p><a + href="https://news.slashdot.org/story/20/09/23/1451213/climate-disruption-is-now-locked-in-the-next-moves-will-be-crucial?utm_source=rss1.0moreanon&amp;utm_medium=feed">Read + more of this story</a> at Slashdot.</p><iframe + src="https://slashdot.org/slashdot-it.pl?op=discuss&amp;id=17251462&amp;smallembed=1" + style="height: 300px; width: 100%; border: none;"></iframe> + + msmash + 2020-09-23T14:51:00+00:00 + earth + closer-look + news + 67 + 67,61,50,33,12,2,1 + + + A New York Clock That Told Time Now Tells the Time Remaining + + https://news.slashdot.org/story/20/09/23/1420240/a-new-york-clock-that-told-time-now-tells-the-time-remaining?utm_source=rss1.0mainlinkanon&utm_medium=feed + + For more than 20 years, Metronome, which includes a 62-foot-wide 15-digit + electronic clock that faces Union Square in Manhattan, has been one of the city's most + prominent and baffling public art projects. Its digital display once told the time in + its own unique way, counting the hours, minutes and seconds (and fractions thereof) to + and from midnight. But for years observers who did not understand how it worked + suggested that it was measuring the acres of rainforest destroyed each year, tracking + the world population or even that it had something to do with pi. On Saturday Metronome + adopted a new ecologically sensitive mission. From a report: Now, instead of measuring + 24-hour cycles, it is measuring what two artists, Gan Golan and Andrew Boyd, present as + a critical window for action to prevent the effects of global warming from becoming + irreversible. On Saturday at 3:20 p.m., messages including "The Earth has a deadline" + began to appear on the display. Then numbers -- 7:103:15:40:07 -- showed up, + representing the years, days, hours, minutes and seconds until that deadline. As a + handful of supporters watched, the number -- which the artists said was based on + calculations by the Mercator Research Institute on Global Commons and Climate Change in + Berlin -- began ticking down, second by second. + + "This is our way to shout that number from the rooftops." Mr. Golan said just before the + countdown began. "The world is literally counting on us." The Climate Clock, as the two + artists call their project, will be displayed on the 14th Street building, One Union + Square South, through Sept. 27, the end of Climate Week. The creators say their aim is + to arrange for the clock to be permanently displayed, there or elsewhere. Mr. Golan said + he came up with the idea to publicly illustrate the urgency of combating climate change + about two years ago, shortly after his daughter was born. He asked Mr. Boyd, an activist + from the Lower East Side, to work with him on the project.<p><div + class="share_submission" style="position:relative;"> <a class="slashpop" + href="http://twitter.com/home?status=A+New+York+Clock+That+Told+Time+Now+Tells+the+Time+Remaining%3A+https%3A%2F%2Fbit.ly%2F2HrAt2b"><img + src="https://a.fsdn.com/sd/twitter_icon_large.png"></a> <a class="slashpop" + href="http://www.facebook.com/sharer.php?u=https%3A%2F%2Fnews.slashdot.org%2Fstory%2F20%2F09%2F23%2F1420240%2Fa-new-york-clock-that-told-time-now-tells-the-time-remaining%3Futm_source%3Dslashdot%26utm_medium%3Dfacebook"><img + src="https://a.fsdn.com/sd/facebook_icon_large.png"></a> + + + </div></p><p><a + href="https://news.slashdot.org/story/20/09/23/1420240/a-new-york-clock-that-told-time-now-tells-the-time-remaining?utm_source=rss1.0moreanon&amp;utm_medium=feed">Read + more of this story</a> at Slashdot.</p><iframe + src="https://slashdot.org/slashdot-it.pl?op=discuss&amp;id=17251272&amp;smallembed=1" + style="height: 300px; width: 100%; border: none;"></iframe> + + msmash + 2020-09-23T14:10:00+00:00 + news + how-about-that + news + 43 + 43,38,33,27,6,1,0 + + \ No newline at end of file diff --git a/api/src/androidTest/java/com/readrops/api/localfeed/rss1/RSS1FeedAdapterTest.kt b/api/src/androidTest/java/com/readrops/api/localfeed/rss1/RSS1FeedAdapterTest.kt new file mode 100644 index 00000000..47d023b0 --- /dev/null +++ b/api/src/androidTest/java/com/readrops/api/localfeed/rss1/RSS1FeedAdapterTest.kt @@ -0,0 +1,28 @@ +package com.readrops.api.localfeed.rss1 + +import android.content.Context +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry +import junit.framework.Assert.assertEquals +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class RSS1FeedAdapterTest { + + private val context: Context = InstrumentationRegistry.getInstrumentation().context + + private val adapter = RSS1FeedAdapter() + + @Test + fun normalCaseTest() { + val stream = context.resources.assets.open("localfeed/rss1/rss1_feed.xml") + + val feed = adapter.fromXml(stream) + + assertEquals(feed.name, "Slashdot") + assertEquals(feed.url, "https://slashdot.org/") + assertEquals(feed.siteUrl, "https://slashdot.org/") + assertEquals(feed.description, "News for nerds, stuff that matters") + } +} \ No newline at end of file diff --git a/api/src/main/java/com/readrops/api/localfeed/LocalRSSHelper.kt b/api/src/main/java/com/readrops/api/localfeed/LocalRSSHelper.kt index 313d564a..b8b020e3 100644 --- a/api/src/main/java/com/readrops/api/localfeed/LocalRSSHelper.kt +++ b/api/src/main/java/com/readrops/api/localfeed/LocalRSSHelper.kt @@ -5,7 +5,8 @@ import java.util.regex.Pattern object LocalRSSHelper { - private const val RSS_DEFAULT_CONTENT_TYPE = "application/rss+xml" + private const val RSS_1_CONTENT_TYPE = "application/rdf+xml" + private const val RSS_2_CONTENT_TYPE = "application/rss+xml" private const val ATOM_CONTENT_TYPE = "application/atom+xml" private const val JSONFEED_CONTENT_TYPE = "application/feed+json" private const val JSON_CONTENT_TYPE = "application/json" @@ -19,7 +20,8 @@ object LocalRSSHelper { */ fun getRSSType(contentType: String): RSSType { return when (contentType) { - RSS_DEFAULT_CONTENT_TYPE -> RSSType.RSS_2 + RSS_1_CONTENT_TYPE -> RSSType.RSS_1 + RSS_2_CONTENT_TYPE -> RSSType.RSS_2 ATOM_CONTENT_TYPE -> RSSType.ATOM JSON_CONTENT_TYPE, JSONFEED_CONTENT_TYPE -> RSSType.JSONFEED else -> RSSType.UNKNOWN @@ -50,6 +52,7 @@ object LocalRSSHelper { } enum class RSSType { + RSS_1, RSS_2, ATOM, JSONFEED, diff --git a/api/src/main/java/com/readrops/api/localfeed/XmlAdapter.kt b/api/src/main/java/com/readrops/api/localfeed/XmlAdapter.kt index 8f21a9e5..1641c00f 100644 --- a/api/src/main/java/com/readrops/api/localfeed/XmlAdapter.kt +++ b/api/src/main/java/com/readrops/api/localfeed/XmlAdapter.kt @@ -2,12 +2,12 @@ package com.readrops.api.localfeed import com.readrops.api.localfeed.atom.ATOMFeedAdapter import com.readrops.api.localfeed.atom.ATOMItemsAdapter +import com.readrops.api.localfeed.rss1.RSS1FeedAdapter import com.readrops.api.localfeed.rss2.RSS2FeedAdapter import com.readrops.api.localfeed.rss2.RSS2ItemsAdapter import com.readrops.db.entities.Feed import com.readrops.db.entities.Item import java.io.InputStream -import java.lang.IllegalArgumentException interface XmlAdapter { @@ -16,6 +16,7 @@ interface XmlAdapter { companion object { fun xmlFeedAdapterFactory(type: LocalRSSHelper.RSSType): XmlAdapter { return when (type) { + LocalRSSHelper.RSSType.RSS_1 -> RSS1FeedAdapter() LocalRSSHelper.RSSType.RSS_2 -> RSS2FeedAdapter() LocalRSSHelper.RSSType.ATOM -> ATOMFeedAdapter() else -> throw IllegalArgumentException("Unknown RSS type : $type") diff --git a/api/src/main/java/com/readrops/api/localfeed/rss1/RSS1FeedAdapter.kt b/api/src/main/java/com/readrops/api/localfeed/rss1/RSS1FeedAdapter.kt new file mode 100644 index 00000000..697258c1 --- /dev/null +++ b/api/src/main/java/com/readrops/api/localfeed/rss1/RSS1FeedAdapter.kt @@ -0,0 +1,48 @@ +package com.readrops.api.localfeed.rss1 + +import com.gitlab.mvysny.konsumexml.Names +import com.gitlab.mvysny.konsumexml.allChildrenAutoIgnore +import com.gitlab.mvysny.konsumexml.konsumeXml +import com.readrops.api.localfeed.XmlAdapter +import com.readrops.api.utils.ParseException +import com.readrops.api.utils.nonNullText +import com.readrops.api.utils.nullableText +import com.readrops.db.entities.Feed +import java.io.InputStream + +class RSS1FeedAdapter : XmlAdapter { + + override fun fromXml(inputStream: InputStream): Feed { + val konsume = inputStream.konsumeXml() + val feed = Feed() + + return try { + konsume.child("RDF") { + allChildrenAutoIgnore("channel") { + feed.url = attributes.getValue("about", + namespace = "http://www.w3.org/1999/02/22-rdf-syntax-ns#") + + allChildrenAutoIgnore(names) { + with(feed) { + when (tagName) { + "title" -> name = nonNullText() + "link" -> siteUrl = nonNullText() + "description" -> description = nullableText() + } + } + } + } + } + + konsume.close() + feed + } catch (e: Exception) { + throw ParseException(e.message) + } + + } + + companion object { + val names = Names.of("title", "link", "description") + } +} \ No newline at end of file diff --git a/api/src/test/java/com/readrops/api/localfeed/LocalRSSHelperTest.kt b/api/src/test/java/com/readrops/api/localfeed/LocalRSSHelperTest.kt index c039d38d..21a225dc 100644 --- a/api/src/test/java/com/readrops/api/localfeed/LocalRSSHelperTest.kt +++ b/api/src/test/java/com/readrops/api/localfeed/LocalRSSHelperTest.kt @@ -8,6 +8,8 @@ class LocalRSSHelperTest { @Test fun standardContentTypesTest() { + assertEquals(LocalRSSHelper.getRSSType("application/rdf+xml"), + LocalRSSHelper.RSSType.RSS_1) assertEquals(LocalRSSHelper.getRSSType("application/rss+xml"), LocalRSSHelper.RSSType.RSS_2) assertEquals(LocalRSSHelper.getRSSType("application/atom+xml"), diff --git a/api/src/test/java/com/readrops/api/localfeed/XmlAdapterTest.kt b/api/src/test/java/com/readrops/api/localfeed/XmlAdapterTest.kt index 23d74683..47df7eb9 100644 --- a/api/src/test/java/com/readrops/api/localfeed/XmlAdapterTest.kt +++ b/api/src/test/java/com/readrops/api/localfeed/XmlAdapterTest.kt @@ -2,6 +2,7 @@ package com.readrops.api.localfeed import com.readrops.api.localfeed.atom.ATOMFeedAdapter import com.readrops.api.localfeed.atom.ATOMItemsAdapter +import com.readrops.api.localfeed.rss1.RSS1FeedAdapter import com.readrops.api.localfeed.rss2.RSS2FeedAdapter import com.readrops.api.localfeed.rss2.RSS2ItemsAdapter import junit.framework.TestCase.assertTrue @@ -12,6 +13,7 @@ class XmlAdapterTest { @Test fun xmlFeedAdapterFactoryTest() { + assertTrue(XmlAdapter.xmlFeedAdapterFactory(LocalRSSHelper.RSSType.RSS_1) is RSS1FeedAdapter) assertTrue(XmlAdapter.xmlFeedAdapterFactory(LocalRSSHelper.RSSType.RSS_2) is RSS2FeedAdapter) assertTrue(XmlAdapter.xmlFeedAdapterFactory(LocalRSSHelper.RSSType.ATOM) is ATOMFeedAdapter) From c742c4fbf2625d2c067e2ea6a1de0c9f47f6599f Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Wed, 23 Sep 2020 22:02:41 +0200 Subject: [PATCH 052/187] Add adapter for RSS1 items --- .../localfeed/rss1/rss1_items_no_date.xml | 43 +++++++ .../localfeed/rss1/rss1_items_no_link.xml | 40 ++++++ .../localfeed/rss1/rss1_items_no_title.xml | 43 +++++++ .../rss1/rss1_items_special_cases.xml | 120 ++++++++++++++++++ .../localfeed/rss1/RSS1ItemsAdapterTest.kt | 70 ++++++++++ .../com/readrops/api/localfeed/XmlAdapter.kt | 2 + .../api/localfeed/rss1/RSS1ItemsAdapter.kt | 68 ++++++++++ .../readrops/api/localfeed/XmlAdapterTest.kt | 2 + 8 files changed, 388 insertions(+) create mode 100644 api/src/androidTest/assets/localfeed/rss1/rss1_items_no_date.xml create mode 100644 api/src/androidTest/assets/localfeed/rss1/rss1_items_no_link.xml create mode 100644 api/src/androidTest/assets/localfeed/rss1/rss1_items_no_title.xml create mode 100644 api/src/androidTest/assets/localfeed/rss1/rss1_items_special_cases.xml create mode 100644 api/src/androidTest/java/com/readrops/api/localfeed/rss1/RSS1ItemsAdapterTest.kt create mode 100644 api/src/main/java/com/readrops/api/localfeed/rss1/RSS1ItemsAdapter.kt diff --git a/api/src/androidTest/assets/localfeed/rss1/rss1_items_no_date.xml b/api/src/androidTest/assets/localfeed/rss1/rss1_items_no_date.xml new file mode 100644 index 00000000..b4aaab5c --- /dev/null +++ b/api/src/androidTest/assets/localfeed/rss1/rss1_items_no_date.xml @@ -0,0 +1,43 @@ + + + Google Expands its Flutter Development Kit To Windows Apps + + https://developers.slashdot.org/story/20/09/23/1616231/google-expands-its-flutter-development-kit-to-windows-apps?utm_source=rss1.0mainlinkanon&utm_medium=feed + + Google has announced that Flutter, its open source UI development kit for + building cross-platform software from the same codebase, is finally available for + Windows apps in alpha. From a report:For the world's leading desktop operating system + with some 1 billion installations of Windows 10 alone, this has been a long time coming. + Flutter's alpha incarnation was initially launched at Google's I/O developer conference + back in 2017, before arriving in beta less than a year later. In its original guise, + Flutter was designed for Android and iOS app development, but it has since expanded to + cover the web, MacOS, and Linux, which are currently available in various alpha or beta + iterations. Developers have had to consider unique platform-specific factors when + designing for the desktop or mobile phones, such as different screen sizes and how + people interact with their devices. On smartphones, people typically use touch and + swipe-based gestures, while keyboards and mice are commonly used on PCs and laptops. + This means Flutter has had to expand its support to cover the additional inputs.<p><div + class="share_submission" style="position:relative;"> <a class="slashpop" + href="http://twitter.com/home?status=Google+Expands+its+Flutter+Development+Kit+To+Windows+Apps%3A+https%3A%2F%2Fbit.ly%2F32X36MW"><img + src="https://a.fsdn.com/sd/twitter_icon_large.png"></a> <a class="slashpop" + href="http://www.facebook.com/sharer.php?u=https%3A%2F%2Fdevelopers.slashdot.org%2Fstory%2F20%2F09%2F23%2F1616231%2Fgoogle-expands-its-flutter-development-kit-to-windows-apps%3Futm_source%3Dslashdot%26utm_medium%3Dfacebook"><img + src="https://a.fsdn.com/sd/facebook_icon_large.png"></a> + + + </div></p><p><a + href="https://developers.slashdot.org/story/20/09/23/1616231/google-expands-its-flutter-development-kit-to-windows-apps?utm_source=rss1.0moreanon&amp;utm_medium=feed">Read + more of this story</a> at Slashdot.</p><iframe + src="https://slashdot.org/slashdot-it.pl?op=discuss&amp;id=17251868&amp;smallembed=1" + style="height: 300px; width: 100%; border: none;"></iframe> + + msmash + programming + how-about-that + developers + 1 + 1,1,1,1,0,0,0 + + \ No newline at end of file diff --git a/api/src/androidTest/assets/localfeed/rss1/rss1_items_no_link.xml b/api/src/androidTest/assets/localfeed/rss1/rss1_items_no_link.xml new file mode 100644 index 00000000..b927c248 --- /dev/null +++ b/api/src/androidTest/assets/localfeed/rss1/rss1_items_no_link.xml @@ -0,0 +1,40 @@ + + + Google Expands its Flutter Development Kit To Windows Apps + Google has announced that Flutter, its open source UI development kit for + building cross-platform software from the same codebase, is finally available for + Windows apps in alpha. From a report:For the world's leading desktop operating system + with some 1 billion installations of Windows 10 alone, this has been a long time coming. + Flutter's alpha incarnation was initially launched at Google's I/O developer conference + back in 2017, before arriving in beta less than a year later. In its original guise, + Flutter was designed for Android and iOS app development, but it has since expanded to + cover the web, MacOS, and Linux, which are currently available in various alpha or beta + iterations. Developers have had to consider unique platform-specific factors when + designing for the desktop or mobile phones, such as different screen sizes and how + people interact with their devices. On smartphones, people typically use touch and + swipe-based gestures, while keyboards and mice are commonly used on PCs and laptops. + This means Flutter has had to expand its support to cover the additional inputs.<p><div + class="share_submission" style="position:relative;"> <a class="slashpop" + href="http://twitter.com/home?status=Google+Expands+its+Flutter+Development+Kit+To+Windows+Apps%3A+https%3A%2F%2Fbit.ly%2F32X36MW"><img + src="https://a.fsdn.com/sd/twitter_icon_large.png"></a> <a class="slashpop" + href="http://www.facebook.com/sharer.php?u=https%3A%2F%2Fdevelopers.slashdot.org%2Fstory%2F20%2F09%2F23%2F1616231%2Fgoogle-expands-its-flutter-development-kit-to-windows-apps%3Futm_source%3Dslashdot%26utm_medium%3Dfacebook"><img + src="https://a.fsdn.com/sd/facebook_icon_large.png"></a> + + + </div></p><p><a + href="https://developers.slashdot.org/story/20/09/23/1616231/google-expands-its-flutter-development-kit-to-windows-apps?utm_source=rss1.0moreanon&amp;utm_medium=feed">Read + more of this story</a> at Slashdot.</p><iframe + src="https://slashdot.org/slashdot-it.pl?op=discuss&amp;id=17251868&amp;smallembed=1" + style="height: 300px; width: 100%; border: none;"></iframe> + + msmash + 2020-09-23T16:15:00+00:00 + programming + how-about-that + developers + 1 + 1,1,1,1,0,0,0 + + \ No newline at end of file diff --git a/api/src/androidTest/assets/localfeed/rss1/rss1_items_no_title.xml b/api/src/androidTest/assets/localfeed/rss1/rss1_items_no_title.xml new file mode 100644 index 00000000..96e0c123 --- /dev/null +++ b/api/src/androidTest/assets/localfeed/rss1/rss1_items_no_title.xml @@ -0,0 +1,43 @@ + + + + https://developers.slashdot.org/story/20/09/23/1616231/google-expands-its-flutter-development-kit-to-windows-apps?utm_source=rss1.0mainlinkanon&utm_medium=feed + + Google has announced that Flutter, its open source UI development kit for + building cross-platform software from the same codebase, is finally available for + Windows apps in alpha. From a report:For the world's leading desktop operating system + with some 1 billion installations of Windows 10 alone, this has been a long time coming. + Flutter's alpha incarnation was initially launched at Google's I/O developer conference + back in 2017, before arriving in beta less than a year later. In its original guise, + Flutter was designed for Android and iOS app development, but it has since expanded to + cover the web, MacOS, and Linux, which are currently available in various alpha or beta + iterations. Developers have had to consider unique platform-specific factors when + designing for the desktop or mobile phones, such as different screen sizes and how + people interact with their devices. On smartphones, people typically use touch and + swipe-based gestures, while keyboards and mice are commonly used on PCs and laptops. + This means Flutter has had to expand its support to cover the additional inputs.<p><div + class="share_submission" style="position:relative;"> <a class="slashpop" + href="http://twitter.com/home?status=Google+Expands+its+Flutter+Development+Kit+To+Windows+Apps%3A+https%3A%2F%2Fbit.ly%2F32X36MW"><img + src="https://a.fsdn.com/sd/twitter_icon_large.png"></a> <a class="slashpop" + href="http://www.facebook.com/sharer.php?u=https%3A%2F%2Fdevelopers.slashdot.org%2Fstory%2F20%2F09%2F23%2F1616231%2Fgoogle-expands-its-flutter-development-kit-to-windows-apps%3Futm_source%3Dslashdot%26utm_medium%3Dfacebook"><img + src="https://a.fsdn.com/sd/facebook_icon_large.png"></a> + + + </div></p><p><a + href="https://developers.slashdot.org/story/20/09/23/1616231/google-expands-its-flutter-development-kit-to-windows-apps?utm_source=rss1.0moreanon&amp;utm_medium=feed">Read + more of this story</a> at Slashdot.</p><iframe + src="https://slashdot.org/slashdot-it.pl?op=discuss&amp;id=17251868&amp;smallembed=1" + style="height: 300px; width: 100%; border: none;"></iframe> + + msmash + 2020-09-23T16:15:00+00:00 + programming + how-about-that + developers + 1 + 1,1,1,1,0,0,0 + + \ No newline at end of file diff --git a/api/src/androidTest/assets/localfeed/rss1/rss1_items_special_cases.xml b/api/src/androidTest/assets/localfeed/rss1/rss1_items_special_cases.xml new file mode 100644 index 00000000..2624009b --- /dev/null +++ b/api/src/androidTest/assets/localfeed/rss1/rss1_items_special_cases.xml @@ -0,0 +1,120 @@ + + + + Slashdot + https://slashdot.org/ + News for nerds, stuff that matters + en-us + Copyright 1997-2016, SlashdotMedia. All Rights Reserved. + 2020-09-23T16:20:20+00:00 + Dice + help@slashdot.org + Technology + 1970-01-01T00:00+00:00 + 1 + hourly + + + + + + + + + + + + + + + + + + + + + + + + + + + Slashdot + https://a.fsdn.com/sd/topics/topicslashdot.gif + https://slashdot.org/ + + + A New York Clock That Told Time Now Tells the Time Remaining + For more than 20 years, Metronome, which includes a 62-foot-wide 15-digit + electronic clock that faces Union Square in Manhattan, has been one of the city's most + prominent and baffling public art projects. Its digital display once told the time in + its own unique way, counting the hours, minutes and seconds (and fractions thereof) to + and from midnight. But for years observers who did not understand how it worked + suggested that it was measuring the acres of rainforest destroyed each year, tracking + the world population or even that it had something to do with pi. On Saturday Metronome + adopted a new ecologically sensitive mission. From a report: Now, instead of measuring + 24-hour cycles, it is measuring what two artists, Gan Golan and Andrew Boyd, present as + a critical window for action to prevent the effects of global warming from becoming + irreversible. On Saturday at 3:20 p.m., messages including "The Earth has a deadline" + began to appear on the display. Then numbers -- 7:103:15:40:07 -- showed up, + representing the years, days, hours, minutes and seconds until that deadline. As a + handful of supporters watched, the number -- which the artists said was based on + calculations by the Mercator Research Institute on Global Commons and Climate Change in + Berlin -- began ticking down, second by second. + + "This is our way to shout that number from the rooftops." Mr. Golan said just before the + countdown began. "The world is literally counting on us." The Climate Clock, as the two + artists call their project, will be displayed on the 14th Street building, One Union + Square South, through Sept. 27, the end of Climate Week. The creators say their aim is + to arrange for the clock to be permanently displayed, there or elsewhere. Mr. Golan said + he came up with the idea to publicly illustrate the urgency of combating climate change + about two years ago, shortly after his daughter was born. He asked Mr. Boyd, an activist + from the Lower East Side, to work with him on the project.<p><div + class="share_submission" style="position:relative;"> <a class="slashpop" + href="http://twitter.com/home?status=A+New+York+Clock+That+Told+Time+Now+Tells+the+Time+Remaining%3A+https%3A%2F%2Fbit.ly%2F2HrAt2b"><img + src="https://a.fsdn.com/sd/twitter_icon_large.png"></a> <a class="slashpop" + href="http://www.facebook.com/sharer.php?u=https%3A%2F%2Fnews.slashdot.org%2Fstory%2F20%2F09%2F23%2F1420240%2Fa-new-york-clock-that-told-time-now-tells-the-time-remaining%3Futm_source%3Dslashdot%26utm_medium%3Dfacebook"><img + src="https://a.fsdn.com/sd/facebook_icon_large.png"></a> + + + </div></p><p><a + href="https://news.slashdot.org/story/20/09/23/1420240/a-new-york-clock-that-told-time-now-tells-the-time-remaining?utm_source=rss1.0moreanon&amp;utm_medium=feed">Read + more of this story</a> at Slashdot.</p><iframe + src="https://slashdot.org/slashdot-it.pl?op=discuss&amp;id=17251272&amp;smallembed=1" + style="height: 300px; width: 100%; border: none;"></iframe> + + msmash + + creator 2 + creator 3 + creator 4 + creator 5 + 2020-09-23T14:10:00+00:00 + news + how-about-that + news + 43 + 43,38,33,27,6,1,0 + + \ No newline at end of file diff --git a/api/src/androidTest/java/com/readrops/api/localfeed/rss1/RSS1ItemsAdapterTest.kt b/api/src/androidTest/java/com/readrops/api/localfeed/rss1/RSS1ItemsAdapterTest.kt new file mode 100644 index 00000000..178220da --- /dev/null +++ b/api/src/androidTest/java/com/readrops/api/localfeed/rss1/RSS1ItemsAdapterTest.kt @@ -0,0 +1,70 @@ +package com.readrops.api.localfeed.rss1 + +import android.content.Context +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry +import com.readrops.api.utils.DateUtils +import com.readrops.api.utils.ParseException +import junit.framework.TestCase.assertEquals +import junit.framework.TestCase.assertNotNull +import org.junit.Assert +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class RSS1ItemsAdapterTest { + + private val context: Context = InstrumentationRegistry.getInstrumentation().context + + private val adapter = RSS1ItemsAdapter() + + @Test + fun normalCasesTest() { + val stream = context.resources.assets.open("localfeed/rss1/rss1_feed.xml") + + val items = adapter.fromXml(stream) + val item = items.first() + + assertEquals(items.size, 4) + assertEquals(item.title, "Google Expands its Flutter Development Kit To Windows Apps") + assertEquals(item.link.trim(), "https://developers.slashdot.org/story/20/09/23/1616231/google-expands-" + + "its-flutter-development-kit-to-windows-apps?utm_source=rss1.0mainlinkanon&utm_medium=feed") + assertEquals(item.guid.trim(), "https://developers.slashdot.org/story/20/09/23/1616231/google-expands-" + + "its-flutter-development-kit-to-windows-apps?utm_source=rss1.0mainlinkanon&utm_medium=feed") + assertEquals(item.pubDate, DateUtils.stringToLocalDateTime("2020-09-23T16:15:00+00:00")) + assertEquals(item.author, "msmash") + assertNotNull(item.description) + } + + @Test + fun specialCasesTest() { + val stream = context.resources.assets.open("localfeed/rss1/rss1_items_special_cases.xml") + + val item = adapter.fromXml(stream).first() + + assertEquals(item.author, "msmash, creator 2, creator 3, creator 4, ...") + assertEquals(item.link, "https://news.slashdot.org/story/20/09/23/1420240/a-new-york-clock-" + + "that-told-time-now-tells-the-time-remaining?utm_source=rss1.0mainlinkanon&utm_medium=feed") + } + + @Test + fun nullTitleTest() { + val stream = context.resources.assets.open("localfeed/rss1/rss1_items_no_title.xml") + + Assert.assertThrows(ParseException::class.java) { adapter.fromXml(stream) } + } + + @Test + fun nullLinkTest() { + val stream = context.resources.assets.open("localfeed/rss1/rss1_items_no_link.xml") + + Assert.assertThrows(ParseException::class.java) { adapter.fromXml(stream) } + } + + @Test + fun nullDateTest() { + val stream = context.resources.assets.open("localfeed/rss1/rss1_items_no_date.xml") + + Assert.assertThrows(ParseException::class.java) { adapter.fromXml(stream) } + } +} \ No newline at end of file diff --git a/api/src/main/java/com/readrops/api/localfeed/XmlAdapter.kt b/api/src/main/java/com/readrops/api/localfeed/XmlAdapter.kt index 1641c00f..91807c54 100644 --- a/api/src/main/java/com/readrops/api/localfeed/XmlAdapter.kt +++ b/api/src/main/java/com/readrops/api/localfeed/XmlAdapter.kt @@ -3,6 +3,7 @@ package com.readrops.api.localfeed import com.readrops.api.localfeed.atom.ATOMFeedAdapter import com.readrops.api.localfeed.atom.ATOMItemsAdapter import com.readrops.api.localfeed.rss1.RSS1FeedAdapter +import com.readrops.api.localfeed.rss1.RSS1ItemsAdapter import com.readrops.api.localfeed.rss2.RSS2FeedAdapter import com.readrops.api.localfeed.rss2.RSS2ItemsAdapter import com.readrops.db.entities.Feed @@ -25,6 +26,7 @@ interface XmlAdapter { fun xmlItemsAdapterFactory(type: LocalRSSHelper.RSSType): XmlAdapter> { return when (type) { + LocalRSSHelper.RSSType.RSS_1 -> RSS1ItemsAdapter() LocalRSSHelper.RSSType.RSS_2 -> RSS2ItemsAdapter() LocalRSSHelper.RSSType.ATOM -> ATOMItemsAdapter() else -> throw IllegalArgumentException("Unknown RSS type : $type") diff --git a/api/src/main/java/com/readrops/api/localfeed/rss1/RSS1ItemsAdapter.kt b/api/src/main/java/com/readrops/api/localfeed/rss1/RSS1ItemsAdapter.kt new file mode 100644 index 00000000..26b6e239 --- /dev/null +++ b/api/src/main/java/com/readrops/api/localfeed/rss1/RSS1ItemsAdapter.kt @@ -0,0 +1,68 @@ +package com.readrops.api.localfeed.rss1 + +import com.gitlab.mvysny.konsumexml.Names +import com.gitlab.mvysny.konsumexml.allChildrenAutoIgnore +import com.gitlab.mvysny.konsumexml.konsumeXml +import com.readrops.api.localfeed.XmlAdapter +import com.readrops.api.utils.DateUtils +import com.readrops.api.utils.ParseException +import com.readrops.api.utils.nonNullText +import com.readrops.api.utils.nullableText +import com.readrops.db.entities.Item +import java.io.InputStream + +class RSS1ItemsAdapter : XmlAdapter> { + + override fun fromXml(inputStream: InputStream): List { + val konsume = inputStream.konsumeXml() + val items = arrayListOf() + + return try { + konsume.child("RDF") { + allChildrenAutoIgnore("item") { + val authors = arrayListOf() + val about = attributes.getValueOpt("about", + namespace = "http://www.w3.org/1999/02/22-rdf-syntax-ns#") + + val item = Item().apply { + allChildrenAutoIgnore(names) { + when (tagName) { + "title" -> title = nonNullText() + "link" -> link = nullableText() + "dc:date" -> pubDate = DateUtils.stringToLocalDateTime(nonNullText()) + "dc:creator" -> authors += nullableText() + "description" -> description = nullableText(failOnElement = false) + else -> skipContents() + } + } + } + + item.guid = item.link + if (authors.filterNotNull().isNotEmpty()) item.author = authors.filterNotNull().joinToString(limit = 4) + if (item.link == null) item.link = about + + validateItem(item) + + items += item + } + } + + konsume.close() + items + } catch (e: Exception) { + throw ParseException(e.message) + } + } + + private fun validateItem(item: Item) { + when { + item.title == null -> throw ParseException("Item title is required") + item.link == null -> throw ParseException("Item link is required") + item.pubDate == null -> throw ParseException("Item date is required") + } + } + + companion object { + val names = Names.of("title", "description", "date", "link", "creator") + } +} \ No newline at end of file diff --git a/api/src/test/java/com/readrops/api/localfeed/XmlAdapterTest.kt b/api/src/test/java/com/readrops/api/localfeed/XmlAdapterTest.kt index 47df7eb9..64e73791 100644 --- a/api/src/test/java/com/readrops/api/localfeed/XmlAdapterTest.kt +++ b/api/src/test/java/com/readrops/api/localfeed/XmlAdapterTest.kt @@ -3,6 +3,7 @@ package com.readrops.api.localfeed import com.readrops.api.localfeed.atom.ATOMFeedAdapter import com.readrops.api.localfeed.atom.ATOMItemsAdapter import com.readrops.api.localfeed.rss1.RSS1FeedAdapter +import com.readrops.api.localfeed.rss1.RSS1ItemsAdapter import com.readrops.api.localfeed.rss2.RSS2FeedAdapter import com.readrops.api.localfeed.rss2.RSS2ItemsAdapter import junit.framework.TestCase.assertTrue @@ -22,6 +23,7 @@ class XmlAdapterTest { @Test fun xmlItemsAdapterFactoryTest() { + assertTrue(XmlAdapter.xmlItemsAdapterFactory(LocalRSSHelper.RSSType.RSS_1) is RSS1ItemsAdapter) assertTrue(XmlAdapter.xmlItemsAdapterFactory(LocalRSSHelper.RSSType.RSS_2) is RSS2ItemsAdapter) assertTrue(XmlAdapter.xmlItemsAdapterFactory(LocalRSSHelper.RSSType.ATOM) is ATOMItemsAdapter) From 22fe77d5cf950953fbf5dbf108fd107f74951335 Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Wed, 23 Sep 2020 22:29:47 +0200 Subject: [PATCH 053/187] Concatenate jsonfeed authors when they are several --- .../localfeed/json/json_items_other_cases.json | 14 +++++++++++++- .../api/localfeed/json/JSONItemsAdapterTest.kt | 2 +- .../api/localfeed/json/JSONItemsAdapter.kt | 13 +++++++------ 3 files changed, 21 insertions(+), 8 deletions(-) diff --git a/api/src/androidTest/assets/localfeed/json/json_items_other_cases.json b/api/src/androidTest/assets/localfeed/json/json_items_other_cases.json index e91ee941..8e54b640 100644 --- a/api/src/androidTest/assets/localfeed/json/json_items_other_cases.json +++ b/api/src/androidTest/assets/localfeed/json/json_items_other_cases.json @@ -16,11 +16,23 @@ }, { "url": "url 2", - "name": "Author 2" + "name": "" }, { "url": "url 3", "name": "Author 3" + }, + { + "url": "url 4", + "name": "Author 4" + }, + { + "url": "url 5", + "name": "Author 5" + }, + { + "url": "url 6", + "name": "Author 6" } ] } diff --git a/api/src/androidTest/java/com/readrops/api/localfeed/json/JSONItemsAdapterTest.kt b/api/src/androidTest/java/com/readrops/api/localfeed/json/JSONItemsAdapterTest.kt index c45e3e34..97411d8b 100644 --- a/api/src/androidTest/java/com/readrops/api/localfeed/json/JSONItemsAdapterTest.kt +++ b/api/src/androidTest/java/com/readrops/api/localfeed/json/JSONItemsAdapterTest.kt @@ -51,7 +51,7 @@ class JSONItemsAdapterTest { assertEquals(item.description, "This is a summary") assertEquals(item.content, "content_html") assertEquals(item.imageLink, "https://image.com") - assertEquals(item.author, "Author 1") + assertEquals(item.author, "Author 1, Author 3, Author 4, Author 5, ...") } @Test diff --git a/api/src/main/java/com/readrops/api/localfeed/json/JSONItemsAdapter.kt b/api/src/main/java/com/readrops/api/localfeed/json/JSONItemsAdapter.kt index b2fbb4f8..4a942b93 100644 --- a/api/src/main/java/com/readrops/api/localfeed/json/JSONItemsAdapter.kt +++ b/api/src/main/java/com/readrops/api/localfeed/json/JSONItemsAdapter.kt @@ -57,6 +57,7 @@ class JSONItemsAdapter : JsonAdapter>() { 7 -> pubDate = DateUtils.stringToLocalDateTime(reader.nextString()) 8 -> author = parseAuthor(reader) // jsonfeed 1.0 9 -> author = parseAuthors(reader) // jsonfeed 1.1 + else -> reader.skipValue() } } } @@ -86,9 +87,6 @@ class JSONItemsAdapter : JsonAdapter>() { return author } - /** - * Returns the first author of the array - */ private fun parseAuthors(reader: JsonReader): String? { val authors = arrayListOf() reader.beginArray() @@ -98,7 +96,10 @@ class JSONItemsAdapter : JsonAdapter>() { } reader.endArray() - return if (authors.filterNotNull().isNotEmpty()) authors.filterNotNull().first() else null + + // here, nextNullableString doesn't check if authors values are empty + return if (authors.filterNot { author -> author.isNullOrEmpty() }.isNotEmpty()) + authors.filterNot { author -> author.isNullOrEmpty() }.joinToString(limit = 4) else null } private fun validateItem(item: Item) { @@ -110,7 +111,7 @@ class JSONItemsAdapter : JsonAdapter>() { } companion object { - val names: JsonReader.Options = JsonReader.Options.of("id", "url", "title", "content_html", "content_text", - "summary", "image", "date_published", "author", "authors") + val names: JsonReader.Options = JsonReader.Options.of("id", "url", "title", + "content_html", "content_text", "summary", "image", "date_published", "author", "authors") } } \ No newline at end of file From 847739c55985946d3573eaba2e86d106b270e370 Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Wed, 23 Sep 2020 22:39:12 +0200 Subject: [PATCH 054/187] Rename rss assets folder to rss2 --- .../{rss => rss2}/rss_feed_special_cases.xml | 0 .../assets/localfeed/{rss => rss2}/rss_full_feed.xml | 0 .../localfeed/{rss => rss2}/rss_items_enclosure.xml | 0 .../{rss => rss2}/rss_items_media_content.xml | 0 .../localfeed/{rss => rss2}/rss_items_no_date.xml | 0 .../localfeed/{rss => rss2}/rss_items_no_link.xml | 0 .../localfeed/{rss => rss2}/rss_items_no_title.xml | 0 .../{rss => rss2}/rss_items_other_namespaces.xml | 0 .../api/localfeed/rss2/RSS2FeedAdapterTest.kt | 4 ++-- .../api/localfeed/rss2/RSS2ItemsAdapterTest.kt | 12 ++++++------ 10 files changed, 8 insertions(+), 8 deletions(-) rename api/src/androidTest/assets/localfeed/{rss => rss2}/rss_feed_special_cases.xml (100%) rename api/src/androidTest/assets/localfeed/{rss => rss2}/rss_full_feed.xml (100%) rename api/src/androidTest/assets/localfeed/{rss => rss2}/rss_items_enclosure.xml (100%) rename api/src/androidTest/assets/localfeed/{rss => rss2}/rss_items_media_content.xml (100%) rename api/src/androidTest/assets/localfeed/{rss => rss2}/rss_items_no_date.xml (100%) rename api/src/androidTest/assets/localfeed/{rss => rss2}/rss_items_no_link.xml (100%) rename api/src/androidTest/assets/localfeed/{rss => rss2}/rss_items_no_title.xml (100%) rename api/src/androidTest/assets/localfeed/{rss => rss2}/rss_items_other_namespaces.xml (100%) diff --git a/api/src/androidTest/assets/localfeed/rss/rss_feed_special_cases.xml b/api/src/androidTest/assets/localfeed/rss2/rss_feed_special_cases.xml similarity index 100% rename from api/src/androidTest/assets/localfeed/rss/rss_feed_special_cases.xml rename to api/src/androidTest/assets/localfeed/rss2/rss_feed_special_cases.xml diff --git a/api/src/androidTest/assets/localfeed/rss/rss_full_feed.xml b/api/src/androidTest/assets/localfeed/rss2/rss_full_feed.xml similarity index 100% rename from api/src/androidTest/assets/localfeed/rss/rss_full_feed.xml rename to api/src/androidTest/assets/localfeed/rss2/rss_full_feed.xml diff --git a/api/src/androidTest/assets/localfeed/rss/rss_items_enclosure.xml b/api/src/androidTest/assets/localfeed/rss2/rss_items_enclosure.xml similarity index 100% rename from api/src/androidTest/assets/localfeed/rss/rss_items_enclosure.xml rename to api/src/androidTest/assets/localfeed/rss2/rss_items_enclosure.xml diff --git a/api/src/androidTest/assets/localfeed/rss/rss_items_media_content.xml b/api/src/androidTest/assets/localfeed/rss2/rss_items_media_content.xml similarity index 100% rename from api/src/androidTest/assets/localfeed/rss/rss_items_media_content.xml rename to api/src/androidTest/assets/localfeed/rss2/rss_items_media_content.xml diff --git a/api/src/androidTest/assets/localfeed/rss/rss_items_no_date.xml b/api/src/androidTest/assets/localfeed/rss2/rss_items_no_date.xml similarity index 100% rename from api/src/androidTest/assets/localfeed/rss/rss_items_no_date.xml rename to api/src/androidTest/assets/localfeed/rss2/rss_items_no_date.xml diff --git a/api/src/androidTest/assets/localfeed/rss/rss_items_no_link.xml b/api/src/androidTest/assets/localfeed/rss2/rss_items_no_link.xml similarity index 100% rename from api/src/androidTest/assets/localfeed/rss/rss_items_no_link.xml rename to api/src/androidTest/assets/localfeed/rss2/rss_items_no_link.xml diff --git a/api/src/androidTest/assets/localfeed/rss/rss_items_no_title.xml b/api/src/androidTest/assets/localfeed/rss2/rss_items_no_title.xml similarity index 100% rename from api/src/androidTest/assets/localfeed/rss/rss_items_no_title.xml rename to api/src/androidTest/assets/localfeed/rss2/rss_items_no_title.xml diff --git a/api/src/androidTest/assets/localfeed/rss/rss_items_other_namespaces.xml b/api/src/androidTest/assets/localfeed/rss2/rss_items_other_namespaces.xml similarity index 100% rename from api/src/androidTest/assets/localfeed/rss/rss_items_other_namespaces.xml rename to api/src/androidTest/assets/localfeed/rss2/rss_items_other_namespaces.xml diff --git a/api/src/androidTest/java/com/readrops/api/localfeed/rss2/RSS2FeedAdapterTest.kt b/api/src/androidTest/java/com/readrops/api/localfeed/rss2/RSS2FeedAdapterTest.kt index 343cdae7..4ac3c43d 100644 --- a/api/src/androidTest/java/com/readrops/api/localfeed/rss2/RSS2FeedAdapterTest.kt +++ b/api/src/androidTest/java/com/readrops/api/localfeed/rss2/RSS2FeedAdapterTest.kt @@ -17,7 +17,7 @@ class RSS2FeedAdapterTest { @Test fun normalCasesTest() { - val stream = context.resources.assets.open("localfeed/rss/rss_full_feed.xml") + val stream = context.resources.assets.open("localfeed/rss2/rss_full_feed.xml") val feed = adapter.fromXml(stream) @@ -30,7 +30,7 @@ class RSS2FeedAdapterTest { @Test(expected = ParseException::class) fun nullTitleTest() { - val stream = context.resources.assets.open("localfeed/rss/rss_feed_special_cases.xml") + val stream = context.resources.assets.open("localfeed/rss2/rss_feed_special_cases.xml") adapter.fromXml(stream) } } \ No newline at end of file diff --git a/api/src/androidTest/java/com/readrops/api/localfeed/rss2/RSS2ItemsAdapterTest.kt b/api/src/androidTest/java/com/readrops/api/localfeed/rss2/RSS2ItemsAdapterTest.kt index b5d75e08..5a5f86eb 100644 --- a/api/src/androidTest/java/com/readrops/api/localfeed/rss2/RSS2ItemsAdapterTest.kt +++ b/api/src/androidTest/java/com/readrops/api/localfeed/rss2/RSS2ItemsAdapterTest.kt @@ -36,7 +36,7 @@ class RSS2ItemsAdapterTest { @Test fun otherNamespacesTest() { - val stream = context.resources.assets.open("localfeed/rss/rss_items_other_namespaces.xml") + val stream = context.resources.assets.open("localfeed/rss2/rss_items_other_namespaces.xml") val item = adapter.fromXml(stream)[0] assertEquals(item.guid, "guid") @@ -47,25 +47,25 @@ class RSS2ItemsAdapterTest { @Test fun noTitleTest() { - val stream = context.resources.assets.open("localfeed/rss/rss_items_no_title.xml") + val stream = context.resources.assets.open("localfeed/rss2/rss_items_no_title.xml") Assert.assertThrows("Item title can't be null", ParseException::class.java) { adapter.fromXml(stream) } } @Test fun noLinkTest() { - val stream = context.resources.assets.open("localfeed/rss/rss_items_no_link.xml") + val stream = context.resources.assets.open("localfeed/rss2/rss_items_no_link.xml") Assert.assertThrows("Item link can't be null", ParseException::class.java) { adapter.fromXml(stream) } } @Test fun noDateTest() { - val stream = context.resources.assets.open("localfeed/rss/rss_items_no_date.xml") + val stream = context.resources.assets.open("localfeed/rss2/rss_items_no_date.xml") Assert.assertThrows("Item date can't be null", ParseException::class.java) { adapter.fromXml(stream) } } @Test fun enclosureTest() { - val stream = context.resources.assets.open("localfeed/rss/rss_items_enclosure.xml") + val stream = context.resources.assets.open("localfeed/rss2/rss_items_enclosure.xml") val item = adapter.fromXml(stream)[0] assertEquals(item.imageLink, "https://image1.jpg") @@ -73,7 +73,7 @@ class RSS2ItemsAdapterTest { @Test fun mediaContentTest() { - val stream = context.resources.assets.open("localfeed/rss/rss_items_media_content.xml") + val stream = context.resources.assets.open("localfeed/rss2/rss_items_media_content.xml") val item = adapter.fromXml(stream)[0] assertEquals(item.imageLink, "https://image2.jpg") From de383d5e7d80715feeddaafffa02fe4f629d82a6 Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Fri, 25 Sep 2020 13:50:00 +0200 Subject: [PATCH 055/187] Parse recursively items content/description --- api/build.gradle | 2 +- .../api/localfeed/rss2/RSS2ItemsAdapterTest.kt | 6 +++--- .../readrops/api/localfeed/atom/ATOMItemsAdapter.kt | 9 +++------ .../readrops/api/localfeed/rss1/RSS1ItemsAdapter.kt | 7 ++----- .../readrops/api/localfeed/rss2/RSS2ItemsAdapter.kt | 10 +++++----- .../java/com/readrops/api/utils/KonsumerExtensions.kt | 8 +++++++- 6 files changed, 21 insertions(+), 21 deletions(-) diff --git a/api/build.gradle b/api/build.gradle index c7fa49aa..bf8dcf78 100644 --- a/api/build.gradle +++ b/api/build.gradle @@ -53,7 +53,7 @@ dependencies { androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0' androidTestImplementation 'com.squareup.okhttp3:mockwebserver:4.8.1' - implementation 'com.gitlab.mvysny.konsume-xml:konsume-xml:0.11' + implementation 'com.gitlab.mvysny.konsume-xml:konsume-xml:0.12' implementation 'com.squareup.okhttp3:okhttp:4.8.1' diff --git a/api/src/androidTest/java/com/readrops/api/localfeed/rss2/RSS2ItemsAdapterTest.kt b/api/src/androidTest/java/com/readrops/api/localfeed/rss2/RSS2ItemsAdapterTest.kt index 5a5f86eb..646dbfeb 100644 --- a/api/src/androidTest/java/com/readrops/api/localfeed/rss2/RSS2ItemsAdapterTest.kt +++ b/api/src/androidTest/java/com/readrops/api/localfeed/rss2/RSS2ItemsAdapterTest.kt @@ -48,19 +48,19 @@ class RSS2ItemsAdapterTest { @Test fun noTitleTest() { val stream = context.resources.assets.open("localfeed/rss2/rss_items_no_title.xml") - Assert.assertThrows("Item title can't be null", ParseException::class.java) { adapter.fromXml(stream) } + Assert.assertThrows("Item title is required", ParseException::class.java) { adapter.fromXml(stream) } } @Test fun noLinkTest() { val stream = context.resources.assets.open("localfeed/rss2/rss_items_no_link.xml") - Assert.assertThrows("Item link can't be null", ParseException::class.java) { adapter.fromXml(stream) } + Assert.assertThrows("Item link is required", ParseException::class.java) { adapter.fromXml(stream) } } @Test fun noDateTest() { val stream = context.resources.assets.open("localfeed/rss2/rss_items_no_date.xml") - Assert.assertThrows("Item date can't be null", ParseException::class.java) { adapter.fromXml(stream) } + Assert.assertThrows("Item date is required", ParseException::class.java) { adapter.fromXml(stream) } } @Test diff --git a/api/src/main/java/com/readrops/api/localfeed/atom/ATOMItemsAdapter.kt b/api/src/main/java/com/readrops/api/localfeed/atom/ATOMItemsAdapter.kt index 3d2ccf8f..b4480e0e 100644 --- a/api/src/main/java/com/readrops/api/localfeed/atom/ATOMItemsAdapter.kt +++ b/api/src/main/java/com/readrops/api/localfeed/atom/ATOMItemsAdapter.kt @@ -4,10 +4,7 @@ import com.gitlab.mvysny.konsumexml.Names import com.gitlab.mvysny.konsumexml.allChildrenAutoIgnore import com.gitlab.mvysny.konsumexml.konsumeXml import com.readrops.api.localfeed.XmlAdapter -import com.readrops.api.utils.DateUtils -import com.readrops.api.utils.ParseException -import com.readrops.api.utils.nonNullText -import com.readrops.api.utils.nullableText +import com.readrops.api.utils.* import com.readrops.db.entities.Item import java.io.InputStream @@ -32,8 +29,8 @@ class ATOMItemsAdapter : XmlAdapter> { link = attributes["href"] } "author" -> allChildrenAutoIgnore("name") { author = text() } - "summary" -> description = nullableText() - "content" -> content = nullableText() + "summary" -> description = nullableTextRecursively() + "content" -> content = nullableTextRecursively() } } } diff --git a/api/src/main/java/com/readrops/api/localfeed/rss1/RSS1ItemsAdapter.kt b/api/src/main/java/com/readrops/api/localfeed/rss1/RSS1ItemsAdapter.kt index 26b6e239..9b040cfb 100644 --- a/api/src/main/java/com/readrops/api/localfeed/rss1/RSS1ItemsAdapter.kt +++ b/api/src/main/java/com/readrops/api/localfeed/rss1/RSS1ItemsAdapter.kt @@ -4,10 +4,7 @@ import com.gitlab.mvysny.konsumexml.Names import com.gitlab.mvysny.konsumexml.allChildrenAutoIgnore import com.gitlab.mvysny.konsumexml.konsumeXml import com.readrops.api.localfeed.XmlAdapter -import com.readrops.api.utils.DateUtils -import com.readrops.api.utils.ParseException -import com.readrops.api.utils.nonNullText -import com.readrops.api.utils.nullableText +import com.readrops.api.utils.* import com.readrops.db.entities.Item import java.io.InputStream @@ -31,7 +28,7 @@ class RSS1ItemsAdapter : XmlAdapter> { "link" -> link = nullableText() "dc:date" -> pubDate = DateUtils.stringToLocalDateTime(nonNullText()) "dc:creator" -> authors += nullableText() - "description" -> description = nullableText(failOnElement = false) + "description" -> description = nullableTextRecursively() else -> skipContents() } } diff --git a/api/src/main/java/com/readrops/api/localfeed/rss2/RSS2ItemsAdapter.kt b/api/src/main/java/com/readrops/api/localfeed/rss2/RSS2ItemsAdapter.kt index fde85f49..7a5c6890 100644 --- a/api/src/main/java/com/readrops/api/localfeed/rss2/RSS2ItemsAdapter.kt +++ b/api/src/main/java/com/readrops/api/localfeed/rss2/RSS2ItemsAdapter.kt @@ -30,8 +30,8 @@ class RSS2ItemsAdapter : XmlAdapter> { "pubDate" -> pubDate = DateUtils.stringToLocalDateTime(nonNullText()) "dc:date" -> pubDate = DateUtils.stringToLocalDateTime(nonNullText()) "guid" -> guid = nullableText() - "description" -> description = text(failOnElement = false) - "content:encoded" -> content = nullableText(failOnElement = false) + "description" -> description = nullableTextRecursively() + "content:encoded" -> content = nullableTextRecursively() "enclosure" -> parseEnclosure(this, enclosures) "media:content" -> parseMediaContent(this, mediaContents) "media:group" -> allChildrenAutoIgnore("content") { @@ -80,9 +80,9 @@ class RSS2ItemsAdapter : XmlAdapter> { private fun validateItem(item: Item) { when { - item.title == null -> throw ParseException("Item title can't be null") - item.link == null -> throw ParseException("Item link can't be null") - item.pubDate == null -> throw ParseException("Item date can't be null") + item.title == null -> throw ParseException("Item title is required") + item.link == null -> throw ParseException("Item link is required") + item.pubDate == null -> throw ParseException("Item date is required") } } diff --git a/api/src/main/java/com/readrops/api/utils/KonsumerExtensions.kt b/api/src/main/java/com/readrops/api/utils/KonsumerExtensions.kt index f1760e85..8da218e6 100644 --- a/api/src/main/java/com/readrops/api/utils/KonsumerExtensions.kt +++ b/api/src/main/java/com/readrops/api/utils/KonsumerExtensions.kt @@ -2,13 +2,19 @@ package com.readrops.api.utils import com.gitlab.mvysny.konsumexml.Konsumer import com.gitlab.mvysny.konsumexml.Whitespace +import com.gitlab.mvysny.konsumexml.textRecursively fun Konsumer.nonNullText(failOnElement: Boolean = true): String { val text = text(failOnElement = failOnElement, whitespace = Whitespace.preserve) - return if (text.isNotEmpty()) text else throw ParseException("Xml field $name can't be null") + return if (text.isNotEmpty()) text else throw ParseException("$name text can't be null") } fun Konsumer.nullableText(failOnElement: Boolean = true): String? { val text = text(failOnElement = failOnElement, whitespace = Whitespace.preserve) return if (text.isNotEmpty()) text else null +} + +fun Konsumer.nullableTextRecursively(): String? { + val text = textRecursively() + return if (text.isNotEmpty()) text else null } \ No newline at end of file From a4d6139848302434a90496238310656eebe8be91 Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Fri, 25 Sep 2020 14:34:19 +0200 Subject: [PATCH 056/187] Parse content:encoded element in RSS1 items --- api/src/androidTest/assets/localfeed/rss1/rss1_feed.xml | 3 ++- .../com/readrops/api/localfeed/rss1/RSS1ItemsAdapterTest.kt | 1 + .../java/com/readrops/api/localfeed/rss1/RSS1ItemsAdapter.kt | 3 ++- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/api/src/androidTest/assets/localfeed/rss1/rss1_feed.xml b/api/src/androidTest/assets/localfeed/rss1/rss1_feed.xml index 2dae7a51..3c60e50e 100644 --- a/api/src/androidTest/assets/localfeed/rss1/rss1_feed.xml +++ b/api/src/androidTest/assets/localfeed/rss1/rss1_feed.xml @@ -1,6 +1,6 @@ + xmlns="http://purl.org/rss/1.0/" xmlns:content="http://purl.org/rss/1.0/modules/content/"> Slashdot @@ -95,6 +95,7 @@ src="https://slashdot.org/slashdot-it.pl?op=discuss&amp;id=17251868&amp;smallembed=1" style="height: 300px; width: 100%; border: none;"></iframe> + content:encoded msmash 2020-09-23T16:15:00+00:00 programming diff --git a/api/src/androidTest/java/com/readrops/api/localfeed/rss1/RSS1ItemsAdapterTest.kt b/api/src/androidTest/java/com/readrops/api/localfeed/rss1/RSS1ItemsAdapterTest.kt index 178220da..c5dcb452 100644 --- a/api/src/androidTest/java/com/readrops/api/localfeed/rss1/RSS1ItemsAdapterTest.kt +++ b/api/src/androidTest/java/com/readrops/api/localfeed/rss1/RSS1ItemsAdapterTest.kt @@ -34,6 +34,7 @@ class RSS1ItemsAdapterTest { assertEquals(item.pubDate, DateUtils.stringToLocalDateTime("2020-09-23T16:15:00+00:00")) assertEquals(item.author, "msmash") assertNotNull(item.description) + assertEquals(item.content, "content:encoded") } @Test diff --git a/api/src/main/java/com/readrops/api/localfeed/rss1/RSS1ItemsAdapter.kt b/api/src/main/java/com/readrops/api/localfeed/rss1/RSS1ItemsAdapter.kt index 9b040cfb..d5134784 100644 --- a/api/src/main/java/com/readrops/api/localfeed/rss1/RSS1ItemsAdapter.kt +++ b/api/src/main/java/com/readrops/api/localfeed/rss1/RSS1ItemsAdapter.kt @@ -29,6 +29,7 @@ class RSS1ItemsAdapter : XmlAdapter> { "dc:date" -> pubDate = DateUtils.stringToLocalDateTime(nonNullText()) "dc:creator" -> authors += nullableText() "description" -> description = nullableTextRecursively() + "content:encoded" -> content = nullableTextRecursively() else -> skipContents() } } @@ -60,6 +61,6 @@ class RSS1ItemsAdapter : XmlAdapter> { } companion object { - val names = Names.of("title", "description", "date", "link", "creator") + val names = Names.of("title", "description", "date", "link", "creator", "encoded") } } \ No newline at end of file From bcedd025bac544e0f55dd51cba1b486a49933385 Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Sat, 26 Sep 2020 13:32:30 +0200 Subject: [PATCH 057/187] Add xml content type detection for RSS1 --- .../com/readrops/api/localfeed/LocalRSSHelper.kt | 14 +++++++------- .../readrops/api/localfeed/LocalRSSHelperTest.kt | 13 ++++++++++++- 2 files changed, 19 insertions(+), 8 deletions(-) diff --git a/api/src/main/java/com/readrops/api/localfeed/LocalRSSHelper.kt b/api/src/main/java/com/readrops/api/localfeed/LocalRSSHelper.kt index b8b020e3..b0cb5431 100644 --- a/api/src/main/java/com/readrops/api/localfeed/LocalRSSHelper.kt +++ b/api/src/main/java/com/readrops/api/localfeed/LocalRSSHelper.kt @@ -1,7 +1,6 @@ package com.readrops.api.localfeed import java.io.InputStream -import java.util.regex.Pattern object LocalRSSHelper { @@ -11,8 +10,8 @@ object LocalRSSHelper { private const val JSONFEED_CONTENT_TYPE = "application/feed+json" private const val JSON_CONTENT_TYPE = "application/json" + private const val RSS_1_REGEX = " RSSType.RSS_1 + RSS_2_REGEX.toRegex().containsMatchIn(string) -> RSSType.RSS_2 + ATOM_REGEX.toRegex().containsMatchIn(string) -> RSSType.ATOM + else -> RSSType.UNKNOWN } reader.close() diff --git a/api/src/test/java/com/readrops/api/localfeed/LocalRSSHelperTest.kt b/api/src/test/java/com/readrops/api/localfeed/LocalRSSHelperTest.kt index 21a225dc..dd6537ff 100644 --- a/api/src/test/java/com/readrops/api/localfeed/LocalRSSHelperTest.kt +++ b/api/src/test/java/com/readrops/api/localfeed/LocalRSSHelperTest.kt @@ -30,9 +30,20 @@ class LocalRSSHelperTest { LocalRSSHelper.RSSType.UNKNOWN) } + @Test + fun rss1ContentTest() { + assertEquals(LocalRSSHelper.getRSSContentType(ByteArrayInputStream( + """ + + Date: Mon, 28 Sep 2020 14:34:06 +0200 Subject: [PATCH 058/187] Set a global limit for the authors concatenation --- api/src/main/java/com/readrops/api/localfeed/XmlAdapter.kt | 4 +++- .../java/com/readrops/api/localfeed/json/JSONItemsAdapter.kt | 3 ++- .../java/com/readrops/api/localfeed/rss1/RSS1ItemsAdapter.kt | 3 ++- .../java/com/readrops/api/localfeed/rss2/RSS2ItemsAdapter.kt | 3 ++- 4 files changed, 9 insertions(+), 4 deletions(-) diff --git a/api/src/main/java/com/readrops/api/localfeed/XmlAdapter.kt b/api/src/main/java/com/readrops/api/localfeed/XmlAdapter.kt index 91807c54..798e476d 100644 --- a/api/src/main/java/com/readrops/api/localfeed/XmlAdapter.kt +++ b/api/src/main/java/com/readrops/api/localfeed/XmlAdapter.kt @@ -11,7 +11,7 @@ import com.readrops.db.entities.Item import java.io.InputStream interface XmlAdapter { - + fun fromXml(inputStream: InputStream): T companion object { @@ -32,6 +32,8 @@ interface XmlAdapter { else -> throw IllegalArgumentException("Unknown RSS type : $type") } } + + const val AUTHORS_MAX = 4 } } diff --git a/api/src/main/java/com/readrops/api/localfeed/json/JSONItemsAdapter.kt b/api/src/main/java/com/readrops/api/localfeed/json/JSONItemsAdapter.kt index 4a942b93..8ff9a80b 100644 --- a/api/src/main/java/com/readrops/api/localfeed/json/JSONItemsAdapter.kt +++ b/api/src/main/java/com/readrops/api/localfeed/json/JSONItemsAdapter.kt @@ -1,5 +1,6 @@ package com.readrops.api.localfeed.json +import com.readrops.api.localfeed.XmlAdapter.Companion.AUTHORS_MAX import com.readrops.api.utils.DateUtils import com.readrops.api.utils.ParseException import com.readrops.api.utils.nextNullableString @@ -99,7 +100,7 @@ class JSONItemsAdapter : JsonAdapter>() { // here, nextNullableString doesn't check if authors values are empty return if (authors.filterNot { author -> author.isNullOrEmpty() }.isNotEmpty()) - authors.filterNot { author -> author.isNullOrEmpty() }.joinToString(limit = 4) else null + authors.filterNot { author -> author.isNullOrEmpty() }.joinToString(limit = AUTHORS_MAX) else null } private fun validateItem(item: Item) { diff --git a/api/src/main/java/com/readrops/api/localfeed/rss1/RSS1ItemsAdapter.kt b/api/src/main/java/com/readrops/api/localfeed/rss1/RSS1ItemsAdapter.kt index d5134784..ddc93353 100644 --- a/api/src/main/java/com/readrops/api/localfeed/rss1/RSS1ItemsAdapter.kt +++ b/api/src/main/java/com/readrops/api/localfeed/rss1/RSS1ItemsAdapter.kt @@ -4,6 +4,7 @@ import com.gitlab.mvysny.konsumexml.Names import com.gitlab.mvysny.konsumexml.allChildrenAutoIgnore import com.gitlab.mvysny.konsumexml.konsumeXml import com.readrops.api.localfeed.XmlAdapter +import com.readrops.api.localfeed.XmlAdapter.Companion.AUTHORS_MAX import com.readrops.api.utils.* import com.readrops.db.entities.Item import java.io.InputStream @@ -36,7 +37,7 @@ class RSS1ItemsAdapter : XmlAdapter> { } item.guid = item.link - if (authors.filterNotNull().isNotEmpty()) item.author = authors.filterNotNull().joinToString(limit = 4) + if (authors.filterNotNull().isNotEmpty()) item.author = authors.filterNotNull().joinToString(limit = AUTHORS_MAX) if (item.link == null) item.link = about validateItem(item) diff --git a/api/src/main/java/com/readrops/api/localfeed/rss2/RSS2ItemsAdapter.kt b/api/src/main/java/com/readrops/api/localfeed/rss2/RSS2ItemsAdapter.kt index 7a5c6890..9ecc01fe 100644 --- a/api/src/main/java/com/readrops/api/localfeed/rss2/RSS2ItemsAdapter.kt +++ b/api/src/main/java/com/readrops/api/localfeed/rss2/RSS2ItemsAdapter.kt @@ -2,6 +2,7 @@ package com.readrops.api.localfeed.rss2 import com.gitlab.mvysny.konsumexml.* import com.readrops.api.localfeed.XmlAdapter +import com.readrops.api.localfeed.XmlAdapter.Companion.AUTHORS_MAX import com.readrops.api.utils.* import com.readrops.db.entities.Item import java.io.InputStream @@ -47,7 +48,7 @@ class RSS2ItemsAdapter : XmlAdapter> { validateItem(item) if (item.guid == null) item.guid = item.link if (item.author == null && creators.filterNotNull().isNotEmpty()) - item.author = creators.filterNotNull().first() + item.author = creators.filterNotNull().joinToString(limit = AUTHORS_MAX) if (enclosures.isNotEmpty()) item.imageLink = enclosures.first() else if (mediaContents.isNotEmpty()) item.imageLink = mediaContents.first() From f6f5f27dd47c8d56fb23702beed7da0ddbd261e2 Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Tue, 29 Sep 2020 11:29:22 +0200 Subject: [PATCH 059/187] Fix RSS2 items tests --- .../com/readrops/api/localfeed/rss2/RSS2ItemsAdapterTest.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/src/androidTest/java/com/readrops/api/localfeed/rss2/RSS2ItemsAdapterTest.kt b/api/src/androidTest/java/com/readrops/api/localfeed/rss2/RSS2ItemsAdapterTest.kt index 646dbfeb..0c06a30e 100644 --- a/api/src/androidTest/java/com/readrops/api/localfeed/rss2/RSS2ItemsAdapterTest.kt +++ b/api/src/androidTest/java/com/readrops/api/localfeed/rss2/RSS2ItemsAdapterTest.kt @@ -40,7 +40,7 @@ class RSS2ItemsAdapterTest { val item = adapter.fromXml(stream)[0] assertEquals(item.guid, "guid") - assertEquals(item.author, "creator 1") + assertEquals(item.author, "creator 1, creator 2, creator 3, creator 4") assertEquals(item.pubDate, DateUtils.stringToLocalDateTime("2020-08-05T14:03:48Z")) assertEquals(item.content, "content:encoded") } From 2bbc8c3e1092e978fefeb1736d9cb24bf6a860ae Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Tue, 29 Sep 2020 22:18:56 +0200 Subject: [PATCH 060/187] Add test for RSS2 media:group element --- .../localfeed/rss2/rss_items_media_group.xml | 30 +++++++++++++++++++ .../localfeed/rss2/RSS2ItemsAdapterTest.kt | 8 +++++ .../api/localfeed/rss2/RSS2ItemsAdapter.kt | 15 ++++++---- 3 files changed, 48 insertions(+), 5 deletions(-) create mode 100644 api/src/androidTest/assets/localfeed/rss2/rss_items_media_group.xml diff --git a/api/src/androidTest/assets/localfeed/rss2/rss_items_media_group.xml b/api/src/androidTest/assets/localfeed/rss2/rss_items_media_group.xml new file mode 100644 index 00000000..39f73f50 --- /dev/null +++ b/api/src/androidTest/assets/localfeed/rss2/rss_items_media_group.xml @@ -0,0 +1,30 @@ + + + + + title + link + + 2020-08-05T14:03:48Z + + + + + + + guid + + + + + + + image2 title + + + + + + \ No newline at end of file diff --git a/api/src/androidTest/java/com/readrops/api/localfeed/rss2/RSS2ItemsAdapterTest.kt b/api/src/androidTest/java/com/readrops/api/localfeed/rss2/RSS2ItemsAdapterTest.kt index 0c06a30e..3894db35 100644 --- a/api/src/androidTest/java/com/readrops/api/localfeed/rss2/RSS2ItemsAdapterTest.kt +++ b/api/src/androidTest/java/com/readrops/api/localfeed/rss2/RSS2ItemsAdapterTest.kt @@ -78,4 +78,12 @@ class RSS2ItemsAdapterTest { assertEquals(item.imageLink, "https://image2.jpg") } + + @Test + fun mediaGroupTest() { + val stream = context.resources.assets.open("localfeed/rss2/rss_items_media_group.xml") + val item = adapter.fromXml(stream).first() + + assertEquals(item.imageLink, "https://image1.jpg") + } } \ No newline at end of file diff --git a/api/src/main/java/com/readrops/api/localfeed/rss2/RSS2ItemsAdapter.kt b/api/src/main/java/com/readrops/api/localfeed/rss2/RSS2ItemsAdapter.kt index 9ecc01fe..5e8d2219 100644 --- a/api/src/main/java/com/readrops/api/localfeed/rss2/RSS2ItemsAdapter.kt +++ b/api/src/main/java/com/readrops/api/localfeed/rss2/RSS2ItemsAdapter.kt @@ -35,11 +35,7 @@ class RSS2ItemsAdapter : XmlAdapter> { "content:encoded" -> content = nullableTextRecursively() "enclosure" -> parseEnclosure(this, enclosures) "media:content" -> parseMediaContent(this, mediaContents) - "media:group" -> allChildrenAutoIgnore("content") { - when (tagName) { - "media:content" -> parseMediaContent(this, mediaContents) - } - } + "media:group" -> parseMediaGroup(this, mediaContents) else -> skipContents() // for example media:description } } @@ -79,6 +75,15 @@ class RSS2ItemsAdapter : XmlAdapter> { konsume.skipContents() // ignore media content sub elements } + private fun parseMediaGroup(konsume: Konsumer, mediaContents: MutableList) { + konsume.allChildrenAutoIgnore("content") { + when (tagName) { + "media:content" -> parseMediaContent(this, mediaContents) + else -> skipContents() + } + } + } + private fun validateItem(item: Item) { when { item.title == null -> throw ParseException("Item title is required") From d958dcbf0442eec3748402010ec5bac833ab3caf Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Tue, 29 Sep 2020 22:23:54 +0200 Subject: [PATCH 061/187] Simplify ATOMItemsAdapter fromXml method --- .../api/localfeed/atom/ATOMItemsAdapter.kt | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/api/src/main/java/com/readrops/api/localfeed/atom/ATOMItemsAdapter.kt b/api/src/main/java/com/readrops/api/localfeed/atom/ATOMItemsAdapter.kt index b4480e0e..9cb00c15 100644 --- a/api/src/main/java/com/readrops/api/localfeed/atom/ATOMItemsAdapter.kt +++ b/api/src/main/java/com/readrops/api/localfeed/atom/ATOMItemsAdapter.kt @@ -1,5 +1,6 @@ package com.readrops.api.localfeed.atom +import com.gitlab.mvysny.konsumexml.Konsumer import com.gitlab.mvysny.konsumexml.Names import com.gitlab.mvysny.konsumexml.allChildrenAutoIgnore import com.gitlab.mvysny.konsumexml.konsumeXml @@ -23,12 +24,8 @@ class ATOMItemsAdapter : XmlAdapter> { "title" -> title = nonNullText() "id" -> guid = nullableText() "updated" -> pubDate = DateUtils.stringToLocalDateTime(nonNullText()) - "link" -> { - if (attributes.getValueOpt("rel") == null || - attributes["rel"] == "alternate") - link = attributes["href"] - } - "author" -> allChildrenAutoIgnore("name") { author = text() } + "link" -> parseLink(this, this@apply) + "author" -> allChildrenAutoIgnore("name") { author = nullableText() } "summary" -> description = nullableTextRecursively() "content" -> content = nullableTextRecursively() } @@ -49,6 +46,15 @@ class ATOMItemsAdapter : XmlAdapter> { } } + private fun parseLink(konsume: Konsumer, item: Item) { + konsume.apply { + if (attributes.getValueOpt("rel") == null || + attributes["rel"] == "alternate") + item.link = attributes["href"] + } + + } + private fun validateItem(item: Item) { when { item.title == null -> throw ParseException("Item title is required") From 2bfb061f484a0c4c4da1a563d409f4444c9d0e42 Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Tue, 29 Sep 2020 22:37:16 +0200 Subject: [PATCH 062/187] Treat empty strings as null values in jsonfeed adapters --- .../api/localfeed/json/JSONFeedAdapter.kt | 3 ++- .../api/localfeed/json/JSONItemsAdapter.kt | 19 +++++++++---------- .../api/utils/JsonReaderExtensions.kt | 7 ++++++- 3 files changed, 17 insertions(+), 12 deletions(-) diff --git a/api/src/main/java/com/readrops/api/localfeed/json/JSONFeedAdapter.kt b/api/src/main/java/com/readrops/api/localfeed/json/JSONFeedAdapter.kt index a67a6a16..772caeec 100644 --- a/api/src/main/java/com/readrops/api/localfeed/json/JSONFeedAdapter.kt +++ b/api/src/main/java/com/readrops/api/localfeed/json/JSONFeedAdapter.kt @@ -1,6 +1,7 @@ package com.readrops.api.localfeed.json import com.readrops.api.utils.ParseException +import com.readrops.api.utils.nextNonEmptyString import com.readrops.api.utils.nextNullableString import com.readrops.db.entities.Feed import com.squareup.moshi.FromJson @@ -21,7 +22,7 @@ class JSONFeedAdapter { while (reader.hasNext()) { with(feed) { when (reader.selectName(names)) { - 0 -> name = reader.nextString() + 0 -> name = reader.nextNonEmptyString() 1 -> siteUrl = reader.nextNullableString() 2 -> url = reader.nextNullableString() 3 -> description = reader.nextNullableString() diff --git a/api/src/main/java/com/readrops/api/localfeed/json/JSONItemsAdapter.kt b/api/src/main/java/com/readrops/api/localfeed/json/JSONItemsAdapter.kt index 8ff9a80b..c239fa17 100644 --- a/api/src/main/java/com/readrops/api/localfeed/json/JSONItemsAdapter.kt +++ b/api/src/main/java/com/readrops/api/localfeed/json/JSONItemsAdapter.kt @@ -3,6 +3,7 @@ package com.readrops.api.localfeed.json import com.readrops.api.localfeed.XmlAdapter.Companion.AUTHORS_MAX import com.readrops.api.utils.DateUtils import com.readrops.api.utils.ParseException +import com.readrops.api.utils.nextNonEmptyString import com.readrops.api.utils.nextNullableString import com.readrops.db.entities.Item import com.squareup.moshi.FromJson @@ -16,9 +17,8 @@ class JSONItemsAdapter : JsonAdapter>() { // not useful } - @FromJson override fun fromJson(reader: JsonReader): List { - try { + return try { val items = arrayListOf() reader.beginObject() @@ -29,7 +29,7 @@ class JSONItemsAdapter : JsonAdapter>() { } } - return items + items } catch (e: Exception) { throw ParseException(e.message) } @@ -48,14 +48,14 @@ class JSONItemsAdapter : JsonAdapter>() { while (reader.hasNext()) { with(item) { when (reader.selectName(names)) { - 0 -> guid = reader.nextString() - 1 -> link = reader.nextString() - 2 -> title = reader.nextString() + 0 -> guid = reader.nextNonEmptyString() + 1 -> link = reader.nextNonEmptyString() + 2 -> title = reader.nextNonEmptyString() 3 -> contentHtml = reader.nextNullableString() 4 -> contentText = reader.nextNullableString() 5 -> description = reader.nextNullableString() 6 -> imageLink = reader.nextNullableString() - 7 -> pubDate = DateUtils.stringToLocalDateTime(reader.nextString()) + 7 -> pubDate = DateUtils.stringToLocalDateTime(reader.nextNonEmptyString()) 8 -> author = parseAuthor(reader) // jsonfeed 1.0 9 -> author = parseAuthors(reader) // jsonfeed 1.1 else -> reader.skipValue() @@ -98,9 +98,8 @@ class JSONItemsAdapter : JsonAdapter>() { reader.endArray() - // here, nextNullableString doesn't check if authors values are empty - return if (authors.filterNot { author -> author.isNullOrEmpty() }.isNotEmpty()) - authors.filterNot { author -> author.isNullOrEmpty() }.joinToString(limit = AUTHORS_MAX) else null + return if (authors.filterNotNull().isNotEmpty()) + authors.filterNotNull().joinToString(limit = AUTHORS_MAX) else null } private fun validateItem(item: Item) { diff --git a/api/src/main/java/com/readrops/api/utils/JsonReaderExtensions.kt b/api/src/main/java/com/readrops/api/utils/JsonReaderExtensions.kt index ae8e4bde..09ecf36a 100644 --- a/api/src/main/java/com/readrops/api/utils/JsonReaderExtensions.kt +++ b/api/src/main/java/com/readrops/api/utils/JsonReaderExtensions.kt @@ -3,4 +3,9 @@ package com.readrops.api.utils import com.squareup.moshi.JsonReader fun JsonReader.nextNullableString(): String? = - if (peek() != JsonReader.Token.NULL) nextString() else nextNull() \ No newline at end of file + if (peek() != JsonReader.Token.NULL) nextString().ifEmpty { null }?.trim() else nextNull() + +fun JsonReader.nextNonEmptyString(): String { + val text = nextString() + return if (text.isNotEmpty()) text.trim() else throw ParseException("Json value can't be null") +} \ No newline at end of file From 9027a1c140feea53a0fda2d43c530d977c1014e4 Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Tue, 29 Sep 2020 23:08:22 +0200 Subject: [PATCH 063/187] Add tests for Konsume-xml extensions --- .../readrops/api/utils/KonsumerExtensions.kt | 14 ++--- .../api/utils/KonsumerExtensionsTest.kt | 61 +++++++++++++++++++ 2 files changed, 68 insertions(+), 7 deletions(-) create mode 100644 api/src/test/java/com/readrops/api/utils/KonsumerExtensionsTest.kt diff --git a/api/src/main/java/com/readrops/api/utils/KonsumerExtensions.kt b/api/src/main/java/com/readrops/api/utils/KonsumerExtensions.kt index 8da218e6..46de8a39 100644 --- a/api/src/main/java/com/readrops/api/utils/KonsumerExtensions.kt +++ b/api/src/main/java/com/readrops/api/utils/KonsumerExtensions.kt @@ -4,17 +4,17 @@ import com.gitlab.mvysny.konsumexml.Konsumer import com.gitlab.mvysny.konsumexml.Whitespace import com.gitlab.mvysny.konsumexml.textRecursively -fun Konsumer.nonNullText(failOnElement: Boolean = true): String { - val text = text(failOnElement = failOnElement, whitespace = Whitespace.preserve) - return if (text.isNotEmpty()) text else throw ParseException("$name text can't be null") +fun Konsumer.nonNullText(): String { + val text = text(whitespace = Whitespace.preserve) + return if (text.isNotEmpty()) text.trim() else throw ParseException("$name text can't be null") } -fun Konsumer.nullableText(failOnElement: Boolean = true): String? { - val text = text(failOnElement = failOnElement, whitespace = Whitespace.preserve) - return if (text.isNotEmpty()) text else null +fun Konsumer.nullableText(): String? { + val text = text(whitespace = Whitespace.preserve) + return if (text.isNotEmpty()) text.trim() else null } fun Konsumer.nullableTextRecursively(): String? { val text = textRecursively() - return if (text.isNotEmpty()) text else null + return if (text.isNotEmpty()) text.trim() else null } \ No newline at end of file diff --git a/api/src/test/java/com/readrops/api/utils/KonsumerExtensionsTest.kt b/api/src/test/java/com/readrops/api/utils/KonsumerExtensionsTest.kt new file mode 100644 index 00000000..4cff9f41 --- /dev/null +++ b/api/src/test/java/com/readrops/api/utils/KonsumerExtensionsTest.kt @@ -0,0 +1,61 @@ +package com.readrops.api.utils + +import com.gitlab.mvysny.konsumexml.KonsumerException +import com.gitlab.mvysny.konsumexml.konsumeXml +import junit.framework.TestCase.assertEquals +import junit.framework.TestCase.assertNull +import org.junit.Test + +class KonsumerExtensionsTest { + + @Test(expected = KonsumerException::class) + fun nonNullTextNullCaseTest() { + val xml = """ + + """.trimIndent() + + xml.konsumeXml().apply { + child("description") { nonNullText() } + } + } + + @Test + fun nonNullTextNonNullCaseTest() { + val xml = """ + +description + + """.trimIndent() + + xml.konsumeXml().apply { + val description = child("description") { nonNullText() } + assertEquals(description, "description") + } + } + + @Test + fun nullableTextNullCaseTest() { + val xml = """ + + """.trimIndent() + + xml.konsumeXml().apply { + val description = child("description") { nullableText() } + assertNull(description) + } + } + + @Test + fun nullableTextNonNullCaseTest() { + val xml = """ + +description + + """.trimIndent() + + xml.konsumeXml().apply { + val description = child("description") { nullableText() } + assertEquals(description, "description") + } + } +} \ No newline at end of file From 7eecbc8e8b439feae8b27abf673e8d104996a329 Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Tue, 29 Sep 2020 23:16:26 +0200 Subject: [PATCH 064/187] Check item link nullabilty before validateItem() in RSS1ItemsAdapter as item link is used as guid --- .../com/readrops/api/localfeed/rss1/RSS1ItemsAdapter.kt | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/api/src/main/java/com/readrops/api/localfeed/rss1/RSS1ItemsAdapter.kt b/api/src/main/java/com/readrops/api/localfeed/rss1/RSS1ItemsAdapter.kt index ddc93353..c03a2c3e 100644 --- a/api/src/main/java/com/readrops/api/localfeed/rss1/RSS1ItemsAdapter.kt +++ b/api/src/main/java/com/readrops/api/localfeed/rss1/RSS1ItemsAdapter.kt @@ -36,9 +36,12 @@ class RSS1ItemsAdapter : XmlAdapter> { } } - item.guid = item.link - if (authors.filterNotNull().isNotEmpty()) item.author = authors.filterNotNull().joinToString(limit = AUTHORS_MAX) if (item.link == null) item.link = about + ?: throw ParseException("RSS1 link or about element is required") + item.guid = item.link + + if (authors.filterNotNull().isNotEmpty()) item.author = authors.filterNotNull() + .joinToString(limit = AUTHORS_MAX) validateItem(item) @@ -56,7 +59,6 @@ class RSS1ItemsAdapter : XmlAdapter> { private fun validateItem(item: Item) { when { item.title == null -> throw ParseException("Item title is required") - item.link == null -> throw ParseException("Item link is required") item.pubDate == null -> throw ParseException("Item date is required") } } From 872a894db068660d95269df46dbec68f904c8090 Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Tue, 29 Sep 2020 23:30:51 +0200 Subject: [PATCH 065/187] Replace RSS2 atom:link element value by response url, as atom:link element is unreliable --- .../com/readrops/api/localfeed/LocalRSSDataSourceTest.kt | 3 ++- .../java/com/readrops/api/localfeed/LocalRSSDataSource.kt | 6 ++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/api/src/androidTest/java/com/readrops/api/localfeed/LocalRSSDataSourceTest.kt b/api/src/androidTest/java/com/readrops/api/localfeed/LocalRSSDataSourceTest.kt index 6960c9f5..880471b5 100644 --- a/api/src/androidTest/java/com/readrops/api/localfeed/LocalRSSDataSourceTest.kt +++ b/api/src/androidTest/java/com/readrops/api/localfeed/LocalRSSDataSourceTest.kt @@ -32,7 +32,7 @@ class LocalRSSDataSourceTest { @Before fun before() { - mockServer.start() + mockServer.start(8080) url = mockServer.url("/rss") } @@ -55,6 +55,7 @@ class LocalRSSDataSourceTest { val feed = pair?.first!! assertEquals(feed.name, "Hacker News") + assertEquals(feed.url, "http://localhost:8080/rss") assertEquals(feed.siteUrl, "https://news.ycombinator.com/") assertEquals(feed.description, "Links for the intellectually curious, ranked by readers.") diff --git a/api/src/main/java/com/readrops/api/localfeed/LocalRSSDataSource.kt b/api/src/main/java/com/readrops/api/localfeed/LocalRSSDataSource.kt index 15074d79..7104b40e 100644 --- a/api/src/main/java/com/readrops/api/localfeed/LocalRSSDataSource.kt +++ b/api/src/main/java/com/readrops/api/localfeed/LocalRSSDataSource.kt @@ -98,7 +98,7 @@ class LocalRSSDataSource(private val httpClient: OkHttpClient) { val adapter = Moshi.Builder() .add(JSONFeedAdapter()) .build() - .adapter(Feed::class.java) + .adapter(Feed::class.java) adapter.fromJson(Buffer().readFrom(stream))!! } @@ -129,7 +129,9 @@ class LocalRSSDataSource(private val httpClient: OkHttpClient) { private fun handleSpecialCases(feed: Feed, type: LocalRSSHelper.RSSType, response: Response) { with(feed) { if (type == LocalRSSHelper.RSSType.RSS_2) { - if (url == null) url = response.request.url.toString() + // if an atom:link element was parsed, we still replace its value as it is unreliable, + // otherwise we just add the rss url + url = response.request.url.toString() } else if (type == LocalRSSHelper.RSSType.ATOM) { if (url == null) url = response.request.url.toString() if (siteUrl == null) siteUrl = response.request.url.scheme + "://" + response.request.url.host From 6f2ff36ecebbf7e947a1891fb290cfc205607f32 Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Wed, 30 Sep 2020 18:10:18 +0200 Subject: [PATCH 066/187] Add tests for Konsumer.nullableTextRecursively() --- .../api/utils/KonsumerExtensionsTest.kt | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/api/src/test/java/com/readrops/api/utils/KonsumerExtensionsTest.kt b/api/src/test/java/com/readrops/api/utils/KonsumerExtensionsTest.kt index 4cff9f41..7698dfb2 100644 --- a/api/src/test/java/com/readrops/api/utils/KonsumerExtensionsTest.kt +++ b/api/src/test/java/com/readrops/api/utils/KonsumerExtensionsTest.kt @@ -58,4 +58,30 @@ description assertEquals(description, "description") } } + + @Test + fun nullableTextRecursivelyNullCaseTest() { + val xml = """ + + """.trimIndent() + + xml.konsumeXml().apply { + val description = child("description") { nullableTextRecursively() } + assertNull(description) + } + } + + @Test + fun nullableTextRecursivelyNonNullCaseTest() { + val xml = """ + +description + + """.trimIndent() + + xml.konsumeXml().apply { + val description = child("description") { nullableTextRecursively() } + assertEquals(description, "description") + } + } } \ No newline at end of file From e41ab29264a5b19224b0fbaebf8a21af2dbb838b Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Thu, 1 Oct 2020 19:38:48 +0200 Subject: [PATCH 067/187] Rename DateUtils.stringToLocalDateTime() to parse() --- .../api/localfeed/atom/ATOMItemsAdapterTest.kt | 2 +- .../api/localfeed/json/JSONItemsAdapterTest.kt | 2 +- .../api/localfeed/rss1/RSS1ItemsAdapterTest.kt | 2 +- .../api/localfeed/rss2/RSS2ItemsAdapterTest.kt | 4 ++-- .../api/localfeed/atom/ATOMItemsAdapter.kt | 2 +- .../api/localfeed/json/JSONItemsAdapter.kt | 3 +-- .../api/localfeed/rss1/RSS1ItemsAdapter.kt | 2 +- .../api/localfeed/rss2/RSS2ItemsAdapter.kt | 4 ++-- .../java/com/readrops/api/utils/DateUtils.java | 2 +- .../java/com/readrops/api/utils/DateUtilsTest.java | 14 +++++++------- 10 files changed, 18 insertions(+), 19 deletions(-) diff --git a/api/src/androidTest/java/com/readrops/api/localfeed/atom/ATOMItemsAdapterTest.kt b/api/src/androidTest/java/com/readrops/api/localfeed/atom/ATOMItemsAdapterTest.kt index f6095a97..0c8085ea 100644 --- a/api/src/androidTest/java/com/readrops/api/localfeed/atom/ATOMItemsAdapterTest.kt +++ b/api/src/androidTest/java/com/readrops/api/localfeed/atom/ATOMItemsAdapterTest.kt @@ -28,7 +28,7 @@ class ATOMItemsAdapterTest { assertEquals(items.size, 4) assertEquals(item.title, "Add an option to open item url in custom tab") assertEquals(item.link, "https://github.com/readrops/Readrops/commit/c15f093a1bc4211e85f8d1817c9073e307afe5ac") - assertEquals(item.pubDate, DateUtils.stringToLocalDateTime("2020-09-06T21:09:59Z")) + assertEquals(item.pubDate, DateUtils.parse("2020-09-06T21:09:59Z")) assertEquals(item.author, "Shinokuni") assertEquals(item.description, "Summary") assertEquals(item.guid, "tag:github.com,2008:Grit::Commit/c15f093a1bc4211e85f8d1817c9073e307afe5ac") diff --git a/api/src/androidTest/java/com/readrops/api/localfeed/json/JSONItemsAdapterTest.kt b/api/src/androidTest/java/com/readrops/api/localfeed/json/JSONItemsAdapterTest.kt index 97411d8b..474ecbff 100644 --- a/api/src/androidTest/java/com/readrops/api/localfeed/json/JSONItemsAdapterTest.kt +++ b/api/src/androidTest/java/com/readrops/api/localfeed/json/JSONItemsAdapterTest.kt @@ -37,7 +37,7 @@ class JSONItemsAdapterTest { assertEquals(item.guid, "http://flyingmeat.com/blog/archives/2017/9/acorn_and_10.13.html") assertEquals(item.title, "Acorn and 10.13") assertEquals(item.link, "http://flyingmeat.com/blog/archives/2017/9/acorn_and_10.13.html") - assertEquals(item.pubDate, DateUtils.stringToLocalDateTime("2017-09-25T14:27:27-07:00")) + assertEquals(item.pubDate, DateUtils.parse("2017-09-25T14:27:27-07:00")) assertEquals(item.author, "Author 1") assertNotNull(item.content) } diff --git a/api/src/androidTest/java/com/readrops/api/localfeed/rss1/RSS1ItemsAdapterTest.kt b/api/src/androidTest/java/com/readrops/api/localfeed/rss1/RSS1ItemsAdapterTest.kt index c5dcb452..4860877d 100644 --- a/api/src/androidTest/java/com/readrops/api/localfeed/rss1/RSS1ItemsAdapterTest.kt +++ b/api/src/androidTest/java/com/readrops/api/localfeed/rss1/RSS1ItemsAdapterTest.kt @@ -31,7 +31,7 @@ class RSS1ItemsAdapterTest { "its-flutter-development-kit-to-windows-apps?utm_source=rss1.0mainlinkanon&utm_medium=feed") assertEquals(item.guid.trim(), "https://developers.slashdot.org/story/20/09/23/1616231/google-expands-" + "its-flutter-development-kit-to-windows-apps?utm_source=rss1.0mainlinkanon&utm_medium=feed") - assertEquals(item.pubDate, DateUtils.stringToLocalDateTime("2020-09-23T16:15:00+00:00")) + assertEquals(item.pubDate, DateUtils.parse("2020-09-23T16:15:00+00:00")) assertEquals(item.author, "msmash") assertNotNull(item.description) assertEquals(item.content, "content:encoded") diff --git a/api/src/androidTest/java/com/readrops/api/localfeed/rss2/RSS2ItemsAdapterTest.kt b/api/src/androidTest/java/com/readrops/api/localfeed/rss2/RSS2ItemsAdapterTest.kt index 3894db35..e7640d2a 100644 --- a/api/src/androidTest/java/com/readrops/api/localfeed/rss2/RSS2ItemsAdapterTest.kt +++ b/api/src/androidTest/java/com/readrops/api/localfeed/rss2/RSS2ItemsAdapterTest.kt @@ -28,7 +28,7 @@ class RSS2ItemsAdapterTest { assertEquals(item.title, "Africa declared free of wild polio") assertEquals(item.link, "https://www.bbc.com/news/world-africa-53887947") - assertEquals(item.pubDate, DateUtils.stringToLocalDateTime("Tue, 25 Aug 2020 17:15:49 +0000")) + assertEquals(item.pubDate, DateUtils.parse("Tue, 25 Aug 2020 17:15:49 +0000")) assertEquals(item.author, "Author 1") assertEquals(item.description, "Comments") assertEquals(item.guid, "https://www.bbc.com/news/world-africa-53887947") @@ -41,7 +41,7 @@ class RSS2ItemsAdapterTest { assertEquals(item.guid, "guid") assertEquals(item.author, "creator 1, creator 2, creator 3, creator 4") - assertEquals(item.pubDate, DateUtils.stringToLocalDateTime("2020-08-05T14:03:48Z")) + assertEquals(item.pubDate, DateUtils.parse("2020-08-05T14:03:48Z")) assertEquals(item.content, "content:encoded") } diff --git a/api/src/main/java/com/readrops/api/localfeed/atom/ATOMItemsAdapter.kt b/api/src/main/java/com/readrops/api/localfeed/atom/ATOMItemsAdapter.kt index 9cb00c15..d7a09e13 100644 --- a/api/src/main/java/com/readrops/api/localfeed/atom/ATOMItemsAdapter.kt +++ b/api/src/main/java/com/readrops/api/localfeed/atom/ATOMItemsAdapter.kt @@ -23,7 +23,7 @@ class ATOMItemsAdapter : XmlAdapter> { when (tagName) { "title" -> title = nonNullText() "id" -> guid = nullableText() - "updated" -> pubDate = DateUtils.stringToLocalDateTime(nonNullText()) + "updated" -> pubDate = DateUtils.parse(nonNullText()) "link" -> parseLink(this, this@apply) "author" -> allChildrenAutoIgnore("name") { author = nullableText() } "summary" -> description = nullableTextRecursively() diff --git a/api/src/main/java/com/readrops/api/localfeed/json/JSONItemsAdapter.kt b/api/src/main/java/com/readrops/api/localfeed/json/JSONItemsAdapter.kt index c239fa17..6dcdc239 100644 --- a/api/src/main/java/com/readrops/api/localfeed/json/JSONItemsAdapter.kt +++ b/api/src/main/java/com/readrops/api/localfeed/json/JSONItemsAdapter.kt @@ -6,7 +6,6 @@ import com.readrops.api.utils.ParseException import com.readrops.api.utils.nextNonEmptyString import com.readrops.api.utils.nextNullableString import com.readrops.db.entities.Item -import com.squareup.moshi.FromJson import com.squareup.moshi.JsonAdapter import com.squareup.moshi.JsonReader import com.squareup.moshi.JsonWriter @@ -55,7 +54,7 @@ class JSONItemsAdapter : JsonAdapter>() { 4 -> contentText = reader.nextNullableString() 5 -> description = reader.nextNullableString() 6 -> imageLink = reader.nextNullableString() - 7 -> pubDate = DateUtils.stringToLocalDateTime(reader.nextNonEmptyString()) + 7 -> pubDate = DateUtils.parse(reader.nextNonEmptyString()) 8 -> author = parseAuthor(reader) // jsonfeed 1.0 9 -> author = parseAuthors(reader) // jsonfeed 1.1 else -> reader.skipValue() diff --git a/api/src/main/java/com/readrops/api/localfeed/rss1/RSS1ItemsAdapter.kt b/api/src/main/java/com/readrops/api/localfeed/rss1/RSS1ItemsAdapter.kt index c03a2c3e..83eaf08a 100644 --- a/api/src/main/java/com/readrops/api/localfeed/rss1/RSS1ItemsAdapter.kt +++ b/api/src/main/java/com/readrops/api/localfeed/rss1/RSS1ItemsAdapter.kt @@ -27,7 +27,7 @@ class RSS1ItemsAdapter : XmlAdapter> { when (tagName) { "title" -> title = nonNullText() "link" -> link = nullableText() - "dc:date" -> pubDate = DateUtils.stringToLocalDateTime(nonNullText()) + "dc:date" -> pubDate = DateUtils.parse(nonNullText()) "dc:creator" -> authors += nullableText() "description" -> description = nullableTextRecursively() "content:encoded" -> content = nullableTextRecursively() diff --git a/api/src/main/java/com/readrops/api/localfeed/rss2/RSS2ItemsAdapter.kt b/api/src/main/java/com/readrops/api/localfeed/rss2/RSS2ItemsAdapter.kt index 5e8d2219..3bc13b51 100644 --- a/api/src/main/java/com/readrops/api/localfeed/rss2/RSS2ItemsAdapter.kt +++ b/api/src/main/java/com/readrops/api/localfeed/rss2/RSS2ItemsAdapter.kt @@ -28,8 +28,8 @@ class RSS2ItemsAdapter : XmlAdapter> { "link" -> link = nonNullText() "author" -> author = nullableText() "dc:creator" -> creators += nullableText() - "pubDate" -> pubDate = DateUtils.stringToLocalDateTime(nonNullText()) - "dc:date" -> pubDate = DateUtils.stringToLocalDateTime(nonNullText()) + "pubDate" -> pubDate = DateUtils.parse(nonNullText()) + "dc:date" -> pubDate = DateUtils.parse(nonNullText()) "guid" -> guid = nullableText() "description" -> description = nullableTextRecursively() "content:encoded" -> content = nullableTextRecursively() diff --git a/api/src/main/java/com/readrops/api/utils/DateUtils.java b/api/src/main/java/com/readrops/api/utils/DateUtils.java index 68133f3a..bfa3dad5 100644 --- a/api/src/main/java/com/readrops/api/utils/DateUtils.java +++ b/api/src/main/java/com/readrops/api/utils/DateUtils.java @@ -30,7 +30,7 @@ public final class DateUtils { */ private static final String ATOM_JSON_DATE_FORMAT = "yyyy-MM-dd'T'HH:mm:ss"; - public static LocalDateTime stringToLocalDateTime(String value) { + public static LocalDateTime parse(String value) { DateTimeFormatter formatter = new DateTimeFormatterBuilder() .appendOptional(DateTimeFormat.forPattern(RSS_2_BASE_PATTERN + " ").getParser()) // with timezone .appendOptional(DateTimeFormat.forPattern(RSS_2_BASE_PATTERN).getParser()) // no timezone, important order here diff --git a/api/src/test/java/com/readrops/api/utils/DateUtilsTest.java b/api/src/test/java/com/readrops/api/utils/DateUtilsTest.java index 7e36f383..af3d98c7 100644 --- a/api/src/test/java/com/readrops/api/utils/DateUtilsTest.java +++ b/api/src/test/java/com/readrops/api/utils/DateUtilsTest.java @@ -11,48 +11,48 @@ public class DateUtilsTest { public void rssDateTest() { LocalDateTime dateTime = new LocalDateTime(2019, 1, 4, 22, 21, 46); - assertEquals(0, dateTime.compareTo(DateUtils.stringToLocalDateTime("Fri, 04 Jan 2019 22:21:46 GMT"))); + assertEquals(0, dateTime.compareTo(DateUtils.parse("Fri, 04 Jan 2019 22:21:46 GMT"))); } @Test public void rssDate2Test() { LocalDateTime dateTime = new LocalDateTime(2019, 1, 4, 22, 21, 46); - assertEquals(0, dateTime.compareTo(DateUtils.stringToLocalDateTime("Fri, 04 Jan 2019 22:21:46 +0000"))); + assertEquals(0, dateTime.compareTo(DateUtils.parse("Fri, 04 Jan 2019 22:21:46 +0000"))); } @Test public void rssDate3Test() { LocalDateTime dateTime = new LocalDateTime(2019, 1, 4, 22, 21, 46); - assertEquals(0, dateTime.compareTo(DateUtils.stringToLocalDateTime("Fri, 04 Jan 2019 22:21:46"))); + assertEquals(0, dateTime.compareTo(DateUtils.parse("Fri, 04 Jan 2019 22:21:46"))); } @Test public void atomJsonDateTest() { LocalDateTime dateTime = new LocalDateTime(2019, 1, 4, 22, 21, 46); - assertEquals(0, dateTime.compareTo(DateUtils.stringToLocalDateTime("2019-01-04T22:21:46+00:00"))); + assertEquals(0, dateTime.compareTo(DateUtils.parse("2019-01-04T22:21:46+00:00"))); } @Test public void atomJsonDate2Test() { LocalDateTime dateTime = new LocalDateTime(2019, 1, 4, 22, 21, 46); - assertEquals(0, dateTime.compareTo(DateUtils.stringToLocalDateTime("2019-01-04T22:21:46-0000"))); + assertEquals(0, dateTime.compareTo(DateUtils.parse("2019-01-04T22:21:46-0000"))); } @Test public void isoPatternTest() { LocalDateTime dateTime = new LocalDateTime(2020, 6, 30, 11, 39, 37, 206); - assertEquals(0, dateTime.compareTo(DateUtils.stringToLocalDateTime("2020-06-30T11:39:37.206-07:00"))); + assertEquals(0, dateTime.compareTo(DateUtils.parse("2020-06-30T11:39:37.206-07:00"))); } @Test public void edtPatternTest() { LocalDateTime dateTime = new LocalDateTime(2020, 7, 17, 16, 30, 0); - assertEquals(0, dateTime.compareTo(DateUtils.stringToLocalDateTime("Fri, 17 Jul 2020 16:30:00 EDT"))); + assertEquals(0, dateTime.compareTo(DateUtils.parse("Fri, 17 Jul 2020 16:30:00 EDT"))); } } \ No newline at end of file From 10a7b99e597804782cdc250f63c47417579d37e2 Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Thu, 1 Oct 2020 21:17:51 +0200 Subject: [PATCH 068/187] Fallback to current date time if a RSS2 item doesn't have any date --- .../localfeed/rss2/RSS2ItemsAdapterTest.kt | 25 ++++---- .../api/localfeed/rss2/RSS2ItemsAdapter.kt | 63 ++++++++++--------- .../com/readrops/api/utils/DateUtils.java | 40 ++++++++---- 3 files changed, 75 insertions(+), 53 deletions(-) diff --git a/api/src/androidTest/java/com/readrops/api/localfeed/rss2/RSS2ItemsAdapterTest.kt b/api/src/androidTest/java/com/readrops/api/localfeed/rss2/RSS2ItemsAdapterTest.kt index e7640d2a..3911c0a5 100644 --- a/api/src/androidTest/java/com/readrops/api/localfeed/rss2/RSS2ItemsAdapterTest.kt +++ b/api/src/androidTest/java/com/readrops/api/localfeed/rss2/RSS2ItemsAdapterTest.kt @@ -6,6 +6,7 @@ import androidx.test.platform.app.InstrumentationRegistry import com.readrops.api.utils.DateUtils import com.readrops.api.utils.ParseException import junit.framework.TestCase.assertEquals +import junit.framework.TestCase.assertNotNull import org.junit.Assert import org.junit.Test import org.junit.runner.RunWith @@ -37,7 +38,7 @@ class RSS2ItemsAdapterTest { @Test fun otherNamespacesTest() { val stream = context.resources.assets.open("localfeed/rss2/rss_items_other_namespaces.xml") - val item = adapter.fromXml(stream)[0] + val item = adapter.fromXml(stream).first() assertEquals(item.guid, "guid") assertEquals(item.author, "creator 1, creator 2, creator 3, creator 4") @@ -45,28 +46,30 @@ class RSS2ItemsAdapterTest { assertEquals(item.content, "content:encoded") } + @Test + fun noDateTest() { + val stream = context.resources.assets.open("localfeed/rss2/rss_items_no_date.xml") + val item = adapter.fromXml(stream).first() + + assertNotNull(item.pubDate) + } + @Test fun noTitleTest() { val stream = context.resources.assets.open("localfeed/rss2/rss_items_no_title.xml") - Assert.assertThrows("Item title is required", ParseException::class.java) { adapter.fromXml(stream) } + Assert.assertThrows(ParseException::class.java) { adapter.fromXml(stream) } } @Test fun noLinkTest() { val stream = context.resources.assets.open("localfeed/rss2/rss_items_no_link.xml") - Assert.assertThrows("Item link is required", ParseException::class.java) { adapter.fromXml(stream) } - } - - @Test - fun noDateTest() { - val stream = context.resources.assets.open("localfeed/rss2/rss_items_no_date.xml") - Assert.assertThrows("Item date is required", ParseException::class.java) { adapter.fromXml(stream) } + Assert.assertThrows(ParseException::class.java) { adapter.fromXml(stream) } } @Test fun enclosureTest() { val stream = context.resources.assets.open("localfeed/rss2/rss_items_enclosure.xml") - val item = adapter.fromXml(stream)[0] + val item = adapter.fromXml(stream).first() assertEquals(item.imageLink, "https://image1.jpg") } @@ -74,7 +77,7 @@ class RSS2ItemsAdapterTest { @Test fun mediaContentTest() { val stream = context.resources.assets.open("localfeed/rss2/rss_items_media_content.xml") - val item = adapter.fromXml(stream)[0] + val item = adapter.fromXml(stream).first() assertEquals(item.imageLink, "https://image2.jpg") } diff --git a/api/src/main/java/com/readrops/api/localfeed/rss2/RSS2ItemsAdapter.kt b/api/src/main/java/com/readrops/api/localfeed/rss2/RSS2ItemsAdapter.kt index 3bc13b51..c2c76fa7 100644 --- a/api/src/main/java/com/readrops/api/localfeed/rss2/RSS2ItemsAdapter.kt +++ b/api/src/main/java/com/readrops/api/localfeed/rss2/RSS2ItemsAdapter.kt @@ -5,20 +5,19 @@ import com.readrops.api.localfeed.XmlAdapter import com.readrops.api.localfeed.XmlAdapter.Companion.AUTHORS_MAX import com.readrops.api.utils.* import com.readrops.db.entities.Item +import org.joda.time.LocalDateTime import java.io.InputStream class RSS2ItemsAdapter : XmlAdapter> { override fun fromXml(inputStream: InputStream): List { - val konsume = inputStream.konsumeXml() + val konsumer = inputStream.konsumeXml() val items = mutableListOf() return try { - konsume.child("rss") { + konsumer.child("rss") { child("channel") { allChildrenAutoIgnore("item") { - val enclosures = arrayListOf() - val mediaContents = arrayListOf() val creators = arrayListOf() val item = Item().apply { @@ -28,67 +27,71 @@ class RSS2ItemsAdapter : XmlAdapter> { "link" -> link = nonNullText() "author" -> author = nullableText() "dc:creator" -> creators += nullableText() - "pubDate" -> pubDate = DateUtils.parse(nonNullText()) - "dc:date" -> pubDate = DateUtils.parse(nonNullText()) + "pubDate" -> pubDate = DateUtils.parse(nullableText()) + "dc:date" -> pubDate = DateUtils.parse(nullableText()) "guid" -> guid = nullableText() "description" -> description = nullableTextRecursively() "content:encoded" -> content = nullableTextRecursively() - "enclosure" -> parseEnclosure(this, enclosures) - "media:content" -> parseMediaContent(this, mediaContents) - "media:group" -> parseMediaGroup(this, mediaContents) + "enclosure" -> parseEnclosure(this, item = this@apply) + "media:content" -> parseMediaContent(this, item = this@apply) + "media:group" -> parseMediaGroup(this, item = this@apply) else -> skipContents() // for example media:description } } } - validateItem(item) - if (item.guid == null) item.guid = item.link - if (item.author == null && creators.filterNotNull().isNotEmpty()) - item.author = creators.filterNotNull().joinToString(limit = AUTHORS_MAX) - - if (enclosures.isNotEmpty()) item.imageLink = enclosures.first() - else if (mediaContents.isNotEmpty()) item.imageLink = mediaContents.first() + finalizeItem(item, creators) items += item } } } - konsume.close() + konsumer.close() items } catch (e: KonsumerException) { throw ParseException(e.message) } } - private fun parseEnclosure(konsume: Konsumer, enclosures: MutableList) { - if (konsume.attributes.getValueOpt("type") != null - && LibUtils.isMimeImage(konsume.attributes["type"])) - enclosures += konsume.attributes["url"] + private fun parseEnclosure(konsumer: Konsumer, item: Item) { + if (konsumer.attributes.getValueOpt("type") != null + && LibUtils.isMimeImage(konsumer.attributes["type"]) && item.imageLink == null) + item.imageLink = konsumer.attributes.getValueOpt("url") } - private fun parseMediaContent(konsume: Konsumer, mediaContents: MutableList) { - if (konsume.attributes.getValueOpt("medium") != null - && LibUtils.isMimeImage(konsume.attributes["medium"])) - mediaContents += konsume.attributes["url"] + private fun parseMediaContent(konsumer: Konsumer, item: Item) { + if (konsumer.attributes.getValueOpt("medium") != null + && LibUtils.isMimeImage(konsumer.attributes["medium"]) && item.imageLink == null) + item.imageLink = konsumer.attributes.getValueOpt("url") - konsume.skipContents() // ignore media content sub elements + konsumer.skipContents() // ignore media content sub elements } - private fun parseMediaGroup(konsume: Konsumer, mediaContents: MutableList) { - konsume.allChildrenAutoIgnore("content") { + private fun parseMediaGroup(konsumer: Konsumer, item: Item) { + konsumer.allChildrenAutoIgnore("content") { when (tagName) { - "media:content" -> parseMediaContent(this, mediaContents) + "media:content" -> parseMediaContent(this, item) else -> skipContents() } } } + private fun finalizeItem(item: Item, creators: List) { + item.apply { + validateItem(this) + + if (pubDate == null) pubDate = LocalDateTime.now() + if (guid == null) guid = link + if (author == null && creators.filterNotNull().isNotEmpty()) + author = creators.filterNotNull().joinToString(limit = AUTHORS_MAX) + } + } + private fun validateItem(item: Item) { when { item.title == null -> throw ParseException("Item title is required") item.link == null -> throw ParseException("Item link is required") - item.pubDate == null -> throw ParseException("Item date is required") } } diff --git a/api/src/main/java/com/readrops/api/utils/DateUtils.java b/api/src/main/java/com/readrops/api/utils/DateUtils.java index bfa3dad5..f681d2ad 100644 --- a/api/src/main/java/com/readrops/api/utils/DateUtils.java +++ b/api/src/main/java/com/readrops/api/utils/DateUtils.java @@ -1,5 +1,9 @@ package com.readrops.api.utils; +import android.util.Log; + +import androidx.annotation.Nullable; + import org.joda.time.LocalDateTime; import org.joda.time.format.DateTimeFormat; import org.joda.time.format.DateTimeFormatter; @@ -9,6 +13,8 @@ import java.util.Locale; public final class DateUtils { + private static final String TAG = DateUtils.class.getSimpleName(); + /** * Base of common RSS 2 date formats. * Examples : @@ -30,20 +36,30 @@ public final class DateUtils { */ private static final String ATOM_JSON_DATE_FORMAT = "yyyy-MM-dd'T'HH:mm:ss"; + @Nullable public static LocalDateTime parse(String value) { - DateTimeFormatter formatter = new DateTimeFormatterBuilder() - .appendOptional(DateTimeFormat.forPattern(RSS_2_BASE_PATTERN + " ").getParser()) // with timezone - .appendOptional(DateTimeFormat.forPattern(RSS_2_BASE_PATTERN).getParser()) // no timezone, important order here - .appendOptional(DateTimeFormat.forPattern(ATOM_JSON_DATE_FORMAT).getParser()) - .appendOptional(DateTimeFormat.forPattern(GMT_PATTERN).getParser()) - .appendOptional(DateTimeFormat.forPattern(OFFSET_PATTERN).getParser()) - .appendOptional(DateTimeFormat.forPattern(ISO_PATTERN).getParser()) - .appendOptional(DateTimeFormat.forPattern(EDT_PATTERN).getParser()) - .toFormatter() - .withLocale(Locale.ENGLISH) - .withOffsetParsed(); + if (value == null) { + return null; + } - return formatter.parseLocalDateTime(value); + try { + DateTimeFormatter formatter = new DateTimeFormatterBuilder() + .appendOptional(DateTimeFormat.forPattern(RSS_2_BASE_PATTERN + " ").getParser()) // with timezone + .appendOptional(DateTimeFormat.forPattern(RSS_2_BASE_PATTERN).getParser()) // no timezone, important order here + .appendOptional(DateTimeFormat.forPattern(ATOM_JSON_DATE_FORMAT).getParser()) + .appendOptional(DateTimeFormat.forPattern(GMT_PATTERN).getParser()) + .appendOptional(DateTimeFormat.forPattern(OFFSET_PATTERN).getParser()) + .appendOptional(DateTimeFormat.forPattern(ISO_PATTERN).getParser()) + .appendOptional(DateTimeFormat.forPattern(EDT_PATTERN).getParser()) + .toFormatter() + .withLocale(Locale.ENGLISH) + .withOffsetParsed(); + + return formatter.parseLocalDateTime(value); + } catch (Exception e) { + Log.d(TAG, e.getMessage()); + return null; + } } public static String formattedDateByLocal(LocalDateTime dateTime) { From 5c4cc8162843e831d12c8396ea5e71f575fc4b7d Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Fri, 2 Oct 2020 18:53:52 +0200 Subject: [PATCH 069/187] Fallback to current date time if it is missing for RSS1 and ATOM items --- .../localfeed/atom/ATOMItemsAdapterTest.kt | 23 ++++++++++-------- .../localfeed/rss1/RSS1ItemsAdapterTest.kt | 24 ++++++++++--------- .../api/localfeed/atom/ATOMItemsAdapter.kt | 18 +++++++------- .../api/localfeed/rss1/RSS1ItemsAdapter.kt | 15 ++++++------ 4 files changed, 43 insertions(+), 37 deletions(-) diff --git a/api/src/androidTest/java/com/readrops/api/localfeed/atom/ATOMItemsAdapterTest.kt b/api/src/androidTest/java/com/readrops/api/localfeed/atom/ATOMItemsAdapterTest.kt index 0c8085ea..3d8b850f 100644 --- a/api/src/androidTest/java/com/readrops/api/localfeed/atom/ATOMItemsAdapterTest.kt +++ b/api/src/androidTest/java/com/readrops/api/localfeed/atom/ATOMItemsAdapterTest.kt @@ -5,8 +5,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.platform.app.InstrumentationRegistry import com.readrops.api.utils.DateUtils import com.readrops.api.utils.ParseException -import junit.framework.TestCase.assertEquals -import junit.framework.TestCase.assertNotNull +import junit.framework.TestCase.* import org.junit.Assert import org.junit.Test import org.junit.runner.RunWith @@ -35,24 +34,28 @@ class ATOMItemsAdapterTest { assertNotNull(item.content) } + @Test + fun noDateTest() { + val stream = context.resources.assets.open("localfeed/atom/atom_items_no_date.xml") + + val item = adapter.fromXml(stream).first() + assertNotNull(item.pubDate) + } + @Test fun noTitleTest() { val stream = context.resources.assets.open("localfeed/atom/atom_items_no_title.xml") - Assert.assertThrows("Item title is required", ParseException::class.java) { adapter.fromXml(stream) } + val exception = Assert.assertThrows(ParseException::class.java) { adapter.fromXml(stream) } + assertTrue(exception.message!!.contains("Item title is required")) } @Test fun noLinkTest() { val stream = context.resources.assets.open("localfeed/atom/atom_items_no_link.xml") - Assert.assertThrows("Item link is required", ParseException::class.java) { adapter.fromXml(stream) } + val exception = Assert.assertThrows(ParseException::class.java) { adapter.fromXml(stream) } + assertTrue(exception.message!!.contains("Item link is required")) } - @Test - fun noDateTest() { - val stream = context.resources.assets.open("localfeed/atom/atom_items_no_date.xml") - - Assert.assertThrows("Item date is required", ParseException::class.java) { adapter.fromXml(stream) } - } } \ No newline at end of file diff --git a/api/src/androidTest/java/com/readrops/api/localfeed/rss1/RSS1ItemsAdapterTest.kt b/api/src/androidTest/java/com/readrops/api/localfeed/rss1/RSS1ItemsAdapterTest.kt index 4860877d..40939c27 100644 --- a/api/src/androidTest/java/com/readrops/api/localfeed/rss1/RSS1ItemsAdapterTest.kt +++ b/api/src/androidTest/java/com/readrops/api/localfeed/rss1/RSS1ItemsAdapterTest.kt @@ -5,8 +5,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.platform.app.InstrumentationRegistry import com.readrops.api.utils.DateUtils import com.readrops.api.utils.ParseException -import junit.framework.TestCase.assertEquals -import junit.framework.TestCase.assertNotNull +import junit.framework.TestCase.* import org.junit.Assert import org.junit.Test import org.junit.runner.RunWith @@ -48,24 +47,27 @@ class RSS1ItemsAdapterTest { "that-told-time-now-tells-the-time-remaining?utm_source=rss1.0mainlinkanon&utm_medium=feed") } + @Test + fun nullDateTest() { + val stream = context.resources.assets.open("localfeed/rss1/rss1_items_no_date.xml") + + val item = adapter.fromXml(stream).first() + assertNotNull(item.pubDate) + } + @Test fun nullTitleTest() { val stream = context.resources.assets.open("localfeed/rss1/rss1_items_no_title.xml") - Assert.assertThrows(ParseException::class.java) { adapter.fromXml(stream) } + val exception = Assert.assertThrows(ParseException::class.java) { adapter.fromXml(stream) } + assertTrue(exception.message!!.contains("Item title is required")) } @Test fun nullLinkTest() { val stream = context.resources.assets.open("localfeed/rss1/rss1_items_no_link.xml") - Assert.assertThrows(ParseException::class.java) { adapter.fromXml(stream) } - } - - @Test - fun nullDateTest() { - val stream = context.resources.assets.open("localfeed/rss1/rss1_items_no_date.xml") - - Assert.assertThrows(ParseException::class.java) { adapter.fromXml(stream) } + val exception = Assert.assertThrows(ParseException::class.java) { adapter.fromXml(stream) } + assertTrue(exception.message!!.contains("RSS1 link or about element is required")) } } \ No newline at end of file diff --git a/api/src/main/java/com/readrops/api/localfeed/atom/ATOMItemsAdapter.kt b/api/src/main/java/com/readrops/api/localfeed/atom/ATOMItemsAdapter.kt index d7a09e13..be61a987 100644 --- a/api/src/main/java/com/readrops/api/localfeed/atom/ATOMItemsAdapter.kt +++ b/api/src/main/java/com/readrops/api/localfeed/atom/ATOMItemsAdapter.kt @@ -7,50 +7,53 @@ import com.gitlab.mvysny.konsumexml.konsumeXml import com.readrops.api.localfeed.XmlAdapter import com.readrops.api.utils.* import com.readrops.db.entities.Item +import org.joda.time.LocalDateTime import java.io.InputStream class ATOMItemsAdapter : XmlAdapter> { override fun fromXml(inputStream: InputStream): List { - val konsume = inputStream.konsumeXml() + val konsumer = inputStream.konsumeXml() val items = arrayListOf() return try { - konsume.child("feed") { + konsumer.child("feed") { allChildrenAutoIgnore("entry") { val item = Item().apply { allChildrenAutoIgnore(names) { when (tagName) { "title" -> title = nonNullText() "id" -> guid = nullableText() - "updated" -> pubDate = DateUtils.parse(nonNullText()) + "updated" -> pubDate = DateUtils.parse(nullableText()) "link" -> parseLink(this, this@apply) "author" -> allChildrenAutoIgnore("name") { author = nullableText() } "summary" -> description = nullableTextRecursively() "content" -> content = nullableTextRecursively() + else -> skipContents() } } } validateItem(item) + if (item.pubDate == null) item.pubDate = LocalDateTime.now() if (item.guid == null) item.guid = item.link items += item } } - konsume.close() + konsumer.close() items } catch (e: Exception) { throw ParseException(e.message) } } - private fun parseLink(konsume: Konsumer, item: Item) { - konsume.apply { + private fun parseLink(konsumer: Konsumer, item: Item) { + konsumer.apply { if (attributes.getValueOpt("rel") == null || attributes["rel"] == "alternate") - item.link = attributes["href"] + item.link = attributes.getValueOpt("href") } } @@ -59,7 +62,6 @@ class ATOMItemsAdapter : XmlAdapter> { when { item.title == null -> throw ParseException("Item title is required") item.link == null -> throw ParseException("Item link is required") - item.pubDate == null -> throw ParseException("Item date id required") } } diff --git a/api/src/main/java/com/readrops/api/localfeed/rss1/RSS1ItemsAdapter.kt b/api/src/main/java/com/readrops/api/localfeed/rss1/RSS1ItemsAdapter.kt index 83eaf08a..0796fed9 100644 --- a/api/src/main/java/com/readrops/api/localfeed/rss1/RSS1ItemsAdapter.kt +++ b/api/src/main/java/com/readrops/api/localfeed/rss1/RSS1ItemsAdapter.kt @@ -7,16 +7,17 @@ import com.readrops.api.localfeed.XmlAdapter import com.readrops.api.localfeed.XmlAdapter.Companion.AUTHORS_MAX import com.readrops.api.utils.* import com.readrops.db.entities.Item +import org.joda.time.LocalDateTime import java.io.InputStream class RSS1ItemsAdapter : XmlAdapter> { override fun fromXml(inputStream: InputStream): List { - val konsume = inputStream.konsumeXml() + val konsumer = inputStream.konsumeXml() val items = arrayListOf() return try { - konsume.child("RDF") { + konsumer.child("RDF") { allChildrenAutoIgnore("item") { val authors = arrayListOf() val about = attributes.getValueOpt("about", @@ -27,7 +28,7 @@ class RSS1ItemsAdapter : XmlAdapter> { when (tagName) { "title" -> title = nonNullText() "link" -> link = nullableText() - "dc:date" -> pubDate = DateUtils.parse(nonNullText()) + "dc:date" -> pubDate = DateUtils.parse(nullableText()) "dc:creator" -> authors += nullableText() "description" -> description = nullableTextRecursively() "content:encoded" -> content = nullableTextRecursively() @@ -36,6 +37,7 @@ class RSS1ItemsAdapter : XmlAdapter> { } } + if (item.pubDate == null) item.pubDate = LocalDateTime.now() if (item.link == null) item.link = about ?: throw ParseException("RSS1 link or about element is required") item.guid = item.link @@ -49,7 +51,7 @@ class RSS1ItemsAdapter : XmlAdapter> { } } - konsume.close() + konsumer.close() items } catch (e: Exception) { throw ParseException(e.message) @@ -57,10 +59,7 @@ class RSS1ItemsAdapter : XmlAdapter> { } private fun validateItem(item: Item) { - when { - item.title == null -> throw ParseException("Item title is required") - item.pubDate == null -> throw ParseException("Item date is required") - } + if (item.title == null) throw ParseException("Item title is required") } companion object { From 6733890c16c08b516749edeb4535a1e9717202d6 Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Fri, 2 Oct 2020 19:08:12 +0200 Subject: [PATCH 070/187] Improve jsonfeed items tests and fallback to current date time if it is missing --- .../localfeed/json/json_items_no_date.json | 10 ++++++ .../localfeed/json/json_items_no_link.json | 10 ++++++ .../localfeed/json/json_items_no_title.json | 10 ++++++ .../json/json_items_required_elements.json | 22 ------------- .../localfeed/json/JSONItemsAdapterTest.kt | 33 ++++++++++--------- .../api/localfeed/json/JSONFeedAdapter.kt | 4 +-- .../api/localfeed/json/JSONItemsAdapter.kt | 5 +-- 7 files changed, 53 insertions(+), 41 deletions(-) create mode 100644 api/src/androidTest/assets/localfeed/json/json_items_no_date.json create mode 100644 api/src/androidTest/assets/localfeed/json/json_items_no_link.json create mode 100644 api/src/androidTest/assets/localfeed/json/json_items_no_title.json delete mode 100644 api/src/androidTest/assets/localfeed/json/json_items_required_elements.json diff --git a/api/src/androidTest/assets/localfeed/json/json_items_no_date.json b/api/src/androidTest/assets/localfeed/json/json_items_no_date.json new file mode 100644 index 00000000..ce9aa1fc --- /dev/null +++ b/api/src/androidTest/assets/localfeed/json/json_items_no_date.json @@ -0,0 +1,10 @@ +{ + "items": [ + { + "id": "http://flyingmeat.com/blog/archives/2017/9/acorn_and_10.13.html", + "title": "Acorn and 10.13", + "content_html": "

Happy Mac OS High Sierra release day everyone.

\n

I'm happy to say that there are no known issues with Acorn 6.0.3 or Acorn 5.6.6 when running on Mac OS 10.13 High Sierra. In fact, you might even notice that some things are actually faster and it can now open HEIF images. How awesome is that?

\n

I'm also working on some 10.13 goodies for Acorn 6 folks later this year. I can't wait to share that with you, but you'll have to wait just a little bit.

\n", + "url": "http://flyingmeat.com/blog/archives/2017/9/acorn_and_10.13.html" + } + ] +} \ No newline at end of file diff --git a/api/src/androidTest/assets/localfeed/json/json_items_no_link.json b/api/src/androidTest/assets/localfeed/json/json_items_no_link.json new file mode 100644 index 00000000..e23377e6 --- /dev/null +++ b/api/src/androidTest/assets/localfeed/json/json_items_no_link.json @@ -0,0 +1,10 @@ +{ + "items": [ + { + "id": "http://flyingmeat.com/blog/archives/2017/9/acorn_and_10.13.html", + "title": "Acorn and 10.13", + "content_html": "

Happy Mac OS High Sierra release day everyone.

\n

I'm happy to say that there are no known issues with Acorn 6.0.3 or Acorn 5.6.6 when running on Mac OS 10.13 High Sierra. In fact, you might even notice that some things are actually faster and it can now open HEIF images. How awesome is that?

\n

I'm also working on some 10.13 goodies for Acorn 6 folks later this year. I can't wait to share that with you, but you'll have to wait just a little bit.

\n", + "date_published": "2017-09-25T14:27:27-07:00" + } + ] +} \ No newline at end of file diff --git a/api/src/androidTest/assets/localfeed/json/json_items_no_title.json b/api/src/androidTest/assets/localfeed/json/json_items_no_title.json new file mode 100644 index 00000000..e63cae47 --- /dev/null +++ b/api/src/androidTest/assets/localfeed/json/json_items_no_title.json @@ -0,0 +1,10 @@ +{ + "items": [ + { + "id": "http://flyingmeat.com/blog/archives/2017/9/acorn_and_10.13.html", + "content_html": "

Happy Mac OS High Sierra release day everyone.

\n

I'm happy to say that there are no known issues with Acorn 6.0.3 or Acorn 5.6.6 when running on Mac OS 10.13 High Sierra. In fact, you might even notice that some things are actually faster and it can now open HEIF images. How awesome is that?

\n

I'm also working on some 10.13 goodies for Acorn 6 folks later this year. I can't wait to share that with you, but you'll have to wait just a little bit.

\n", + "date_published": "2017-09-25T14:27:27-07:00", + "url": "http://flyingmeat.com/blog/archives/2017/9/acorn_and_10.13.html" + } + ] +} \ No newline at end of file diff --git a/api/src/androidTest/assets/localfeed/json/json_items_required_elements.json b/api/src/androidTest/assets/localfeed/json/json_items_required_elements.json deleted file mode 100644 index f46aa9a1..00000000 --- a/api/src/androidTest/assets/localfeed/json/json_items_required_elements.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "items": [ - { - "id": "http://flyingmeat.com/blog/archives/2017/9/acorn_and_10.13.html", - "content_html": "

Happy Mac OS High Sierra release day everyone.

\n

I'm happy to say that there are no known issues with Acorn 6.0.3 or Acorn 5.6.6 when running on Mac OS 10.13 High Sierra. In fact, you might even notice that some things are actually faster and it can now open HEIF images. How awesome is that?

\n

I'm also working on some 10.13 goodies for Acorn 6 folks later this year. I can't wait to share that with you, but you'll have to wait just a little bit.

\n", - "date_published": "2017-09-25T14:27:27-07:00", - "url": "http://flyingmeat.com/blog/archives/2017/9/acorn_and_10.13.html" - }, - { - "id": "http://flyingmeat.com/blog/archives/2018/2/acorn_6.1_is_out.html", - "title": "Acorn 6.1 Is Out", - "content_html": "

Acorn 6.1 has been released.

\n

You can read a longer post about it over on Gus's blog, but the short of it is: Better, faster, smoother, stronger. And now with Metal 2 support.

\n", - "date_published": "2018-02-16T09:59:11-08:00", - }, - { - "id": "http://flyingmeat.com/blog/archives/2018/6/a_pair_of_updates.html", - "title": "A Pair of Updates", - "content_html": "

Happy summer solstice everybody! (at least for folks in the northern hemisphere, and for folks in the south… sorry. It's going to start getting brighter for you though).

\n

Today I've got a pair of minor app updates to annouce for you.

\n

First up is Acorn 6.1.3, which fixes a number of bugs including one that stemmed from trying to use QuickLook on a file that was created with Acorn 1.0. For the one or two of you that this was affecting, hurray!

\n

Next up is Retrobatch, which also includes some bug fixes, the beginnings of Voice Over support, performance improvements, and more.

\n

What's next for these apps? Work on Acorn 6.2 will begin shortly, as will Retrobatch 1.1. WWDC introduced some great new APIs that I want to take advantage of (cool new machine learning things), so that'll be a focus- as well as Dark Mode for Acorn and one other major thing I've got planned. Retrobatch will probably also get the Dark Mode treatment, but not until I've done it for Acorn first.

\n

So it's going to be a busy summer, but I'm looking forward to it.

\n", - "url": "http://flyingmeat.com/blog/archives/2018/6/a_pair_of_updates.html" - } - ] -} \ No newline at end of file diff --git a/api/src/androidTest/java/com/readrops/api/localfeed/json/JSONItemsAdapterTest.kt b/api/src/androidTest/java/com/readrops/api/localfeed/json/JSONItemsAdapterTest.kt index 474ecbff..3d62a836 100644 --- a/api/src/androidTest/java/com/readrops/api/localfeed/json/JSONItemsAdapterTest.kt +++ b/api/src/androidTest/java/com/readrops/api/localfeed/json/JSONItemsAdapterTest.kt @@ -8,8 +8,7 @@ import com.readrops.api.utils.ParseException import com.readrops.db.entities.Item import com.squareup.moshi.Moshi import com.squareup.moshi.Types -import junit.framework.TestCase.assertEquals -import junit.framework.TestCase.assertNotNull +import junit.framework.TestCase.* import okio.Buffer import org.junit.Assert import org.junit.Test @@ -31,7 +30,7 @@ class JSONItemsAdapterTest { val stream = context.resources.assets.open("localfeed/json/json_feed.json") val items = adapter.fromJson(Buffer().readFrom(stream))!! - val item = items[0] + val item = items.first() assertEquals(items.size, 10) assertEquals(item.guid, "http://flyingmeat.com/blog/archives/2017/9/acorn_and_10.13.html") @@ -46,7 +45,7 @@ class JSONItemsAdapterTest { fun otherCasesTest() { val stream = context.resources.assets.open("localfeed/json/json_items_other_cases.json") - val item = adapter.fromJson(Buffer().readFrom(stream))!![0] + val item = adapter.fromJson(Buffer().readFrom(stream))!!.first() assertEquals(item.description, "This is a summary") assertEquals(item.content, "content_html") @@ -55,23 +54,27 @@ class JSONItemsAdapterTest { } @Test - fun nullTitleTest() { - val stream = context.resources.assets.open("localfeed/json/json_items_required_elements.json") + fun nullDateTest() { + val stream = context.resources.assets.open("localfeed/json/json_items_no_date.json") - Assert.assertThrows("Item title is required", ParseException::class.java) { adapter.fromJson(Buffer().readFrom(stream))!![0] } + val item = adapter.fromJson(Buffer().readFrom(stream))!!.first() + assertNotNull(item.pubDate) + } + + @Test + fun nullTitleTest() { + val stream = context.resources.assets.open("localfeed/json/json_items_no_title.json") + + val exception = Assert.assertThrows(ParseException::class.java) { adapter.fromJson(Buffer().readFrom(stream)) } + assertTrue(exception.message!!.contains("Item title is required")) } @Test fun nullLinkTest() { - val stream = context.resources.assets.open("localfeed/json/json_items_required_elements.json") + val stream = context.resources.assets.open("localfeed/json/json_items_no_link.json") - Assert.assertThrows("Item link is required", ParseException::class.java) { adapter.fromJson(Buffer().readFrom(stream))!![1] } + val exception = Assert.assertThrows(ParseException::class.java) { adapter.fromJson(Buffer().readFrom(stream)) } + assertTrue(exception.message!!.contains("Item link is required")) } - @Test - fun nullDateTest() { - val stream = context.resources.assets.open("localfeed/json/json_items_required_elements.json") - - Assert.assertThrows("Item date is required", ParseException::class.java) { adapter.fromJson(Buffer().readFrom(stream))!![2] } - } } \ No newline at end of file diff --git a/api/src/main/java/com/readrops/api/localfeed/json/JSONFeedAdapter.kt b/api/src/main/java/com/readrops/api/localfeed/json/JSONFeedAdapter.kt index 772caeec..81ff0828 100644 --- a/api/src/main/java/com/readrops/api/localfeed/json/JSONFeedAdapter.kt +++ b/api/src/main/java/com/readrops/api/localfeed/json/JSONFeedAdapter.kt @@ -15,7 +15,7 @@ class JSONFeedAdapter { @FromJson fun fromJson(reader: JsonReader): Feed { - try { + return try { val feed = Feed() reader.beginObject() @@ -32,7 +32,7 @@ class JSONFeedAdapter { } reader.endObject() - return feed + feed } catch (e: Exception) { throw ParseException(e.message) } diff --git a/api/src/main/java/com/readrops/api/localfeed/json/JSONItemsAdapter.kt b/api/src/main/java/com/readrops/api/localfeed/json/JSONItemsAdapter.kt index 6dcdc239..e209ff3f 100644 --- a/api/src/main/java/com/readrops/api/localfeed/json/JSONItemsAdapter.kt +++ b/api/src/main/java/com/readrops/api/localfeed/json/JSONItemsAdapter.kt @@ -9,6 +9,7 @@ import com.readrops.db.entities.Item import com.squareup.moshi.JsonAdapter import com.squareup.moshi.JsonReader import com.squareup.moshi.JsonWriter +import org.joda.time.LocalDateTime class JSONItemsAdapter : JsonAdapter>() { @@ -54,7 +55,7 @@ class JSONItemsAdapter : JsonAdapter>() { 4 -> contentText = reader.nextNullableString() 5 -> description = reader.nextNullableString() 6 -> imageLink = reader.nextNullableString() - 7 -> pubDate = DateUtils.parse(reader.nextNonEmptyString()) + 7 -> pubDate = DateUtils.parse(reader.nextNullableString()) 8 -> author = parseAuthor(reader) // jsonfeed 1.0 9 -> author = parseAuthors(reader) // jsonfeed 1.1 else -> reader.skipValue() @@ -64,6 +65,7 @@ class JSONItemsAdapter : JsonAdapter>() { validateItem(item) item.content = if (contentHtml != null) contentHtml else contentText + if (item.pubDate == null) item.pubDate = LocalDateTime.now() reader.endObject() items += item @@ -105,7 +107,6 @@ class JSONItemsAdapter : JsonAdapter>() { when { item.title == null -> throw ParseException("Item title is required") item.link == null -> throw ParseException("Item link is required") - item.pubDate == null -> throw ParseException("Item date id required") } } From ca3efd45092862f829a0f5656d91c902f1310a70 Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Fri, 2 Oct 2020 19:46:40 +0200 Subject: [PATCH 071/187] Add support for media:content element type attribute --- .../rss2/rss_items_media_content.xml | 19 ++++++++++++++++++- .../localfeed/rss2/RSS2ItemsAdapterTest.kt | 5 +++-- .../api/localfeed/rss2/RSS2ItemsAdapter.kt | 11 +++++++++-- 3 files changed, 30 insertions(+), 5 deletions(-) diff --git a/api/src/androidTest/assets/localfeed/rss2/rss_items_media_content.xml b/api/src/androidTest/assets/localfeed/rss2/rss_items_media_content.xml index 74355f7b..fcefa3c4 100644 --- a/api/src/androidTest/assets/localfeed/rss2/rss_items_media_content.xml +++ b/api/src/androidTest/assets/localfeed/rss2/rss_items_media_content.xml @@ -18,7 +18,24 @@ - image2 title + image1 title +
+ + title + link + + 2020-08-05T14:03:48Z + + + + + + + guid + + + + image2 title
\ No newline at end of file diff --git a/api/src/androidTest/java/com/readrops/api/localfeed/rss2/RSS2ItemsAdapterTest.kt b/api/src/androidTest/java/com/readrops/api/localfeed/rss2/RSS2ItemsAdapterTest.kt index 3911c0a5..d3844363 100644 --- a/api/src/androidTest/java/com/readrops/api/localfeed/rss2/RSS2ItemsAdapterTest.kt +++ b/api/src/androidTest/java/com/readrops/api/localfeed/rss2/RSS2ItemsAdapterTest.kt @@ -77,9 +77,10 @@ class RSS2ItemsAdapterTest { @Test fun mediaContentTest() { val stream = context.resources.assets.open("localfeed/rss2/rss_items_media_content.xml") - val item = adapter.fromXml(stream).first() + val items = adapter.fromXml(stream) - assertEquals(item.imageLink, "https://image2.jpg") + assertEquals(items.first().imageLink, "https://image1.jpg") + assertEquals(items[1].imageLink, "https://image2.jpg") } @Test diff --git a/api/src/main/java/com/readrops/api/localfeed/rss2/RSS2ItemsAdapter.kt b/api/src/main/java/com/readrops/api/localfeed/rss2/RSS2ItemsAdapter.kt index c2c76fa7..ef2dfcc2 100644 --- a/api/src/main/java/com/readrops/api/localfeed/rss2/RSS2ItemsAdapter.kt +++ b/api/src/main/java/com/readrops/api/localfeed/rss2/RSS2ItemsAdapter.kt @@ -60,9 +60,16 @@ class RSS2ItemsAdapter : XmlAdapter> { item.imageLink = konsumer.attributes.getValueOpt("url") } + private fun isMediumImage(konsumer: Konsumer) = with(konsumer) { + attributes.getValueOpt("medium") != null && LibUtils.isMimeImage(attributes["medium"]) + } + + private fun isTypeImage(konsumer: Konsumer) = with(konsumer) { + attributes.getValueOpt("type") != null && LibUtils.isMimeImage(attributes["type"]) + } + private fun parseMediaContent(konsumer: Konsumer, item: Item) { - if (konsumer.attributes.getValueOpt("medium") != null - && LibUtils.isMimeImage(konsumer.attributes["medium"]) && item.imageLink == null) + if ((isMediumImage(konsumer) || isTypeImage(konsumer)) && item.imageLink == null) item.imageLink = konsumer.attributes.getValueOpt("url") konsumer.skipContents() // ignore media content sub elements From 101753a1a73fe547cdb126fd8cc9b2f3c194798a Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Sat, 3 Oct 2020 16:29:15 +0200 Subject: [PATCH 072/187] Improve some RSS2 items tests --- .../api/localfeed/rss2/RSS2ItemsAdapterTest.kt | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/api/src/androidTest/java/com/readrops/api/localfeed/rss2/RSS2ItemsAdapterTest.kt b/api/src/androidTest/java/com/readrops/api/localfeed/rss2/RSS2ItemsAdapterTest.kt index d3844363..1d3aa690 100644 --- a/api/src/androidTest/java/com/readrops/api/localfeed/rss2/RSS2ItemsAdapterTest.kt +++ b/api/src/androidTest/java/com/readrops/api/localfeed/rss2/RSS2ItemsAdapterTest.kt @@ -5,8 +5,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.platform.app.InstrumentationRegistry import com.readrops.api.utils.DateUtils import com.readrops.api.utils.ParseException -import junit.framework.TestCase.assertEquals -import junit.framework.TestCase.assertNotNull +import junit.framework.TestCase.* import org.junit.Assert import org.junit.Test import org.junit.runner.RunWith @@ -23,10 +22,9 @@ class RSS2ItemsAdapterTest { val stream = context.resources.assets.open("localfeed/rss_feed.xml") val items = adapter.fromXml(stream) + val item = items.first() + assertEquals(items.size, 7) - - val item = items[0] - assertEquals(item.title, "Africa declared free of wild polio") assertEquals(item.link, "https://www.bbc.com/news/world-africa-53887947") assertEquals(item.pubDate, DateUtils.parse("Tue, 25 Aug 2020 17:15:49 +0000")) @@ -57,13 +55,17 @@ class RSS2ItemsAdapterTest { @Test fun noTitleTest() { val stream = context.resources.assets.open("localfeed/rss2/rss_items_no_title.xml") - Assert.assertThrows(ParseException::class.java) { adapter.fromXml(stream) } + + val exception = Assert.assertThrows(ParseException::class.java) { adapter.fromXml(stream) } + assertTrue(exception.message!!.contains("Item title is required")) } @Test fun noLinkTest() { val stream = context.resources.assets.open("localfeed/rss2/rss_items_no_link.xml") - Assert.assertThrows(ParseException::class.java) { adapter.fromXml(stream) } + + val exception = Assert.assertThrows(ParseException::class.java) { adapter.fromXml(stream) } + assertTrue(exception.message!!.contains("Item link is required")) } @Test From 63497bd0493fc3db1763e81786b1d2dd240894d9 Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Sun, 4 Oct 2020 22:17:52 +0200 Subject: [PATCH 073/187] Always parse feed and items in the same time to detect malformed feeds --- .../api/localfeed/LocalRSSDataSourceTest.kt | 16 ++++++++-------- .../readrops/api/localfeed/LocalRSSDataSource.kt | 7 +++---- .../app/repositories/LocalFeedRepository.java | 4 ++-- 3 files changed, 13 insertions(+), 14 deletions(-) diff --git a/api/src/androidTest/java/com/readrops/api/localfeed/LocalRSSDataSourceTest.kt b/api/src/androidTest/java/com/readrops/api/localfeed/LocalRSSDataSourceTest.kt index 880471b5..9e90506d 100644 --- a/api/src/androidTest/java/com/readrops/api/localfeed/LocalRSSDataSourceTest.kt +++ b/api/src/androidTest/java/com/readrops/api/localfeed/LocalRSSDataSourceTest.kt @@ -51,7 +51,7 @@ class LocalRSSDataSourceTest { .addHeader(LibUtils.LAST_MODIFIED_HEADER, "Last-Modified") .setBody(Buffer().readFrom(stream))) - val pair = localRSSDataSource.queryRSSResource(url.toString(), null, true) + val pair = localRSSDataSource.queryRSSResource(url.toString(), null) val feed = pair?.first!! assertEquals(feed.name, "Hacker News") @@ -74,7 +74,7 @@ class LocalRSSDataSourceTest { .setBody(Buffer().readFrom(stream))) val headers = Headers.headersOf(LibUtils.ETAG_HEADER, "ETag", LibUtils.LAST_MODIFIED_HEADER, "Last-Modified") - localRSSDataSource.queryRSSResource(url.toString(), headers, false) + localRSSDataSource.queryRSSResource(url.toString(), headers) val request = mockServer.takeRequest() @@ -90,7 +90,7 @@ class LocalRSSDataSourceTest { .addHeader(LibUtils.CONTENT_TYPE_HEADER, "application/feed+json") .setBody(Buffer().readFrom(stream))) - val pair = localRSSDataSource.queryRSSResource(url.toString(), null, true)!! + val pair = localRSSDataSource.queryRSSResource(url.toString(), null)!! assertEquals(pair.first.name, "News from Flying Meat") assertEquals(pair.second.size, 10) @@ -100,7 +100,7 @@ class LocalRSSDataSourceTest { fun response304Test() { mockServer.enqueue(MockResponse().setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED)) - val pair = localRSSDataSource.queryRSSResource(url.toString(), null, false) + val pair = localRSSDataSource.queryRSSResource(url.toString(), null) assertNull(pair) } @@ -109,14 +109,14 @@ class LocalRSSDataSourceTest { fun response404Test() { mockServer.enqueue(MockResponse().setResponseCode(HttpURLConnection.HTTP_NOT_FOUND)) - localRSSDataSource.queryRSSResource(url.toString(), null, false) + localRSSDataSource.queryRSSResource(url.toString(), null) } @Test(expected = ParseException::class) fun noContentTypeTest() { mockServer.enqueue(MockResponse().setResponseCode(HttpURLConnection.HTTP_OK)) - localRSSDataSource.queryRSSResource(url.toString(), null, false) + localRSSDataSource.queryRSSResource(url.toString(), null) } @Test(expected = ParseException::class) @@ -124,7 +124,7 @@ class LocalRSSDataSourceTest { mockServer.enqueue(MockResponse().setResponseCode(HttpURLConnection.HTTP_OK) .addHeader("Content-Type", "")) - localRSSDataSource.queryRSSResource(url.toString(), null, false) + localRSSDataSource.queryRSSResource(url.toString(), null) } @Test(expected = UnknownFormatException::class) @@ -133,7 +133,7 @@ class LocalRSSDataSourceTest { .addHeader("Content-Type", "application/xml") .setBody(" ")) - localRSSDataSource.queryRSSResource(url.toString(), null, false) + localRSSDataSource.queryRSSResource(url.toString(), null) } @Test diff --git a/api/src/main/java/com/readrops/api/localfeed/LocalRSSDataSource.kt b/api/src/main/java/com/readrops/api/localfeed/LocalRSSDataSource.kt index 7104b40e..2a6b5af7 100644 --- a/api/src/main/java/com/readrops/api/localfeed/LocalRSSDataSource.kt +++ b/api/src/main/java/com/readrops/api/localfeed/LocalRSSDataSource.kt @@ -27,12 +27,11 @@ class LocalRSSDataSource(private val httpClient: OkHttpClient) { * Query RSS url * @param url url to query * @param headers request headers - * @param withItems parse items with their feed - * @return a Feed object with its items if specified by [withItems] + * @return a Feed object with its items */ @Throws(ParseException::class, UnknownFormatException::class, NetworkErrorException::class, IOException::class) @WorkerThread - fun queryRSSResource(url: String, headers: Headers?, withItems: Boolean): Pair>? { + fun queryRSSResource(url: String, headers: Headers?): Pair>? { val response = queryUrl(url, headers) return when { @@ -54,7 +53,7 @@ class LocalRSSDataSource(private val httpClient: OkHttpClient) { if (type == LocalRSSHelper.RSSType.UNKNOWN) throw UnknownFormatException("Unable to guess $url RSS type") val feed = parseFeed(ByteArrayInputStream(bodyArray), type, response) - val items = if (withItems) parseItems(ByteArrayInputStream(bodyArray), type) else listOf() + val items = parseItems(ByteArrayInputStream(bodyArray), type) response.body?.close() Pair(feed, items) diff --git a/app/src/main/java/com/readrops/app/repositories/LocalFeedRepository.java b/app/src/main/java/com/readrops/app/repositories/LocalFeedRepository.java index dc21cb85..2167dd40 100644 --- a/app/src/main/java/com/readrops/app/repositories/LocalFeedRepository.java +++ b/app/src/main/java/com/readrops/app/repositories/LocalFeedRepository.java @@ -81,7 +81,7 @@ public class LocalFeedRepository extends ARepository { headers.add(LibUtils.IF_MODIFIED_HEADER, feed.getLastModified()); } - Pair> pair = dataSource.queryRSSResource(feed.getUrl(), headers.build(), true); + Pair> pair = dataSource.queryRSSResource(feed.getUrl(), headers.build()); if (pair != null) { insertNewItems(feed, pair.getSecond()); @@ -105,7 +105,7 @@ public class LocalFeedRepository extends ARepository { try { Pair> pair = dataSource.queryRSSResource(parsingResult.getUrl(), - null, false); + null); Feed feed = insertFeed(pair.getFirst(), parsingResult); if (feed != null) { From 8304e7709fc990bc428526329cd5de32727dc210 Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Sun, 4 Oct 2020 22:53:00 +0200 Subject: [PATCH 074/187] Add tests for method LocalRSSDataSource.handleSpecialCases() --- .../atom/atom_feed_no_url_siteurl.xml | 7 +++++ .../rss1/rss1_feed_no_url_siteurl.xml | 31 +++++++++++++++++++ .../api/localfeed/LocalRSSDataSourceTest.kt | 28 +++++++++++++++++ .../api/localfeed/LocalRSSDataSource.kt | 7 ++++- .../api/localfeed/rss1/RSS1FeedAdapter.kt | 2 +- 5 files changed, 73 insertions(+), 2 deletions(-) create mode 100644 api/src/androidTest/assets/localfeed/atom/atom_feed_no_url_siteurl.xml create mode 100644 api/src/androidTest/assets/localfeed/rss1/rss1_feed_no_url_siteurl.xml diff --git a/api/src/androidTest/assets/localfeed/atom/atom_feed_no_url_siteurl.xml b/api/src/androidTest/assets/localfeed/atom/atom_feed_no_url_siteurl.xml new file mode 100644 index 00000000..6b2d1530 --- /dev/null +++ b/api/src/androidTest/assets/localfeed/atom/atom_feed_no_url_siteurl.xml @@ -0,0 +1,7 @@ + + + tag:github.com,2008:/readrops/Readrops/commits/develop + Recent Commits to Readrops:develop + 2020-09-06T21:09:59Z + Here is a subtitle + \ No newline at end of file diff --git a/api/src/androidTest/assets/localfeed/rss1/rss1_feed_no_url_siteurl.xml b/api/src/androidTest/assets/localfeed/rss1/rss1_feed_no_url_siteurl.xml new file mode 100644 index 00000000..dd49bb0c --- /dev/null +++ b/api/src/androidTest/assets/localfeed/rss1/rss1_feed_no_url_siteurl.xml @@ -0,0 +1,31 @@ + + + + Slashdot + News for nerds, stuff that matters + en-us + Copyright 1997-2016, SlashdotMedia. All Rights Reserved. + 2020-09-23T16:20:20+00:00 + Dice + help@slashdot.org + Technology + 1970-01-01T00:00+00:00 + 1 + hourly + + + + + + + + Slashdot + https://a.fsdn.com/sd/topics/topicslashdot.gif + https://slashdot.org/ + + \ No newline at end of file diff --git a/api/src/androidTest/java/com/readrops/api/localfeed/LocalRSSDataSourceTest.kt b/api/src/androidTest/java/com/readrops/api/localfeed/LocalRSSDataSourceTest.kt index 9e90506d..f51ce144 100644 --- a/api/src/androidTest/java/com/readrops/api/localfeed/LocalRSSDataSourceTest.kt +++ b/api/src/androidTest/java/com/readrops/api/localfeed/LocalRSSDataSourceTest.kt @@ -96,6 +96,34 @@ class LocalRSSDataSourceTest { assertEquals(pair.second.size, 10) } + @Test + fun specialCasesAtomTest() { + val stream = context.resources.assets.open("localfeed/atom/atom_feed_no_url_siteurl.xml") + + mockServer.enqueue(MockResponse().setResponseCode(HttpURLConnection.HTTP_OK) + .addHeader(LibUtils.CONTENT_TYPE_HEADER, "application/atom+xml") + .setBody(Buffer().readFrom(stream))) + + val pair = localRSSDataSource.queryRSSResource(url.toString(), null)!! + + assertEquals(pair.first.url, "http://localhost:8080/rss") + assertEquals(pair.first.siteUrl, "http://localhost") + } + + @Test + fun specialCasesRSS1Test() { + val stream = context.resources.assets.open("localfeed/rss1/rss1_feed_no_url_siteurl.xml") + + mockServer.enqueue(MockResponse().setResponseCode(HttpURLConnection.HTTP_OK) + .addHeader(LibUtils.CONTENT_TYPE_HEADER, "application/rdf+xml") + .setBody(Buffer().readFrom(stream))) + + val pair = localRSSDataSource.queryRSSResource(url.toString(), null)!! + + assertEquals(pair.first.url, "http://localhost:8080/rss") + assertEquals(pair.first.siteUrl, "http://localhost") + } + @Test fun response304Test() { mockServer.enqueue(MockResponse().setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED)) diff --git a/api/src/main/java/com/readrops/api/localfeed/LocalRSSDataSource.kt b/api/src/main/java/com/readrops/api/localfeed/LocalRSSDataSource.kt index 2a6b5af7..594857bb 100644 --- a/api/src/main/java/com/readrops/api/localfeed/LocalRSSDataSource.kt +++ b/api/src/main/java/com/readrops/api/localfeed/LocalRSSDataSource.kt @@ -63,6 +63,11 @@ class LocalRSSDataSource(private val httpClient: OkHttpClient) { } } + /** + * Checks if the provided url is a RSS resource + * @param url url to check + * @return true if [url] is a RSS resource, false otherwise + */ @WorkerThread fun isUrlRSSResource(url: String): Boolean { val response = queryUrl(url, null) @@ -131,7 +136,7 @@ class LocalRSSDataSource(private val httpClient: OkHttpClient) { // if an atom:link element was parsed, we still replace its value as it is unreliable, // otherwise we just add the rss url url = response.request.url.toString() - } else if (type == LocalRSSHelper.RSSType.ATOM) { + } else if (type == LocalRSSHelper.RSSType.ATOM || type == LocalRSSHelper.RSSType.RSS_1) { if (url == null) url = response.request.url.toString() if (siteUrl == null) siteUrl = response.request.url.scheme + "://" + response.request.url.host } diff --git a/api/src/main/java/com/readrops/api/localfeed/rss1/RSS1FeedAdapter.kt b/api/src/main/java/com/readrops/api/localfeed/rss1/RSS1FeedAdapter.kt index 697258c1..0b7e9cb0 100644 --- a/api/src/main/java/com/readrops/api/localfeed/rss1/RSS1FeedAdapter.kt +++ b/api/src/main/java/com/readrops/api/localfeed/rss1/RSS1FeedAdapter.kt @@ -19,7 +19,7 @@ class RSS1FeedAdapter : XmlAdapter { return try { konsume.child("RDF") { allChildrenAutoIgnore("channel") { - feed.url = attributes.getValue("about", + feed.url = attributes.getValueOpt("about", namespace = "http://www.w3.org/1999/02/22-rdf-syntax-ns#") allChildrenAutoIgnore(names) { From cc17c8884b1bc689daea44033050b68bdbeb7015 Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Sun, 4 Oct 2020 23:13:37 +0200 Subject: [PATCH 075/187] Add tests for JsonReaderExtensions --- .../api/utils/JsonReaderExtensionsTest.kt | 85 +++++++++++++++++++ 1 file changed, 85 insertions(+) create mode 100644 api/src/test/java/com/readrops/api/utils/JsonReaderExtensionsTest.kt diff --git a/api/src/test/java/com/readrops/api/utils/JsonReaderExtensionsTest.kt b/api/src/test/java/com/readrops/api/utils/JsonReaderExtensionsTest.kt new file mode 100644 index 00000000..0bba568b --- /dev/null +++ b/api/src/test/java/com/readrops/api/utils/JsonReaderExtensionsTest.kt @@ -0,0 +1,85 @@ +package com.readrops.api.utils + +import com.squareup.moshi.JsonReader +import junit.framework.TestCase.assertEquals +import junit.framework.TestCase.assertNull +import okio.Buffer +import org.junit.Test + +class JsonReaderExtensionsTest { + + @Test + fun nextNullableStringNullCaseTest() { + val reader = JsonReader.of(Buffer().readFrom(""" + { + "field": null + } + """.trimIndent().byteInputStream())) + + reader.beginObject() + reader.nextName() + + assertNull(reader.nextNullableString()) + reader.endObject() + } + + @Test + fun nextNullableStringEmptyCaseTest() { + val reader = JsonReader.of(Buffer().readFrom(""" + { + "field": "" + } + """.trimIndent().byteInputStream())) + + reader.beginObject() + reader.nextName() + + assertNull(reader.nextNullableString()) + reader.endObject() + } + + @Test + fun nextNullableValueNormalCaseTest() { + val reader = JsonReader.of(Buffer().readFrom(""" + { + "field": "value" + } + """.trimIndent().byteInputStream())) + + reader.beginObject() + reader.nextName() + + assertEquals(reader.nextNullableString(), "value") + reader.endObject() + } + + @Test + fun nextNonEmptyStringTest() { + val reader = JsonReader.of(Buffer().readFrom(""" + { + "field": "value" + } + """.trimIndent().byteInputStream())) + + reader.beginObject() + reader.nextName() + + assertEquals(reader.nextNullableString(), "value") + reader.endObject() + } + + @Test(expected = ParseException::class) + fun nextNonEmptyStringEmptyCaseTest() { + val reader = JsonReader.of(Buffer().readFrom(""" + { + "field": "" + } + """.trimIndent().byteInputStream())) + + reader.beginObject() + reader.nextName() + + reader.nextNonEmptyString() + } + +} \ No newline at end of file From eb5831104067eccdcb130271854975b6b56ffccc Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Mon, 5 Oct 2020 22:11:48 +0200 Subject: [PATCH 076/187] Log LocalFeedRepository.addFeeds() exceptions --- .../com/readrops/app/repositories/LocalFeedRepository.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/src/main/java/com/readrops/app/repositories/LocalFeedRepository.java b/app/src/main/java/com/readrops/app/repositories/LocalFeedRepository.java index 2167dd40..f5159027 100644 --- a/app/src/main/java/com/readrops/app/repositories/LocalFeedRepository.java +++ b/app/src/main/java/com/readrops/app/repositories/LocalFeedRepository.java @@ -112,12 +112,16 @@ public class LocalFeedRepository extends ARepository { insertionResult.setFeed(feed); } } catch (ParseException e) { + Log.d(TAG, "addFeeds: " + e.getMessage()); insertionResult.setInsertionError(FeedInsertionResult.FeedInsertionError.PARSE_ERROR); } catch (UnknownFormatException e) { + Log.d(TAG, "addFeeds: " + e.getMessage()); insertionResult.setInsertionError(FeedInsertionResult.FeedInsertionError.FORMAT_ERROR); } catch (NetworkErrorException | IOException e) { + Log.d(TAG, "addFeeds: " + e.getMessage()); insertionResult.setInsertionError(FeedInsertionResult.FeedInsertionError.NETWORK_ERROR); } catch (Exception e) { + Log.d(TAG, "addFeeds: " + e.getMessage()); insertionResult.setInsertionError(FeedInsertionResult.FeedInsertionError.UNKNOWN_ERROR); } finally { insertionResult.setParsingResult(parsingResult); From ec53cbd6833977d5f6ffaf6982fe20f105ccedb2 Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Mon, 5 Oct 2020 22:18:02 +0200 Subject: [PATCH 077/187] Fix content-type not being parsed when getting url rss type --- .../com/readrops/api/localfeed/LocalRSSDataSourceTest.kt | 4 ++-- .../java/com/readrops/api/localfeed/LocalRSSDataSource.kt | 5 ++++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/api/src/androidTest/java/com/readrops/api/localfeed/LocalRSSDataSourceTest.kt b/api/src/androidTest/java/com/readrops/api/localfeed/LocalRSSDataSourceTest.kt index f51ce144..017fd9fa 100644 --- a/api/src/androidTest/java/com/readrops/api/localfeed/LocalRSSDataSourceTest.kt +++ b/api/src/androidTest/java/com/readrops/api/localfeed/LocalRSSDataSourceTest.kt @@ -167,7 +167,7 @@ class LocalRSSDataSourceTest { @Test fun isUrlResourceSuccessfulTest() { mockServer.enqueue(MockResponse().setResponseCode(HttpURLConnection.HTTP_OK) - .addHeader("Content-Type", "application/atom+xml")) + .addHeader("Content-Type", "application/atom+xml; charset=UTF-8")) assertTrue(localRSSDataSource.isUrlRSSResource(url.toString())) } @@ -182,7 +182,7 @@ class LocalRSSDataSourceTest { @Test fun isUrlRSSResourceBadContentTypeTest() { mockServer.enqueue(MockResponse().setResponseCode(HttpURLConnection.HTTP_OK) - .addHeader("Content-Type", "application/xml") + .addHeader("Content-Type", "application/xml; charset=UTF-8") .setBody(" ")) assertFalse(localRSSDataSource.isUrlRSSResource(url.toString())) diff --git a/api/src/main/java/com/readrops/api/localfeed/LocalRSSDataSource.kt b/api/src/main/java/com/readrops/api/localfeed/LocalRSSDataSource.kt index 594857bb..20a38da9 100644 --- a/api/src/main/java/com/readrops/api/localfeed/LocalRSSDataSource.kt +++ b/api/src/main/java/com/readrops/api/localfeed/LocalRSSDataSource.kt @@ -73,7 +73,10 @@ class LocalRSSDataSource(private val httpClient: OkHttpClient) { val response = queryUrl(url, null) return if (response.isSuccessful) { - val contentType = response.header(LibUtils.CONTENT_TYPE_HEADER) + val header = response.header(LibUtils.CONTENT_TYPE_HEADER) + ?: return false + + val contentType = LibUtils.parseContentType(header) ?: return false var type = LocalRSSHelper.getRSSType(contentType) From 6874ef2452adc1a21ad52a8711089adb11bec9ae Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Mon, 5 Oct 2020 22:24:47 +0200 Subject: [PATCH 078/187] If a response doesn't have a content-type header, throw UnknownFormatException instead of ParseException --- .../java/com/readrops/api/localfeed/LocalRSSDataSourceTest.kt | 2 +- .../main/java/com/readrops/api/localfeed/LocalRSSDataSource.kt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/api/src/androidTest/java/com/readrops/api/localfeed/LocalRSSDataSourceTest.kt b/api/src/androidTest/java/com/readrops/api/localfeed/LocalRSSDataSourceTest.kt index 017fd9fa..a03f6032 100644 --- a/api/src/androidTest/java/com/readrops/api/localfeed/LocalRSSDataSourceTest.kt +++ b/api/src/androidTest/java/com/readrops/api/localfeed/LocalRSSDataSourceTest.kt @@ -140,7 +140,7 @@ class LocalRSSDataSourceTest { localRSSDataSource.queryRSSResource(url.toString(), null) } - @Test(expected = ParseException::class) + @Test(expected = UnknownFormatException::class) fun noContentTypeTest() { mockServer.enqueue(MockResponse().setResponseCode(HttpURLConnection.HTTP_OK)) diff --git a/api/src/main/java/com/readrops/api/localfeed/LocalRSSDataSource.kt b/api/src/main/java/com/readrops/api/localfeed/LocalRSSDataSource.kt index 20a38da9..6069b2b0 100644 --- a/api/src/main/java/com/readrops/api/localfeed/LocalRSSDataSource.kt +++ b/api/src/main/java/com/readrops/api/localfeed/LocalRSSDataSource.kt @@ -37,7 +37,7 @@ class LocalRSSDataSource(private val httpClient: OkHttpClient) { return when { response.isSuccessful -> { val header = response.header(LibUtils.CONTENT_TYPE_HEADER) - ?: throw ParseException("Unable to get $url content-type") + ?: throw UnknownFormatException("Unable to get $url content-type") val contentType = LibUtils.parseContentType(header) ?: throw ParseException("Unable to parse $url content-type") From 52fda0f8d8be57b4515b3f78b34ed9bbcc047886 Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Tue, 6 Oct 2020 21:43:09 +0200 Subject: [PATCH 079/187] Remove local RSS header image manual parsing --- .../app/repositories/LocalFeedRepository.java | 25 ++-------- .../com/readrops/app/utils/HtmlParser.java | 49 ------------------- 2 files changed, 4 insertions(+), 70 deletions(-) diff --git a/app/src/main/java/com/readrops/app/repositories/LocalFeedRepository.java b/app/src/main/java/com/readrops/app/repositories/LocalFeedRepository.java index f5159027..f6b05c3b 100644 --- a/app/src/main/java/com/readrops/app/repositories/LocalFeedRepository.java +++ b/app/src/main/java/com/readrops/app/repositories/LocalFeedRepository.java @@ -14,7 +14,6 @@ import com.readrops.api.utils.LibUtils; import com.readrops.api.utils.ParseException; import com.readrops.api.utils.UnknownFormatException; import com.readrops.app.utils.FeedInsertionResult; -import com.readrops.app.utils.HtmlParser; import com.readrops.app.utils.ParsingResult; import com.readrops.app.utils.SharedPreferencesManager; import com.readrops.app.utils.Utils; @@ -174,29 +173,13 @@ public class LocalFeedRepository extends ARepository { if (!database.itemDao().itemExists(dbItem.getGuid(), feed.getAccountId())) { if (dbItem.getDescription() != null) { dbItem.setCleanDescription(Jsoup.parse(dbItem.getDescription()).text()); - - if (dbItem.getImageLink() == null) { - String imageUrl = HtmlParser.getDescImageLink(dbItem.getDescription(), feed.getSiteUrl()); - - if (imageUrl != null) - dbItem.setImageLink(imageUrl); - } } - // we check a second time because imageLink could have been set earlier with media:content tag value - if (dbItem.getImageLink() != null) { - if (dbItem.getContent() != null) { - // removing cover image in content if found in description - dbItem.setContent(HtmlParser.deleteCoverImage(dbItem.getContent())); - - } else if (dbItem.getDescription() != null) - dbItem.setDescription(HtmlParser.deleteCoverImage(dbItem.getDescription())); - } - - if (dbItem.getContent() != null) - dbItem.setReadTime(Utils.readTimeFromString(Jsoup.parse(dbItem.getContent()).text())); - else if (dbItem.getDescription() != null) + if (dbItem.getContent() != null) { + dbItem.setReadTime(Utils.readTimeFromString(dbItem.getContent())); + } else if (dbItem.getDescription() != null) { dbItem.setReadTime(Utils.readTimeFromString(dbItem.getCleanDescription())); + } itemsToInsert.add(dbItem); } diff --git a/app/src/main/java/com/readrops/app/utils/HtmlParser.java b/app/src/main/java/com/readrops/app/utils/HtmlParser.java index 85c7965d..b408a08b 100644 --- a/app/src/main/java/com/readrops/app/utils/HtmlParser.java +++ b/app/src/main/java/com/readrops/app/utils/HtmlParser.java @@ -13,11 +13,9 @@ import org.jsoup.nodes.Document; import org.jsoup.nodes.Element; import org.jsoup.select.Elements; -import java.io.IOException; import java.util.ArrayList; import java.util.Collections; import java.util.List; -import java.util.regex.Pattern; import okhttp3.Request; import okhttp3.Response; @@ -26,8 +24,6 @@ public final class HtmlParser { private static final String TAG = HtmlParser.class.getSimpleName(); - public static final String COVER_IMAGE_REGEX = "^(

|())?"; - /** * Parse the html page to get all rss urls * @@ -68,27 +64,6 @@ public final class HtmlParser { type.equals(LibUtils.RSS_APPLICATION_CONTENT_TYPE); } - /** - * get the feed item image based on open graph metadata. - * Warning, This method is slow. - * - * @param url url to request - * @return the item image - */ - public static String getOGImageLink(String url) throws IOException { - String imageUrl = null; - - String head = getHTMLHeadFromUrl(url); - - Document document = Jsoup.parse(head); - Element element = document.select("meta[property=og:image]").first(); - - if (element != null) - imageUrl = element.attributes().get("content"); - - return imageUrl; - } - @Nullable public static String getFaviconLink(@NonNull String url) { String favUrl = null; @@ -134,28 +109,4 @@ public final class HtmlParser { } } - - public static String getDescImageLink(String description, String url) { - Document document = Jsoup.parse(description, url); - Elements elements = document.select("img"); - - if (!elements.isEmpty()) - return elements.first().absUrl("src"); - else - return null; - } - - public static String deleteCoverImage(String content) { - Document document = Jsoup.parse(content); - - if (Pattern.compile(COVER_IMAGE_REGEX).matcher(document.body().html()).find()) { - Elements elements = document.select("img"); - - if (!elements.isEmpty()) - elements.first().remove(); - - return document.toString(); - } else - return content; - } } From b6d4f9296d35ca7bb7cffd7960108f0f78a1a318 Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Tue, 6 Oct 2020 22:15:19 +0200 Subject: [PATCH 080/187] Move isRSSType() method to LocalRSSHelper --- .../com/readrops/api/localfeed/LocalRSSHelper.kt | 5 +++++ .../readrops/api/localfeed/LocalRSSHelperTest.kt | 14 +++++++++++++- .../java/com/readrops/app/utils/HtmlParser.java | 11 ++--------- 3 files changed, 20 insertions(+), 10 deletions(-) diff --git a/api/src/main/java/com/readrops/api/localfeed/LocalRSSHelper.kt b/api/src/main/java/com/readrops/api/localfeed/LocalRSSHelper.kt index b0cb5431..3651ebe9 100644 --- a/api/src/main/java/com/readrops/api/localfeed/LocalRSSHelper.kt +++ b/api/src/main/java/com/readrops/api/localfeed/LocalRSSHelper.kt @@ -51,6 +51,11 @@ object LocalRSSHelper { return type } + @JvmStatic + fun isRSSType(type: String?): Boolean { + return if (type != null) getRSSType(type) != RSSType.UNKNOWN else false + } + enum class RSSType { RSS_1, RSS_2, diff --git a/api/src/test/java/com/readrops/api/localfeed/LocalRSSHelperTest.kt b/api/src/test/java/com/readrops/api/localfeed/LocalRSSHelperTest.kt index dd6537ff..74414ce6 100644 --- a/api/src/test/java/com/readrops/api/localfeed/LocalRSSHelperTest.kt +++ b/api/src/test/java/com/readrops/api/localfeed/LocalRSSHelperTest.kt @@ -1,6 +1,6 @@ package com.readrops.api.localfeed -import junit.framework.TestCase.assertEquals +import junit.framework.TestCase.* import org.junit.Test import java.io.ByteArrayInputStream @@ -76,4 +76,16 @@ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://purl.org/r )), LocalRSSHelper.RSSType.UNKNOWN) } + + @Test + fun isRSSTypeTest() { + assertTrue(LocalRSSHelper.isRSSType("application/rss+xml")) + } + + @Test + fun isRSSTypeNullCaseTest() { + assertFalse(LocalRSSHelper.isRSSType(null)) + } + + } \ No newline at end of file diff --git a/app/src/main/java/com/readrops/app/utils/HtmlParser.java b/app/src/main/java/com/readrops/app/utils/HtmlParser.java index b408a08b..369244e6 100644 --- a/app/src/main/java/com/readrops/app/utils/HtmlParser.java +++ b/app/src/main/java/com/readrops/app/utils/HtmlParser.java @@ -5,6 +5,7 @@ import android.util.Log; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import com.readrops.api.localfeed.LocalRSSHelper; import com.readrops.api.utils.HttpManager; import com.readrops.api.utils.LibUtils; @@ -42,7 +43,7 @@ public final class HtmlParser { for (Element element : elements) { String type = element.attributes().get("type"); - if (isTypeRssFeed(type)) { + if (LocalRSSHelper.isRSSType(type)) { String feedUrl = element.absUrl("href"); String label = element.attributes().get("title"); @@ -56,14 +57,6 @@ public final class HtmlParser { } } - private static boolean isTypeRssFeed(String type) { - return type.equals(LibUtils.RSS_DEFAULT_CONTENT_TYPE) || - type.equals(LibUtils.ATOM_CONTENT_TYPE) || - type.equals(LibUtils.JSON_CONTENT_TYPE) || - type.equals(LibUtils.RSS_TEXT_CONTENT_TYPE) || - type.equals(LibUtils.RSS_APPLICATION_CONTENT_TYPE); - } - @Nullable public static String getFaviconLink(@NonNull String url) { String favUrl = null; From b8219b5ddeec9518f2b06479488f1619b5ad49f4 Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Tue, 6 Oct 2020 22:20:53 +0200 Subject: [PATCH 081/187] Clean LibUtils class --- .../java/com/readrops/api/opml/OPMLParser.kt | 3 +-- .../java/com/readrops/api/utils/LibUtils.java | 23 ------------------- 2 files changed, 1 insertion(+), 25 deletions(-) diff --git a/api/src/main/java/com/readrops/api/opml/OPMLParser.kt b/api/src/main/java/com/readrops/api/opml/OPMLParser.kt index 094751d8..ce9dba51 100644 --- a/api/src/main/java/com/readrops/api/opml/OPMLParser.kt +++ b/api/src/main/java/com/readrops/api/opml/OPMLParser.kt @@ -35,10 +35,9 @@ object OPMLParser { fun read(stream: InputStream): Single>> { return Single.create { emitter -> try { - val fileString = LibUtils.inputStreamToString(stream) val serializer: Serializer = Persister() - val opml: OPML = serializer.read(OPML::class.java, fileString) + val opml: OPML = serializer.read(OPML::class.java, stream) emitter.onSuccess(opmlToFoldersAndFeeds(opml)) } catch (e: Exception) { diff --git a/api/src/main/java/com/readrops/api/utils/LibUtils.java b/api/src/main/java/com/readrops/api/utils/LibUtils.java index cf7a6813..7e914a2d 100644 --- a/api/src/main/java/com/readrops/api/utils/LibUtils.java +++ b/api/src/main/java/com/readrops/api/utils/LibUtils.java @@ -1,26 +1,15 @@ package com.readrops.api.utils; -import android.content.Context; -import android.net.Uri; - import androidx.annotation.NonNull; import androidx.annotation.Nullable; import org.jsoup.Jsoup; -import java.io.FileNotFoundException; -import java.io.InputStream; -import java.util.Scanner; import java.util.regex.Matcher; import java.util.regex.Pattern; public final class LibUtils { - public static final String RSS_DEFAULT_CONTENT_TYPE = "application/rss+xml"; - public static final String RSS_TEXT_CONTENT_TYPE = "text/xml"; - public static final String RSS_APPLICATION_CONTENT_TYPE = "application/xml"; - public static final String ATOM_CONTENT_TYPE = "application/atom+xml"; - public static final String JSON_CONTENT_TYPE = "application/json"; public static final String HTML_CONTENT_TYPE = "text/html"; public static final String CONTENT_TYPE_HEADER = "content-type"; @@ -35,18 +24,6 @@ public final class LibUtils { private static final String RSS_CONTENT_TYPE_REGEX = "([^;]+)"; - - public static String inputStreamToString(InputStream input) { - Scanner scanner = new Scanner(input).useDelimiter("\\A"); - return scanner.hasNext() ? scanner.next() : ""; - } - - public static String fileToString(Uri uri, Context context) throws FileNotFoundException { - InputStream inputStream = context.getContentResolver().openInputStream(uri); - - return inputStreamToString(inputStream); - } - public static boolean isMimeImage(@NonNull String type) { return type.equals("image") || type.equals("image/jpeg") || type.equals("image/jpg") || type.equals("image/png"); From c26a2da6a740b6f1ae7b895e9a6a8397d99ce45b Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Tue, 6 Oct 2020 22:45:19 +0200 Subject: [PATCH 082/187] Refactor and update gradle dependencies --- api/build.gradle | 30 ++++++++++-------------------- app/build.gradle | 30 +++++++++++------------------- db/build.gradle | 13 ++++++++----- 3 files changed, 29 insertions(+), 44 deletions(-) diff --git a/api/build.gradle b/api/build.gradle index bf8dcf78..27e3a701 100644 --- a/api/build.gradle +++ b/api/build.gradle @@ -41,42 +41,32 @@ android { dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) - implementation project(':db') + api project(':db') - implementation "androidx.core:core-ktx:1.2.0" - implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" - - testImplementation 'junit:junit:4.13' - androidTestImplementation 'androidx.test.ext:junit:1.1.2' - androidTestImplementation 'androidx.test:runner:1.3.0' - androidTestImplementation 'androidx.test:rules:1.3.0' - androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0' - androidTestImplementation 'com.squareup.okhttp3:mockwebserver:4.8.1' + androidTestImplementation 'com.squareup.okhttp3:mockwebserver:4.9.0' implementation 'com.gitlab.mvysny.konsume-xml:konsume-xml:0.12' - implementation 'com.squareup.okhttp3:okhttp:4.8.1' + implementation 'com.squareup.okhttp3:okhttp:4.9.0' - implementation('com.squareup.retrofit2:retrofit:2.7.1') { + implementation('com.squareup.retrofit2:retrofit:2.9.0') { exclude group: 'okhttp3', module: 'okhttp3' } - implementation('com.squareup.retrofit2:converter-moshi:2.7.1') { + implementation('com.squareup.retrofit2:converter-moshi:2.9.0') { exclude group: 'moshi', module: 'moshi' // moshi converter uses moshi 1.8.0 which breaks codegen 1.9.2 } - implementation ('com.squareup.retrofit2:converter-simplexml:2.7.1') { + implementation ('com.squareup.retrofit2:converter-simplexml:2.9.0') { exclude module: 'stax' exclude module: 'stax-api' exclude module: 'xpp3' } - implementation 'com.squareup.retrofit2:adapter-rxjava2:2.7.1' + implementation 'com.squareup.retrofit2:adapter-rxjava2:2.9.0' - implementation 'com.squareup.moshi:moshi:1.9.2' - kapt 'com.squareup.moshi:moshi-kotlin-codegen:1.9.2' - - implementation 'com.squareup.okhttp3:logging-interceptor:4.2.0' + implementation 'com.squareup.moshi:moshi:1.11.0' + kapt 'com.squareup.moshi:moshi-kotlin-codegen:1.11.0' api 'io.reactivex.rxjava2:rxandroid:2.1.1' - api 'org.jsoup:jsoup:1.12.1' + api 'org.jsoup:jsoup:1.13.1' } diff --git a/app/build.gradle b/app/build.gradle index 0cd879c0..5bdcc285 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -55,34 +55,26 @@ android { dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) - implementation project(':api') - implementation project(':db') + api project(':api') + api project(':db') coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.0.10' - implementation 'com.google.android.material:material:1.2.0' + implementation 'com.google.android.material:material:1.2.1' implementation 'androidx.cardview:cardview:1.0.0' implementation 'androidx.palette:palette:1.0.0' implementation 'androidx.recyclerview:recyclerview:1.1.0' - implementation 'androidx.constraintlayout:constraintlayout:1.1.3' + implementation 'androidx.constraintlayout:constraintlayout:2.0.1' implementation 'androidx.legacy:legacy-support-v4:1.0.0' - implementation 'androidx.preference:preference:1.1.0' - implementation "androidx.core:core-ktx:1.2.0" - implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" + implementation 'androidx.preference:preference:1.1.1' implementation "androidx.work:work-runtime-ktx:2.4.0" - implementation "androidx.fragment:fragment-ktx:1.2.3" + implementation "androidx.fragment:fragment-ktx:1.2.5" implementation "androidx.browser:browser:1.2.0" - testImplementation 'junit:junit:4.12' - androidTestImplementation 'androidx.test.ext:junit:1.1.1' - androidTestImplementation 'androidx.test:runner:1.2.0' - androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' - - implementation 'com.github.bumptech.glide:glide:4.10.0' - kapt 'com.github.bumptech.glide:compiler:4.10.0' - implementation 'com.github.bumptech.glide:okhttp3-integration:4.10.0' - implementation('com.github.bumptech.glide:recyclerview-integration:4.10.0') { - // Excludes the support library because it's already included by Glide. + implementation 'com.github.bumptech.glide:glide:4.11.0' + kapt 'com.github.bumptech.glide:compiler:4.11.0' + implementation 'com.github.bumptech.glide:okhttp3-integration:4.11.0' + implementation('com.github.bumptech.glide:recyclerview-integration:4.11.0') { transitive = false } @@ -97,7 +89,7 @@ dependencies { implementation "com.mikepenz:aboutlibraries:6.2.3" debugImplementation 'com.facebook.flipper:flipper:0.30.1' - debugImplementation 'com.facebook.soloader:soloader:0.8.0' + debugImplementation 'com.facebook.soloader:soloader:0.9.0' debugImplementation 'com.facebook.flipper:flipper-network-plugin:0.30.1' debugImplementation 'com.icapps.niddler:niddler:1.2.0' diff --git a/db/build.gradle b/db/build.gradle index 1cb655c4..0d611550 100644 --- a/db/build.gradle +++ b/db/build.gradle @@ -53,12 +53,15 @@ android { dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) - api 'androidx.appcompat:appcompat:1.2.0-rc02' + api "androidx.core:core-ktx:1.3.2" + api 'androidx.appcompat:appcompat:1.2.0' + api "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" - testImplementation 'junit:junit:4.12' - androidTestImplementation 'androidx.test.ext:junit:1.1.1' - androidTestImplementation 'androidx.test:runner:1.2.0' - androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' + testImplementation 'junit:junit:4.13' + androidTestImplementation 'androidx.test.ext:junit:1.1.2' + androidTestImplementation 'androidx.test:runner:1.3.0' + androidTestImplementation 'androidx.test:rules:1.3.0' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0' api 'androidx.room:room-runtime:2.2.5' kapt 'androidx.room:room-compiler:2.2.5' From 343562657b0c6db481dd4224226d365f31eec804 Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Wed, 7 Oct 2020 22:10:44 +0200 Subject: [PATCH 083/187] Fix test dependencies build error --- api/build.gradle | 7 ++++++- app/build.gradle | 10 ++++++++-- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/api/build.gradle b/api/build.gradle index 27e3a701..c1ab0760 100644 --- a/api/build.gradle +++ b/api/build.gradle @@ -41,8 +41,13 @@ android { dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) - api project(':db') + implementation project(':db') + testImplementation 'junit:junit:4.13' + androidTestImplementation 'androidx.test.ext:junit:1.1.2' + androidTestImplementation 'androidx.test:runner:1.3.0' + androidTestImplementation 'androidx.test:rules:1.3.0' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0' androidTestImplementation 'com.squareup.okhttp3:mockwebserver:4.9.0' implementation 'com.gitlab.mvysny.konsume-xml:konsume-xml:0.12' diff --git a/app/build.gradle b/app/build.gradle index 5bdcc285..b096650f 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -55,11 +55,17 @@ android { dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) - api project(':api') - api project(':db') + implementation project(':api') + implementation project(':db') coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.0.10' + testImplementation 'junit:junit:4.13' + androidTestImplementation 'androidx.test.ext:junit:1.1.2' + androidTestImplementation 'androidx.test:runner:1.3.0' + androidTestImplementation 'androidx.test:rules:1.3.0' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0' + implementation 'com.google.android.material:material:1.2.1' implementation 'androidx.cardview:cardview:1.0.0' implementation 'androidx.palette:palette:1.0.0' From 9b5dddeff41276b3f87aa92e773972e70a0ce049 Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Sun, 11 Oct 2020 22:31:55 +0200 Subject: [PATCH 084/187] Don't build repositories and data sources dependencies in them --- ...eshRSSAPI.java => FreshRSSDataSource.java} | 19 +++---- .../freshrss/adapters/FreshRSSItemsAdapter.kt | 2 +- ...xtNewsAPI.java => NextNewsDataSource.java} | 15 +++--- .../app/repositories/ARepository.java | 24 ++++----- .../app/repositories/FreshRSSRepository.java | 49 +++++++------------ .../app/repositories/LocalFeedRepository.java | 15 ++---- .../app/repositories/NextNewsRepository.java | 41 ++++++---------- 7 files changed, 68 insertions(+), 97 deletions(-) rename api/src/main/java/com/readrops/api/services/freshrss/{FreshRSSAPI.java => FreshRSSDataSource.java} (96%) rename api/src/main/java/com/readrops/api/services/nextcloudnews/{NextNewsAPI.java => NextNewsDataSource.java} (96%) diff --git a/api/src/main/java/com/readrops/api/services/freshrss/FreshRSSAPI.java b/api/src/main/java/com/readrops/api/services/freshrss/FreshRSSDataSource.java similarity index 96% rename from api/src/main/java/com/readrops/api/services/freshrss/FreshRSSAPI.java rename to api/src/main/java/com/readrops/api/services/freshrss/FreshRSSDataSource.java index 86fccf7d..a3e25493 100644 --- a/api/src/main/java/com/readrops/api/services/freshrss/FreshRSSAPI.java +++ b/api/src/main/java/com/readrops/api/services/freshrss/FreshRSSDataSource.java @@ -3,17 +3,15 @@ package com.readrops.api.services.freshrss; import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import com.readrops.db.entities.Feed; -import com.readrops.db.entities.Folder; -import com.readrops.db.entities.Item; -import com.readrops.api.services.API; -import com.readrops.api.services.Credentials; import com.readrops.api.services.SyncResult; import com.readrops.api.services.SyncType; import com.readrops.api.services.freshrss.adapters.FreshRSSFeedsAdapter; import com.readrops.api.services.freshrss.adapters.FreshRSSFoldersAdapter; import com.readrops.api.services.freshrss.adapters.FreshRSSItemsAdapter; import com.readrops.api.services.freshrss.json.FreshRSSUserInfo; +import com.readrops.db.entities.Feed; +import com.readrops.db.entities.Folder; +import com.readrops.db.entities.Item; import com.squareup.moshi.Moshi; import com.squareup.moshi.Types; @@ -26,17 +24,20 @@ import io.reactivex.Single; import okhttp3.MultipartBody; import okhttp3.RequestBody; -public class FreshRSSAPI extends API { +public class FreshRSSDataSource { + + private static final int MAX_ITEMS = 5000; public static final String GOOGLE_READ = "user/-/state/com.google/read"; private static final String FEED_PREFIX = "feed/"; - public FreshRSSAPI(Credentials credentials) { - super(credentials, FreshRSSService.class, FreshRSSService.END_POINT); + private FreshRSSService api; + + public FreshRSSDataSource(FreshRSSService api) { + this.api = api; } - @Override protected Moshi buildMoshi() { return new Moshi.Builder() .add(Types.newParameterizedType(List.class, Item.class), new FreshRSSItemsAdapter()) diff --git a/api/src/main/java/com/readrops/api/services/freshrss/adapters/FreshRSSItemsAdapter.kt b/api/src/main/java/com/readrops/api/services/freshrss/adapters/FreshRSSItemsAdapter.kt index 8667fa51..f6ef5f95 100644 --- a/api/src/main/java/com/readrops/api/services/freshrss/adapters/FreshRSSItemsAdapter.kt +++ b/api/src/main/java/com/readrops/api/services/freshrss/adapters/FreshRSSItemsAdapter.kt @@ -2,7 +2,7 @@ package com.readrops.api.services.freshrss.adapters import android.util.TimingLogger import com.readrops.db.entities.Item -import com.readrops.api.services.freshrss.FreshRSSAPI.GOOGLE_READ +import com.readrops.api.services.freshrss.FreshRSSDataSource.GOOGLE_READ import com.squareup.moshi.JsonAdapter import com.squareup.moshi.JsonReader import com.squareup.moshi.JsonWriter diff --git a/api/src/main/java/com/readrops/api/services/nextcloudnews/NextNewsAPI.java b/api/src/main/java/com/readrops/api/services/nextcloudnews/NextNewsDataSource.java similarity index 96% rename from api/src/main/java/com/readrops/api/services/nextcloudnews/NextNewsAPI.java rename to api/src/main/java/com/readrops/api/services/nextcloudnews/NextNewsDataSource.java index eb9d8d16..d39261c0 100644 --- a/api/src/main/java/com/readrops/api/services/nextcloudnews/NextNewsAPI.java +++ b/api/src/main/java/com/readrops/api/services/nextcloudnews/NextNewsDataSource.java @@ -8,8 +8,6 @@ import androidx.annotation.Nullable; import com.readrops.db.entities.Feed; import com.readrops.db.entities.Folder; import com.readrops.db.entities.Item; -import com.readrops.api.services.API; -import com.readrops.api.services.Credentials; import com.readrops.api.services.SyncResult; import com.readrops.api.services.SyncType; import com.readrops.api.services.nextcloudnews.adapters.NextNewsFeedsAdapter; @@ -30,15 +28,18 @@ import java.util.Map; import retrofit2.Response; -public class NextNewsAPI extends API { +public class NextNewsDataSource { - private static final String TAG = NextNewsAPI.class.getSimpleName(); + private static final String TAG = NextNewsDataSource.class.getSimpleName(); - public NextNewsAPI(Credentials credentials) { - super(credentials, NextNewsService.class, NextNewsService.END_POINT); + protected static final int MAX_ITEMS = 5000; + + private NextNewsService api; + + public NextNewsDataSource(NextNewsService api) { + this.api = api; } - @Override protected Moshi buildMoshi() { return new Moshi.Builder() .add(new NextNewsFeedsAdapter()) diff --git a/app/src/main/java/com/readrops/app/repositories/ARepository.java b/app/src/main/java/com/readrops/app/repositories/ARepository.java index faa55acb..88c3f224 100644 --- a/app/src/main/java/com/readrops/app/repositories/ARepository.java +++ b/app/src/main/java/com/readrops/app/repositories/ARepository.java @@ -30,26 +30,20 @@ import io.reactivex.Single; import static com.readrops.app.utils.ReadropsKeys.FEEDS; -public abstract class ARepository { +public abstract class ARepository { protected Context context; protected Database database; protected Account account; - protected T api; - protected SyncResult syncResult; - protected ARepository(@NonNull Context context, @Nullable Account account) { + protected ARepository(Database database, @NonNull Context context, @Nullable Account account) { this.context = context; - this.database = Database.getInstance(context); + this.database = database; this.account = account; - - api = createAPI(); } - protected abstract T createAPI(); - // TODO : replace Single by Completable public abstract Single login(Account account, boolean insert); @@ -171,20 +165,20 @@ public abstract class ARepository { context.startService(intent); } - public static ARepository repositoryFactory(Account account, AccountType accountType, Context context) throws Exception { + public static ARepository repositoryFactory(Account account, AccountType accountType, Context context) { switch (accountType) { case LOCAL: - return new LocalFeedRepository(context, account); + return new LocalFeedRepository(null, Database.getInstance(context), context, account); case NEXTCLOUD_NEWS: - return new NextNewsRepository(context, account); + return new NextNewsRepository(null, Database.getInstance(context), context, account); case FRESHRSS: - return new FreshRSSRepository(context, account); + return new FreshRSSRepository(null, Database.getInstance(context), context, account); default: - throw new Exception("account type not supported"); + throw new IllegalArgumentException("account type not supported"); } } - public static ARepository repositoryFactory(Account account, Context context) throws Exception { + public static ARepository repositoryFactory(Account account, Context context) { return ARepository.repositoryFactory(account, account.getAccountType(), context); } diff --git a/app/src/main/java/com/readrops/app/repositories/FreshRSSRepository.java b/app/src/main/java/com/readrops/app/repositories/FreshRSSRepository.java index 744e80f6..c37be56f 100644 --- a/app/src/main/java/com/readrops/app/repositories/FreshRSSRepository.java +++ b/app/src/main/java/com/readrops/app/repositories/FreshRSSRepository.java @@ -7,18 +7,17 @@ import android.util.TimingLogger; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import com.readrops.api.services.SyncType; +import com.readrops.api.services.freshrss.FreshRSSDataSource; +import com.readrops.api.services.freshrss.FreshRSSSyncData; import com.readrops.app.utils.FeedInsertionResult; import com.readrops.app.utils.ParsingResult; import com.readrops.app.utils.Utils; +import com.readrops.db.Database; import com.readrops.db.entities.Feed; import com.readrops.db.entities.Folder; import com.readrops.db.entities.Item; import com.readrops.db.entities.account.Account; -import com.readrops.api.services.Credentials; -import com.readrops.api.services.SyncType; -import com.readrops.api.services.freshrss.FreshRSSAPI; -import com.readrops.api.services.freshrss.FreshRSSCredentials; -import com.readrops.api.services.freshrss.FreshRSSSyncData; import org.joda.time.DateTime; @@ -30,40 +29,30 @@ import io.reactivex.Completable; import io.reactivex.Observable; import io.reactivex.Single; -public class FreshRSSRepository extends ARepository { +public class FreshRSSRepository extends ARepository { private static final String TAG = FreshRSSRepository.class.getSimpleName(); - public FreshRSSRepository(@NonNull Context context, @Nullable Account account) { - super(context, account); - } + private FreshRSSDataSource dataSource; - @Override - protected FreshRSSAPI createAPI() { - if (account != null) - return new FreshRSSAPI(Credentials.toCredentials(account)); + public FreshRSSRepository(FreshRSSDataSource dataSource, Database database, @NonNull Context context, @Nullable Account account) { + super(database, context, account); - return null; + this.dataSource = dataSource; } @Override public Single login(Account account, boolean insert) { - if (api == null) - api = new FreshRSSAPI(Credentials.toCredentials(account)); - else - api.setCredentials(Credentials.toCredentials(account)); - - return api.login(account.getLogin(), account.getPassword()) + return dataSource.login(account.getLogin(), account.getPassword()) .flatMap(token -> { account.setToken(token); - api.setCredentials(new FreshRSSCredentials(token, account.getUrl())); - return api.getWriteToken(); + return dataSource.getWriteToken(); }) .flatMap(writeToken -> { account.setWriteToken(writeToken); - return api.getUserInfo(); + return dataSource.getUserInfo(); }) .flatMap(userInfo -> { account.setDisplayedName(userInfo.getUserName()); @@ -100,7 +89,7 @@ public class FreshRSSRepository extends ARepository { syncData.setUnreadItemsIds(database.itemDao().getUnreadChanges(account.getId())); emitter.onSuccess(syncData); - }).flatMap(syncData1 -> api.sync(syncType, syncData1, account.getWriteToken())) + }).flatMap(syncData1 -> dataSource.sync(syncType, syncData1, account.getWriteToken())) .flatMapObservable(syncResult -> { logger.addSplit("server queries"); @@ -131,7 +120,7 @@ public class FreshRSSRepository extends ARepository { List insertionResults = new ArrayList<>(); for (ParsingResult result : results) { - completableList.add(api.createFeed(account.getWriteToken(), result.getUrl()) + completableList.add(dataSource.createFeed(account.getWriteToken(), result.getUrl()) .doOnComplete(() -> { FeedInsertionResult feedInsertionResult = new FeedInsertionResult(); feedInsertionResult.setParsingResult(result); @@ -159,26 +148,26 @@ public class FreshRSSRepository extends ARepository { Folder folder = feed.getFolderId() == null ? null : database.folderDao().select(feed.getFolderId()); emitter.onSuccess(folder); - }).flatMapCompletable(folder -> api.updateFeed(account.getWriteToken(), + }).flatMapCompletable(folder -> dataSource.updateFeed(account.getWriteToken(), feed.getUrl(), feed.getName(), folder == null ? null : folder.getRemoteId()) .andThen(super.updateFeed(feed))); } @Override public Completable deleteFeed(Feed feed) { - return api.deleteFeed(account.getWriteToken(), feed.getUrl()) + return dataSource.deleteFeed(account.getWriteToken(), feed.getUrl()) .andThen(super.deleteFeed(feed)); } @Override public Single addFolder(Folder folder) { - return api.createFolder(account.getWriteToken(), folder.getName()) + return dataSource.createFolder(account.getWriteToken(), folder.getName()) .andThen(super.addFolder(folder)); } @Override public Completable updateFolder(Folder folder) { - return api.updateFolder(account.getWriteToken(), folder.getRemoteId(), folder.getName()) + return dataSource.updateFolder(account.getWriteToken(), folder.getRemoteId(), folder.getName()) .andThen(Completable.create(emitter -> { folder.setRemoteId("user/-/label/" + folder.getName()); emitter.onComplete(); @@ -188,7 +177,7 @@ public class FreshRSSRepository extends ARepository { @Override public Completable deleteFolder(Folder folder) { - return api.deleteFolder(account.getWriteToken(), folder.getRemoteId()) + return dataSource.deleteFolder(account.getWriteToken(), folder.getRemoteId()) .andThen(super.deleteFolder(folder)); } diff --git a/app/src/main/java/com/readrops/app/repositories/LocalFeedRepository.java b/app/src/main/java/com/readrops/app/repositories/LocalFeedRepository.java index f6b05c3b..65aa3553 100644 --- a/app/src/main/java/com/readrops/app/repositories/LocalFeedRepository.java +++ b/app/src/main/java/com/readrops/app/repositories/LocalFeedRepository.java @@ -9,7 +9,6 @@ import androidx.annotation.Nullable; import com.readrops.api.localfeed.LocalRSSDataSource; import com.readrops.api.services.SyncResult; -import com.readrops.api.utils.HttpManager; import com.readrops.api.utils.LibUtils; import com.readrops.api.utils.ParseException; import com.readrops.api.utils.UnknownFormatException; @@ -17,6 +16,7 @@ import com.readrops.app.utils.FeedInsertionResult; import com.readrops.app.utils.ParsingResult; import com.readrops.app.utils.SharedPreferencesManager; import com.readrops.app.utils.Utils; +import com.readrops.db.Database; import com.readrops.db.entities.Feed; import com.readrops.db.entities.Item; import com.readrops.db.entities.account.Account; @@ -34,22 +34,17 @@ import io.reactivex.Single; import kotlin.Pair; import okhttp3.Headers; -public class LocalFeedRepository extends ARepository { +public class LocalFeedRepository extends ARepository { private static final String TAG = LocalFeedRepository.class.getSimpleName(); private LocalRSSDataSource dataSource; - public LocalFeedRepository(@NonNull Context context, @Nullable Account account) { - super(context, account); + public LocalFeedRepository(LocalRSSDataSource dataSource, Database database, @NonNull Context context, @Nullable Account account) { + super(database, context, account); syncResult = new SyncResult(); - dataSource = new LocalRSSDataSource(HttpManager.getInstance().getOkHttpClient()); - } - - @Override - protected Void createAPI() { - return null; + this.dataSource = dataSource; } @Override diff --git a/app/src/main/java/com/readrops/app/repositories/NextNewsRepository.java b/app/src/main/java/com/readrops/app/repositories/NextNewsRepository.java index 018d6a7d..e76309a5 100644 --- a/app/src/main/java/com/readrops/app/repositories/NextNewsRepository.java +++ b/app/src/main/java/com/readrops/app/repositories/NextNewsRepository.java @@ -7,16 +7,16 @@ import android.util.TimingLogger; import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import com.readrops.api.services.Credentials; import com.readrops.api.services.SyncResult; import com.readrops.api.services.SyncType; -import com.readrops.api.services.nextcloudnews.NextNewsAPI; +import com.readrops.api.services.nextcloudnews.NextNewsDataSource; import com.readrops.api.services.nextcloudnews.NextNewsSyncData; import com.readrops.api.services.nextcloudnews.json.NextNewsUser; import com.readrops.api.utils.UnknownFormatException; import com.readrops.app.utils.FeedInsertionResult; import com.readrops.app.utils.ParsingResult; import com.readrops.app.utils.Utils; +import com.readrops.db.Database; import com.readrops.db.entities.Feed; import com.readrops.db.entities.Folder; import com.readrops.db.entities.Item; @@ -33,31 +33,22 @@ import io.reactivex.Completable; import io.reactivex.Observable; import io.reactivex.Single; -public class NextNewsRepository extends ARepository { +public class NextNewsRepository extends ARepository { private static final String TAG = NextNewsRepository.class.getSimpleName(); - public NextNewsRepository(@NonNull Context context, @Nullable Account account) { - super(context, account); - } + private NextNewsDataSource dataSource; - @Override - protected NextNewsAPI createAPI() { - if (account != null) - return new NextNewsAPI(Credentials.toCredentials(account)); + public NextNewsRepository(NextNewsDataSource dataSource, Database database, @NonNull Context context, @Nullable Account account) { + super(database, context, account); - return null; + this.dataSource = dataSource; } @Override public Single login(Account account, boolean insert) { return Single.create(emitter -> { - if (api == null) - api = new NextNewsAPI(Credentials.toCredentials(account)); - else - api.setCredentials(Credentials.toCredentials(account)); - - NextNewsUser user = api.login(); + NextNewsUser user = dataSource.login(); if (user != null) { emitter.onSuccess(user); @@ -101,7 +92,7 @@ public class NextNewsRepository extends ARepository { } TimingLogger timings = new TimingLogger(TAG, "nextcloud news " + syncType.name().toLowerCase()); - SyncResult result = api.sync(syncType, syncData); + SyncResult result = dataSource.sync(syncType, syncData); timings.addSplit("server queries"); if (!result.isError()) { @@ -141,11 +132,11 @@ public class NextNewsRepository extends ARepository { FeedInsertionResult insertionResult = new FeedInsertionResult(); try { - List nextNewsFeeds = api.createFeed(result.getUrl(), 0); + List nextNewsFeeds = dataSource.createFeed(result.getUrl(), 0); if (nextNewsFeeds != null) { List newFeeds = insertFeeds(nextNewsFeeds, true); - // there is always only one object in the list, see nextcloud news api doc + // there is always only one object in the list, see nextcloud news dataSource doc insertionResult.setFeed(newFeeds.get(0)); } else insertionResult.setInsertionError(FeedInsertionResult.FeedInsertionError.UNKNOWN_ERROR); @@ -180,7 +171,7 @@ public class NextNewsRepository extends ARepository { feed.setRemoteFolderId(String.valueOf(0)); // 0 for no folder try { - if (api.renameFeed(feed) && api.changeFeedFolder(feed)) { + if (dataSource.renameFeed(feed) && dataSource.changeFeedFolder(feed)) { emitter.onComplete(); } else emitter.onError(new Exception("Unknown error when updating feed")); @@ -194,7 +185,7 @@ public class NextNewsRepository extends ARepository { public Completable deleteFeed(Feed feed) { return Completable.create(emitter -> { try { - if (api.deleteFeed(Integer.parseInt(feed.getRemoteId()))) { + if (dataSource.deleteFeed(Integer.parseInt(feed.getRemoteId()))) { emitter.onComplete(); } else emitter.onError(new Exception("Unknown error")); @@ -210,7 +201,7 @@ public class NextNewsRepository extends ARepository { public Single addFolder(Folder folder) { return Single.create(emitter -> { try { - List folders = api.createFolder(folder); + List folders = dataSource.createFolder(folder); if (folders != null) { Folder nextNewsFolder = folders.get(0); // always only one item returned by the server, see doc @@ -229,7 +220,7 @@ public class NextNewsRepository extends ARepository { public Completable updateFolder(Folder folder) { return Completable.create(emitter -> { try { - if (api.renameFolder(folder)) { + if (dataSource.renameFolder(folder)) { emitter.onComplete(); } else emitter.onError(new Exception("Unknown error")); @@ -246,7 +237,7 @@ public class NextNewsRepository extends ARepository { public Completable deleteFolder(Folder folder) { return Completable.create(emitter -> { try { - if (api.deleteFolder(folder)) { + if (dataSource.deleteFolder(folder)) { emitter.onComplete(); } else emitter.onError(new Exception("Unknown error")); From add729f5e22ca2140b7c7e05bc64a89927a63f34 Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Tue, 13 Oct 2020 22:11:13 +0200 Subject: [PATCH 085/187] Add Koin with api, db and app modules --- .../main/java/com/readrops/api/ApiModule.kt | 98 +++++++++++++++++++ .../main/java/com/readrops/app/AppModule.kt | 45 +++++++++ .../java/com/readrops/app/ReadropsApp.java | 61 ------------ .../main/java/com/readrops/app/ReadropsApp.kt | 68 +++++++++++++ .../app/viewmodels/MainViewModel.java | 8 +- db/build.gradle | 5 + db/src/main/java/com/readrops/db/DbModule.kt | 13 +++ 7 files changed, 235 insertions(+), 63 deletions(-) create mode 100644 api/src/main/java/com/readrops/api/ApiModule.kt create mode 100644 app/src/main/java/com/readrops/app/AppModule.kt delete mode 100644 app/src/main/java/com/readrops/app/ReadropsApp.java create mode 100644 app/src/main/java/com/readrops/app/ReadropsApp.kt create mode 100644 db/src/main/java/com/readrops/db/DbModule.kt diff --git a/api/src/main/java/com/readrops/api/ApiModule.kt b/api/src/main/java/com/readrops/api/ApiModule.kt new file mode 100644 index 00000000..a1f73155 --- /dev/null +++ b/api/src/main/java/com/readrops/api/ApiModule.kt @@ -0,0 +1,98 @@ +package com.readrops.api + +import com.readrops.api.localfeed.LocalRSSDataSource +import com.readrops.api.services.freshrss.FreshRSSDataSource +import com.readrops.api.services.freshrss.FreshRSSService +import com.readrops.api.services.freshrss.adapters.FreshRSSFeedsAdapter +import com.readrops.api.services.freshrss.adapters.FreshRSSFoldersAdapter +import com.readrops.api.services.freshrss.adapters.FreshRSSItemsAdapter +import com.readrops.api.services.nextcloudnews.NextNewsDataSource +import com.readrops.api.services.nextcloudnews.NextNewsService +import com.readrops.api.services.nextcloudnews.adapters.NextNewsFeedsAdapter +import com.readrops.api.services.nextcloudnews.adapters.NextNewsFoldersAdapter +import com.readrops.api.services.nextcloudnews.adapters.NextNewsItemsAdapter +import com.readrops.api.utils.HttpManager +import com.readrops.db.entities.Item +import com.squareup.moshi.Moshi +import com.squareup.moshi.Types +import okhttp3.OkHttpClient +import org.koin.core.qualifier.named +import org.koin.dsl.module +import retrofit2.Retrofit +import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory +import retrofit2.converter.moshi.MoshiConverterFactory +import java.util.concurrent.TimeUnit + +val apiModule = module { + + single(createdAtStart = true) { + OkHttpClient.Builder() + .callTimeout(1, TimeUnit.MINUTES) + .readTimeout(1, TimeUnit.HOURS) + .addInterceptor(HttpManager.getInstance().AuthInterceptor()) + .build() + } + + single { LocalRSSDataSource(get()) } + + //region freshrss + + single { + FreshRSSDataSource(get()) + } + + single { + get(named("freshrssRetrofit")) + .create(FreshRSSService::class.java) + } + + single(named("freshrssRetrofit")) { + get() + .addConverterFactory(MoshiConverterFactory.create(get(named("freshrssMoshi")))) + .build() + } + + single(named("freshrssMoshi")) { + Moshi.Builder() + .add(Types.newParameterizedType(List::class.java, Item::class.java), FreshRSSItemsAdapter()) + .add(FreshRSSFeedsAdapter()) + .add(FreshRSSFoldersAdapter()) + .build() + } + + //endregion freshrss + + //region nextcloud news + + single { + NextNewsDataSource(get()) + } + + single { + get(named("nextcloudNewsRetrofit")) + .create(NextNewsService::class.java) + } + + single(named("nextcloudNewsRetrofit")) { + get() + .addConverterFactory(MoshiConverterFactory.create(get(named("nextcloudNewsMoshi")))) + .build() + } + + single(named("nextcloudNewsMoshi")) { + Moshi.Builder() + .add(NextNewsFeedsAdapter()) + .add(NextNewsFoldersAdapter()) + .add(Types.newParameterizedType(List::class.java, Item::class.java), NextNewsItemsAdapter()) + .build() + } + + //endregion nextcloud news + + single { + Retrofit.Builder() // url will be set dynamically in an interceptor + .baseUrl("https://baseurl.com") + .addCallAdapterFactory(RxJava2CallAdapterFactory.create()) + .client(get()) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/readrops/app/AppModule.kt b/app/src/main/java/com/readrops/app/AppModule.kt new file mode 100644 index 00000000..7ea2af78 --- /dev/null +++ b/app/src/main/java/com/readrops/app/AppModule.kt @@ -0,0 +1,45 @@ +package com.readrops.app + +import com.readrops.app.repositories.FreshRSSRepository +import com.readrops.app.repositories.LocalFeedRepository +import com.readrops.app.repositories.NextNewsRepository +import com.readrops.app.viewmodels.* +import com.readrops.db.entities.account.Account +import com.readrops.db.entities.account.AccountType +import org.koin.android.ext.koin.androidApplication +import org.koin.android.ext.koin.androidContext +import org.koin.androidx.viewmodel.dsl.viewModel +import org.koin.dsl.module + +val appModule = module { + + factory { (account: Account) -> + when (account.accountType) { + AccountType.LOCAL -> LocalFeedRepository(get(), get(), androidContext(), account) + AccountType.NEXTCLOUD_NEWS -> NextNewsRepository(get(), get(), androidContext(), account) + AccountType.FRESHRSS -> FreshRSSRepository(get(), get(), androidContext(), account) + else -> throw IllegalArgumentException("Account type not supported") + } + } + + viewModel { + MainViewModel(androidApplication()) + } + + viewModel { + AddFeedsViewModel(androidApplication()) + } + + viewModel { + ItemViewModel(androidApplication()) + } + + viewModel { + ManageFeedsFoldersViewModel(androidApplication()) + } + + viewModel { + NotificationPermissionViewModel(androidApplication()) + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/readrops/app/ReadropsApp.java b/app/src/main/java/com/readrops/app/ReadropsApp.java deleted file mode 100644 index b2a9c58a..00000000 --- a/app/src/main/java/com/readrops/app/ReadropsApp.java +++ /dev/null @@ -1,61 +0,0 @@ -package com.readrops.app; - -import android.annotation.SuppressLint; -import android.app.Application; -import android.app.NotificationChannel; -import android.app.NotificationManager; -import android.os.Build; - -import androidx.appcompat.app.AppCompatDelegate; -import androidx.preference.PreferenceManager; - -import com.readrops.app.utils.SharedPreferencesManager; - -import io.reactivex.plugins.RxJavaPlugins; - -@SuppressLint("Registered") -public class ReadropsApp extends Application { - - public static final String FEEDS_COLORS_CHANNEL_ID = "feedsColorsChannel"; - public static final String OPML_EXPORT_CHANNEL_ID = "opmlExportChannel"; - public static final String SYNC_CHANNEL_ID = "syncChannel"; - - @Override - public void onCreate() { - super.onCreate(); - - RxJavaPlugins.setErrorHandler(e -> { - }); - - createNotificationChannels(); - - PreferenceManager.setDefaultValues(this, R.xml.preferences, false); - - if (Boolean.valueOf(SharedPreferencesManager.readString(this, SharedPreferencesManager.SharedPrefKey.DARK_THEME))) - AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES); - else - AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO); - } - - private void createNotificationChannels() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - NotificationChannel feedsColorsChannel = new NotificationChannel(FEEDS_COLORS_CHANNEL_ID, - getString(R.string.feeds_colors), NotificationManager.IMPORTANCE_DEFAULT); - feedsColorsChannel.setDescription(getString(R.string.get_feeds_colors)); - - NotificationChannel opmlExportChannel = new NotificationChannel(OPML_EXPORT_CHANNEL_ID, - getString(R.string.opml_export), NotificationManager.IMPORTANCE_DEFAULT); - opmlExportChannel.setDescription(getString(R.string.opml_export_description)); - - NotificationChannel syncChannel = new NotificationChannel(SYNC_CHANNEL_ID, - getString(R.string.auto_synchro), NotificationManager.IMPORTANCE_LOW); - syncChannel.setDescription(getString(R.string.account_synchro)); - - NotificationManager manager = getSystemService(NotificationManager.class); - - manager.createNotificationChannel(feedsColorsChannel); - manager.createNotificationChannel(opmlExportChannel); - manager.createNotificationChannel(syncChannel); - } - } -} diff --git a/app/src/main/java/com/readrops/app/ReadropsApp.kt b/app/src/main/java/com/readrops/app/ReadropsApp.kt new file mode 100644 index 00000000..8d856bd8 --- /dev/null +++ b/app/src/main/java/com/readrops/app/ReadropsApp.kt @@ -0,0 +1,68 @@ +package com.readrops.app + +import android.app.Application +import android.app.NotificationChannel +import android.app.NotificationManager +import android.os.Build +import androidx.appcompat.app.AppCompatDelegate +import androidx.preference.PreferenceManager +import com.readrops.api.apiModule +import com.readrops.app.utils.SharedPreferencesManager +import com.readrops.db.dbModule +import io.reactivex.plugins.RxJavaPlugins +import org.koin.android.ext.koin.androidContext +import org.koin.android.ext.koin.androidLogger +import org.koin.core.context.startKoin +import org.koin.core.logger.Level + +open class ReadropsApp : Application() { + + override fun onCreate() { + super.onCreate() + RxJavaPlugins.setErrorHandler { e: Throwable? -> } + + createNotificationChannels() + PreferenceManager.setDefaultValues(this, R.xml.preferences, false) + + if (SharedPreferencesManager.readString(this, SharedPreferencesManager.SharedPrefKey.DARK_THEME).toBoolean()) + AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES) + else + AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO) + + startKoin { + androidLogger(Level.ERROR) + androidContext(this@ReadropsApp) + + modules(apiModule, dbModule, appModule) + } + } + + private fun createNotificationChannels() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + val feedsColorsChannel = NotificationChannel(FEEDS_COLORS_CHANNEL_ID, + getString(R.string.feeds_colors), NotificationManager.IMPORTANCE_DEFAULT) + feedsColorsChannel.description = getString(R.string.get_feeds_colors) + + val opmlExportChannel = NotificationChannel(OPML_EXPORT_CHANNEL_ID, + getString(R.string.opml_export), NotificationManager.IMPORTANCE_DEFAULT) + opmlExportChannel.description = getString(R.string.opml_export_description) + + val syncChannel = NotificationChannel(SYNC_CHANNEL_ID, + getString(R.string.auto_synchro), NotificationManager.IMPORTANCE_LOW) + syncChannel.description = getString(R.string.account_synchro) + + val manager = getSystemService(NotificationManager::class.java)!! + + manager.createNotificationChannel(feedsColorsChannel) + manager.createNotificationChannel(opmlExportChannel) + manager.createNotificationChannel(syncChannel) + } + } + + companion object { + const val FEEDS_COLORS_CHANNEL_ID = "feedsColorsChannel" + const val OPML_EXPORT_CHANNEL_ID = "opmlExportChannel" + const val SYNC_CHANNEL_ID = "syncChannel" + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/readrops/app/viewmodels/MainViewModel.java b/app/src/main/java/com/readrops/app/viewmodels/MainViewModel.java index 39ce3359..417dcb93 100644 --- a/app/src/main/java/com/readrops/app/viewmodels/MainViewModel.java +++ b/app/src/main/java/com/readrops/app/viewmodels/MainViewModel.java @@ -9,6 +9,7 @@ import androidx.lifecycle.MediatorLiveData; import androidx.paging.LivePagedListBuilder; import androidx.paging.PagedList; +import com.readrops.app.repositories.ARepository; import com.readrops.db.Database; import com.readrops.db.ItemsListQueryBuilder; import com.readrops.db.RoomFactoryWrapper; @@ -18,7 +19,9 @@ import com.readrops.db.entities.account.Account; import com.readrops.db.filters.FilterType; import com.readrops.db.filters.ListSortType; import com.readrops.db.pojo.ItemWithFeed; -import com.readrops.app.repositories.ARepository; + +import org.koin.core.parameter.DefinitionParametersKt; +import org.koin.java.KoinJavaComponent; import java.util.ArrayList; import java.util.Arrays; @@ -59,7 +62,8 @@ public class MainViewModel extends AndroidViewModel { private void setRepository() { try { - repository = ARepository.repositoryFactory(currentAccount, getApplication()); + repository = KoinJavaComponent.get(ARepository.class, null, + () -> DefinitionParametersKt.parametersOf(currentAccount)); } catch (Exception e) { e.printStackTrace(); } diff --git a/db/build.gradle b/db/build.gradle index 1cb655c4..3f6ddabe 100644 --- a/db/build.gradle +++ b/db/build.gradle @@ -69,4 +69,9 @@ dependencies { api 'androidx.paging:paging-common:2.1.2' api 'joda-time:joda-time:2.10.5' + + def koin_version = "2.1.6" + api "org.koin:koin-android:$koin_version" + api "org.koin:koin-androidx-scope:$koin_version" + api "org.koin:koin-androidx-viewmodel:$koin_version" } diff --git a/db/src/main/java/com/readrops/db/DbModule.kt b/db/src/main/java/com/readrops/db/DbModule.kt new file mode 100644 index 00000000..031f9361 --- /dev/null +++ b/db/src/main/java/com/readrops/db/DbModule.kt @@ -0,0 +1,13 @@ +package com.readrops.db + +import androidx.room.Room +import org.koin.dsl.module + +val dbModule = module { + + single(createdAtStart = true) { + Room.databaseBuilder(get(), Database::class.java, "readrops-db") + .addMigrations(Database.MIGRATION_1_2) + .build() + } +} \ No newline at end of file From 49c5ba06b3c3a59f06b1b495d8b92c4a76d3aa18 Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Thu, 15 Oct 2020 19:04:50 +0200 Subject: [PATCH 086/187] Inject AuthInterceptor to deal with retrofit dynamic url and credentials --- .../main/java/com/readrops/api/ApiModule.kt | 8 ++- .../java/com/readrops/api/services/API.java | 61 ------------------- .../readrops/api/services/Credentials.java | 31 +++++++--- .../com/readrops/api/utils/AuthInterceptor.kt | 35 +++++++++++ .../com/readrops/api/utils/HttpManager.java | 25 -------- .../app/repositories/ARepository.java | 9 ++- 6 files changed, 71 insertions(+), 98 deletions(-) delete mode 100644 api/src/main/java/com/readrops/api/services/API.java create mode 100644 api/src/main/java/com/readrops/api/utils/AuthInterceptor.kt diff --git a/api/src/main/java/com/readrops/api/ApiModule.kt b/api/src/main/java/com/readrops/api/ApiModule.kt index a1f73155..81d9b800 100644 --- a/api/src/main/java/com/readrops/api/ApiModule.kt +++ b/api/src/main/java/com/readrops/api/ApiModule.kt @@ -11,7 +11,7 @@ import com.readrops.api.services.nextcloudnews.NextNewsService import com.readrops.api.services.nextcloudnews.adapters.NextNewsFeedsAdapter import com.readrops.api.services.nextcloudnews.adapters.NextNewsFoldersAdapter import com.readrops.api.services.nextcloudnews.adapters.NextNewsItemsAdapter -import com.readrops.api.utils.HttpManager +import com.readrops.api.utils.AuthInterceptor import com.readrops.db.entities.Item import com.squareup.moshi.Moshi import com.squareup.moshi.Types @@ -29,7 +29,7 @@ val apiModule = module { OkHttpClient.Builder() .callTimeout(1, TimeUnit.MINUTES) .readTimeout(1, TimeUnit.HOURS) - .addInterceptor(HttpManager.getInstance().AuthInterceptor()) + .addInterceptor(get()) .build() } @@ -95,4 +95,8 @@ val apiModule = module { .addCallAdapterFactory(RxJava2CallAdapterFactory.create()) .client(get()) } + + single { + AuthInterceptor() + } } \ No newline at end of file diff --git a/api/src/main/java/com/readrops/api/services/API.java b/api/src/main/java/com/readrops/api/services/API.java deleted file mode 100644 index d86e938a..00000000 --- a/api/src/main/java/com/readrops/api/services/API.java +++ /dev/null @@ -1,61 +0,0 @@ -package com.readrops.api.services; - -import androidx.annotation.NonNull; - -import com.readrops.api.utils.HttpManager; -import com.squareup.moshi.Moshi; - -import retrofit2.Retrofit; -import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory; -import retrofit2.converter.moshi.MoshiConverterFactory; - -/** - * Abstraction level for services APIs - * - * @param an API service interface - */ -public abstract class API { - - protected static final int MAX_ITEMS = 5000; - - protected T api; - private Retrofit retrofit; - - private Class clazz; - private String endPoint; - - public API(Credentials credentials, @NonNull Class clazz, @NonNull String endPoint) { - this.clazz = clazz; - this.endPoint = endPoint; - - api = createAPI(credentials); - } - - protected abstract Moshi buildMoshi(); - - protected Retrofit getConfiguredRetrofitInstance() { - return new Retrofit.Builder() - .baseUrl(HttpManager.getInstance().getCredentials().getUrl() + endPoint) - .addConverterFactory(MoshiConverterFactory.create(buildMoshi())) - .addCallAdapterFactory(RxJava2CallAdapterFactory.create()) - .client(HttpManager.getInstance().getOkHttpClient()) - .build(); - } - - private T createAPI(@NonNull Credentials credentials) { - HttpManager.getInstance().setCredentials(credentials); - retrofit = getConfiguredRetrofitInstance(); - - return retrofit.create(clazz); - } - - public void setCredentials(@NonNull Credentials credentials) { - HttpManager.getInstance().setCredentials(credentials); - - retrofit = retrofit.newBuilder() - .baseUrl(credentials.getUrl() + endPoint) - .build(); - - api = retrofit.create(clazz); - } -} diff --git a/api/src/main/java/com/readrops/api/services/Credentials.java b/api/src/main/java/com/readrops/api/services/Credentials.java index 53fee513..b77df749 100644 --- a/api/src/main/java/com/readrops/api/services/Credentials.java +++ b/api/src/main/java/com/readrops/api/services/Credentials.java @@ -1,16 +1,17 @@ package com.readrops.api.services; -import androidx.annotation.Nullable; - -import com.readrops.db.entities.account.Account; import com.readrops.api.services.freshrss.FreshRSSCredentials; +import com.readrops.api.services.freshrss.FreshRSSService; import com.readrops.api.services.nextcloudnews.NextNewsCredentials; +import com.readrops.api.services.nextcloudnews.NextNewsService; +import com.readrops.db.entities.account.Account; +import com.readrops.db.entities.account.AccountType; public abstract class Credentials { - private String authorization; + private final String authorization; - private String url; + private final String url; public Credentials(String authorization, String url) { this.authorization = authorization; @@ -25,15 +26,27 @@ public abstract class Credentials { return url; } - @Nullable public static Credentials toCredentials(Account account) { + String endPoint = getEndPoint(account.getAccountType()); + switch (account.getAccountType()) { case NEXTCLOUD_NEWS: - return new NextNewsCredentials(account.getLogin(), account.getPassword(), account.getUrl()); + return new NextNewsCredentials(account.getLogin(), account.getPassword(), account.getUrl() + endPoint); case FRESHRSS: - return new FreshRSSCredentials(account.getToken(), account.getUrl()); + return new FreshRSSCredentials(account.getToken(), account.getUrl() + endPoint); default: - return null; + throw new IllegalArgumentException("Unknown account type"); + } + } + + private static String getEndPoint(AccountType accountType) { + switch (accountType) { + case FRESHRSS: + return FreshRSSService.END_POINT; + case NEXTCLOUD_NEWS: + return NextNewsService.END_POINT; + default: + throw new IllegalArgumentException("Unknown account type"); } } } diff --git a/api/src/main/java/com/readrops/api/utils/AuthInterceptor.kt b/api/src/main/java/com/readrops/api/utils/AuthInterceptor.kt new file mode 100644 index 00000000..76828f4b --- /dev/null +++ b/api/src/main/java/com/readrops/api/utils/AuthInterceptor.kt @@ -0,0 +1,35 @@ +package com.readrops.api.utils + +import android.net.Uri +import com.readrops.api.services.Credentials +import com.readrops.api.services.freshrss.FreshRSSService +import com.readrops.api.services.nextcloudnews.NextNewsService +import com.readrops.db.entities.account.Account +import com.readrops.db.entities.account.AccountType +import okhttp3.Interceptor +import okhttp3.Response +import java.lang.IllegalArgumentException + +class AuthInterceptor(var credentials: Credentials? = null) : Interceptor { + + override fun intercept(chain: Interceptor.Chain): Response { + val requestBuilder = chain.request().newBuilder() + val urlBuilder = chain.request().url.newBuilder() + + if (credentials != null) { + if (credentials!!.url != null) { + val uri = Uri.parse(credentials!!.url) + urlBuilder + .scheme(uri.scheme!!) + .host(uri.host!!) + .encodedPath(uri.encodedPath + chain.request().url.encodedPath) + } + + if (credentials!!.authorization != null) { + requestBuilder.addHeader("Authorization", credentials!!.authorization) + } + } + + return chain.proceed(requestBuilder.url(urlBuilder.build()).build()) + } +} \ No newline at end of file diff --git a/api/src/main/java/com/readrops/api/utils/HttpManager.java b/api/src/main/java/com/readrops/api/utils/HttpManager.java index 0f4605e0..5b1cd8c5 100644 --- a/api/src/main/java/com/readrops/api/utils/HttpManager.java +++ b/api/src/main/java/com/readrops/api/utils/HttpManager.java @@ -4,13 +4,9 @@ import androidx.annotation.Nullable; import com.readrops.api.services.Credentials; -import java.io.IOException; import java.util.concurrent.TimeUnit; -import okhttp3.Interceptor; import okhttp3.OkHttpClient; -import okhttp3.Request; -import okhttp3.Response; public class HttpManager { @@ -25,7 +21,6 @@ public class HttpManager { okHttpClient = new OkHttpClient.Builder() .callTimeout(1, TimeUnit.MINUTES) .readTimeout(1, TimeUnit.HOURS) - .addInterceptor(new AuthInterceptor()) .build(); } @@ -54,24 +49,4 @@ public class HttpManager { public static void setInstance(OkHttpClient client) { instance.okHttpClient = client; } - - public class AuthInterceptor implements Interceptor { - - public AuthInterceptor() { - // empty constructor - } - - @Override - public Response intercept(Chain chain) throws IOException { - Request request = chain.request(); - - if (credentials != null && credentials.getAuthorization() != null) { - request = request.newBuilder() - .addHeader("Authorization", credentials.getAuthorization()) - .build(); - } - - return chain.proceed(request); - } - } } diff --git a/app/src/main/java/com/readrops/app/repositories/ARepository.java b/app/src/main/java/com/readrops/app/repositories/ARepository.java index 88c3f224..694c3289 100644 --- a/app/src/main/java/com/readrops/app/repositories/ARepository.java +++ b/app/src/main/java/com/readrops/app/repositories/ARepository.java @@ -6,6 +6,9 @@ import android.content.Intent; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import com.readrops.api.services.Credentials; +import com.readrops.api.services.SyncResult; +import com.readrops.api.utils.AuthInterceptor; import com.readrops.app.utils.FeedInsertionResult; import com.readrops.app.utils.ParsingResult; import com.readrops.app.utils.feedscolors.FeedColorsKt; @@ -16,7 +19,8 @@ import com.readrops.db.entities.Folder; import com.readrops.db.entities.Item; import com.readrops.db.entities.account.Account; import com.readrops.db.entities.account.AccountType; -import com.readrops.api.services.SyncResult; + +import org.koin.java.KoinJavaComponent; import java.util.ArrayList; import java.util.Comparator; @@ -42,6 +46,9 @@ public abstract class ARepository { this.context = context; this.database = database; this.account = account; + + KoinJavaComponent.get(AuthInterceptor.class) + .setCredentials(account != null && !account.isLocal() ? Credentials.toCredentials(account) : null); } // TODO : replace Single by Completable From 950c8506ad12c7cefcca7f89329ed10abd5baf61 Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Fri, 16 Oct 2020 11:58:00 +0200 Subject: [PATCH 087/187] Inject viewModels in activities and fragments --- .../main/java/com/readrops/app/AppModule.kt | 14 ++++--- .../activities/AccountTypeListActivity.java | 7 ++-- .../app/activities/AddAccountActivity.java | 5 ++- .../app/activities/AddFeedActivity.java | 5 ++- .../readrops/app/activities/ItemActivity.java | 9 ++-- .../readrops/app/activities/MainActivity.java | 4 +- .../ManageFeedsFoldersActivity.java | 9 ++-- .../NotificationPermissionActivity.kt | 3 +- .../app/activities/SplashActivity.java | 5 ++- .../app/fragments/EditFeedDialogFragment.java | 5 ++- .../readrops/app/fragments/FeedsFragment.java | 5 ++- .../app/fragments/FoldersFragment.java | 9 ++-- .../settings/AccountSettingsFragment.java | 7 ++-- .../app/viewmodels/AccountViewModel.java | 36 ++++++++-------- .../app/viewmodels/AddFeedsViewModel.java | 38 +++++++---------- .../app/viewmodels/ItemViewModel.java | 22 +++++----- .../app/viewmodels/MainViewModel.java | 41 ++++++++----------- .../ManageFeedsFoldersViewModel.java | 34 +++++++-------- .../NotificationPermissionViewModel.kt | 6 +-- 19 files changed, 124 insertions(+), 140 deletions(-) diff --git a/app/src/main/java/com/readrops/app/AppModule.kt b/app/src/main/java/com/readrops/app/AppModule.kt index 7ea2af78..88f3accd 100644 --- a/app/src/main/java/com/readrops/app/AppModule.kt +++ b/app/src/main/java/com/readrops/app/AppModule.kt @@ -23,23 +23,27 @@ val appModule = module { } viewModel { - MainViewModel(androidApplication()) + MainViewModel(get()) } viewModel { - AddFeedsViewModel(androidApplication()) + AddFeedsViewModel(get(), get()) } viewModel { - ItemViewModel(androidApplication()) + ItemViewModel(get()) } viewModel { - ManageFeedsFoldersViewModel(androidApplication()) + ManageFeedsFoldersViewModel(get()) } viewModel { - NotificationPermissionViewModel(androidApplication()) + NotificationPermissionViewModel(get()) + } + + viewModel { + AccountViewModel(get()) } } \ No newline at end of file diff --git a/app/src/main/java/com/readrops/app/activities/AccountTypeListActivity.java b/app/src/main/java/com/readrops/app/activities/AccountTypeListActivity.java index 3af60e23..1ae20d00 100644 --- a/app/src/main/java/com/readrops/app/activities/AccountTypeListActivity.java +++ b/app/src/main/java/com/readrops/app/activities/AccountTypeListActivity.java @@ -11,7 +11,6 @@ import android.widget.LinearLayout; import androidx.annotation.Nullable; import androidx.appcompat.app.AppCompatActivity; -import androidx.lifecycle.ViewModelProvider; import androidx.recyclerview.widget.DividerItemDecoration; import androidx.recyclerview.widget.LinearLayoutManager; @@ -25,6 +24,8 @@ import com.readrops.app.viewmodels.AccountViewModel; import com.readrops.db.entities.account.Account; import com.readrops.db.entities.account.AccountType; +import org.koin.androidx.viewmodel.compat.ViewModelCompat; + import java.util.ArrayList; import java.util.List; @@ -55,7 +56,7 @@ public class AccountTypeListActivity extends AppCompatActivity { binding = ActivityAccountTypeListBinding.inflate(getLayoutInflater()); setContentView(binding.getRoot()); - viewModel = new ViewModelProvider(this).get(AccountViewModel.class); + viewModel = ViewModelCompat.getViewModel(this, AccountViewModel.class); setTitle(R.string.new_account); @@ -158,7 +159,7 @@ public class AccountTypeListActivity extends AppCompatActivity { account.setId(id.intValue()); viewModel.setAccount(account); - return viewModel.parseOPMLFile(uri); + return viewModel.parseOPMLFile(uri, this); }) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) diff --git a/app/src/main/java/com/readrops/app/activities/AddAccountActivity.java b/app/src/main/java/com/readrops/app/activities/AddAccountActivity.java index c9167725..e72596ce 100644 --- a/app/src/main/java/com/readrops/app/activities/AddAccountActivity.java +++ b/app/src/main/java/com/readrops/app/activities/AddAccountActivity.java @@ -8,7 +8,6 @@ import android.view.MenuItem; import android.view.View; import androidx.appcompat.app.AppCompatActivity; -import androidx.lifecycle.ViewModelProvider; import com.readrops.app.R; import com.readrops.app.databinding.ActivityAddAccountBinding; @@ -18,6 +17,8 @@ import com.readrops.app.viewmodels.AccountViewModel; import com.readrops.db.entities.account.Account; import com.readrops.db.entities.account.AccountType; +import org.koin.androidx.viewmodel.compat.ViewModelCompat; + import io.reactivex.Completable; import io.reactivex.CompletableObserver; import io.reactivex.SingleObserver; @@ -46,7 +47,7 @@ public class AddAccountActivity extends AppCompatActivity { binding = ActivityAddAccountBinding.inflate(getLayoutInflater()); setContentView(binding.getRoot()); - viewModel = new ViewModelProvider(this).get(AccountViewModel.class); + viewModel = ViewModelCompat.getViewModel(this, AccountViewModel.class); accountType = getIntent().getParcelableExtra(ACCOUNT_TYPE); diff --git a/app/src/main/java/com/readrops/app/activities/AddFeedActivity.java b/app/src/main/java/com/readrops/app/activities/AddFeedActivity.java index 9e231ff6..2973ecc5 100644 --- a/app/src/main/java/com/readrops/app/activities/AddFeedActivity.java +++ b/app/src/main/java/com/readrops/app/activities/AddFeedActivity.java @@ -11,7 +11,6 @@ import android.view.View; import androidx.annotation.Nullable; import androidx.appcompat.app.AppCompatActivity; -import androidx.lifecycle.ViewModelProvider; import androidx.recyclerview.widget.DiffUtil; import androidx.recyclerview.widget.ItemTouchHelper; import androidx.recyclerview.widget.LinearLayoutManager; @@ -33,6 +32,8 @@ import com.readrops.app.viewmodels.AddFeedsViewModel; import com.readrops.db.entities.Feed; import com.readrops.db.entities.account.Account; +import org.koin.androidx.viewmodel.compat.ViewModelCompat; + import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -69,7 +70,7 @@ public class AddFeedActivity extends AppCompatActivity implements View.OnClickLi binding.addFeedOk.setOnClickListener(this); binding.addFeedOk.setEnabled(false); - viewModel = new ViewModelProvider(this).get(AddFeedsViewModel.class); + viewModel = ViewModelCompat.getViewModel(this, AddFeedsViewModel.class); parseItemsAdapter = new ItemAdapter<>(); fastAdapter = FastAdapter.with(parseItemsAdapter); diff --git a/app/src/main/java/com/readrops/app/activities/ItemActivity.java b/app/src/main/java/com/readrops/app/activities/ItemActivity.java index 83e56894..d7dfa2f4 100644 --- a/app/src/main/java/com/readrops/app/activities/ItemActivity.java +++ b/app/src/main/java/com/readrops/app/activities/ItemActivity.java @@ -25,15 +25,14 @@ import androidx.appcompat.app.AppCompatActivity; import androidx.browser.customtabs.CustomTabsIntent; import androidx.core.app.ActivityCompat; import androidx.core.app.ShareCompat; -import androidx.lifecycle.ViewModelProvider; import com.afollestad.materialdialogs.MaterialDialog; import com.bumptech.glide.load.engine.DiskCacheStrategy; import com.bumptech.glide.request.target.CustomTarget; import com.bumptech.glide.request.transition.Transition; +import com.readrops.api.utils.DateUtils; import com.readrops.app.R; import com.readrops.app.databinding.ActivityItemBinding; -import com.readrops.api.utils.DateUtils; import com.readrops.app.utils.GlideApp; import com.readrops.app.utils.PermissionManager; import com.readrops.app.utils.SharedPreferencesManager; @@ -42,6 +41,8 @@ import com.readrops.app.viewmodels.ItemViewModel; import com.readrops.db.entities.Item; import com.readrops.db.pojo.ItemWithFeed; +import org.koin.androidx.viewmodel.compat.ViewModelCompat; + import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -110,7 +111,7 @@ public class ItemActivity extends AppCompatActivity { invalidateOptionsMenu(); })); - viewModel = new ViewModelProvider(this).get(ItemViewModel.class); + viewModel = ViewModelCompat.getViewModel(this, ItemViewModel.class); viewModel.getItemById(itemId).observe(this, this::bindUI); binding.activityItemFab.setOnClickListener(v -> openInNavigator()); } @@ -365,7 +366,7 @@ public class ItemActivity extends AppCompatActivity { @Override public void onResourceReady(@NonNull Bitmap resource, @Nullable Transition transition) { try { - Uri uri = viewModel.saveImageInCache(resource); + Uri uri = viewModel.saveImageInCache(resource, ItemActivity.this); Intent intent = ShareCompat.IntentBuilder.from(ItemActivity.this) .setType("image/png") .setStream(uri) diff --git a/app/src/main/java/com/readrops/app/activities/MainActivity.java b/app/src/main/java/com/readrops/app/activities/MainActivity.java index 0cb7faf9..c2102f2c 100644 --- a/app/src/main/java/com/readrops/app/activities/MainActivity.java +++ b/app/src/main/java/com/readrops/app/activities/MainActivity.java @@ -16,7 +16,6 @@ import androidx.appcompat.app.AppCompatActivity; import androidx.core.content.ContextCompat; import androidx.core.graphics.drawable.DrawableCompat; import androidx.drawerlayout.widget.DrawerLayout; -import androidx.lifecycle.ViewModelProvider; import androidx.paging.PagedList; import androidx.recyclerview.widget.DividerItemDecoration; import androidx.recyclerview.widget.ItemTouchHelper; @@ -51,6 +50,7 @@ import com.readrops.db.filters.ListSortType; import com.readrops.db.pojo.ItemWithFeed; import org.jetbrains.annotations.NotNull; +import org.koin.androidx.viewmodel.compat.ViewModelCompat; import java.lang.ref.WeakReference; import java.util.Collections; @@ -116,7 +116,7 @@ public class MainActivity extends AppCompatActivity implements SwipeRefreshLayou feedCount = 0; initRecyclerView(); - viewModel = new ViewModelProvider(this).get(MainViewModel.class); + viewModel = ViewModelCompat.getViewModel(this, MainViewModel.class); viewModel.setShowReadItems(SharedPreferencesManager.readBoolean(this, SharedPreferencesManager.SharedPrefKey.SHOW_READ_ARTICLES)); diff --git a/app/src/main/java/com/readrops/app/activities/ManageFeedsFoldersActivity.java b/app/src/main/java/com/readrops/app/activities/ManageFeedsFoldersActivity.java index 4b833b78..9404647a 100644 --- a/app/src/main/java/com/readrops/app/activities/ManageFeedsFoldersActivity.java +++ b/app/src/main/java/com/readrops/app/activities/ManageFeedsFoldersActivity.java @@ -9,9 +9,10 @@ import androidx.appcompat.app.AppCompatActivity; import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentManager; import androidx.fragment.app.FragmentPagerAdapter; -import androidx.lifecycle.ViewModelProvider; import com.afollestad.materialdialogs.MaterialDialog; +import com.readrops.api.utils.ConflictException; +import com.readrops.api.utils.UnknownFormatException; import com.readrops.app.R; import com.readrops.app.databinding.ActivityManageFeedsFoldersBinding; import com.readrops.app.fragments.FeedsFragment; @@ -20,8 +21,8 @@ import com.readrops.app.utils.Utils; import com.readrops.app.viewmodels.ManageFeedsFoldersViewModel; import com.readrops.db.entities.Folder; import com.readrops.db.entities.account.Account; -import com.readrops.api.utils.ConflictException; -import com.readrops.api.utils.UnknownFormatException; + +import org.koin.androidx.viewmodel.compat.ViewModelCompat; import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.schedulers.Schedulers; @@ -52,7 +53,7 @@ public class ManageFeedsFoldersActivity extends AppCompatActivity { binding.manageFeedsFoldersViewpager.setAdapter(pageAdapter); binding.manageFeedsFoldersTablayout.setupWithViewPager(binding.manageFeedsFoldersViewpager); - viewModel = new ViewModelProvider(this).get(ManageFeedsFoldersViewModel.class); + viewModel = ViewModelCompat.getViewModel(this, ManageFeedsFoldersViewModel.class); viewModel.setAccount(account); } diff --git a/app/src/main/java/com/readrops/app/activities/NotificationPermissionActivity.kt b/app/src/main/java/com/readrops/app/activities/NotificationPermissionActivity.kt index 511de722..4c6e0c3d 100644 --- a/app/src/main/java/com/readrops/app/activities/NotificationPermissionActivity.kt +++ b/app/src/main/java/com/readrops/app/activities/NotificationPermissionActivity.kt @@ -20,11 +20,12 @@ import com.readrops.db.entities.Feed import com.readrops.db.entities.account.Account import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.schedulers.Schedulers +import org.koin.androidx.viewmodel.ext.android.getViewModel class NotificationPermissionActivity : AppCompatActivity() { private lateinit var binding: ActivityNotificationPermissionBinding - private val viewModel by viewModels() + private val viewModel = getViewModel() private var adapter: NotificationPermissionListAdapter? = null private var isFirstCheck = true diff --git a/app/src/main/java/com/readrops/app/activities/SplashActivity.java b/app/src/main/java/com/readrops/app/activities/SplashActivity.java index ef74650b..33edd643 100644 --- a/app/src/main/java/com/readrops/app/activities/SplashActivity.java +++ b/app/src/main/java/com/readrops/app/activities/SplashActivity.java @@ -4,11 +4,12 @@ import android.content.Intent; import android.os.Bundle; import androidx.appcompat.app.AppCompatActivity; -import androidx.lifecycle.ViewModelProvider; import com.readrops.app.R; import com.readrops.app.viewmodels.AccountViewModel; +import org.koin.androidx.viewmodel.compat.ViewModelCompat; + import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.observers.DisposableSingleObserver; import io.reactivex.schedulers.Schedulers; @@ -22,7 +23,7 @@ public class SplashActivity extends AppCompatActivity { super.onCreate(savedInstanceState); setContentView(R.layout.activity_splash); - viewModel = new ViewModelProvider(this).get(AccountViewModel.class); + viewModel = ViewModelCompat.getViewModel(this, AccountViewModel.class); viewModel.getAccountCount() .subscribeOn(Schedulers.io()) diff --git a/app/src/main/java/com/readrops/app/fragments/EditFeedDialogFragment.java b/app/src/main/java/com/readrops/app/fragments/EditFeedDialogFragment.java index 16bfc365..5fc3c2f6 100644 --- a/app/src/main/java/com/readrops/app/fragments/EditFeedDialogFragment.java +++ b/app/src/main/java/com/readrops/app/fragments/EditFeedDialogFragment.java @@ -11,7 +11,6 @@ import android.widget.Spinner; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.fragment.app.DialogFragment; -import androidx.lifecycle.ViewModelProvider; import com.google.android.material.textfield.TextInputEditText; import com.readrops.app.R; @@ -21,6 +20,8 @@ import com.readrops.db.entities.Folder; import com.readrops.db.entities.account.Account; import com.readrops.db.pojo.FeedWithFolder; +import org.koin.androidx.viewmodel.compat.SharedViewModelCompat; + import java.util.ArrayList; import java.util.Map; import java.util.TreeMap; @@ -59,7 +60,7 @@ public class EditFeedDialogFragment extends DialogFragment implements AdapterVie @NonNull @Override public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) { - viewModel = new ViewModelProvider(getActivity()).get(ManageFeedsFoldersViewModel.class); + viewModel = SharedViewModelCompat.getSharedViewModel(this, ManageFeedsFoldersViewModel.class); feedWithFolder = getArguments().getParcelable("feedWithFolder"); account = getArguments().getParcelable(ACCOUNT); diff --git a/app/src/main/java/com/readrops/app/fragments/FeedsFragment.java b/app/src/main/java/com/readrops/app/fragments/FeedsFragment.java index f07f66b9..82dcf0a9 100644 --- a/app/src/main/java/com/readrops/app/fragments/FeedsFragment.java +++ b/app/src/main/java/com/readrops/app/fragments/FeedsFragment.java @@ -10,7 +10,6 @@ import android.view.ViewGroup; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.fragment.app.Fragment; -import androidx.lifecycle.ViewModelProvider; import androidx.recyclerview.widget.LinearLayoutManager; import com.afollestad.materialdialogs.MaterialDialog; @@ -24,6 +23,8 @@ import com.readrops.db.entities.Feed; import com.readrops.db.entities.account.Account; import com.readrops.db.pojo.FeedWithFolder; +import org.koin.androidx.viewmodel.compat.SharedViewModelCompat; + import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.observers.DisposableCompletableObserver; import io.reactivex.schedulers.Schedulers; @@ -64,7 +65,7 @@ public class FeedsFragment extends Fragment { if (account.getPassword() == null) account.setPassword(SharedPreferencesManager.readString(getContext(), account.getPasswordKey())); - viewModel = new ViewModelProvider(this).get(ManageFeedsFoldersViewModel.class); + viewModel = SharedViewModelCompat.getSharedViewModel(this, ManageFeedsFoldersViewModel.class); viewModel.setAccount(account); viewModel.getFeedsWithFolder().observe(this, feedWithFolders -> { diff --git a/app/src/main/java/com/readrops/app/fragments/FoldersFragment.java b/app/src/main/java/com/readrops/app/fragments/FoldersFragment.java index 9019a09c..9ecb0e63 100644 --- a/app/src/main/java/com/readrops/app/fragments/FoldersFragment.java +++ b/app/src/main/java/com/readrops/app/fragments/FoldersFragment.java @@ -10,10 +10,11 @@ import android.view.ViewGroup; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.fragment.app.Fragment; -import androidx.lifecycle.ViewModelProvider; import androidx.recyclerview.widget.LinearLayoutManager; import com.afollestad.materialdialogs.MaterialDialog; +import com.readrops.api.utils.ConflictException; +import com.readrops.api.utils.UnknownFormatException; import com.readrops.app.R; import com.readrops.app.adapters.FoldersAdapter; import com.readrops.app.databinding.FragmentFoldersBinding; @@ -22,8 +23,8 @@ import com.readrops.app.utils.Utils; import com.readrops.app.viewmodels.ManageFeedsFoldersViewModel; import com.readrops.db.entities.Folder; import com.readrops.db.entities.account.Account; -import com.readrops.api.utils.ConflictException; -import com.readrops.api.utils.UnknownFormatException; + +import org.koin.androidx.viewmodel.compat.SharedViewModelCompat; import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.observers.DisposableSingleObserver; @@ -65,7 +66,7 @@ public class FoldersFragment extends Fragment { account.setPassword(SharedPreferencesManager.readString(getContext(), account.getPasswordKey())); adapter = new FoldersAdapter(this::openFolderOptionsDialog); - viewModel = new ViewModelProvider(this).get(ManageFeedsFoldersViewModel.class); + viewModel = SharedViewModelCompat.getSharedViewModel(this, ManageFeedsFoldersViewModel.class); viewModel.setAccount(account); viewModel.getFeedCountByAccount() diff --git a/app/src/main/java/com/readrops/app/fragments/settings/AccountSettingsFragment.java b/app/src/main/java/com/readrops/app/fragments/settings/AccountSettingsFragment.java index 71f8867b..30d91490 100644 --- a/app/src/main/java/com/readrops/app/fragments/settings/AccountSettingsFragment.java +++ b/app/src/main/java/com/readrops/app/fragments/settings/AccountSettingsFragment.java @@ -16,7 +16,6 @@ import androidx.annotation.Nullable; import androidx.core.app.NotificationCompat; import androidx.core.app.NotificationManagerCompat; import androidx.fragment.app.Fragment; -import androidx.lifecycle.ViewModelProvider; import androidx.preference.Preference; import androidx.preference.PreferenceFragmentCompat; @@ -36,6 +35,8 @@ import com.readrops.app.viewmodels.AccountViewModel; import com.readrops.db.entities.account.Account; import com.readrops.db.entities.account.AccountType; +import org.koin.androidx.viewmodel.compat.ViewModelCompat; + import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.observers.DisposableCompletableObserver; import io.reactivex.schedulers.Schedulers; @@ -136,7 +137,7 @@ public class AccountSettingsFragment extends PreferenceFragmentCompat { public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); - viewModel = new ViewModelProvider(this).get(AccountViewModel.class); + viewModel = ViewModelCompat.getViewModel(this, AccountViewModel.class); viewModel.setAccount(account); } @@ -204,7 +205,7 @@ public class AccountSettingsFragment extends PreferenceFragmentCompat { } private void parseOPMLFile(Uri uri, MaterialDialog dialog) { - viewModel.parseOPMLFile(uri) + viewModel.parseOPMLFile(uri, getContext()) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(new DisposableCompletableObserver() { diff --git a/app/src/main/java/com/readrops/app/viewmodels/AccountViewModel.java b/app/src/main/java/com/readrops/app/viewmodels/AccountViewModel.java index b34aa46c..e93677e0 100644 --- a/app/src/main/java/com/readrops/app/viewmodels/AccountViewModel.java +++ b/app/src/main/java/com/readrops/app/viewmodels/AccountViewModel.java @@ -1,11 +1,10 @@ package com.readrops.app.viewmodels; -import android.app.Application; +import android.content.Context; import android.net.Uri; -import android.util.Log; import androidx.annotation.NonNull; -import androidx.lifecycle.AndroidViewModel; +import androidx.lifecycle.ViewModel; import com.readrops.api.opml.OPMLParser; import com.readrops.app.repositories.ARepository; @@ -15,35 +14,32 @@ import com.readrops.db.entities.Folder; import com.readrops.db.entities.account.Account; import com.readrops.db.entities.account.AccountType; +import org.koin.core.parameter.DefinitionParametersKt; +import org.koin.java.KoinJavaComponent; + import java.util.List; import java.util.Map; import io.reactivex.Completable; import io.reactivex.Single; -public class AccountViewModel extends AndroidViewModel { - - private static final String TAG = AccountViewModel.class.getSimpleName(); +public class AccountViewModel extends ViewModel { private ARepository repository; - private Database database; + private final Database database; - public AccountViewModel(@NonNull Application application) { - super(application); - - database = Database.getInstance(application); + public AccountViewModel(@NonNull Database database) { + this.database = database; } - public void setAccountType(AccountType accountType) throws Exception { - repository = ARepository.repositoryFactory(null, accountType, getApplication()); + public void setAccountType(AccountType accountType) { + repository = KoinJavaComponent.get(ARepository.class, null, + () -> DefinitionParametersKt.parametersOf(new Account(null, null, accountType))); } public void setAccount(Account account) { - try { - repository = ARepository.repositoryFactory(account, getApplication()); - } catch (Exception e) { - Log.e(TAG, e.getMessage()); - } + repository = KoinJavaComponent.get(ARepository.class, null, + () -> DefinitionParametersKt.parametersOf(account)); } public Single login(Account account, boolean insert) { @@ -71,8 +67,8 @@ public class AccountViewModel extends AndroidViewModel { return repository.getFoldersWithFeeds(); } - public Completable parseOPMLFile(Uri uri) { - return OPMLParser.read(uri, getApplication()) + public Completable parseOPMLFile(Uri uri, Context context) { + return OPMLParser.read(uri, context) .flatMapCompletable(foldersAndFeeds -> repository.insertOPMLFoldersAndFeeds(foldersAndFeeds)); } } diff --git a/app/src/main/java/com/readrops/app/viewmodels/AddFeedsViewModel.java b/app/src/main/java/com/readrops/app/viewmodels/AddFeedsViewModel.java index 303fdb0e..905fdd9d 100644 --- a/app/src/main/java/com/readrops/app/viewmodels/AddFeedsViewModel.java +++ b/app/src/main/java/com/readrops/app/viewmodels/AddFeedsViewModel.java @@ -1,14 +1,10 @@ package com.readrops.app.viewmodels; -import android.app.Application; -import android.util.Log; - import androidx.annotation.NonNull; -import androidx.lifecycle.AndroidViewModel; import androidx.lifecycle.LiveData; +import androidx.lifecycle.ViewModel; import com.readrops.api.localfeed.LocalRSSDataSource; -import com.readrops.api.utils.HttpManager; import com.readrops.app.repositories.ARepository; import com.readrops.app.utils.FeedInsertionResult; import com.readrops.app.utils.HtmlParser; @@ -16,42 +12,36 @@ import com.readrops.app.utils.ParsingResult; import com.readrops.db.Database; import com.readrops.db.entities.account.Account; +import org.koin.core.parameter.DefinitionParametersKt; +import org.koin.java.KoinJavaComponent; + import java.util.ArrayList; import java.util.List; import io.reactivex.Single; -public class AddFeedsViewModel extends AndroidViewModel { +public class AddFeedsViewModel extends ViewModel { - private static final String TAG = AddFeedsViewModel.class.getSimpleName(); + private final Database database; + private final LocalRSSDataSource localRSSDataSource; - private ARepository repository; - private Database database; - - public AddFeedsViewModel(@NonNull Application application) { - super(application); - - database = Database.getInstance(application); + public AddFeedsViewModel(@NonNull Database database, @NonNull LocalRSSDataSource localRSSDataSource) { + this.database = database; + this.localRSSDataSource = localRSSDataSource; } public Single> addFeeds(List results, Account account) { - try { - repository = ARepository.repositoryFactory(account, getApplication()); + ARepository repository = KoinJavaComponent.get(ARepository.class, null, + () -> DefinitionParametersKt.parametersOf(account)); - return repository.addFeeds(results); - } catch (Exception e) { - Log.d(TAG, e.getMessage()); - } - - return null; + return repository.addFeeds(results); } public Single> parseUrl(String url) { return Single.create(emitter -> { - LocalRSSDataSource dataSource = new LocalRSSDataSource(HttpManager.getInstance().getOkHttpClient()); List results = new ArrayList<>(); - if (dataSource.isUrlRSSResource(url)) { + if (localRSSDataSource.isUrlRSSResource(url)) { ParsingResult parsingResult = new ParsingResult(url, null); results.add(parsingResult); } else { diff --git a/app/src/main/java/com/readrops/app/viewmodels/ItemViewModel.java b/app/src/main/java/com/readrops/app/viewmodels/ItemViewModel.java index a6243e99..a3a94a25 100644 --- a/app/src/main/java/com/readrops/app/viewmodels/ItemViewModel.java +++ b/app/src/main/java/com/readrops/app/viewmodels/ItemViewModel.java @@ -1,16 +1,15 @@ package com.readrops.app.viewmodels; -import android.app.Application; +import android.content.Context; import android.graphics.Bitmap; import android.net.Uri; import androidx.annotation.NonNull; import androidx.core.content.FileProvider; -import androidx.lifecycle.AndroidViewModel; import androidx.lifecycle.LiveData; +import androidx.lifecycle.ViewModel; import com.readrops.db.Database; -import com.readrops.db.dao.ItemDao; import com.readrops.db.pojo.ItemWithFeed; import java.io.File; @@ -18,22 +17,21 @@ import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStream; -public class ItemViewModel extends AndroidViewModel { +public class ItemViewModel extends ViewModel { - private ItemDao itemDao; + private final Database database; - public ItemViewModel(@NonNull Application application) { - super(application); - itemDao = Database.getInstance(application).itemDao(); + public ItemViewModel(@NonNull Database database) { + this.database = database; } public LiveData getItemById(int id) { - return itemDao.getItemById(id); + return database.itemDao().getItemById(id); } - public Uri saveImageInCache(Bitmap bitmap) throws IOException { - File imagesFolder = new File(getApplication().getCacheDir().getAbsolutePath(), "images"); + public Uri saveImageInCache(Bitmap bitmap, Context context) throws IOException { + File imagesFolder = new File(context.getCacheDir().getAbsolutePath(), "images"); if (!imagesFolder.exists()) imagesFolder.mkdirs(); @@ -45,6 +43,6 @@ public class ItemViewModel extends AndroidViewModel { stream.flush(); stream.close(); - return FileProvider.getUriForFile(getApplication(), getApplication().getPackageName(), image); + return FileProvider.getUriForFile(context, context.getPackageName(), image); } } diff --git a/app/src/main/java/com/readrops/app/viewmodels/MainViewModel.java b/app/src/main/java/com/readrops/app/viewmodels/MainViewModel.java index 417dcb93..d2071308 100644 --- a/app/src/main/java/com/readrops/app/viewmodels/MainViewModel.java +++ b/app/src/main/java/com/readrops/app/viewmodels/MainViewModel.java @@ -1,11 +1,9 @@ package com.readrops.app.viewmodels; -import android.app.Application; - import androidx.annotation.NonNull; -import androidx.lifecycle.AndroidViewModel; import androidx.lifecycle.LiveData; import androidx.lifecycle.MediatorLiveData; +import androidx.lifecycle.ViewModel; import androidx.paging.LivePagedListBuilder; import androidx.paging.PagedList; @@ -34,46 +32,40 @@ import io.reactivex.Single; import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.schedulers.Schedulers; -public class MainViewModel extends AndroidViewModel { +public class MainViewModel extends ViewModel { - private MediatorLiveData> itemsWithFeed; + private final MediatorLiveData> itemsWithFeed; private LiveData> lastFetch; private ARepository repository; - private Database db; + private final Database database; - private ItemsListQueryBuilder queryBuilder; + private final ItemsListQueryBuilder queryBuilder; private Account currentAccount; private List accounts; - public MainViewModel(@NonNull Application application) { - super(application); - + public MainViewModel(@NonNull Database database) { queryBuilder = new ItemsListQueryBuilder(); queryBuilder.setFilterType(FilterType.NO_FILTER); queryBuilder.setSortType(ListSortType.NEWEST_TO_OLDEST); - db = Database.getInstance(application); + this.database = database; itemsWithFeed = new MediatorLiveData<>(); } //region main query private void setRepository() { - try { - repository = KoinJavaComponent.get(ARepository.class, null, - () -> DefinitionParametersKt.parametersOf(currentAccount)); - } catch (Exception e) { - e.printStackTrace(); - } + repository = KoinJavaComponent.get(ARepository.class, null, + () -> DefinitionParametersKt.parametersOf(currentAccount)); } private void buildPagedList() { if (lastFetch != null) itemsWithFeed.removeSource(lastFetch); - lastFetch = new LivePagedListBuilder<>(new RoomFactoryWrapper<>(db.itemDao().selectAll(queryBuilder.getQuery())), + lastFetch = new LivePagedListBuilder<>(new RoomFactoryWrapper<>(database.itemDao().selectAll(queryBuilder.getQuery())), new PagedList.Config.Builder() .setPageSize(100) .setPrefetchDistance(150) @@ -81,7 +73,7 @@ public class MainViewModel extends AndroidViewModel { .build()) .build(); - itemsWithFeed.addSource(lastFetch, itemWithFeeds -> itemsWithFeed.setValue(itemWithFeeds)); + itemsWithFeed.addSource(lastFetch, itemsWithFeed::setValue); } public void invalidate() { @@ -128,7 +120,6 @@ public class MainViewModel extends AndroidViewModel { return repository.getFeedCount(currentAccount.getId()); } - @SuppressWarnings("unchecked") public Single>> getFoldersWithFeeds() { return repository.getFoldersWithFeeds(); } @@ -138,12 +129,12 @@ public class MainViewModel extends AndroidViewModel { //region Account public LiveData> getAllAccounts() { - return db.accountDao().selectAllAsync(); + return database.accountDao().selectAllAsync(); } private Completable deselectOldCurrentAccount(int accountId) { return Completable.create(emitter -> { - db.accountDao().deselectOldCurrentAccount(accountId); + database.accountDao().deselectOldCurrentAccount(accountId); emitter.onComplete(); }); } @@ -174,7 +165,7 @@ public class MainViewModel extends AndroidViewModel { // set the new account as the current one Completable setCurrentAccount = Completable.create(emitter -> { - db.accountDao().setCurrentAccount(currentAccount.getId()); + database.accountDao().setCurrentAccount(currentAccount.getId()); emitter.onComplete(); }); @@ -206,7 +197,7 @@ public class MainViewModel extends AndroidViewModel { } } - if (!currentAccountExists && accounts.size() > 0) { + if (!currentAccountExists && !accounts.isEmpty()) { setCurrentAccount(accounts.get(0)); accounts.get(0).setCurrentAccount(true); } @@ -246,7 +237,7 @@ public class MainViewModel extends AndroidViewModel { } public Completable setItemReadItLater(int itemId) { - return db.itemDao().setReadItLater(itemId); + return database.itemDao().setReadItLater(itemId); } //endregion diff --git a/app/src/main/java/com/readrops/app/viewmodels/ManageFeedsFoldersViewModel.java b/app/src/main/java/com/readrops/app/viewmodels/ManageFeedsFoldersViewModel.java index 7a357449..802006b3 100644 --- a/app/src/main/java/com/readrops/app/viewmodels/ManageFeedsFoldersViewModel.java +++ b/app/src/main/java/com/readrops/app/viewmodels/ManageFeedsFoldersViewModel.java @@ -1,48 +1,44 @@ package com.readrops.app.viewmodels; -import android.app.Application; - import androidx.annotation.NonNull; -import androidx.lifecycle.AndroidViewModel; import androidx.lifecycle.LiveData; +import androidx.lifecycle.ViewModel; +import com.readrops.app.repositories.ARepository; import com.readrops.db.Database; import com.readrops.db.entities.Feed; import com.readrops.db.entities.Folder; import com.readrops.db.entities.account.Account; import com.readrops.db.pojo.FeedWithFolder; import com.readrops.db.pojo.FolderWithFeedCount; -import com.readrops.app.repositories.ARepository; + +import org.koin.core.parameter.DefinitionParametersKt; +import org.koin.java.KoinJavaComponent; import java.util.List; import io.reactivex.Completable; import io.reactivex.Single; -public class ManageFeedsFoldersViewModel extends AndroidViewModel { +public class ManageFeedsFoldersViewModel extends ViewModel { - private Database db; + private final Database database; private LiveData> feedsWithFolder; private LiveData> folders; private ARepository repository; private Account account; - public ManageFeedsFoldersViewModel(@NonNull Application application) { - super(application); - - db = Database.getInstance(application); + public ManageFeedsFoldersViewModel(@NonNull Database database) { + this.database = database; } private void setup() { - try { - repository = ARepository.repositoryFactory(account, getApplication()); + repository = KoinJavaComponent.get(ARepository.class, null, + () -> DefinitionParametersKt.parametersOf(account)); - feedsWithFolder = db.feedDao().getAllFeedsWithFolder(account.getId()); - folders = db.folderDao().getAllFolders(account.getId()); - } catch (Exception e) { - e.printStackTrace(); - } + feedsWithFolder = database.feedDao().getAllFeedsWithFolder(account.getId()); + folders = database.folderDao().getAllFolders(account.getId()); } public LiveData> getFeedsWithFolder() { @@ -67,7 +63,7 @@ public class ManageFeedsFoldersViewModel extends AndroidViewModel { } public LiveData> getFoldersWithFeedCount() { - return db.folderDao().getFoldersWithFeedCount(account.getId()); + return database.folderDao().getFoldersWithFeedCount(account.getId()); } public Single addFolder(Folder folder) { @@ -87,6 +83,6 @@ public class ManageFeedsFoldersViewModel extends AndroidViewModel { } public Single getFeedCountByAccount() { - return db.feedDao().getFeedCount(account.getId()); + return database.feedDao().getFeedCount(account.getId()); } } diff --git a/app/src/main/java/com/readrops/app/viewmodels/NotificationPermissionViewModel.kt b/app/src/main/java/com/readrops/app/viewmodels/NotificationPermissionViewModel.kt index b425e70e..1e2fd243 100644 --- a/app/src/main/java/com/readrops/app/viewmodels/NotificationPermissionViewModel.kt +++ b/app/src/main/java/com/readrops/app/viewmodels/NotificationPermissionViewModel.kt @@ -1,16 +1,14 @@ package com.readrops.app.viewmodels -import android.app.Application -import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.LiveData +import androidx.lifecycle.ViewModel import com.readrops.db.Database import com.readrops.db.entities.Feed import com.readrops.db.entities.account.Account import io.reactivex.Completable -class NotificationPermissionViewModel(application: Application) : AndroidViewModel(application) { +class NotificationPermissionViewModel(val database: Database) : ViewModel() { - val database: Database = Database.getInstance(application) var account: Account? = null fun getAccount(accountId: Int): LiveData = database.accountDao().selectAsync(accountId) From 725cb291ef07f62bde90aaf2c15ca307e484654d Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Fri, 16 Oct 2020 12:36:24 +0200 Subject: [PATCH 088/187] Fix FreshRSS and Nextcloud News authentications --- .../nextcloudnews/NextNewsCredentials.java | 2 +- .../app/activities/AddAccountActivity.java | 28 ++++++++----------- .../app/repositories/ARepository.java | 4 +++ .../app/repositories/FreshRSSRepository.java | 5 +++- .../app/repositories/NextNewsRepository.java | 3 +- 5 files changed, 22 insertions(+), 20 deletions(-) diff --git a/api/src/main/java/com/readrops/api/services/nextcloudnews/NextNewsCredentials.java b/api/src/main/java/com/readrops/api/services/nextcloudnews/NextNewsCredentials.java index 40644621..24c44ab7 100644 --- a/api/src/main/java/com/readrops/api/services/nextcloudnews/NextNewsCredentials.java +++ b/api/src/main/java/com/readrops/api/services/nextcloudnews/NextNewsCredentials.java @@ -5,6 +5,6 @@ import com.readrops.api.services.Credentials; public class NextNewsCredentials extends Credentials { public NextNewsCredentials(String login, String password, String url) { - super(okhttp3.Credentials.basic(login, password), url); + super(login != null && password != null ? okhttp3.Credentials.basic(login, password) : null, url); } } diff --git a/app/src/main/java/com/readrops/app/activities/AddAccountActivity.java b/app/src/main/java/com/readrops/app/activities/AddAccountActivity.java index e72596ce..7f3653cf 100644 --- a/app/src/main/java/com/readrops/app/activities/AddAccountActivity.java +++ b/app/src/main/java/com/readrops/app/activities/AddAccountActivity.java @@ -59,26 +59,20 @@ public class AddAccountActivity extends AppCompatActivity { if (forwardResult || accountToEdit != null) getSupportActionBar().setDisplayHomeAsUpEnabled(true); - try { - if (accountToEdit != null) { - viewModel.setAccountType(accountToEdit.getAccountType()); - editAccount = true; - fillFields(); - } else { - viewModel.setAccountType(accountType); + if (accountToEdit != null) { + viewModel.setAccountType(accountToEdit.getAccountType()); + editAccount = true; + fillFields(); + } else { + viewModel.setAccountType(accountType); - binding.providerImage.setImageResource(accountType.getIconRes()); - binding.providerName.setText(accountType.getName()); - binding.addAccountName.setText(accountType.getName()); - if (accountType == AccountType.FRESHRSS) { - binding.addAccountPasswordLayout.setHelperText(getString(R.string.password_helper)); - } + binding.providerImage.setImageResource(accountType.getIconRes()); + binding.providerName.setText(accountType.getName()); + binding.addAccountName.setText(accountType.getName()); + if (accountType == AccountType.FRESHRSS) { + binding.addAccountPasswordLayout.setHelperText(getString(R.string.password_helper)); } - } catch (Exception e) { - // TODO : see how to handle this exception - e.printStackTrace(); } - } public void createAccount(View view) { diff --git a/app/src/main/java/com/readrops/app/repositories/ARepository.java b/app/src/main/java/com/readrops/app/repositories/ARepository.java index 694c3289..51610b30 100644 --- a/app/src/main/java/com/readrops/app/repositories/ARepository.java +++ b/app/src/main/java/com/readrops/app/repositories/ARepository.java @@ -47,6 +47,10 @@ public abstract class ARepository { this.database = database; this.account = account; + setCredentials(account); + } + + protected void setCredentials(@Nullable Account account) { KoinJavaComponent.get(AuthInterceptor.class) .setCredentials(account != null && !account.isLocal() ? Credentials.toCredentials(account) : null); } diff --git a/app/src/main/java/com/readrops/app/repositories/FreshRSSRepository.java b/app/src/main/java/com/readrops/app/repositories/FreshRSSRepository.java index c37be56f..0ab8a89c 100644 --- a/app/src/main/java/com/readrops/app/repositories/FreshRSSRepository.java +++ b/app/src/main/java/com/readrops/app/repositories/FreshRSSRepository.java @@ -33,7 +33,7 @@ public class FreshRSSRepository extends ARepository { private static final String TAG = FreshRSSRepository.class.getSimpleName(); - private FreshRSSDataSource dataSource; + private final FreshRSSDataSource dataSource; public FreshRSSRepository(FreshRSSDataSource dataSource, Database database, @NonNull Context context, @Nullable Account account) { super(database, context, account); @@ -43,9 +43,12 @@ public class FreshRSSRepository extends ARepository { @Override public Single login(Account account, boolean insert) { + setCredentials(account); + return dataSource.login(account.getLogin(), account.getPassword()) .flatMap(token -> { account.setToken(token); + setCredentials(account); return dataSource.getWriteToken(); }) diff --git a/app/src/main/java/com/readrops/app/repositories/NextNewsRepository.java b/app/src/main/java/com/readrops/app/repositories/NextNewsRepository.java index e76309a5..1d10f93b 100644 --- a/app/src/main/java/com/readrops/app/repositories/NextNewsRepository.java +++ b/app/src/main/java/com/readrops/app/repositories/NextNewsRepository.java @@ -37,7 +37,7 @@ public class NextNewsRepository extends ARepository { private static final String TAG = NextNewsRepository.class.getSimpleName(); - private NextNewsDataSource dataSource; + private final NextNewsDataSource dataSource; public NextNewsRepository(NextNewsDataSource dataSource, Database database, @NonNull Context context, @Nullable Account account) { super(database, context, account); @@ -47,6 +47,7 @@ public class NextNewsRepository extends ARepository { @Override public Single login(Account account, boolean insert) { + setCredentials(account); return Single.create(emitter -> { NextNewsUser user = dataSource.login(); From f3330ba07d9043fba7c6b494b7ca627812b44ee6 Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Mon, 19 Oct 2020 22:54:29 +0200 Subject: [PATCH 089/187] Remove last references to the old http manager --- api/build.gradle | 1 + .../api/localfeed/LocalRSSDataSourceTest.kt | 4 +- .../com/readrops/api/utils/HttpManager.java | 52 ------------------- .../com/readrops/app/ReadropsDebugApp.java | 16 ------ .../com/readrops/app/utils/HtmlParser.java | 6 ++- .../readrops/app/utils/ReadropsGlideModule.kt | 10 ++-- .../java/com/readrops/app/utils/Utils.java | 6 +-- 7 files changed, 16 insertions(+), 79 deletions(-) delete mode 100644 api/src/main/java/com/readrops/api/utils/HttpManager.java diff --git a/api/build.gradle b/api/build.gradle index bf8dcf78..8068e379 100644 --- a/api/build.gradle +++ b/api/build.gradle @@ -52,6 +52,7 @@ dependencies { androidTestImplementation 'androidx.test:rules:1.3.0' androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0' androidTestImplementation 'com.squareup.okhttp3:mockwebserver:4.8.1' + testImplementation "org.koin:koin-test:2.1.6" implementation 'com.gitlab.mvysny.konsume-xml:konsume-xml:0.12' diff --git a/api/src/androidTest/java/com/readrops/api/localfeed/LocalRSSDataSourceTest.kt b/api/src/androidTest/java/com/readrops/api/localfeed/LocalRSSDataSourceTest.kt index a03f6032..22b87e68 100644 --- a/api/src/androidTest/java/com/readrops/api/localfeed/LocalRSSDataSourceTest.kt +++ b/api/src/androidTest/java/com/readrops/api/localfeed/LocalRSSDataSourceTest.kt @@ -4,13 +4,13 @@ import android.accounts.NetworkErrorException import android.content.Context import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.platform.app.InstrumentationRegistry -import com.readrops.api.utils.HttpManager import com.readrops.api.utils.LibUtils import com.readrops.api.utils.ParseException import com.readrops.api.utils.UnknownFormatException import junit.framework.TestCase.* import okhttp3.Headers import okhttp3.HttpUrl +import okhttp3.OkHttpClient import okhttp3.mockwebserver.MockResponse import okhttp3.mockwebserver.MockWebServer import okio.Buffer @@ -28,7 +28,7 @@ class LocalRSSDataSourceTest { private lateinit var url: HttpUrl private val mockServer: MockWebServer = MockWebServer() - private val localRSSDataSource = LocalRSSDataSource(HttpManager.getInstance().okHttpClient) + private val localRSSDataSource = LocalRSSDataSource(OkHttpClient()) @Before fun before() { diff --git a/api/src/main/java/com/readrops/api/utils/HttpManager.java b/api/src/main/java/com/readrops/api/utils/HttpManager.java deleted file mode 100644 index 5b1cd8c5..00000000 --- a/api/src/main/java/com/readrops/api/utils/HttpManager.java +++ /dev/null @@ -1,52 +0,0 @@ -package com.readrops.api.utils; - -import androidx.annotation.Nullable; - -import com.readrops.api.services.Credentials; - -import java.util.concurrent.TimeUnit; - -import okhttp3.OkHttpClient; - -public class HttpManager { - - private OkHttpClient okHttpClient; - private Credentials credentials; - - public HttpManager() { - buildOkHttp(); - } - - private void buildOkHttp() { - okHttpClient = new OkHttpClient.Builder() - .callTimeout(1, TimeUnit.MINUTES) - .readTimeout(1, TimeUnit.HOURS) - .build(); - } - - public OkHttpClient getOkHttpClient() { - return okHttpClient; - } - - public void setCredentials(@Nullable Credentials credentials) { - this.credentials = credentials; - } - - public Credentials getCredentials() { - return credentials; - } - - private static HttpManager instance; - - public static HttpManager getInstance() { - if (instance == null) { - instance = new HttpManager(); - } - - return instance; - } - - public static void setInstance(OkHttpClient client) { - instance.okHttpClient = client; - } -} diff --git a/app/src/debug/java/com/readrops/app/ReadropsDebugApp.java b/app/src/debug/java/com/readrops/app/ReadropsDebugApp.java index 244f9afd..673e3081 100644 --- a/app/src/debug/java/com/readrops/app/ReadropsDebugApp.java +++ b/app/src/debug/java/com/readrops/app/ReadropsDebugApp.java @@ -13,13 +13,10 @@ import com.facebook.flipper.plugins.databases.DatabasesFlipperPlugin; import com.facebook.flipper.plugins.inspector.DescriptorMapping; import com.facebook.flipper.plugins.inspector.InspectorFlipperPlugin; import com.facebook.flipper.plugins.navigation.NavigationFlipperPlugin; -import com.facebook.flipper.plugins.network.FlipperOkhttpInterceptor; import com.facebook.flipper.plugins.network.NetworkFlipperPlugin; import com.facebook.flipper.plugins.sharedpreferences.SharedPreferencesFlipperPlugin; import com.facebook.soloader.SoLoader; import com.icapps.niddler.core.AndroidNiddler; -import com.icapps.niddler.interceptor.okhttp.NiddlerOkHttpInterceptor; -import com.readrops.api.utils.HttpManager; public class ReadropsDebugApp extends ReadropsApp implements Configuration.Provider { @@ -40,13 +37,6 @@ public class ReadropsDebugApp extends ReadropsApp implements Configuration.Provi NetworkFlipperPlugin networkPlugin = new NetworkFlipperPlugin(); client.addPlugin(networkPlugin); - HttpManager.setInstance( - HttpManager.getInstance() - .getOkHttpClient() - .newBuilder() - .addInterceptor(new FlipperOkhttpInterceptor(networkPlugin)) - .build()); - client.addPlugin(new DatabasesFlipperPlugin(this)); client.addPlugin(CrashReporterPlugin.getInstance()); client.addPlugin(NavigationFlipperPlugin.getInstance()); @@ -65,12 +55,6 @@ public class ReadropsDebugApp extends ReadropsApp implements Configuration.Provi niddler.attachToApplication(this); - HttpManager.setInstance(HttpManager.getInstance(). - getOkHttpClient(). - newBuilder(). - addInterceptor(new NiddlerOkHttpInterceptor(niddler, "default")) - .build()); - niddler.start(); } diff --git a/app/src/main/java/com/readrops/app/utils/HtmlParser.java b/app/src/main/java/com/readrops/app/utils/HtmlParser.java index 369244e6..daebc212 100644 --- a/app/src/main/java/com/readrops/app/utils/HtmlParser.java +++ b/app/src/main/java/com/readrops/app/utils/HtmlParser.java @@ -6,18 +6,19 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.readrops.api.localfeed.LocalRSSHelper; -import com.readrops.api.utils.HttpManager; import com.readrops.api.utils.LibUtils; import org.jsoup.Jsoup; import org.jsoup.nodes.Document; import org.jsoup.nodes.Element; import org.jsoup.select.Elements; +import org.koin.java.KoinJavaComponent; import java.util.ArrayList; import java.util.Collections; import java.util.List; +import okhttp3.OkHttpClient; import okhttp3.Request; import okhttp3.Response; @@ -83,7 +84,7 @@ public final class HtmlParser { long start = System.currentTimeMillis(); try { - Response response = HttpManager.getInstance().getOkHttpClient() + Response response = KoinJavaComponent.get(OkHttpClient.class) .newCall(new Request.Builder().url(url).build()).execute(); if (response.header("Content-Type").contains(LibUtils.HTML_CONTENT_TYPE)) { @@ -98,6 +99,7 @@ public final class HtmlParser { return null; } } catch (Exception e) { + Log.d(TAG, e.getMessage()); return null; } diff --git a/app/src/main/java/com/readrops/app/utils/ReadropsGlideModule.kt b/app/src/main/java/com/readrops/app/utils/ReadropsGlideModule.kt index 3138c7dd..621e241e 100644 --- a/app/src/main/java/com/readrops/app/utils/ReadropsGlideModule.kt +++ b/app/src/main/java/com/readrops/app/utils/ReadropsGlideModule.kt @@ -7,15 +7,17 @@ import com.bumptech.glide.annotation.GlideModule import com.bumptech.glide.integration.okhttp3.OkHttpUrlLoader import com.bumptech.glide.load.model.GlideUrl import com.bumptech.glide.module.AppGlideModule -import com.readrops.api.utils.HttpManager +import okhttp3.OkHttpClient +import org.koin.core.KoinComponent +import org.koin.core.get import java.io.InputStream @GlideModule -class ReadropsGlideModule : AppGlideModule() { +class ReadropsGlideModule : AppGlideModule(), KoinComponent { override fun registerComponents(context: Context, glide: Glide, registry: Registry) { - val factory = OkHttpUrlLoader.Factory(HttpManager.getInstance().okHttpClient) - + val factory = OkHttpUrlLoader.Factory(get()) + glide.registry.replace(GlideUrl::class.java, InputStream::class.java, factory) } } \ No newline at end of file diff --git a/app/src/main/java/com/readrops/app/utils/Utils.java b/app/src/main/java/com/readrops/app/utils/Utils.java index 7da6514a..aad043b1 100644 --- a/app/src/main/java/com/readrops/app/utils/Utils.java +++ b/app/src/main/java/com/readrops/app/utils/Utils.java @@ -15,7 +15,8 @@ import androidx.annotation.ColorInt; import androidx.annotation.NonNull; import com.google.android.material.snackbar.Snackbar; -import com.readrops.api.utils.HttpManager; + +import org.koin.java.KoinJavaComponent; import java.io.InputStream; import java.util.Locale; @@ -34,10 +35,9 @@ public final class Utils { public static Bitmap getImageFromUrl(String url) { try { - OkHttpClient okHttpClient = HttpManager.getInstance().getOkHttpClient(); Request request = new Request.Builder().url(url).build(); - Response response = okHttpClient.newCall(request).execute(); + Response response = KoinJavaComponent.get(OkHttpClient.class).newCall(request).execute(); if (response.isSuccessful()) { InputStream inputStream = response.body().byteStream(); From aa59f890bb7498158045efb801b850127ff66469 Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Tue, 20 Oct 2020 22:04:00 +0200 Subject: [PATCH 090/187] Inject Glide --- app/src/main/java/com/readrops/app/AppModule.kt | 2 ++ .../java/com/readrops/app/activities/ItemActivity.java | 7 ++++--- .../java/com/readrops/app/activities/MainActivity.java | 7 ++++--- .../main/java/com/readrops/app/adapters/FeedsAdapter.java | 6 ++++-- .../app/adapters/NotificationPermissionListAdapter.kt | 7 +++++-- .../main/java/com/readrops/app/utils/SyncResultAnalyser.kt | 6 ++++-- 6 files changed, 23 insertions(+), 12 deletions(-) diff --git a/app/src/main/java/com/readrops/app/AppModule.kt b/app/src/main/java/com/readrops/app/AppModule.kt index 88f3accd..9f111ce5 100644 --- a/app/src/main/java/com/readrops/app/AppModule.kt +++ b/app/src/main/java/com/readrops/app/AppModule.kt @@ -3,6 +3,7 @@ package com.readrops.app import com.readrops.app.repositories.FreshRSSRepository import com.readrops.app.repositories.LocalFeedRepository import com.readrops.app.repositories.NextNewsRepository +import com.readrops.app.utils.GlideApp import com.readrops.app.viewmodels.* import com.readrops.db.entities.account.Account import com.readrops.db.entities.account.AccountType @@ -46,4 +47,5 @@ val appModule = module { AccountViewModel(get()) } + single { GlideApp.with(androidApplication()) } } \ No newline at end of file diff --git a/app/src/main/java/com/readrops/app/activities/ItemActivity.java b/app/src/main/java/com/readrops/app/activities/ItemActivity.java index d7dfa2f4..8d45d45a 100644 --- a/app/src/main/java/com/readrops/app/activities/ItemActivity.java +++ b/app/src/main/java/com/readrops/app/activities/ItemActivity.java @@ -33,7 +33,7 @@ import com.bumptech.glide.request.transition.Transition; import com.readrops.api.utils.DateUtils; import com.readrops.app.R; import com.readrops.app.databinding.ActivityItemBinding; -import com.readrops.app.utils.GlideApp; +import com.readrops.app.utils.GlideRequests; import com.readrops.app.utils.PermissionManager; import com.readrops.app.utils.SharedPreferencesManager; import com.readrops.app.utils.Utils; @@ -42,6 +42,7 @@ import com.readrops.db.entities.Item; import com.readrops.db.pojo.ItemWithFeed; import org.koin.androidx.viewmodel.compat.ViewModelCompat; +import org.koin.java.KoinJavaComponent; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -93,7 +94,7 @@ public class ItemActivity extends AppCompatActivity { binding.appBarLayout.setExpanded(true); binding.collapsingLayout.setTitleEnabled(true); - GlideApp.with(this) + KoinJavaComponent.get(GlideRequests.class) .load(imageUrl) .diskCacheStrategy(DiskCacheStrategy.ALL) .into(binding.collapsingLayoutImage); @@ -358,7 +359,7 @@ public class ItemActivity extends AppCompatActivity { } private void shareImage(String url) { - GlideApp.with(this) + KoinJavaComponent.get(GlideRequests.class) .asBitmap() .diskCacheStrategy(DiskCacheStrategy.ALL) .load(url) diff --git a/app/src/main/java/com/readrops/app/activities/MainActivity.java b/app/src/main/java/com/readrops/app/activities/MainActivity.java index c2102f2c..826b7001 100644 --- a/app/src/main/java/com/readrops/app/activities/MainActivity.java +++ b/app/src/main/java/com/readrops/app/activities/MainActivity.java @@ -37,7 +37,7 @@ import com.readrops.app.R; import com.readrops.app.adapters.MainItemListAdapter; import com.readrops.app.databinding.ActivityMainBinding; import com.readrops.app.utils.DrawerManager; -import com.readrops.app.utils.GlideApp; +import com.readrops.app.utils.GlideRequests; import com.readrops.app.utils.ReadropsItemTouchCallback; import com.readrops.app.utils.SharedPreferencesManager; import com.readrops.app.utils.Utils; @@ -51,6 +51,7 @@ import com.readrops.db.pojo.ItemWithFeed; import org.jetbrains.annotations.NotNull; import org.koin.androidx.viewmodel.compat.ViewModelCompat; +import org.koin.java.KoinJavaComponent; import java.lang.ref.WeakReference; import java.util.Collections; @@ -300,7 +301,7 @@ public class MainActivity extends AppCompatActivity implements SwipeRefreshLayou private void initRecyclerView() { ViewPreloadSizeProvider preloadSizeProvider = new ViewPreloadSizeProvider(); - adapter = new MainItemListAdapter(GlideApp.with(this), preloadSizeProvider); + adapter = new MainItemListAdapter(KoinJavaComponent.get(GlideRequests.class), preloadSizeProvider); adapter.setOnItemClickListener(new MainItemListAdapter.OnItemClickListener() { @Override public void onItemClick(ItemWithFeed itemWithFeed, int position) { @@ -349,7 +350,7 @@ public class MainActivity extends AppCompatActivity implements SwipeRefreshLayou binding.itemsRecyclerView.setRecyclerListener(viewHolder -> { MainItemListAdapter.ItemViewHolder vh = (MainItemListAdapter.ItemViewHolder) viewHolder; - GlideApp.with(this).clear(vh.getItemImage()); + KoinJavaComponent.get(GlideRequests.class).clear(vh.getItemImage()); }); LinearLayoutManager layoutManager = new LinearLayoutManager(this); diff --git a/app/src/main/java/com/readrops/app/adapters/FeedsAdapter.java b/app/src/main/java/com/readrops/app/adapters/FeedsAdapter.java index 385813e4..9363b85e 100644 --- a/app/src/main/java/com/readrops/app/adapters/FeedsAdapter.java +++ b/app/src/main/java/com/readrops/app/adapters/FeedsAdapter.java @@ -13,9 +13,11 @@ import androidx.recyclerview.widget.RecyclerView; import com.bumptech.glide.load.engine.DiskCacheStrategy; import com.readrops.app.R; import com.readrops.app.databinding.FeedLayoutBinding; -import com.readrops.app.utils.GlideApp; +import com.readrops.app.utils.GlideRequests; import com.readrops.db.pojo.FeedWithFolder; +import org.koin.java.KoinJavaComponent; + import java.util.List; public class FeedsAdapter extends ListAdapter { @@ -64,7 +66,7 @@ public class FeedsAdapter extends ListAdapter Unit) : - ListAdapter(DIFF_CALLBACK) { + ListAdapter(DIFF_CALLBACK), KoinComponent { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): NotificationPermissionViewHolder { val binding = NotificationPermissionLayoutBinding.inflate(LayoutInflater.from(parent.context)) @@ -30,7 +33,7 @@ class NotificationPermissionListAdapter(var enableAll: Boolean, val listener: (f holder.itemView.setOnClickListener { if (enableAll) listener(getItem(position)) } - GlideApp.with(holder.itemView.context) + get() .load(feed.iconUrl) .diskCacheStrategy(DiskCacheStrategy.ALL) .placeholder(R.drawable.ic_rss_feed_grey) diff --git a/app/src/main/java/com/readrops/app/utils/SyncResultAnalyser.kt b/app/src/main/java/com/readrops/app/utils/SyncResultAnalyser.kt index 497c24fe..a95c37dd 100644 --- a/app/src/main/java/com/readrops/app/utils/SyncResultAnalyser.kt +++ b/app/src/main/java/com/readrops/app/utils/SyncResultAnalyser.kt @@ -9,11 +9,13 @@ import com.readrops.db.entities.Feed import com.readrops.db.entities.Item import com.readrops.db.entities.account.Account import com.readrops.api.services.SyncResult +import org.koin.core.KoinComponent +import org.koin.core.get /** * Simple class to get synchro notification content (title, content and largeIcon) according to some rules */ -class SyncResultAnalyser(val context: Context, private val syncResults: Map, val database: Database) { +class SyncResultAnalyser(val context: Context, private val syncResults: Map, val database: Database) : KoinComponent { private val notifContent = SyncResultNotifContent() @@ -66,7 +68,7 @@ class SyncResultAnalyser(val context: Context, private val syncResults: Map() .asBitmap() .load(it) .diskCacheStrategy(DiskCacheStrategy.ALL) From c780f265339746f6476f1f0db82d87f3b9bc6867 Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Tue, 20 Oct 2020 22:43:49 +0200 Subject: [PATCH 091/187] Fix broken tests --- .../java/com/readrops/app/HtmlParserTest.java | 35 ---------------- .../java/com/readrops/app/HtmlParserTest.kt | 42 +++++++++++++++++++ 2 files changed, 42 insertions(+), 35 deletions(-) delete mode 100644 app/src/test/java/com/readrops/app/HtmlParserTest.java create mode 100644 app/src/test/java/com/readrops/app/HtmlParserTest.kt diff --git a/app/src/test/java/com/readrops/app/HtmlParserTest.java b/app/src/test/java/com/readrops/app/HtmlParserTest.java deleted file mode 100644 index bf8f6e47..00000000 --- a/app/src/test/java/com/readrops/app/HtmlParserTest.java +++ /dev/null @@ -1,35 +0,0 @@ -package com.readrops.app; - -import com.readrops.app.utils.HtmlParser; -import com.readrops.app.utils.ParsingResult; - -import org.junit.Assert; -import org.junit.Test; - -import java.util.ArrayList; -import java.util.List; - -import static junit.framework.TestCase.assertEquals; - -public class HtmlParserTest { - - @Test - public void getFeedLinkTest() throws Exception { - String url = "https://github.com/readrops/Readrops"; - - ParsingResult parsingResult = new ParsingResult("https://github.com/readrops/Readrops/commits/develop.atom", "Recent Commits to Readrops:develop"); - List parsingResultList = new ArrayList<>(); - parsingResultList.add(parsingResult); - - List parsingResultList1 = HtmlParser.getFeedLink(url); - - Assert.assertEquals(parsingResultList, parsingResultList1); - } - - @Test - public void getFaviconLinkTest() { - String url = "https://github.com/readrops/Readrops"; - - assertEquals("https://github.com/fluidicon.png", HtmlParser.getFaviconLink(url)); - } -} diff --git a/app/src/test/java/com/readrops/app/HtmlParserTest.kt b/app/src/test/java/com/readrops/app/HtmlParserTest.kt new file mode 100644 index 00000000..760c85ae --- /dev/null +++ b/app/src/test/java/com/readrops/app/HtmlParserTest.kt @@ -0,0 +1,42 @@ +package com.readrops.app + +import com.readrops.app.utils.HtmlParser +import com.readrops.app.utils.ParsingResult +import junit.framework.TestCase +import okhttp3.OkHttpClient +import org.junit.Assert +import org.junit.Before +import org.junit.Test +import org.koin.core.context.startKoin +import org.koin.dsl.module + +class HtmlParserTest { + + @Before + fun setupKoin() { + startKoin { + modules(module { + single { OkHttpClient() } + }) + } + } + + @Test + fun getFeedLinkTest() { + val url = "https://github.com/readrops/Readrops" + val parsingResult = ParsingResult("https://github.com/readrops/Readrops/commits/develop.atom", + "Recent Commits to Readrops:develop") + + val parsingResultList = mutableListOf(parsingResult) + + val parsingResultList1 = HtmlParser.getFeedLink(url) + Assert.assertEquals(parsingResultList, parsingResultList1) + } + + @Test + fun getFaviconLinkTest() { + val url = "https://github.com/readrops/Readrops" + + TestCase.assertEquals("https://github.com/fluidicon.png", HtmlParser.getFaviconLink(url)) + } +} \ No newline at end of file From 2d23369de785288f08901c6c50bc4d7d65ebd4b4 Mon Sep 17 00:00:00 2001 From: Riku Viitanen Date: Wed, 21 Oct 2020 01:53:38 +0300 Subject: [PATCH 092/187] Fix typo: newsest -> newest --- app/src/main/res/values/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 8021181f..1e1f835e 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -74,7 +74,7 @@ Credentials Newest to oldest - Oldest to newsest + Oldest to newest Login failed. Please check your credentials New account App released under the GPLv3 licence From 5c88a3f467bdafab6ea94e7e3cf4052bbac7f90c Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Wed, 21 Oct 2020 21:31:08 +0200 Subject: [PATCH 093/187] FIx koin context starting twice --- app/build.gradle | 1 + .../test/java/com/readrops/app/HtmlParserTest.kt | 16 +++++++--------- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 0cd879c0..34916adf 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -72,6 +72,7 @@ dependencies { implementation "androidx.work:work-runtime-ktx:2.4.0" implementation "androidx.fragment:fragment-ktx:1.2.3" implementation "androidx.browser:browser:1.2.0" + testImplementation "org.koin:koin-test:2.1.6" testImplementation 'junit:junit:4.12' androidTestImplementation 'androidx.test.ext:junit:1.1.1' diff --git a/app/src/test/java/com/readrops/app/HtmlParserTest.kt b/app/src/test/java/com/readrops/app/HtmlParserTest.kt index 760c85ae..bcff0ac2 100644 --- a/app/src/test/java/com/readrops/app/HtmlParserTest.kt +++ b/app/src/test/java/com/readrops/app/HtmlParserTest.kt @@ -5,20 +5,18 @@ import com.readrops.app.utils.ParsingResult import junit.framework.TestCase import okhttp3.OkHttpClient import org.junit.Assert -import org.junit.Before +import org.junit.Rule import org.junit.Test -import org.koin.core.context.startKoin import org.koin.dsl.module +import org.koin.test.KoinTestRule class HtmlParserTest { - @Before - fun setupKoin() { - startKoin { - modules(module { - single { OkHttpClient() } - }) - } + @get:Rule + val koinTestRule = KoinTestRule.create { + modules(module { + single { OkHttpClient() } + }) } @Test From 6294b5556f25b6fab7688ff44891b6b5f6d9a5d3 Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Wed, 21 Oct 2020 21:42:22 +0200 Subject: [PATCH 094/187] Delete unused code --- .../services/freshrss/FreshRSSDataSource.java | 13 ------------- .../nextcloudnews/NextNewsDataSource.java | 19 +++---------------- .../app/repositories/ARepository.java | 18 ------------------ 3 files changed, 3 insertions(+), 47 deletions(-) diff --git a/api/src/main/java/com/readrops/api/services/freshrss/FreshRSSDataSource.java b/api/src/main/java/com/readrops/api/services/freshrss/FreshRSSDataSource.java index a3e25493..ac4a8b56 100644 --- a/api/src/main/java/com/readrops/api/services/freshrss/FreshRSSDataSource.java +++ b/api/src/main/java/com/readrops/api/services/freshrss/FreshRSSDataSource.java @@ -5,15 +5,10 @@ import androidx.annotation.Nullable; import com.readrops.api.services.SyncResult; import com.readrops.api.services.SyncType; -import com.readrops.api.services.freshrss.adapters.FreshRSSFeedsAdapter; -import com.readrops.api.services.freshrss.adapters.FreshRSSFoldersAdapter; -import com.readrops.api.services.freshrss.adapters.FreshRSSItemsAdapter; import com.readrops.api.services.freshrss.json.FreshRSSUserInfo; import com.readrops.db.entities.Feed; import com.readrops.db.entities.Folder; import com.readrops.db.entities.Item; -import com.squareup.moshi.Moshi; -import com.squareup.moshi.Types; import java.io.StringReader; import java.util.List; @@ -38,14 +33,6 @@ public class FreshRSSDataSource { this.api = api; } - protected Moshi buildMoshi() { - return new Moshi.Builder() - .add(Types.newParameterizedType(List.class, Item.class), new FreshRSSItemsAdapter()) - .add(new FreshRSSFeedsAdapter()) - .add(new FreshRSSFoldersAdapter()) - .build(); - } - /** * Call token API to generate a new token from account credentials * diff --git a/api/src/main/java/com/readrops/api/services/nextcloudnews/NextNewsDataSource.java b/api/src/main/java/com/readrops/api/services/nextcloudnews/NextNewsDataSource.java index d39261c0..ed06044d 100644 --- a/api/src/main/java/com/readrops/api/services/nextcloudnews/NextNewsDataSource.java +++ b/api/src/main/java/com/readrops/api/services/nextcloudnews/NextNewsDataSource.java @@ -5,20 +5,15 @@ import android.content.res.Resources; import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import com.readrops.db.entities.Feed; -import com.readrops.db.entities.Folder; -import com.readrops.db.entities.Item; import com.readrops.api.services.SyncResult; import com.readrops.api.services.SyncType; -import com.readrops.api.services.nextcloudnews.adapters.NextNewsFeedsAdapter; -import com.readrops.api.services.nextcloudnews.adapters.NextNewsFoldersAdapter; -import com.readrops.api.services.nextcloudnews.adapters.NextNewsItemsAdapter; import com.readrops.api.services.nextcloudnews.json.NextNewsUser; import com.readrops.api.utils.ConflictException; import com.readrops.api.utils.LibUtils; import com.readrops.api.utils.UnknownFormatException; -import com.squareup.moshi.Moshi; -import com.squareup.moshi.Types; +import com.readrops.db.entities.Feed; +import com.readrops.db.entities.Folder; +import com.readrops.db.entities.Item; import java.io.IOException; import java.util.ArrayList; @@ -40,14 +35,6 @@ public class NextNewsDataSource { this.api = api; } - protected Moshi buildMoshi() { - return new Moshi.Builder() - .add(new NextNewsFeedsAdapter()) - .add(new NextNewsFoldersAdapter()) - .add(Types.newParameterizedType(List.class, Item.class), new NextNewsItemsAdapter()) - .build(); - } - @Nullable public NextNewsUser login() throws IOException { Response response = api.getUser().execute(); diff --git a/app/src/main/java/com/readrops/app/repositories/ARepository.java b/app/src/main/java/com/readrops/app/repositories/ARepository.java index 51610b30..47425232 100644 --- a/app/src/main/java/com/readrops/app/repositories/ARepository.java +++ b/app/src/main/java/com/readrops/app/repositories/ARepository.java @@ -18,7 +18,6 @@ import com.readrops.db.entities.Feed; import com.readrops.db.entities.Folder; import com.readrops.db.entities.Item; import com.readrops.db.entities.account.Account; -import com.readrops.db.entities.account.AccountType; import org.koin.java.KoinJavaComponent; @@ -176,23 +175,6 @@ public abstract class ARepository { context.startService(intent); } - public static ARepository repositoryFactory(Account account, AccountType accountType, Context context) { - switch (accountType) { - case LOCAL: - return new LocalFeedRepository(null, Database.getInstance(context), context, account); - case NEXTCLOUD_NEWS: - return new NextNewsRepository(null, Database.getInstance(context), context, account); - case FRESHRSS: - return new FreshRSSRepository(null, Database.getInstance(context), context, account); - default: - throw new IllegalArgumentException("account type not supported"); - } - } - - public static ARepository repositoryFactory(Account account, Context context) { - return ARepository.repositoryFactory(account, account.getAccountType(), context); - } - public SyncResult getSyncResult() { return syncResult; } From e32b20bc8740cac348333d6e7e79c163712c6b84 Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Fri, 23 Oct 2020 18:51:01 +0200 Subject: [PATCH 095/187] Remove last references to Database singleton --- .../fragments/settings/SettingsFragment.java | 4 +++- .../readrops/app/utils/SyncResultDebugData.kt | 14 +++++++------ .../java/com/readrops/app/utils/SyncWorker.kt | 20 +++++++++++-------- .../feedscolors/FeedsColorsIntentService.kt | 8 +++++--- .../main/java/com/readrops/db/Database.java | 16 +-------------- 5 files changed, 29 insertions(+), 33 deletions(-) diff --git a/app/src/main/java/com/readrops/app/fragments/settings/SettingsFragment.java b/app/src/main/java/com/readrops/app/fragments/settings/SettingsFragment.java index ce84fd77..4a28bad7 100644 --- a/app/src/main/java/com/readrops/app/fragments/settings/SettingsFragment.java +++ b/app/src/main/java/com/readrops/app/fragments/settings/SettingsFragment.java @@ -19,6 +19,8 @@ import com.readrops.app.utils.SyncWorker; import com.readrops.app.utils.feedscolors.FeedsColorsIntentService; import com.readrops.db.Database; +import org.koin.java.KoinJavaComponent; + import java.util.ArrayList; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; @@ -38,7 +40,7 @@ public class SettingsFragment extends PreferenceFragmentCompat { AtomicBoolean serviceStarted = new AtomicBoolean(false); feedsColorsPreference.setOnPreferenceClickListener(preference -> { - Database database = Database.getInstance(getContext()); + Database database = KoinJavaComponent.get(Database.class); database.feedDao().getAllFeeds().observe(getActivity(), feeds -> { if (!serviceStarted.get()) { diff --git a/app/src/main/java/com/readrops/app/utils/SyncResultDebugData.kt b/app/src/main/java/com/readrops/app/utils/SyncResultDebugData.kt index 5018b464..e80de538 100644 --- a/app/src/main/java/com/readrops/app/utils/SyncResultDebugData.kt +++ b/app/src/main/java/com/readrops/app/utils/SyncResultDebugData.kt @@ -1,20 +1,22 @@ package com.readrops.app.utils import android.content.Context +import com.readrops.api.services.SyncResult import com.readrops.db.Database import com.readrops.db.entities.Item import com.readrops.db.entities.account.Account import com.readrops.db.entities.account.AccountType -import com.readrops.api.services.SyncResult import org.jetbrains.annotations.TestOnly +import org.koin.core.KoinComponent +import org.koin.core.get class SyncResultDebugData { - companion object { + companion object : KoinComponent { @TestOnly - fun oneAccountOneFeedOneItem(context: Context): Map { - val database = Database.getInstance(context) + fun oneAccountOneFeedOneItem(): Map { + val database = get() val account1 = database.accountDao().select(2) @@ -27,14 +29,14 @@ class SyncResultDebugData { } @TestOnly - fun oneAccountOneFeedMultipleItems(context: Context): Map { + fun oneAccountOneFeedMultipleItems(): Map { val account1 = Account().apply { id = 1 accountType = AccountType.FRESHRSS isNotificationsEnabled = true } - val database = Database.getInstance(context) + val database = get() val item = database.itemDao().select(5055) database.feedDao().updateFeedNotificationState(item.feedId, false).subscribe() diff --git a/app/src/main/java/com/readrops/app/utils/SyncWorker.kt b/app/src/main/java/com/readrops/app/utils/SyncWorker.kt index 474b189d..e544df2e 100644 --- a/app/src/main/java/com/readrops/app/utils/SyncWorker.kt +++ b/app/src/main/java/com/readrops/app/utils/SyncWorker.kt @@ -7,6 +7,7 @@ import android.content.Intent import android.util.Log import androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat +import androidx.work.Data import androidx.work.Worker import androidx.work.WorkerParameters import com.readrops.api.services.SyncResult @@ -19,13 +20,16 @@ import com.readrops.db.entities.Item import com.readrops.db.entities.account.Account import io.reactivex.disposables.Disposable import io.reactivex.schedulers.Schedulers +import org.koin.core.KoinComponent +import org.koin.core.get +import org.koin.core.parameter.parametersOf -class SyncWorker(context: Context, parameters: WorkerParameters) : Worker(context, parameters) { +class SyncWorker(context: Context, parameters: WorkerParameters) : Worker(context, parameters), KoinComponent { private var disposable: Disposable? = null private val notificationManager = NotificationManagerCompat.from(applicationContext) - private val database = Database.getInstance(applicationContext) + private val database = get() override fun doWork(): Result { var result = Result.success() @@ -39,7 +43,7 @@ class SyncWorker(context: Context, parameters: WorkerParameters) : Worker(contex .setProgress(0, 0, true) .setSmallIcon(R.drawable.ic_notif) .setOnlyAlertOnce(true) - + accounts.forEach { notificationBuilder.setContentText(it.accountName) notificationManager.notify(SYNC_NOTIFICATION_ID, notificationBuilder.build()) @@ -47,7 +51,7 @@ class SyncWorker(context: Context, parameters: WorkerParameters) : Worker(contex it.login = SharedPreferencesManager.readString(applicationContext, it.loginKey) it.password = SharedPreferencesManager.readString(applicationContext, it.passwordKey) - val repository = ARepository.repositoryFactory(it, applicationContext) + val repository = get(parameters = { parametersOf(it) }) disposable = repository.sync(null) .doOnError { throwable -> @@ -138,12 +142,12 @@ class SyncWorker(context: Context, parameters: WorkerParameters) : Worker(contex .build() } - class MarkReadReceiver : BroadcastReceiver() { + class MarkReadReceiver : BroadcastReceiver(), KoinComponent { override fun onReceive(context: Context?, intent: Intent?) { val itemId = intent?.getIntExtra(ReadropsKeys.ITEM_ID, 0)!! - with(Database.getInstance(context)) { + with(get()) { itemDao().setReadState(itemId, true, true) .subscribeOn(Schedulers.io()) .subscribe() @@ -155,12 +159,12 @@ class SyncWorker(context: Context, parameters: WorkerParameters) : Worker(contex } } - class ReadLaterReceiver : BroadcastReceiver() { + class ReadLaterReceiver : BroadcastReceiver(), KoinComponent { override fun onReceive(context: Context?, intent: Intent?) { val itemId = intent?.getIntExtra(ReadropsKeys.ITEM_ID, 0)!! - with(Database.getInstance(context)) { + with(get()) { itemDao().setReadItLater(itemId) .subscribeOn(Schedulers.io()) .subscribe() diff --git a/app/src/main/java/com/readrops/app/utils/feedscolors/FeedsColorsIntentService.kt b/app/src/main/java/com/readrops/app/utils/feedscolors/FeedsColorsIntentService.kt index 880eb4c9..6fbf9912 100644 --- a/app/src/main/java/com/readrops/app/utils/feedscolors/FeedsColorsIntentService.kt +++ b/app/src/main/java/com/readrops/app/utils/feedscolors/FeedsColorsIntentService.kt @@ -6,15 +6,17 @@ import androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat import com.readrops.app.R import com.readrops.app.ReadropsApp +import com.readrops.app.utils.ReadropsKeys.FEEDS import com.readrops.db.Database import com.readrops.db.entities.Feed -import com.readrops.app.utils.ReadropsKeys.FEEDS +import org.koin.core.KoinComponent +import org.koin.core.get -class FeedsColorsIntentService : IntentService("FeedsColorsIntentService") { +class FeedsColorsIntentService : IntentService("FeedsColorsIntentService"), KoinComponent { override fun onHandleIntent(intent: Intent?) { val feeds: List = intent!!.getParcelableArrayListExtra(FEEDS)!! - val database = Database.getInstance(this) + val database = get() val notificationBuilder = NotificationCompat.Builder(this, ReadropsApp.FEEDS_COLORS_CHANNEL_ID) .setContentTitle(getString(R.string.get_feeds_colors)) diff --git a/db/src/main/java/com/readrops/db/Database.java b/db/src/main/java/com/readrops/db/Database.java index f9cd69f8..3d23536a 100644 --- a/db/src/main/java/com/readrops/db/Database.java +++ b/db/src/main/java/com/readrops/db/Database.java @@ -1,9 +1,6 @@ package com.readrops.db; -import android.content.Context; - import androidx.annotation.NonNull; -import androidx.room.Room; import androidx.room.RoomDatabase; import androidx.room.TypeConverters; import androidx.room.migration.Migration; @@ -30,18 +27,7 @@ public abstract class Database extends RoomDatabase { public abstract FolderDao folderDao(); public abstract AccountDao accountDao(); - - private static Database database; - - public static Database getInstance(Context context) { - if (database == null) - database = Room.databaseBuilder(context, Database.class, "readrops-db") - .addMigrations(MIGRATION_1_2) - .build(); - - return database; - } - + public static final Migration MIGRATION_1_2 = new Migration(1, 2) { @Override public void migrate(@NonNull SupportSQLiteDatabase database) { From afce72bbe1c3d3c49c1df4b7f4d28273eb135173 Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Fri, 23 Oct 2020 19:44:13 +0200 Subject: [PATCH 096/187] Add tests for AuthInterceptor --- .../readrops/api/utils/AuthInterceptorTest.kt | 54 +++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 api/src/androidTest/java/com/readrops/api/utils/AuthInterceptorTest.kt diff --git a/api/src/androidTest/java/com/readrops/api/utils/AuthInterceptorTest.kt b/api/src/androidTest/java/com/readrops/api/utils/AuthInterceptorTest.kt new file mode 100644 index 00000000..ea425a05 --- /dev/null +++ b/api/src/androidTest/java/com/readrops/api/utils/AuthInterceptorTest.kt @@ -0,0 +1,54 @@ +package com.readrops.api.utils + +import com.readrops.api.services.freshrss.FreshRSSCredentials +import junit.framework.TestCase.assertEquals +import junit.framework.TestCase.assertNull +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.mockwebserver.MockResponse +import okhttp3.mockwebserver.MockWebServer +import org.junit.After +import org.junit.Before +import org.junit.Test + +class AuthInterceptorTest { + + private val interceptor = AuthInterceptor() + private val mockServer = MockWebServer() + private lateinit var okHttpClient: OkHttpClient + + @Before + fun before() { + okHttpClient = OkHttpClient.Builder().addInterceptor(interceptor).build() + mockServer.start(8080) + } + + @After + fun tearDown() { + mockServer.close() + } + + @Test + fun credentialsUrlTest() { + mockServer.enqueue(MockResponse()) + interceptor.credentials = FreshRSSCredentials("token", "http://localhost:8080/rss") + + okHttpClient.newCall(Request.Builder().url(mockServer.url("/url")).build()).execute() + val request = mockServer.takeRequest() + + assertEquals(request.requestUrl.toString(), "http://localhost:8080/rss/url") + assertEquals(request.headers["Authorization"], "GoogleLogin auth=token") + } + + @Test + fun nullCredentialsTest() { + mockServer.enqueue(MockResponse()) + interceptor.credentials = null + + okHttpClient.newCall(Request.Builder().url(mockServer.url("/url")).build()).execute() + val request = mockServer.takeRequest() + + assertEquals(request.requestUrl.toString(), "http://localhost:8080/url") + assertNull(request.headers["Authorization"]) + } +} \ No newline at end of file From 45c91219460f0b860e9d7e4a57f768de7a71c557 Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Fri, 23 Oct 2020 21:16:48 +0200 Subject: [PATCH 097/187] Inject SharedPreferences --- .../main/java/com/readrops/app/AppModule.kt | 3 ++ .../main/java/com/readrops/app/ReadropsApp.kt | 10 +++--- .../app/activities/AddAccountActivity.java | 8 ++--- .../app/activities/AddFeedActivity.java | 4 +-- .../readrops/app/activities/ItemActivity.java | 4 +-- .../readrops/app/activities/MainActivity.java | 12 +++---- .../NotificationPermissionActivity.kt | 3 +- .../readrops/app/fragments/FeedsFragment.java | 4 +-- .../app/fragments/FoldersFragment.java | 4 +-- .../settings/AccountSettingsFragment.java | 4 +-- .../app/repositories/LocalFeedRepository.java | 2 +- .../app/utils/SharedPreferencesManager.java | 36 +++++++++---------- .../java/com/readrops/app/utils/SyncWorker.kt | 5 ++- 13 files changed, 48 insertions(+), 51 deletions(-) diff --git a/app/src/main/java/com/readrops/app/AppModule.kt b/app/src/main/java/com/readrops/app/AppModule.kt index 9f111ce5..ee0b5696 100644 --- a/app/src/main/java/com/readrops/app/AppModule.kt +++ b/app/src/main/java/com/readrops/app/AppModule.kt @@ -1,5 +1,6 @@ package com.readrops.app +import androidx.preference.PreferenceManager import com.readrops.app.repositories.FreshRSSRepository import com.readrops.app.repositories.LocalFeedRepository import com.readrops.app.repositories.NextNewsRepository @@ -48,4 +49,6 @@ val appModule = module { } single { GlideApp.with(androidApplication()) } + + single { PreferenceManager.getDefaultSharedPreferences(androidContext()) } } \ No newline at end of file diff --git a/app/src/main/java/com/readrops/app/ReadropsApp.kt b/app/src/main/java/com/readrops/app/ReadropsApp.kt index 8d856bd8..559bfa33 100644 --- a/app/src/main/java/com/readrops/app/ReadropsApp.kt +++ b/app/src/main/java/com/readrops/app/ReadropsApp.kt @@ -24,17 +24,17 @@ open class ReadropsApp : Application() { createNotificationChannels() PreferenceManager.setDefaultValues(this, R.xml.preferences, false) - if (SharedPreferencesManager.readString(this, SharedPreferencesManager.SharedPrefKey.DARK_THEME).toBoolean()) - AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES) - else - AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO) - startKoin { androidLogger(Level.ERROR) androidContext(this@ReadropsApp) modules(apiModule, dbModule, appModule) } + + if (SharedPreferencesManager.readString(SharedPreferencesManager.SharedPrefKey.DARK_THEME).toBoolean()) + AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES) + else + AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO) } private fun createNotificationChannels() { diff --git a/app/src/main/java/com/readrops/app/activities/AddAccountActivity.java b/app/src/main/java/com/readrops/app/activities/AddAccountActivity.java index 7f3653cf..f952c5bd 100644 --- a/app/src/main/java/com/readrops/app/activities/AddAccountActivity.java +++ b/app/src/main/java/com/readrops/app/activities/AddAccountActivity.java @@ -178,8 +178,8 @@ public class AddAccountActivity extends AppCompatActivity { } private void saveLoginPassword(Account account) { - SharedPreferencesManager.writeValue(this, account.getLoginKey(), account.getLogin()); - SharedPreferencesManager.writeValue(this, account.getPasswordKey(), account.getPassword()); + SharedPreferencesManager.writeValue(account.getLoginKey(), account.getLogin()); + SharedPreferencesManager.writeValue(account.getPasswordKey(), account.getPassword()); account.setLogin(null); account.setPassword(null); @@ -191,8 +191,8 @@ public class AddAccountActivity extends AppCompatActivity { binding.addAccountUrl.setText(accountToEdit.getUrl()); binding.addAccountName.setText(accountToEdit.getAccountName()); - binding.addAccountLogin.setText(SharedPreferencesManager.readString(this, accountToEdit.getLoginKey())); - binding.addAccountPassword.setText(SharedPreferencesManager.readString(this, accountToEdit.getPasswordKey())); + binding.addAccountLogin.setText(SharedPreferencesManager.readString(accountToEdit.getLoginKey())); + binding.addAccountPassword.setText(SharedPreferencesManager.readString(accountToEdit.getPasswordKey())); } private void updateAccount() { diff --git a/app/src/main/java/com/readrops/app/activities/AddFeedActivity.java b/app/src/main/java/com/readrops/app/activities/AddFeedActivity.java index 2973ecc5..5828ce1a 100644 --- a/app/src/main/java/com/readrops/app/activities/AddFeedActivity.java +++ b/app/src/main/java/com/readrops/app/activities/AddFeedActivity.java @@ -260,8 +260,8 @@ public class AddFeedActivity extends AppCompatActivity implements View.OnClickLi Account account = (Account) binding.addFeedAccountSpinner.getSelectedItem(); - account.setLogin(SharedPreferencesManager.readString(this, account.getLoginKey())); - account.setPassword(SharedPreferencesManager.readString(this, account.getPasswordKey())); + account.setLogin(SharedPreferencesManager.readString(account.getLoginKey())); + account.setPassword(SharedPreferencesManager.readString(account.getPasswordKey())); viewModel.addFeeds(feedsToInsert, account) .subscribeOn(Schedulers.io()) diff --git a/app/src/main/java/com/readrops/app/activities/ItemActivity.java b/app/src/main/java/com/readrops/app/activities/ItemActivity.java index 8d45d45a..14a96800 100644 --- a/app/src/main/java/com/readrops/app/activities/ItemActivity.java +++ b/app/src/main/java/com/readrops/app/activities/ItemActivity.java @@ -216,7 +216,7 @@ public class ItemActivity extends AppCompatActivity { } private void openUrl() { - int value = Integer.parseInt(SharedPreferencesManager.readString(this, + int value = Integer.parseInt(SharedPreferencesManager.readString( SharedPreferencesManager.SharedPrefKey.OPEN_ITEMS_IN)); switch (value) { case 0: @@ -245,7 +245,7 @@ public class ItemActivity extends AppCompatActivity { } private void openInCustomTab() { - boolean darkTheme = Boolean.parseBoolean(SharedPreferencesManager.readString(this, SharedPreferencesManager.SharedPrefKey.DARK_THEME)); + boolean darkTheme = Boolean.parseBoolean(SharedPreferencesManager.readString(SharedPreferencesManager.SharedPrefKey.DARK_THEME)); int color = itemWithFeed.getBgColor() != 0 ? itemWithFeed.getBgColor() : itemWithFeed.getColor(); CustomTabsIntent customTabsIntent = new CustomTabsIntent.Builder() diff --git a/app/src/main/java/com/readrops/app/activities/MainActivity.java b/app/src/main/java/com/readrops/app/activities/MainActivity.java index 826b7001..e8dac12f 100644 --- a/app/src/main/java/com/readrops/app/activities/MainActivity.java +++ b/app/src/main/java/com/readrops/app/activities/MainActivity.java @@ -119,7 +119,7 @@ public class MainActivity extends AppCompatActivity implements SwipeRefreshLayou viewModel = ViewModelCompat.getViewModel(this, MainViewModel.class); - viewModel.setShowReadItems(SharedPreferencesManager.readBoolean(this, + viewModel.setShowReadItems(SharedPreferencesManager.readBoolean( SharedPreferencesManager.SharedPrefKey.SHOW_READ_ARTICLES)); viewModel.getItemsWithFeed().observe(this, itemWithFeeds -> { @@ -660,12 +660,12 @@ public class MainActivity extends AppCompatActivity implements SwipeRefreshLayou if (item.isChecked()) { item.setChecked(false); viewModel.setShowReadItems(false); - SharedPreferencesManager.writeValue(this, + SharedPreferencesManager.writeValue( SharedPreferencesManager.SharedPrefKey.SHOW_READ_ARTICLES, false); } else { item.setChecked(true); viewModel.setShowReadItems(true); - SharedPreferencesManager.writeValue(this, + SharedPreferencesManager.writeValue( SharedPreferencesManager.SharedPrefKey.SHOW_READ_ARTICLES, true); } @@ -709,16 +709,16 @@ public class MainActivity extends AppCompatActivity implements SwipeRefreshLayou private void getAccountCredentials(List accounts) { for (Account account : accounts) { if (account.getLogin() == null) - account.setLogin(SharedPreferencesManager.readString(this, account.getLoginKey())); + account.setLogin(SharedPreferencesManager.readString(account.getLoginKey())); if (account.getPassword() == null) - account.setPassword(SharedPreferencesManager.readString(this, account.getPasswordKey())); + account.setPassword(SharedPreferencesManager.readString(account.getPasswordKey())); } } private void startAboutActivity() { Libs.ActivityStyle activityStyle; - if (Boolean.valueOf(SharedPreferencesManager.readString(this, SharedPreferencesManager.SharedPrefKey.DARK_THEME))) + if (Boolean.valueOf(SharedPreferencesManager.readString(SharedPreferencesManager.SharedPrefKey.DARK_THEME))) activityStyle = Libs.ActivityStyle.DARK; else activityStyle = Libs.ActivityStyle.LIGHT_DARK_TOOLBAR; diff --git a/app/src/main/java/com/readrops/app/activities/NotificationPermissionActivity.kt b/app/src/main/java/com/readrops/app/activities/NotificationPermissionActivity.kt index 4c6e0c3d..a0b2ff07 100644 --- a/app/src/main/java/com/readrops/app/activities/NotificationPermissionActivity.kt +++ b/app/src/main/java/com/readrops/app/activities/NotificationPermissionActivity.kt @@ -3,7 +3,6 @@ package com.readrops.app.activities import android.content.Intent import android.os.Bundle import android.view.MenuItem -import androidx.activity.viewModels import androidx.appcompat.app.AppCompatActivity import androidx.lifecycle.Observer import androidx.recyclerview.widget.LinearLayoutManager @@ -121,7 +120,7 @@ class NotificationPermissionActivity : AppCompatActivity() { } private fun displayAutoSynchroPopup() { - val autoSynchroValue = SharedPreferencesManager.readString(this, SharedPreferencesManager.SharedPrefKey.AUTO_SYNCHRO) + val autoSynchroValue = SharedPreferencesManager.readString(SharedPreferencesManager.SharedPrefKey.AUTO_SYNCHRO) if (autoSynchroValue.toFloat() <= 0) { MaterialDialog.Builder(this) diff --git a/app/src/main/java/com/readrops/app/fragments/FeedsFragment.java b/app/src/main/java/com/readrops/app/fragments/FeedsFragment.java index 82dcf0a9..ff9a31e2 100644 --- a/app/src/main/java/com/readrops/app/fragments/FeedsFragment.java +++ b/app/src/main/java/com/readrops/app/fragments/FeedsFragment.java @@ -61,9 +61,9 @@ public class FeedsFragment extends Fragment { account = getArguments().getParcelable(ACCOUNT); if (account.getLogin() == null) - account.setLogin(SharedPreferencesManager.readString(getContext(), account.getLoginKey())); + account.setLogin(SharedPreferencesManager.readString(account.getLoginKey())); if (account.getPassword() == null) - account.setPassword(SharedPreferencesManager.readString(getContext(), account.getPasswordKey())); + account.setPassword(SharedPreferencesManager.readString(account.getPasswordKey())); viewModel = SharedViewModelCompat.getSharedViewModel(this, ManageFeedsFoldersViewModel.class); viewModel.setAccount(account); diff --git a/app/src/main/java/com/readrops/app/fragments/FoldersFragment.java b/app/src/main/java/com/readrops/app/fragments/FoldersFragment.java index 9ecb0e63..587660ee 100644 --- a/app/src/main/java/com/readrops/app/fragments/FoldersFragment.java +++ b/app/src/main/java/com/readrops/app/fragments/FoldersFragment.java @@ -61,9 +61,9 @@ public class FoldersFragment extends Fragment { account = getArguments().getParcelable(ACCOUNT); if (account.getLogin() == null) - account.setLogin(SharedPreferencesManager.readString(getContext(), account.getLoginKey())); + account.setLogin(SharedPreferencesManager.readString(account.getLoginKey())); if (account.getPassword() == null) - account.setPassword(SharedPreferencesManager.readString(getContext(), account.getPasswordKey())); + account.setPassword(SharedPreferencesManager.readString(account.getPasswordKey())); adapter = new FoldersAdapter(this::openFolderOptionsDialog); viewModel = SharedViewModelCompat.getSharedViewModel(this, ManageFeedsFoldersViewModel.class); diff --git a/app/src/main/java/com/readrops/app/fragments/settings/AccountSettingsFragment.java b/app/src/main/java/com/readrops/app/fragments/settings/AccountSettingsFragment.java index 30d91490..b0f1cc95 100644 --- a/app/src/main/java/com/readrops/app/fragments/settings/AccountSettingsFragment.java +++ b/app/src/main/java/com/readrops/app/fragments/settings/AccountSettingsFragment.java @@ -147,8 +147,8 @@ public class AccountSettingsFragment extends PreferenceFragmentCompat { .positiveText(R.string.validate) .negativeText(R.string.cancel) .onPositive(((dialog, which) -> { - SharedPreferencesManager.remove(getContext(), account.getLoginKey()); - SharedPreferencesManager.remove(getContext(), account.getPasswordKey()); + SharedPreferencesManager.remove(account.getLoginKey()); + SharedPreferencesManager.remove(account.getPasswordKey()); viewModel.delete(account) .subscribeOn(Schedulers.io()) diff --git a/app/src/main/java/com/readrops/app/repositories/LocalFeedRepository.java b/app/src/main/java/com/readrops/app/repositories/LocalFeedRepository.java index 65aa3553..f1d27653 100644 --- a/app/src/main/java/com/readrops/app/repositories/LocalFeedRepository.java +++ b/app/src/main/java/com/readrops/app/repositories/LocalFeedRepository.java @@ -133,7 +133,7 @@ public class LocalFeedRepository extends ARepository { Collections.sort(items, Item::compareTo); - int maxItems = Integer.parseInt(SharedPreferencesManager.readString(context, + int maxItems = Integer.parseInt(SharedPreferencesManager.readString( SharedPreferencesManager.SharedPrefKey.ITEMS_TO_PARSE_MAX_NB)); if (maxItems > 0 && items.size() > maxItems) { items = items.subList(items.size() - maxItems, items.size()); diff --git a/app/src/main/java/com/readrops/app/utils/SharedPreferencesManager.java b/app/src/main/java/com/readrops/app/utils/SharedPreferencesManager.java index 0da4ee57..9d42e45b 100644 --- a/app/src/main/java/com/readrops/app/utils/SharedPreferencesManager.java +++ b/app/src/main/java/com/readrops/app/utils/SharedPreferencesManager.java @@ -1,19 +1,15 @@ package com.readrops.app.utils; -import android.content.Context; import android.content.SharedPreferences; -import android.preference.PreferenceManager; import androidx.annotation.NonNull; +import org.koin.java.KoinJavaComponent; + public final class SharedPreferencesManager { - private static SharedPreferences getSharedPreferences(Context context) { - return PreferenceManager.getDefaultSharedPreferences(context); - } - - public static void writeValue(Context context, String key, Object value) { - SharedPreferences sharedPref = getSharedPreferences(context); + public static void writeValue(String key, Object value) { + SharedPreferences sharedPref = KoinJavaComponent.get(SharedPreferences.class); SharedPreferences.Editor editor = sharedPref.edit(); if (value instanceof Boolean) @@ -24,32 +20,32 @@ public final class SharedPreferencesManager { editor.apply(); } - public static void writeValue(Context context, SharedPrefKey sharedPrefKey, Object value) { - writeValue(context, sharedPrefKey.key, value); + public static void writeValue(SharedPrefKey sharedPrefKey, Object value) { + writeValue(sharedPrefKey.key, value); } - public static int readInt(Context context, SharedPrefKey sharedPrefKey) { - SharedPreferences sharedPreferences = getSharedPreferences(context); + public static int readInt(SharedPrefKey sharedPrefKey) { + SharedPreferences sharedPreferences = KoinJavaComponent.get(SharedPreferences.class); return sharedPreferences.getInt(sharedPrefKey.key, sharedPrefKey.getIntDefaultValue()); } - public static boolean readBoolean(Context context, SharedPrefKey sharedPrefKey) { - SharedPreferences sharedPreferences = getSharedPreferences(context); + public static boolean readBoolean(SharedPrefKey sharedPrefKey) { + SharedPreferences sharedPreferences = KoinJavaComponent.get(SharedPreferences.class); return sharedPreferences.getBoolean(sharedPrefKey.key, sharedPrefKey.getBooleanDefaultValue()); } - public static String readString(Context context, String key) { - SharedPreferences sharedPreferences = getSharedPreferences(context); + public static String readString(String key) { + SharedPreferences sharedPreferences = KoinJavaComponent.get(SharedPreferences.class); return sharedPreferences.getString(key, null); } - public static String readString(Context context, SharedPrefKey sharedPrefKey) { - SharedPreferences sharedPreferences = getSharedPreferences(context); + public static String readString(SharedPrefKey sharedPrefKey) { + SharedPreferences sharedPreferences = KoinJavaComponent.get(SharedPreferences.class); return sharedPreferences.getString(sharedPrefKey.key, sharedPrefKey.getStringDefaultValue()); } - public static void remove(Context context, String key) { - SharedPreferences sharedPreferences = getSharedPreferences(context); + public static void remove(String key) { + SharedPreferences sharedPreferences = KoinJavaComponent.get(SharedPreferences.class); SharedPreferences.Editor editor = sharedPreferences.edit(); editor.remove(key); diff --git a/app/src/main/java/com/readrops/app/utils/SyncWorker.kt b/app/src/main/java/com/readrops/app/utils/SyncWorker.kt index e544df2e..e788690b 100644 --- a/app/src/main/java/com/readrops/app/utils/SyncWorker.kt +++ b/app/src/main/java/com/readrops/app/utils/SyncWorker.kt @@ -7,7 +7,6 @@ import android.content.Intent import android.util.Log import androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat -import androidx.work.Data import androidx.work.Worker import androidx.work.WorkerParameters import com.readrops.api.services.SyncResult @@ -48,8 +47,8 @@ class SyncWorker(context: Context, parameters: WorkerParameters) : Worker(contex notificationBuilder.setContentText(it.accountName) notificationManager.notify(SYNC_NOTIFICATION_ID, notificationBuilder.build()) - it.login = SharedPreferencesManager.readString(applicationContext, it.loginKey) - it.password = SharedPreferencesManager.readString(applicationContext, it.passwordKey) + it.login = SharedPreferencesManager.readString(it.loginKey) + it.password = SharedPreferencesManager.readString(it.passwordKey) val repository = get(parameters = { parametersOf(it) }) From 738e20165389217590517f60a7a69f8a8bd2107c Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Fri, 23 Oct 2020 21:26:36 +0200 Subject: [PATCH 098/187] Update kotlin and android gradle plugins --- build.gradle | 4 ++-- gradle/wrapper/gradle-wrapper.properties | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/build.gradle b/build.gradle index 00210d22..665e510e 100644 --- a/build.gradle +++ b/build.gradle @@ -1,14 +1,14 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. buildscript { - ext.kotlin_version = '1.3.72' + ext.kotlin_version = '1.4.10' repositories { google() jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:4.0.1' + classpath 'com.android.tools.build:gradle:4.1.0' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" } } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index ebe84a9e..9f2e253f 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Fri Jul 10 17:43:55 CEST 2020 +#Fri Oct 23 19:12:43 CEST 2020 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-6.1.1-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-6.5-bin.zip From 5e76eb41344be0ddba21fc56a70d1cc870fd06ed Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Fri, 23 Oct 2020 21:30:34 +0200 Subject: [PATCH 099/187] Reorganize api utils package --- api/src/androidTest/java/com/readrops/api/OPMLParserTest.kt | 2 +- .../com/readrops/api/localfeed/LocalRSSDataSourceTest.kt | 4 ++-- .../com/readrops/api/localfeed/atom/ATOMItemsAdapterTest.kt | 2 +- .../com/readrops/api/localfeed/json/JSONItemsAdapterTest.kt | 2 +- .../com/readrops/api/localfeed/rss1/RSS1ItemsAdapterTest.kt | 2 +- .../com/readrops/api/localfeed/rss2/RSS2FeedAdapterTest.kt | 2 +- .../com/readrops/api/localfeed/rss2/RSS2ItemsAdapterTest.kt | 2 +- .../java/com/readrops/api/localfeed/LocalRSSDataSource.kt | 4 ++-- .../java/com/readrops/api/localfeed/atom/ATOMFeedAdapter.kt | 6 +++--- .../com/readrops/api/localfeed/atom/ATOMItemsAdapter.kt | 4 ++++ .../java/com/readrops/api/localfeed/json/JSONFeedAdapter.kt | 6 +++--- .../com/readrops/api/localfeed/json/JSONItemsAdapter.kt | 6 +++--- .../java/com/readrops/api/localfeed/rss1/RSS1FeedAdapter.kt | 6 +++--- .../com/readrops/api/localfeed/rss1/RSS1ItemsAdapter.kt | 4 ++++ .../java/com/readrops/api/localfeed/rss2/RSS2FeedAdapter.kt | 6 +++--- .../com/readrops/api/localfeed/rss2/RSS2ItemsAdapter.kt | 4 ++++ api/src/main/java/com/readrops/api/opml/OPMLParser.kt | 3 +-- .../api/services/nextcloudnews/NextNewsDataSource.java | 4 ++-- .../services/nextcloudnews/adapters/NextNewsFeedsAdapter.kt | 2 +- .../services/nextcloudnews/adapters/NextNewsItemsAdapter.kt | 2 +- .../api/utils/{ => exceptions}/ConflictException.java | 2 +- .../readrops/api/utils/{ => exceptions}/ParseException.java | 2 +- .../api/utils/{ => exceptions}/UnknownFormatException.java | 2 +- .../api/utils/{ => extensions}/JsonReaderExtensions.kt | 3 ++- .../api/utils/{ => extensions}/KonsumerExtensions.kt | 3 ++- .../java/com/readrops/api/utils/JsonReaderExtensionsTest.kt | 3 +++ .../java/com/readrops/api/utils/KonsumerExtensionsTest.kt | 3 +++ .../readrops/app/activities/ManageFeedsFoldersActivity.java | 4 ++-- .../java/com/readrops/app/fragments/FoldersFragment.java | 4 ++-- .../com/readrops/app/repositories/LocalFeedRepository.java | 4 ++-- .../com/readrops/app/repositories/NextNewsRepository.java | 2 +- 31 files changed, 62 insertions(+), 43 deletions(-) rename api/src/main/java/com/readrops/api/utils/{ => exceptions}/ConflictException.java (80%) rename api/src/main/java/com/readrops/api/utils/{ => exceptions}/ParseException.java (80%) rename api/src/main/java/com/readrops/api/utils/{ => exceptions}/UnknownFormatException.java (81%) rename api/src/main/java/com/readrops/api/utils/{ => extensions}/JsonReaderExtensions.kt (78%) rename api/src/main/java/com/readrops/api/utils/{ => extensions}/KonsumerExtensions.kt (86%) diff --git a/api/src/androidTest/java/com/readrops/api/OPMLParserTest.kt b/api/src/androidTest/java/com/readrops/api/OPMLParserTest.kt index 33da88f0..2fd10595 100644 --- a/api/src/androidTest/java/com/readrops/api/OPMLParserTest.kt +++ b/api/src/androidTest/java/com/readrops/api/OPMLParserTest.kt @@ -7,7 +7,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.platform.app.InstrumentationRegistry import androidx.test.rule.GrantPermissionRule import com.readrops.api.opml.OPMLParser -import com.readrops.api.utils.ParseException +import com.readrops.api.utils.exceptions.ParseException import com.readrops.db.entities.Feed import com.readrops.db.entities.Folder import io.reactivex.schedulers.Schedulers diff --git a/api/src/androidTest/java/com/readrops/api/localfeed/LocalRSSDataSourceTest.kt b/api/src/androidTest/java/com/readrops/api/localfeed/LocalRSSDataSourceTest.kt index 22b87e68..5259109a 100644 --- a/api/src/androidTest/java/com/readrops/api/localfeed/LocalRSSDataSourceTest.kt +++ b/api/src/androidTest/java/com/readrops/api/localfeed/LocalRSSDataSourceTest.kt @@ -5,8 +5,8 @@ import android.content.Context import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.platform.app.InstrumentationRegistry import com.readrops.api.utils.LibUtils -import com.readrops.api.utils.ParseException -import com.readrops.api.utils.UnknownFormatException +import com.readrops.api.utils.exceptions.ParseException +import com.readrops.api.utils.exceptions.UnknownFormatException import junit.framework.TestCase.* import okhttp3.Headers import okhttp3.HttpUrl diff --git a/api/src/androidTest/java/com/readrops/api/localfeed/atom/ATOMItemsAdapterTest.kt b/api/src/androidTest/java/com/readrops/api/localfeed/atom/ATOMItemsAdapterTest.kt index 3d8b850f..b737272a 100644 --- a/api/src/androidTest/java/com/readrops/api/localfeed/atom/ATOMItemsAdapterTest.kt +++ b/api/src/androidTest/java/com/readrops/api/localfeed/atom/ATOMItemsAdapterTest.kt @@ -4,7 +4,7 @@ import android.content.Context import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.platform.app.InstrumentationRegistry import com.readrops.api.utils.DateUtils -import com.readrops.api.utils.ParseException +import com.readrops.api.utils.exceptions.ParseException import junit.framework.TestCase.* import org.junit.Assert import org.junit.Test diff --git a/api/src/androidTest/java/com/readrops/api/localfeed/json/JSONItemsAdapterTest.kt b/api/src/androidTest/java/com/readrops/api/localfeed/json/JSONItemsAdapterTest.kt index 3d62a836..4b95a665 100644 --- a/api/src/androidTest/java/com/readrops/api/localfeed/json/JSONItemsAdapterTest.kt +++ b/api/src/androidTest/java/com/readrops/api/localfeed/json/JSONItemsAdapterTest.kt @@ -4,7 +4,7 @@ import android.content.Context import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.platform.app.InstrumentationRegistry import com.readrops.api.utils.DateUtils -import com.readrops.api.utils.ParseException +import com.readrops.api.utils.exceptions.ParseException import com.readrops.db.entities.Item import com.squareup.moshi.Moshi import com.squareup.moshi.Types diff --git a/api/src/androidTest/java/com/readrops/api/localfeed/rss1/RSS1ItemsAdapterTest.kt b/api/src/androidTest/java/com/readrops/api/localfeed/rss1/RSS1ItemsAdapterTest.kt index 40939c27..99ce97f4 100644 --- a/api/src/androidTest/java/com/readrops/api/localfeed/rss1/RSS1ItemsAdapterTest.kt +++ b/api/src/androidTest/java/com/readrops/api/localfeed/rss1/RSS1ItemsAdapterTest.kt @@ -4,7 +4,7 @@ import android.content.Context import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.platform.app.InstrumentationRegistry import com.readrops.api.utils.DateUtils -import com.readrops.api.utils.ParseException +import com.readrops.api.utils.exceptions.ParseException import junit.framework.TestCase.* import org.junit.Assert import org.junit.Test diff --git a/api/src/androidTest/java/com/readrops/api/localfeed/rss2/RSS2FeedAdapterTest.kt b/api/src/androidTest/java/com/readrops/api/localfeed/rss2/RSS2FeedAdapterTest.kt index 4ac3c43d..3368c0e6 100644 --- a/api/src/androidTest/java/com/readrops/api/localfeed/rss2/RSS2FeedAdapterTest.kt +++ b/api/src/androidTest/java/com/readrops/api/localfeed/rss2/RSS2FeedAdapterTest.kt @@ -3,7 +3,7 @@ package com.readrops.api.localfeed.rss2 import android.content.Context import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.platform.app.InstrumentationRegistry -import com.readrops.api.utils.ParseException +import com.readrops.api.utils.exceptions.ParseException import junit.framework.TestCase.assertEquals import org.junit.Test import org.junit.runner.RunWith diff --git a/api/src/androidTest/java/com/readrops/api/localfeed/rss2/RSS2ItemsAdapterTest.kt b/api/src/androidTest/java/com/readrops/api/localfeed/rss2/RSS2ItemsAdapterTest.kt index 1d3aa690..4154f924 100644 --- a/api/src/androidTest/java/com/readrops/api/localfeed/rss2/RSS2ItemsAdapterTest.kt +++ b/api/src/androidTest/java/com/readrops/api/localfeed/rss2/RSS2ItemsAdapterTest.kt @@ -4,7 +4,7 @@ import android.content.Context import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.platform.app.InstrumentationRegistry import com.readrops.api.utils.DateUtils -import com.readrops.api.utils.ParseException +import com.readrops.api.utils.exceptions.ParseException import junit.framework.TestCase.* import org.junit.Assert import org.junit.Test diff --git a/api/src/main/java/com/readrops/api/localfeed/LocalRSSDataSource.kt b/api/src/main/java/com/readrops/api/localfeed/LocalRSSDataSource.kt index 6069b2b0..f682e808 100644 --- a/api/src/main/java/com/readrops/api/localfeed/LocalRSSDataSource.kt +++ b/api/src/main/java/com/readrops/api/localfeed/LocalRSSDataSource.kt @@ -5,8 +5,8 @@ import androidx.annotation.WorkerThread import com.readrops.api.localfeed.json.JSONFeedAdapter import com.readrops.api.localfeed.json.JSONItemsAdapter import com.readrops.api.utils.LibUtils -import com.readrops.api.utils.ParseException -import com.readrops.api.utils.UnknownFormatException +import com.readrops.api.utils.exceptions.ParseException +import com.readrops.api.utils.exceptions.UnknownFormatException import com.readrops.db.entities.Feed import com.readrops.db.entities.Item import com.squareup.moshi.Moshi diff --git a/api/src/main/java/com/readrops/api/localfeed/atom/ATOMFeedAdapter.kt b/api/src/main/java/com/readrops/api/localfeed/atom/ATOMFeedAdapter.kt index 43446073..4e88f5c8 100644 --- a/api/src/main/java/com/readrops/api/localfeed/atom/ATOMFeedAdapter.kt +++ b/api/src/main/java/com/readrops/api/localfeed/atom/ATOMFeedAdapter.kt @@ -5,9 +5,9 @@ import com.gitlab.mvysny.konsumexml.Names import com.gitlab.mvysny.konsumexml.allChildrenAutoIgnore import com.gitlab.mvysny.konsumexml.konsumeXml import com.readrops.api.localfeed.XmlAdapter -import com.readrops.api.utils.ParseException -import com.readrops.api.utils.nonNullText -import com.readrops.api.utils.nullableText +import com.readrops.api.utils.exceptions.ParseException +import com.readrops.api.utils.extensions.nonNullText +import com.readrops.api.utils.extensions.nullableText import com.readrops.db.entities.Feed import java.io.InputStream diff --git a/api/src/main/java/com/readrops/api/localfeed/atom/ATOMItemsAdapter.kt b/api/src/main/java/com/readrops/api/localfeed/atom/ATOMItemsAdapter.kt index be61a987..151bf382 100644 --- a/api/src/main/java/com/readrops/api/localfeed/atom/ATOMItemsAdapter.kt +++ b/api/src/main/java/com/readrops/api/localfeed/atom/ATOMItemsAdapter.kt @@ -6,6 +6,10 @@ import com.gitlab.mvysny.konsumexml.allChildrenAutoIgnore import com.gitlab.mvysny.konsumexml.konsumeXml import com.readrops.api.localfeed.XmlAdapter import com.readrops.api.utils.* +import com.readrops.api.utils.exceptions.ParseException +import com.readrops.api.utils.extensions.nonNullText +import com.readrops.api.utils.extensions.nullableText +import com.readrops.api.utils.extensions.nullableTextRecursively import com.readrops.db.entities.Item import org.joda.time.LocalDateTime import java.io.InputStream diff --git a/api/src/main/java/com/readrops/api/localfeed/json/JSONFeedAdapter.kt b/api/src/main/java/com/readrops/api/localfeed/json/JSONFeedAdapter.kt index 81ff0828..80086873 100644 --- a/api/src/main/java/com/readrops/api/localfeed/json/JSONFeedAdapter.kt +++ b/api/src/main/java/com/readrops/api/localfeed/json/JSONFeedAdapter.kt @@ -1,8 +1,8 @@ package com.readrops.api.localfeed.json -import com.readrops.api.utils.ParseException -import com.readrops.api.utils.nextNonEmptyString -import com.readrops.api.utils.nextNullableString +import com.readrops.api.utils.exceptions.ParseException +import com.readrops.api.utils.extensions.nextNonEmptyString +import com.readrops.api.utils.extensions.nextNullableString import com.readrops.db.entities.Feed import com.squareup.moshi.FromJson import com.squareup.moshi.JsonReader diff --git a/api/src/main/java/com/readrops/api/localfeed/json/JSONItemsAdapter.kt b/api/src/main/java/com/readrops/api/localfeed/json/JSONItemsAdapter.kt index e209ff3f..71e26506 100644 --- a/api/src/main/java/com/readrops/api/localfeed/json/JSONItemsAdapter.kt +++ b/api/src/main/java/com/readrops/api/localfeed/json/JSONItemsAdapter.kt @@ -2,9 +2,9 @@ package com.readrops.api.localfeed.json import com.readrops.api.localfeed.XmlAdapter.Companion.AUTHORS_MAX import com.readrops.api.utils.DateUtils -import com.readrops.api.utils.ParseException -import com.readrops.api.utils.nextNonEmptyString -import com.readrops.api.utils.nextNullableString +import com.readrops.api.utils.exceptions.ParseException +import com.readrops.api.utils.extensions.nextNonEmptyString +import com.readrops.api.utils.extensions.nextNullableString import com.readrops.db.entities.Item import com.squareup.moshi.JsonAdapter import com.squareup.moshi.JsonReader diff --git a/api/src/main/java/com/readrops/api/localfeed/rss1/RSS1FeedAdapter.kt b/api/src/main/java/com/readrops/api/localfeed/rss1/RSS1FeedAdapter.kt index 0b7e9cb0..cbbdbe2a 100644 --- a/api/src/main/java/com/readrops/api/localfeed/rss1/RSS1FeedAdapter.kt +++ b/api/src/main/java/com/readrops/api/localfeed/rss1/RSS1FeedAdapter.kt @@ -4,9 +4,9 @@ import com.gitlab.mvysny.konsumexml.Names import com.gitlab.mvysny.konsumexml.allChildrenAutoIgnore import com.gitlab.mvysny.konsumexml.konsumeXml import com.readrops.api.localfeed.XmlAdapter -import com.readrops.api.utils.ParseException -import com.readrops.api.utils.nonNullText -import com.readrops.api.utils.nullableText +import com.readrops.api.utils.exceptions.ParseException +import com.readrops.api.utils.extensions.nonNullText +import com.readrops.api.utils.extensions.nullableText import com.readrops.db.entities.Feed import java.io.InputStream diff --git a/api/src/main/java/com/readrops/api/localfeed/rss1/RSS1ItemsAdapter.kt b/api/src/main/java/com/readrops/api/localfeed/rss1/RSS1ItemsAdapter.kt index 0796fed9..416de501 100644 --- a/api/src/main/java/com/readrops/api/localfeed/rss1/RSS1ItemsAdapter.kt +++ b/api/src/main/java/com/readrops/api/localfeed/rss1/RSS1ItemsAdapter.kt @@ -6,6 +6,10 @@ import com.gitlab.mvysny.konsumexml.konsumeXml import com.readrops.api.localfeed.XmlAdapter import com.readrops.api.localfeed.XmlAdapter.Companion.AUTHORS_MAX import com.readrops.api.utils.* +import com.readrops.api.utils.exceptions.ParseException +import com.readrops.api.utils.extensions.nonNullText +import com.readrops.api.utils.extensions.nullableText +import com.readrops.api.utils.extensions.nullableTextRecursively import com.readrops.db.entities.Item import org.joda.time.LocalDateTime import java.io.InputStream diff --git a/api/src/main/java/com/readrops/api/localfeed/rss2/RSS2FeedAdapter.kt b/api/src/main/java/com/readrops/api/localfeed/rss2/RSS2FeedAdapter.kt index 2751a428..e28c1478 100644 --- a/api/src/main/java/com/readrops/api/localfeed/rss2/RSS2FeedAdapter.kt +++ b/api/src/main/java/com/readrops/api/localfeed/rss2/RSS2FeedAdapter.kt @@ -4,9 +4,9 @@ import com.gitlab.mvysny.konsumexml.Names import com.gitlab.mvysny.konsumexml.allChildrenAutoIgnore import com.gitlab.mvysny.konsumexml.konsumeXml import com.readrops.api.localfeed.XmlAdapter -import com.readrops.api.utils.ParseException -import com.readrops.api.utils.nonNullText -import com.readrops.api.utils.nullableText +import com.readrops.api.utils.exceptions.ParseException +import com.readrops.api.utils.extensions.nonNullText +import com.readrops.api.utils.extensions.nullableText import com.readrops.db.entities.Feed import org.jsoup.Jsoup import java.io.InputStream diff --git a/api/src/main/java/com/readrops/api/localfeed/rss2/RSS2ItemsAdapter.kt b/api/src/main/java/com/readrops/api/localfeed/rss2/RSS2ItemsAdapter.kt index ef2dfcc2..2a367efa 100644 --- a/api/src/main/java/com/readrops/api/localfeed/rss2/RSS2ItemsAdapter.kt +++ b/api/src/main/java/com/readrops/api/localfeed/rss2/RSS2ItemsAdapter.kt @@ -4,6 +4,10 @@ import com.gitlab.mvysny.konsumexml.* import com.readrops.api.localfeed.XmlAdapter import com.readrops.api.localfeed.XmlAdapter.Companion.AUTHORS_MAX import com.readrops.api.utils.* +import com.readrops.api.utils.exceptions.ParseException +import com.readrops.api.utils.extensions.nonNullText +import com.readrops.api.utils.extensions.nullableText +import com.readrops.api.utils.extensions.nullableTextRecursively import com.readrops.db.entities.Item import org.joda.time.LocalDateTime import java.io.InputStream diff --git a/api/src/main/java/com/readrops/api/opml/OPMLParser.kt b/api/src/main/java/com/readrops/api/opml/OPMLParser.kt index ce9dba51..8b55bba0 100644 --- a/api/src/main/java/com/readrops/api/opml/OPMLParser.kt +++ b/api/src/main/java/com/readrops/api/opml/OPMLParser.kt @@ -7,8 +7,7 @@ import com.readrops.api.opml.model.Body import com.readrops.api.opml.model.Head import com.readrops.api.opml.model.OPML import com.readrops.api.opml.model.Outline -import com.readrops.api.utils.LibUtils -import com.readrops.api.utils.ParseException +import com.readrops.api.utils.exceptions.ParseException import com.readrops.db.entities.Feed import com.readrops.db.entities.Folder import io.reactivex.Completable diff --git a/api/src/main/java/com/readrops/api/services/nextcloudnews/NextNewsDataSource.java b/api/src/main/java/com/readrops/api/services/nextcloudnews/NextNewsDataSource.java index ed06044d..bfe3ae96 100644 --- a/api/src/main/java/com/readrops/api/services/nextcloudnews/NextNewsDataSource.java +++ b/api/src/main/java/com/readrops/api/services/nextcloudnews/NextNewsDataSource.java @@ -8,9 +8,9 @@ import androidx.annotation.Nullable; import com.readrops.api.services.SyncResult; import com.readrops.api.services.SyncType; import com.readrops.api.services.nextcloudnews.json.NextNewsUser; -import com.readrops.api.utils.ConflictException; +import com.readrops.api.utils.exceptions.ConflictException; import com.readrops.api.utils.LibUtils; -import com.readrops.api.utils.UnknownFormatException; +import com.readrops.api.utils.exceptions.UnknownFormatException; import com.readrops.db.entities.Feed; import com.readrops.db.entities.Folder; import com.readrops.db.entities.Item; diff --git a/api/src/main/java/com/readrops/api/services/nextcloudnews/adapters/NextNewsFeedsAdapter.kt b/api/src/main/java/com/readrops/api/services/nextcloudnews/adapters/NextNewsFeedsAdapter.kt index 4bfc3832..ba1c34e0 100644 --- a/api/src/main/java/com/readrops/api/services/nextcloudnews/adapters/NextNewsFeedsAdapter.kt +++ b/api/src/main/java/com/readrops/api/services/nextcloudnews/adapters/NextNewsFeedsAdapter.kt @@ -2,7 +2,7 @@ package com.readrops.api.services.nextcloudnews.adapters import android.annotation.SuppressLint import com.readrops.db.entities.Feed -import com.readrops.api.utils.nextNullableString +import com.readrops.api.utils.extensions.nextNullableString import com.squareup.moshi.FromJson import com.squareup.moshi.JsonReader import com.squareup.moshi.ToJson diff --git a/api/src/main/java/com/readrops/api/services/nextcloudnews/adapters/NextNewsItemsAdapter.kt b/api/src/main/java/com/readrops/api/services/nextcloudnews/adapters/NextNewsItemsAdapter.kt index 992a1e44..1d4f145a 100644 --- a/api/src/main/java/com/readrops/api/services/nextcloudnews/adapters/NextNewsItemsAdapter.kt +++ b/api/src/main/java/com/readrops/api/services/nextcloudnews/adapters/NextNewsItemsAdapter.kt @@ -3,7 +3,7 @@ package com.readrops.api.services.nextcloudnews.adapters import android.annotation.SuppressLint import com.readrops.db.entities.Item import com.readrops.api.utils.LibUtils -import com.readrops.api.utils.nextNullableString +import com.readrops.api.utils.extensions.nextNullableString import com.squareup.moshi.JsonAdapter import com.squareup.moshi.JsonReader import com.squareup.moshi.JsonWriter diff --git a/api/src/main/java/com/readrops/api/utils/ConflictException.java b/api/src/main/java/com/readrops/api/utils/exceptions/ConflictException.java similarity index 80% rename from api/src/main/java/com/readrops/api/utils/ConflictException.java rename to api/src/main/java/com/readrops/api/utils/exceptions/ConflictException.java index 83467a44..eda1d4d6 100644 --- a/api/src/main/java/com/readrops/api/utils/ConflictException.java +++ b/api/src/main/java/com/readrops/api/utils/exceptions/ConflictException.java @@ -1,4 +1,4 @@ -package com.readrops.api.utils; +package com.readrops.api.utils.exceptions; public class ConflictException extends Exception { diff --git a/api/src/main/java/com/readrops/api/utils/ParseException.java b/api/src/main/java/com/readrops/api/utils/exceptions/ParseException.java similarity index 80% rename from api/src/main/java/com/readrops/api/utils/ParseException.java rename to api/src/main/java/com/readrops/api/utils/exceptions/ParseException.java index 46065da3..9ff5e50d 100644 --- a/api/src/main/java/com/readrops/api/utils/ParseException.java +++ b/api/src/main/java/com/readrops/api/utils/exceptions/ParseException.java @@ -1,4 +1,4 @@ -package com.readrops.api.utils; +package com.readrops.api.utils.exceptions; public class ParseException extends Exception { diff --git a/api/src/main/java/com/readrops/api/utils/UnknownFormatException.java b/api/src/main/java/com/readrops/api/utils/exceptions/UnknownFormatException.java similarity index 81% rename from api/src/main/java/com/readrops/api/utils/UnknownFormatException.java rename to api/src/main/java/com/readrops/api/utils/exceptions/UnknownFormatException.java index 3e76fe56..8e6da51b 100644 --- a/api/src/main/java/com/readrops/api/utils/UnknownFormatException.java +++ b/api/src/main/java/com/readrops/api/utils/exceptions/UnknownFormatException.java @@ -1,4 +1,4 @@ -package com.readrops.api.utils; +package com.readrops.api.utils.exceptions; public class UnknownFormatException extends Exception { diff --git a/api/src/main/java/com/readrops/api/utils/JsonReaderExtensions.kt b/api/src/main/java/com/readrops/api/utils/extensions/JsonReaderExtensions.kt similarity index 78% rename from api/src/main/java/com/readrops/api/utils/JsonReaderExtensions.kt rename to api/src/main/java/com/readrops/api/utils/extensions/JsonReaderExtensions.kt index 09ecf36a..d06ca5dd 100644 --- a/api/src/main/java/com/readrops/api/utils/JsonReaderExtensions.kt +++ b/api/src/main/java/com/readrops/api/utils/extensions/JsonReaderExtensions.kt @@ -1,5 +1,6 @@ -package com.readrops.api.utils +package com.readrops.api.utils.extensions +import com.readrops.api.utils.exceptions.ParseException import com.squareup.moshi.JsonReader fun JsonReader.nextNullableString(): String? = diff --git a/api/src/main/java/com/readrops/api/utils/KonsumerExtensions.kt b/api/src/main/java/com/readrops/api/utils/extensions/KonsumerExtensions.kt similarity index 86% rename from api/src/main/java/com/readrops/api/utils/KonsumerExtensions.kt rename to api/src/main/java/com/readrops/api/utils/extensions/KonsumerExtensions.kt index 46de8a39..25a8ba81 100644 --- a/api/src/main/java/com/readrops/api/utils/KonsumerExtensions.kt +++ b/api/src/main/java/com/readrops/api/utils/extensions/KonsumerExtensions.kt @@ -1,8 +1,9 @@ -package com.readrops.api.utils +package com.readrops.api.utils.extensions import com.gitlab.mvysny.konsumexml.Konsumer import com.gitlab.mvysny.konsumexml.Whitespace import com.gitlab.mvysny.konsumexml.textRecursively +import com.readrops.api.utils.exceptions.ParseException fun Konsumer.nonNullText(): String { val text = text(whitespace = Whitespace.preserve) diff --git a/api/src/test/java/com/readrops/api/utils/JsonReaderExtensionsTest.kt b/api/src/test/java/com/readrops/api/utils/JsonReaderExtensionsTest.kt index 0bba568b..fb27e07a 100644 --- a/api/src/test/java/com/readrops/api/utils/JsonReaderExtensionsTest.kt +++ b/api/src/test/java/com/readrops/api/utils/JsonReaderExtensionsTest.kt @@ -1,5 +1,8 @@ package com.readrops.api.utils +import com.readrops.api.utils.exceptions.ParseException +import com.readrops.api.utils.extensions.nextNonEmptyString +import com.readrops.api.utils.extensions.nextNullableString import com.squareup.moshi.JsonReader import junit.framework.TestCase.assertEquals import junit.framework.TestCase.assertNull diff --git a/api/src/test/java/com/readrops/api/utils/KonsumerExtensionsTest.kt b/api/src/test/java/com/readrops/api/utils/KonsumerExtensionsTest.kt index 7698dfb2..40699959 100644 --- a/api/src/test/java/com/readrops/api/utils/KonsumerExtensionsTest.kt +++ b/api/src/test/java/com/readrops/api/utils/KonsumerExtensionsTest.kt @@ -2,6 +2,9 @@ package com.readrops.api.utils import com.gitlab.mvysny.konsumexml.KonsumerException import com.gitlab.mvysny.konsumexml.konsumeXml +import com.readrops.api.utils.extensions.nonNullText +import com.readrops.api.utils.extensions.nullableText +import com.readrops.api.utils.extensions.nullableTextRecursively import junit.framework.TestCase.assertEquals import junit.framework.TestCase.assertNull import org.junit.Test diff --git a/app/src/main/java/com/readrops/app/activities/ManageFeedsFoldersActivity.java b/app/src/main/java/com/readrops/app/activities/ManageFeedsFoldersActivity.java index 9404647a..888534c4 100644 --- a/app/src/main/java/com/readrops/app/activities/ManageFeedsFoldersActivity.java +++ b/app/src/main/java/com/readrops/app/activities/ManageFeedsFoldersActivity.java @@ -11,8 +11,8 @@ import androidx.fragment.app.FragmentManager; import androidx.fragment.app.FragmentPagerAdapter; import com.afollestad.materialdialogs.MaterialDialog; -import com.readrops.api.utils.ConflictException; -import com.readrops.api.utils.UnknownFormatException; +import com.readrops.api.utils.exceptions.ConflictException; +import com.readrops.api.utils.exceptions.UnknownFormatException; import com.readrops.app.R; import com.readrops.app.databinding.ActivityManageFeedsFoldersBinding; import com.readrops.app.fragments.FeedsFragment; diff --git a/app/src/main/java/com/readrops/app/fragments/FoldersFragment.java b/app/src/main/java/com/readrops/app/fragments/FoldersFragment.java index 587660ee..444c0d0a 100644 --- a/app/src/main/java/com/readrops/app/fragments/FoldersFragment.java +++ b/app/src/main/java/com/readrops/app/fragments/FoldersFragment.java @@ -13,8 +13,8 @@ import androidx.fragment.app.Fragment; import androidx.recyclerview.widget.LinearLayoutManager; import com.afollestad.materialdialogs.MaterialDialog; -import com.readrops.api.utils.ConflictException; -import com.readrops.api.utils.UnknownFormatException; +import com.readrops.api.utils.exceptions.ConflictException; +import com.readrops.api.utils.exceptions.UnknownFormatException; import com.readrops.app.R; import com.readrops.app.adapters.FoldersAdapter; import com.readrops.app.databinding.FragmentFoldersBinding; diff --git a/app/src/main/java/com/readrops/app/repositories/LocalFeedRepository.java b/app/src/main/java/com/readrops/app/repositories/LocalFeedRepository.java index f1d27653..98cced76 100644 --- a/app/src/main/java/com/readrops/app/repositories/LocalFeedRepository.java +++ b/app/src/main/java/com/readrops/app/repositories/LocalFeedRepository.java @@ -10,8 +10,8 @@ import androidx.annotation.Nullable; import com.readrops.api.localfeed.LocalRSSDataSource; import com.readrops.api.services.SyncResult; import com.readrops.api.utils.LibUtils; -import com.readrops.api.utils.ParseException; -import com.readrops.api.utils.UnknownFormatException; +import com.readrops.api.utils.exceptions.ParseException; +import com.readrops.api.utils.exceptions.UnknownFormatException; import com.readrops.app.utils.FeedInsertionResult; import com.readrops.app.utils.ParsingResult; import com.readrops.app.utils.SharedPreferencesManager; diff --git a/app/src/main/java/com/readrops/app/repositories/NextNewsRepository.java b/app/src/main/java/com/readrops/app/repositories/NextNewsRepository.java index 1d10f93b..18adc41a 100644 --- a/app/src/main/java/com/readrops/app/repositories/NextNewsRepository.java +++ b/app/src/main/java/com/readrops/app/repositories/NextNewsRepository.java @@ -12,7 +12,7 @@ import com.readrops.api.services.SyncType; import com.readrops.api.services.nextcloudnews.NextNewsDataSource; import com.readrops.api.services.nextcloudnews.NextNewsSyncData; import com.readrops.api.services.nextcloudnews.json.NextNewsUser; -import com.readrops.api.utils.UnknownFormatException; +import com.readrops.api.utils.exceptions.UnknownFormatException; import com.readrops.app.utils.FeedInsertionResult; import com.readrops.app.utils.ParsingResult; import com.readrops.app.utils.Utils; From 412ec3f1b8659273fb12b3ea530e8f5aa046311a Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Fri, 23 Oct 2020 21:33:27 +0200 Subject: [PATCH 100/187] Rename LibUtils to ApiUtils --- .../api/localfeed/LocalRSSDataSourceTest.kt | 20 ++++++++--------- .../api/localfeed/LocalRSSDataSource.kt | 14 ++++++------ .../api/localfeed/rss2/RSS2ItemsAdapter.kt | 8 +++---- .../nextcloudnews/NextNewsDataSource.java | 22 +++++++++---------- .../adapters/NextNewsItemsAdapter.kt | 4 ++-- .../utils/{LibUtils.java => ApiUtils.java} | 2 +- .../{LibUtilsTest.kt => ApiUtilsTest.kt} | 8 +++---- .../app/repositories/LocalFeedRepository.java | 6 ++--- .../com/readrops/app/utils/HtmlParser.java | 4 ++-- 9 files changed, 44 insertions(+), 44 deletions(-) rename api/src/main/java/com/readrops/api/utils/{LibUtils.java => ApiUtils.java} (98%) rename api/src/test/java/com/readrops/api/utils/{LibUtilsTest.kt => ApiUtilsTest.kt} (66%) diff --git a/api/src/androidTest/java/com/readrops/api/localfeed/LocalRSSDataSourceTest.kt b/api/src/androidTest/java/com/readrops/api/localfeed/LocalRSSDataSourceTest.kt index 5259109a..34fa8826 100644 --- a/api/src/androidTest/java/com/readrops/api/localfeed/LocalRSSDataSourceTest.kt +++ b/api/src/androidTest/java/com/readrops/api/localfeed/LocalRSSDataSourceTest.kt @@ -4,7 +4,7 @@ import android.accounts.NetworkErrorException import android.content.Context import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.platform.app.InstrumentationRegistry -import com.readrops.api.utils.LibUtils +import com.readrops.api.utils.ApiUtils import com.readrops.api.utils.exceptions.ParseException import com.readrops.api.utils.exceptions.UnknownFormatException import junit.framework.TestCase.* @@ -46,9 +46,9 @@ class LocalRSSDataSourceTest { val stream = context.resources.assets.open("localfeed/rss_feed.xml") mockServer.enqueue(MockResponse().setResponseCode(HttpURLConnection.HTTP_OK) - .addHeader(LibUtils.CONTENT_TYPE_HEADER, "application/xml; charset=UTF-8") - .addHeader(LibUtils.ETAG_HEADER, "ETag-value") - .addHeader(LibUtils.LAST_MODIFIED_HEADER, "Last-Modified") + .addHeader(ApiUtils.CONTENT_TYPE_HEADER, "application/xml; charset=UTF-8") + .addHeader(ApiUtils.ETAG_HEADER, "ETag-value") + .addHeader(ApiUtils.LAST_MODIFIED_HEADER, "Last-Modified") .setBody(Buffer().readFrom(stream))) val pair = localRSSDataSource.queryRSSResource(url.toString(), null) @@ -73,13 +73,13 @@ class LocalRSSDataSourceTest { .addHeader("Content-Type", "application/rss+xml; charset=UTF-8") .setBody(Buffer().readFrom(stream))) - val headers = Headers.headersOf(LibUtils.ETAG_HEADER, "ETag", LibUtils.LAST_MODIFIED_HEADER, "Last-Modified") + val headers = Headers.headersOf(ApiUtils.ETAG_HEADER, "ETag", ApiUtils.LAST_MODIFIED_HEADER, "Last-Modified") localRSSDataSource.queryRSSResource(url.toString(), headers) val request = mockServer.takeRequest() - assertEquals(request.headers[LibUtils.ETAG_HEADER], "ETag") - assertEquals(request.headers[LibUtils.LAST_MODIFIED_HEADER], "Last-Modified") + assertEquals(request.headers[ApiUtils.ETAG_HEADER], "ETag") + assertEquals(request.headers[ApiUtils.LAST_MODIFIED_HEADER], "Last-Modified") } @Test @@ -87,7 +87,7 @@ class LocalRSSDataSourceTest { val stream = context.resources.assets.open("localfeed/json/json_feed.json") mockServer.enqueue(MockResponse().setResponseCode(HttpURLConnection.HTTP_OK) - .addHeader(LibUtils.CONTENT_TYPE_HEADER, "application/feed+json") + .addHeader(ApiUtils.CONTENT_TYPE_HEADER, "application/feed+json") .setBody(Buffer().readFrom(stream))) val pair = localRSSDataSource.queryRSSResource(url.toString(), null)!! @@ -101,7 +101,7 @@ class LocalRSSDataSourceTest { val stream = context.resources.assets.open("localfeed/atom/atom_feed_no_url_siteurl.xml") mockServer.enqueue(MockResponse().setResponseCode(HttpURLConnection.HTTP_OK) - .addHeader(LibUtils.CONTENT_TYPE_HEADER, "application/atom+xml") + .addHeader(ApiUtils.CONTENT_TYPE_HEADER, "application/atom+xml") .setBody(Buffer().readFrom(stream))) val pair = localRSSDataSource.queryRSSResource(url.toString(), null)!! @@ -115,7 +115,7 @@ class LocalRSSDataSourceTest { val stream = context.resources.assets.open("localfeed/rss1/rss1_feed_no_url_siteurl.xml") mockServer.enqueue(MockResponse().setResponseCode(HttpURLConnection.HTTP_OK) - .addHeader(LibUtils.CONTENT_TYPE_HEADER, "application/rdf+xml") + .addHeader(ApiUtils.CONTENT_TYPE_HEADER, "application/rdf+xml") .setBody(Buffer().readFrom(stream))) val pair = localRSSDataSource.queryRSSResource(url.toString(), null)!! diff --git a/api/src/main/java/com/readrops/api/localfeed/LocalRSSDataSource.kt b/api/src/main/java/com/readrops/api/localfeed/LocalRSSDataSource.kt index f682e808..4a7cfd46 100644 --- a/api/src/main/java/com/readrops/api/localfeed/LocalRSSDataSource.kt +++ b/api/src/main/java/com/readrops/api/localfeed/LocalRSSDataSource.kt @@ -4,7 +4,7 @@ import android.accounts.NetworkErrorException import androidx.annotation.WorkerThread import com.readrops.api.localfeed.json.JSONFeedAdapter import com.readrops.api.localfeed.json.JSONItemsAdapter -import com.readrops.api.utils.LibUtils +import com.readrops.api.utils.ApiUtils import com.readrops.api.utils.exceptions.ParseException import com.readrops.api.utils.exceptions.UnknownFormatException import com.readrops.db.entities.Feed @@ -36,10 +36,10 @@ class LocalRSSDataSource(private val httpClient: OkHttpClient) { return when { response.isSuccessful -> { - val header = response.header(LibUtils.CONTENT_TYPE_HEADER) + val header = response.header(ApiUtils.CONTENT_TYPE_HEADER) ?: throw UnknownFormatException("Unable to get $url content-type") - val contentType = LibUtils.parseContentType(header) + val contentType = ApiUtils.parseContentType(header) ?: throw ParseException("Unable to parse $url content-type") var type = LocalRSSHelper.getRSSType(contentType) @@ -73,10 +73,10 @@ class LocalRSSDataSource(private val httpClient: OkHttpClient) { val response = queryUrl(url, null) return if (response.isSuccessful) { - val header = response.header(LibUtils.CONTENT_TYPE_HEADER) + val header = response.header(ApiUtils.CONTENT_TYPE_HEADER) ?: return false - val contentType = LibUtils.parseContentType(header) + val contentType = ApiUtils.parseContentType(header) ?: return false var type = LocalRSSHelper.getRSSType(contentType) @@ -112,8 +112,8 @@ class LocalRSSDataSource(private val httpClient: OkHttpClient) { handleSpecialCases(feed, type, response) - feed.etag = response.header(LibUtils.ETAG_HEADER) - feed.lastModified = response.header(LibUtils.LAST_MODIFIED_HEADER) + feed.etag = response.header(ApiUtils.ETAG_HEADER) + feed.lastModified = response.header(ApiUtils.LAST_MODIFIED_HEADER) return feed } diff --git a/api/src/main/java/com/readrops/api/localfeed/rss2/RSS2ItemsAdapter.kt b/api/src/main/java/com/readrops/api/localfeed/rss2/RSS2ItemsAdapter.kt index 2a367efa..3d37adbe 100644 --- a/api/src/main/java/com/readrops/api/localfeed/rss2/RSS2ItemsAdapter.kt +++ b/api/src/main/java/com/readrops/api/localfeed/rss2/RSS2ItemsAdapter.kt @@ -27,7 +27,7 @@ class RSS2ItemsAdapter : XmlAdapter> { val item = Item().apply { allChildrenAutoIgnore(names) { when (tagName) { - "title" -> title = LibUtils.cleanText(nonNullText()) + "title" -> title = ApiUtils.cleanText(nonNullText()) "link" -> link = nonNullText() "author" -> author = nullableText() "dc:creator" -> creators += nullableText() @@ -60,16 +60,16 @@ class RSS2ItemsAdapter : XmlAdapter> { private fun parseEnclosure(konsumer: Konsumer, item: Item) { if (konsumer.attributes.getValueOpt("type") != null - && LibUtils.isMimeImage(konsumer.attributes["type"]) && item.imageLink == null) + && ApiUtils.isMimeImage(konsumer.attributes["type"]) && item.imageLink == null) item.imageLink = konsumer.attributes.getValueOpt("url") } private fun isMediumImage(konsumer: Konsumer) = with(konsumer) { - attributes.getValueOpt("medium") != null && LibUtils.isMimeImage(attributes["medium"]) + attributes.getValueOpt("medium") != null && ApiUtils.isMimeImage(attributes["medium"]) } private fun isTypeImage(konsumer: Konsumer) = with(konsumer) { - attributes.getValueOpt("type") != null && LibUtils.isMimeImage(attributes["type"]) + attributes.getValueOpt("type") != null && ApiUtils.isMimeImage(attributes["type"]) } private fun parseMediaContent(konsumer: Konsumer, item: Item) { diff --git a/api/src/main/java/com/readrops/api/services/nextcloudnews/NextNewsDataSource.java b/api/src/main/java/com/readrops/api/services/nextcloudnews/NextNewsDataSource.java index bfe3ae96..eaa6d30c 100644 --- a/api/src/main/java/com/readrops/api/services/nextcloudnews/NextNewsDataSource.java +++ b/api/src/main/java/com/readrops/api/services/nextcloudnews/NextNewsDataSource.java @@ -9,7 +9,7 @@ import com.readrops.api.services.SyncResult; import com.readrops.api.services.SyncType; import com.readrops.api.services.nextcloudnews.json.NextNewsUser; import com.readrops.api.utils.exceptions.ConflictException; -import com.readrops.api.utils.LibUtils; +import com.readrops.api.utils.ApiUtils; import com.readrops.api.utils.exceptions.UnknownFormatException; import com.readrops.db.entities.Feed; import com.readrops.db.entities.Folder; @@ -50,7 +50,7 @@ public class NextNewsDataSource { Response> response = api.createFeed(url, folderId).execute(); if (!response.isSuccessful()) { - if (response.code() == LibUtils.HTTP_UNPROCESSABLE) + if (response.code() == ApiUtils.HTTP_UNPROCESSABLE) throw new UnknownFormatException(); else return null; @@ -156,9 +156,9 @@ public class NextNewsDataSource { if (foldersResponse.isSuccessful()) return foldersResponse.body(); - else if (foldersResponse.code() == LibUtils.HTTP_UNPROCESSABLE) + else if (foldersResponse.code() == ApiUtils.HTTP_UNPROCESSABLE) throw new UnknownFormatException(); - else if (foldersResponse.code() == LibUtils.HTTP_CONFLICT) + else if (foldersResponse.code() == ApiUtils.HTTP_CONFLICT) throw new ConflictException(); else return new ArrayList<>(); @@ -169,7 +169,7 @@ public class NextNewsDataSource { if (response.isSuccessful()) return true; - else if (response.code() == LibUtils.HTTP_NOT_FOUND) + else if (response.code() == ApiUtils.HTTP_NOT_FOUND) throw new Resources.NotFoundException(); else return false; @@ -185,11 +185,11 @@ public class NextNewsDataSource { return true; else { switch (response.code()) { - case LibUtils.HTTP_NOT_FOUND: + case ApiUtils.HTTP_NOT_FOUND: throw new Resources.NotFoundException(); - case LibUtils.HTTP_UNPROCESSABLE: + case ApiUtils.HTTP_UNPROCESSABLE: throw new UnknownFormatException(); - case LibUtils.HTTP_CONFLICT: + case ApiUtils.HTTP_CONFLICT: throw new ConflictException(); default: return false; @@ -202,7 +202,7 @@ public class NextNewsDataSource { if (response.isSuccessful()) return true; - else if (response.code() == LibUtils.HTTP_NOT_FOUND) + else if (response.code() == ApiUtils.HTTP_NOT_FOUND) throw new Resources.NotFoundException(); else return false; @@ -216,7 +216,7 @@ public class NextNewsDataSource { if (response.isSuccessful()) return true; - else if (response.code() == LibUtils.HTTP_NOT_FOUND) + else if (response.code() == ApiUtils.HTTP_NOT_FOUND) throw new Resources.NotFoundException(); else return false; @@ -230,7 +230,7 @@ public class NextNewsDataSource { if (response.isSuccessful()) return true; - else if (response.code() == LibUtils.HTTP_NOT_FOUND) + else if (response.code() == ApiUtils.HTTP_NOT_FOUND) throw new Resources.NotFoundException(); else return false; diff --git a/api/src/main/java/com/readrops/api/services/nextcloudnews/adapters/NextNewsItemsAdapter.kt b/api/src/main/java/com/readrops/api/services/nextcloudnews/adapters/NextNewsItemsAdapter.kt index 1d4f145a..9599e2c3 100644 --- a/api/src/main/java/com/readrops/api/services/nextcloudnews/adapters/NextNewsItemsAdapter.kt +++ b/api/src/main/java/com/readrops/api/services/nextcloudnews/adapters/NextNewsItemsAdapter.kt @@ -2,7 +2,7 @@ package com.readrops.api.services.nextcloudnews.adapters import android.annotation.SuppressLint import com.readrops.db.entities.Item -import com.readrops.api.utils.LibUtils +import com.readrops.api.utils.ApiUtils import com.readrops.api.utils.extensions.nextNullableString import com.squareup.moshi.JsonAdapter import com.squareup.moshi.JsonReader @@ -50,7 +50,7 @@ class NextNewsItemsAdapter : JsonAdapter>() { } } - if (enclosureMime != null && LibUtils.isMimeImage(enclosureMime!!)) + if (enclosureMime != null && ApiUtils.isMimeImage(enclosureMime!!)) item.imageLink = enclosureLink items += item diff --git a/api/src/main/java/com/readrops/api/utils/LibUtils.java b/api/src/main/java/com/readrops/api/utils/ApiUtils.java similarity index 98% rename from api/src/main/java/com/readrops/api/utils/LibUtils.java rename to api/src/main/java/com/readrops/api/utils/ApiUtils.java index 7e914a2d..65209555 100644 --- a/api/src/main/java/com/readrops/api/utils/LibUtils.java +++ b/api/src/main/java/com/readrops/api/utils/ApiUtils.java @@ -8,7 +8,7 @@ import org.jsoup.Jsoup; import java.util.regex.Matcher; import java.util.regex.Pattern; -public final class LibUtils { +public final class ApiUtils { public static final String HTML_CONTENT_TYPE = "text/html"; diff --git a/api/src/test/java/com/readrops/api/utils/LibUtilsTest.kt b/api/src/test/java/com/readrops/api/utils/ApiUtilsTest.kt similarity index 66% rename from api/src/test/java/com/readrops/api/utils/LibUtilsTest.kt rename to api/src/test/java/com/readrops/api/utils/ApiUtilsTest.kt index ba6b8bc4..8a8bcae4 100644 --- a/api/src/test/java/com/readrops/api/utils/LibUtilsTest.kt +++ b/api/src/test/java/com/readrops/api/utils/ApiUtilsTest.kt @@ -3,23 +3,23 @@ package com.readrops.api.utils import junit.framework.TestCase.assertEquals import org.junit.Test -class LibUtilsTest { +class ApiUtilsTest { @Test fun contentTypeWithCharsetTest() { - assertEquals(LibUtils.parseContentType("application/rss+xml; charset=UTF-8"), + assertEquals(ApiUtils.parseContentType("application/rss+xml; charset=UTF-8"), "application/rss+xml") } @Test fun contentTypeWithoutCharsetText() { - assertEquals(LibUtils.parseContentType("text/xml"), + assertEquals(ApiUtils.parseContentType("text/xml"), "text/xml") } @Test fun cleanTextTest() { val text = "

This is a text
to

clean " - assertEquals("This is a text to clean", LibUtils.cleanText(text)) + assertEquals("This is a text to clean", ApiUtils.cleanText(text)) } } \ No newline at end of file diff --git a/app/src/main/java/com/readrops/app/repositories/LocalFeedRepository.java b/app/src/main/java/com/readrops/app/repositories/LocalFeedRepository.java index 98cced76..874644fe 100644 --- a/app/src/main/java/com/readrops/app/repositories/LocalFeedRepository.java +++ b/app/src/main/java/com/readrops/app/repositories/LocalFeedRepository.java @@ -9,7 +9,7 @@ import androidx.annotation.Nullable; import com.readrops.api.localfeed.LocalRSSDataSource; import com.readrops.api.services.SyncResult; -import com.readrops.api.utils.LibUtils; +import com.readrops.api.utils.ApiUtils; import com.readrops.api.utils.exceptions.ParseException; import com.readrops.api.utils.exceptions.UnknownFormatException; import com.readrops.app.utils.FeedInsertionResult; @@ -69,10 +69,10 @@ public class LocalFeedRepository extends ARepository { try { Headers.Builder headers = new Headers.Builder(); if (feed.getEtag() != null) { - headers.add(LibUtils.IF_NONE_MATCH_HEADER, feed.getEtag()); + headers.add(ApiUtils.IF_NONE_MATCH_HEADER, feed.getEtag()); } if (feed.getLastModified() != null) { - headers.add(LibUtils.IF_MODIFIED_HEADER, feed.getLastModified()); + headers.add(ApiUtils.IF_MODIFIED_HEADER, feed.getLastModified()); } Pair> pair = dataSource.queryRSSResource(feed.getUrl(), headers.build()); diff --git a/app/src/main/java/com/readrops/app/utils/HtmlParser.java b/app/src/main/java/com/readrops/app/utils/HtmlParser.java index daebc212..180ff2bd 100644 --- a/app/src/main/java/com/readrops/app/utils/HtmlParser.java +++ b/app/src/main/java/com/readrops/app/utils/HtmlParser.java @@ -6,7 +6,7 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.readrops.api.localfeed.LocalRSSHelper; -import com.readrops.api.utils.LibUtils; +import com.readrops.api.utils.ApiUtils; import org.jsoup.Jsoup; import org.jsoup.nodes.Document; @@ -87,7 +87,7 @@ public final class HtmlParser { Response response = KoinJavaComponent.get(OkHttpClient.class) .newCall(new Request.Builder().url(url).build()).execute(); - if (response.header("Content-Type").contains(LibUtils.HTML_CONTENT_TYPE)) { + if (response.header("Content-Type").contains(ApiUtils.HTML_CONTENT_TYPE)) { String body = response.body().string(); String head = body.substring(body.indexOf("")); From ecdc7143169a84c18845c2823a2dd33241ed68d7 Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Fri, 23 Oct 2020 23:00:43 +0200 Subject: [PATCH 101/187] Reorganize app module files by feature --- .../readrops/app/SyncResultAnalyserTest.kt | 2 +- app/src/main/AndroidManifest.xml | 30 +++++++++---------- .../main/java/com/readrops/app/AppModule.kt | 7 ++++- .../app/{activities => }/SplashActivity.java | 7 +++-- .../AccountTypeListActivity.java | 5 ++-- .../AccountTypeListAdapter.java | 2 +- .../AccountViewModel.java | 2 +- .../AddAccountActivity.java | 4 +-- .../AccountArrayAdapter.java | 2 +- .../AddFeedActivity.java | 8 ++--- .../AddFeedsViewModel.java | 4 +-- .../FeedInsertionResult.java | 2 +- .../app/{utils => addfeed}/ParsingResult.java | 2 +- .../ManageFeedsFoldersActivity.java | 7 ++--- .../ManageFeedsFoldersViewModel.java | 2 +- .../feeds}/EditFeedDialogFragment.java | 4 +-- .../feeds}/FeedOptionsDialogFragment.kt | 2 +- .../feeds}/FeedsAdapter.java | 2 +- .../feeds}/FeedsFragment.java | 5 ++-- .../folders}/FolderOptionsDialogFragment.kt | 2 +- .../folders}/FoldersAdapter.java | 2 +- .../folders}/FoldersFragment.java | 5 ++-- .../{activities => item}/ItemActivity.java | 3 +- .../{viewmodels => item}/ItemViewModel.java | 2 +- .../{activities => item}/WebViewActivity.kt | 3 +- .../{utils => itemslist}/DrawerManager.java | 2 +- .../MainActivity.java | 11 +++---- .../MainItemListAdapter.java | 2 +- .../MainViewModel.java | 2 +- .../NotificationPermissionActivity.kt | 5 ++-- .../NotificationPermissionListAdapter.kt | 3 +- .../NotificationPermissionViewModel.kt | 2 +- .../sync}/SyncResultAnalyser.kt | 4 ++- .../sync}/SyncResultDebugData.kt | 3 +- .../sync}/SyncResultNotifContent.kt | 2 +- .../sync}/SyncWorker.kt | 6 ++-- .../app/repositories/ARepository.java | 4 +-- .../app/repositories/FreshRSSRepository.java | 4 +-- .../app/repositories/LocalFeedRepository.java | 4 +-- .../app/repositories/NextNewsRepository.java | 4 +-- .../settings/AccountSettingsFragment.java | 10 +++---- .../SettingsActivity.java | 4 +-- .../settings/SettingsFragment.java | 4 +-- .../com/readrops/app/utils/HtmlParser.java | 1 + .../utils/{ => customviews}/EmptyListView.kt | 2 +- .../ReadropsItemTouchCallback.kt | 2 +- .../{ => customviews}/ReadropsWebView.java | 3 +- .../res/layout/activity_account_type_list.xml | 2 +- .../main/res/layout/activity_add_account.xml | 2 +- app/src/main/res/layout/activity_add_feed.xml | 2 +- app/src/main/res/layout/activity_item.xml | 4 +-- app/src/main/res/layout/activity_main.xml | 4 +-- .../layout/activity_manage_feeds_folders.xml | 2 +- .../activity_notification_permission.xml | 2 +- app/src/main/res/layout/activity_settings.xml | 2 +- app/src/main/res/layout/activity_splash.xml | 2 +- app/src/main/res/layout/activity_web_view.xml | 2 +- app/src/main/res/layout/fragment_feeds.xml | 4 +-- app/src/main/res/layout/fragment_folders.xml | 4 +-- .../java/com/readrops/app/HtmlParserTest.kt | 2 +- 60 files changed, 115 insertions(+), 119 deletions(-) rename app/src/main/java/com/readrops/app/{activities => }/SplashActivity.java (91%) rename app/src/main/java/com/readrops/app/{activities => account}/AccountTypeListActivity.java (97%) rename app/src/main/java/com/readrops/app/{adapters => account}/AccountTypeListAdapter.java (98%) rename app/src/main/java/com/readrops/app/{viewmodels => account}/AccountViewModel.java (98%) rename app/src/main/java/com/readrops/app/{activities => account}/AddAccountActivity.java (99%) rename app/src/main/java/com/readrops/app/{adapters => addfeed}/AccountArrayAdapter.java (97%) rename app/src/main/java/com/readrops/app/{activities => addfeed}/AddFeedActivity.java (97%) rename app/src/main/java/com/readrops/app/{viewmodels => addfeed}/AddFeedsViewModel.java (93%) rename app/src/main/java/com/readrops/app/{utils => addfeed}/FeedInsertionResult.java (99%) rename app/src/main/java/com/readrops/app/{utils => addfeed}/ParsingResult.java (99%) rename app/src/main/java/com/readrops/app/{activities => feedsfolders}/ManageFeedsFoldersActivity.java (96%) rename app/src/main/java/com/readrops/app/{viewmodels => feedsfolders}/ManageFeedsFoldersViewModel.java (98%) rename app/src/main/java/com/readrops/app/{fragments => feedsfolders/feeds}/EditFeedDialogFragment.java (97%) rename app/src/main/java/com/readrops/app/{fragments => feedsfolders/feeds}/FeedOptionsDialogFragment.kt (98%) rename app/src/main/java/com/readrops/app/{adapters => feedsfolders/feeds}/FeedsAdapter.java (99%) rename app/src/main/java/com/readrops/app/{fragments => feedsfolders/feeds}/FeedsFragment.java (97%) rename app/src/main/java/com/readrops/app/{fragments => feedsfolders/folders}/FolderOptionsDialogFragment.kt (97%) rename app/src/main/java/com/readrops/app/{adapters => feedsfolders/folders}/FoldersAdapter.java (98%) rename app/src/main/java/com/readrops/app/{fragments => feedsfolders/folders}/FoldersFragment.java (97%) rename app/src/main/java/com/readrops/app/{activities => item}/ItemActivity.java (99%) rename app/src/main/java/com/readrops/app/{viewmodels => item}/ItemViewModel.java (97%) rename app/src/main/java/com/readrops/app/{activities => item}/WebViewActivity.kt (98%) rename app/src/main/java/com/readrops/app/{utils => itemslist}/DrawerManager.java (99%) rename app/src/main/java/com/readrops/app/{activities => itemslist}/MainActivity.java (98%) rename app/src/main/java/com/readrops/app/{adapters => itemslist}/MainItemListAdapter.java (99%) rename app/src/main/java/com/readrops/app/{viewmodels => itemslist}/MainViewModel.java (99%) rename app/src/main/java/com/readrops/app/{activities => notifications}/NotificationPermissionActivity.kt (97%) rename app/src/main/java/com/readrops/app/{adapters => notifications}/NotificationPermissionListAdapter.kt (97%) rename app/src/main/java/com/readrops/app/{viewmodels => notifications}/NotificationPermissionViewModel.kt (96%) rename app/src/main/java/com/readrops/app/{utils => notifications/sync}/SyncResultAnalyser.kt (97%) rename app/src/main/java/com/readrops/app/{utils => notifications/sync}/SyncResultDebugData.kt (98%) rename app/src/main/java/com/readrops/app/{utils => notifications/sync}/SyncResultNotifContent.kt (85%) rename app/src/main/java/com/readrops/app/{utils => notifications/sync}/SyncWorker.kt (97%) rename app/src/main/java/com/readrops/app/{fragments => }/settings/AccountSettingsFragment.java (97%) rename app/src/main/java/com/readrops/app/{activities => settings}/SettingsActivity.java (91%) rename app/src/main/java/com/readrops/app/{fragments => }/settings/SettingsFragment.java (98%) rename app/src/main/java/com/readrops/app/utils/{ => customviews}/EmptyListView.kt (94%) rename app/src/main/java/com/readrops/app/utils/{ => customviews}/ReadropsItemTouchCallback.kt (99%) rename app/src/main/java/com/readrops/app/utils/{ => customviews}/ReadropsWebView.java (97%) diff --git a/app/src/androidTest/java/com/readrops/app/SyncResultAnalyserTest.kt b/app/src/androidTest/java/com/readrops/app/SyncResultAnalyserTest.kt index 9b3d9b4f..782c724d 100644 --- a/app/src/androidTest/java/com/readrops/app/SyncResultAnalyserTest.kt +++ b/app/src/androidTest/java/com/readrops/app/SyncResultAnalyserTest.kt @@ -4,7 +4,7 @@ import android.content.Context import androidx.room.Room import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.platform.app.InstrumentationRegistry -import com.readrops.app.utils.SyncResultAnalyser +import com.readrops.app.notifications.sync.SyncResultAnalyser import com.readrops.db.Database import com.readrops.db.entities.Feed import com.readrops.db.entities.Item diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 9d47cb49..fb1ff613 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -32,21 +32,21 @@ - - + + - + @@ -54,28 +54,28 @@ - + + android:parentActivityName=".itemslist.MainActivity"> diff --git a/app/src/main/java/com/readrops/app/AppModule.kt b/app/src/main/java/com/readrops/app/AppModule.kt index ee0b5696..0202b6ba 100644 --- a/app/src/main/java/com/readrops/app/AppModule.kt +++ b/app/src/main/java/com/readrops/app/AppModule.kt @@ -1,11 +1,16 @@ package com.readrops.app import androidx.preference.PreferenceManager +import com.readrops.app.account.AccountViewModel +import com.readrops.app.addfeed.AddFeedsViewModel +import com.readrops.app.feedsfolders.ManageFeedsFoldersViewModel +import com.readrops.app.item.ItemViewModel +import com.readrops.app.itemslist.MainViewModel +import com.readrops.app.notifications.NotificationPermissionViewModel import com.readrops.app.repositories.FreshRSSRepository import com.readrops.app.repositories.LocalFeedRepository import com.readrops.app.repositories.NextNewsRepository import com.readrops.app.utils.GlideApp -import com.readrops.app.viewmodels.* import com.readrops.db.entities.account.Account import com.readrops.db.entities.account.AccountType import org.koin.android.ext.koin.androidApplication diff --git a/app/src/main/java/com/readrops/app/activities/SplashActivity.java b/app/src/main/java/com/readrops/app/SplashActivity.java similarity index 91% rename from app/src/main/java/com/readrops/app/activities/SplashActivity.java rename to app/src/main/java/com/readrops/app/SplashActivity.java index 33edd643..6af8cfb3 100644 --- a/app/src/main/java/com/readrops/app/activities/SplashActivity.java +++ b/app/src/main/java/com/readrops/app/SplashActivity.java @@ -1,12 +1,13 @@ -package com.readrops.app.activities; +package com.readrops.app; import android.content.Intent; import android.os.Bundle; import androidx.appcompat.app.AppCompatActivity; -import com.readrops.app.R; -import com.readrops.app.viewmodels.AccountViewModel; +import com.readrops.app.account.AccountTypeListActivity; +import com.readrops.app.account.AccountViewModel; +import com.readrops.app.itemslist.MainActivity; import org.koin.androidx.viewmodel.compat.ViewModelCompat; diff --git a/app/src/main/java/com/readrops/app/activities/AccountTypeListActivity.java b/app/src/main/java/com/readrops/app/account/AccountTypeListActivity.java similarity index 97% rename from app/src/main/java/com/readrops/app/activities/AccountTypeListActivity.java rename to app/src/main/java/com/readrops/app/account/AccountTypeListActivity.java index 1ae20d00..c197eba7 100644 --- a/app/src/main/java/com/readrops/app/activities/AccountTypeListActivity.java +++ b/app/src/main/java/com/readrops/app/account/AccountTypeListActivity.java @@ -1,4 +1,4 @@ -package com.readrops.app.activities; +package com.readrops.app.account; import android.content.Intent; import android.net.Uri; @@ -17,10 +17,9 @@ import androidx.recyclerview.widget.LinearLayoutManager; import com.afollestad.materialdialogs.MaterialDialog; import com.readrops.api.opml.OPMLHelper; import com.readrops.app.R; -import com.readrops.app.adapters.AccountTypeListAdapter; import com.readrops.app.databinding.ActivityAccountTypeListBinding; +import com.readrops.app.itemslist.MainActivity; import com.readrops.app.utils.Utils; -import com.readrops.app.viewmodels.AccountViewModel; import com.readrops.db.entities.account.Account; import com.readrops.db.entities.account.AccountType; diff --git a/app/src/main/java/com/readrops/app/adapters/AccountTypeListAdapter.java b/app/src/main/java/com/readrops/app/account/AccountTypeListAdapter.java similarity index 98% rename from app/src/main/java/com/readrops/app/adapters/AccountTypeListAdapter.java rename to app/src/main/java/com/readrops/app/account/AccountTypeListAdapter.java index 8eea5135..3b93d325 100644 --- a/app/src/main/java/com/readrops/app/adapters/AccountTypeListAdapter.java +++ b/app/src/main/java/com/readrops/app/account/AccountTypeListAdapter.java @@ -1,4 +1,4 @@ -package com.readrops.app.adapters; +package com.readrops.app.account; import android.view.LayoutInflater; import android.view.ViewGroup; diff --git a/app/src/main/java/com/readrops/app/viewmodels/AccountViewModel.java b/app/src/main/java/com/readrops/app/account/AccountViewModel.java similarity index 98% rename from app/src/main/java/com/readrops/app/viewmodels/AccountViewModel.java rename to app/src/main/java/com/readrops/app/account/AccountViewModel.java index e93677e0..353e1cea 100644 --- a/app/src/main/java/com/readrops/app/viewmodels/AccountViewModel.java +++ b/app/src/main/java/com/readrops/app/account/AccountViewModel.java @@ -1,4 +1,4 @@ -package com.readrops.app.viewmodels; +package com.readrops.app.account; import android.content.Context; import android.net.Uri; diff --git a/app/src/main/java/com/readrops/app/activities/AddAccountActivity.java b/app/src/main/java/com/readrops/app/account/AddAccountActivity.java similarity index 99% rename from app/src/main/java/com/readrops/app/activities/AddAccountActivity.java rename to app/src/main/java/com/readrops/app/account/AddAccountActivity.java index f952c5bd..051949f7 100644 --- a/app/src/main/java/com/readrops/app/activities/AddAccountActivity.java +++ b/app/src/main/java/com/readrops/app/account/AddAccountActivity.java @@ -1,4 +1,4 @@ -package com.readrops.app.activities; +package com.readrops.app.account; import android.content.Intent; import android.os.Bundle; @@ -11,9 +11,9 @@ import androidx.appcompat.app.AppCompatActivity; import com.readrops.app.R; import com.readrops.app.databinding.ActivityAddAccountBinding; +import com.readrops.app.itemslist.MainActivity; import com.readrops.app.utils.SharedPreferencesManager; import com.readrops.app.utils.Utils; -import com.readrops.app.viewmodels.AccountViewModel; import com.readrops.db.entities.account.Account; import com.readrops.db.entities.account.AccountType; diff --git a/app/src/main/java/com/readrops/app/adapters/AccountArrayAdapter.java b/app/src/main/java/com/readrops/app/addfeed/AccountArrayAdapter.java similarity index 97% rename from app/src/main/java/com/readrops/app/adapters/AccountArrayAdapter.java rename to app/src/main/java/com/readrops/app/addfeed/AccountArrayAdapter.java index d2369167..af594ea1 100644 --- a/app/src/main/java/com/readrops/app/adapters/AccountArrayAdapter.java +++ b/app/src/main/java/com/readrops/app/addfeed/AccountArrayAdapter.java @@ -1,4 +1,4 @@ -package com.readrops.app.adapters; +package com.readrops.app.addfeed; import android.content.Context; import android.view.LayoutInflater; diff --git a/app/src/main/java/com/readrops/app/activities/AddFeedActivity.java b/app/src/main/java/com/readrops/app/addfeed/AddFeedActivity.java similarity index 97% rename from app/src/main/java/com/readrops/app/activities/AddFeedActivity.java rename to app/src/main/java/com/readrops/app/addfeed/AddFeedActivity.java index 5828ce1a..a8b5cb33 100644 --- a/app/src/main/java/com/readrops/app/activities/AddFeedActivity.java +++ b/app/src/main/java/com/readrops/app/addfeed/AddFeedActivity.java @@ -1,4 +1,4 @@ -package com.readrops.app.activities; +package com.readrops.app.addfeed; import android.annotation.SuppressLint; import android.content.Intent; @@ -21,14 +21,10 @@ import com.mikepenz.fastadapter.adapters.ItemAdapter; import com.mikepenz.fastadapter.commons.utils.DiffCallback; import com.mikepenz.fastadapter.commons.utils.FastAdapterDiffUtil; import com.readrops.app.R; -import com.readrops.app.adapters.AccountArrayAdapter; import com.readrops.app.databinding.ActivityAddFeedBinding; -import com.readrops.app.utils.FeedInsertionResult; -import com.readrops.app.utils.ParsingResult; -import com.readrops.app.utils.ReadropsItemTouchCallback; +import com.readrops.app.utils.customviews.ReadropsItemTouchCallback; import com.readrops.app.utils.SharedPreferencesManager; import com.readrops.app.utils.Utils; -import com.readrops.app.viewmodels.AddFeedsViewModel; import com.readrops.db.entities.Feed; import com.readrops.db.entities.account.Account; diff --git a/app/src/main/java/com/readrops/app/viewmodels/AddFeedsViewModel.java b/app/src/main/java/com/readrops/app/addfeed/AddFeedsViewModel.java similarity index 93% rename from app/src/main/java/com/readrops/app/viewmodels/AddFeedsViewModel.java rename to app/src/main/java/com/readrops/app/addfeed/AddFeedsViewModel.java index 905fdd9d..18e71d2d 100644 --- a/app/src/main/java/com/readrops/app/viewmodels/AddFeedsViewModel.java +++ b/app/src/main/java/com/readrops/app/addfeed/AddFeedsViewModel.java @@ -1,4 +1,4 @@ -package com.readrops.app.viewmodels; +package com.readrops.app.addfeed; import androidx.annotation.NonNull; import androidx.lifecycle.LiveData; @@ -6,9 +6,7 @@ import androidx.lifecycle.ViewModel; import com.readrops.api.localfeed.LocalRSSDataSource; import com.readrops.app.repositories.ARepository; -import com.readrops.app.utils.FeedInsertionResult; import com.readrops.app.utils.HtmlParser; -import com.readrops.app.utils.ParsingResult; import com.readrops.db.Database; import com.readrops.db.entities.account.Account; diff --git a/app/src/main/java/com/readrops/app/utils/FeedInsertionResult.java b/app/src/main/java/com/readrops/app/addfeed/FeedInsertionResult.java similarity index 99% rename from app/src/main/java/com/readrops/app/utils/FeedInsertionResult.java rename to app/src/main/java/com/readrops/app/addfeed/FeedInsertionResult.java index 1ffb4976..fd2e8379 100644 --- a/app/src/main/java/com/readrops/app/utils/FeedInsertionResult.java +++ b/app/src/main/java/com/readrops/app/addfeed/FeedInsertionResult.java @@ -1,4 +1,4 @@ -package com.readrops.app.utils; +package com.readrops.app.addfeed; import android.view.View; import android.widget.ImageView; diff --git a/app/src/main/java/com/readrops/app/utils/ParsingResult.java b/app/src/main/java/com/readrops/app/addfeed/ParsingResult.java similarity index 99% rename from app/src/main/java/com/readrops/app/utils/ParsingResult.java rename to app/src/main/java/com/readrops/app/addfeed/ParsingResult.java index c5b594c1..09e05489 100644 --- a/app/src/main/java/com/readrops/app/utils/ParsingResult.java +++ b/app/src/main/java/com/readrops/app/addfeed/ParsingResult.java @@ -1,4 +1,4 @@ -package com.readrops.app.utils; +package com.readrops.app.addfeed; import android.view.View; import android.widget.CheckBox; diff --git a/app/src/main/java/com/readrops/app/activities/ManageFeedsFoldersActivity.java b/app/src/main/java/com/readrops/app/feedsfolders/ManageFeedsFoldersActivity.java similarity index 96% rename from app/src/main/java/com/readrops/app/activities/ManageFeedsFoldersActivity.java rename to app/src/main/java/com/readrops/app/feedsfolders/ManageFeedsFoldersActivity.java index 888534c4..7b79f1a4 100644 --- a/app/src/main/java/com/readrops/app/activities/ManageFeedsFoldersActivity.java +++ b/app/src/main/java/com/readrops/app/feedsfolders/ManageFeedsFoldersActivity.java @@ -1,4 +1,4 @@ -package com.readrops.app.activities; +package com.readrops.app.feedsfolders; import android.os.Bundle; import android.view.Menu; @@ -15,10 +15,9 @@ import com.readrops.api.utils.exceptions.ConflictException; import com.readrops.api.utils.exceptions.UnknownFormatException; import com.readrops.app.R; import com.readrops.app.databinding.ActivityManageFeedsFoldersBinding; -import com.readrops.app.fragments.FeedsFragment; -import com.readrops.app.fragments.FoldersFragment; +import com.readrops.app.feedsfolders.feeds.FeedsFragment; +import com.readrops.app.feedsfolders.folders.FoldersFragment; import com.readrops.app.utils.Utils; -import com.readrops.app.viewmodels.ManageFeedsFoldersViewModel; import com.readrops.db.entities.Folder; import com.readrops.db.entities.account.Account; diff --git a/app/src/main/java/com/readrops/app/viewmodels/ManageFeedsFoldersViewModel.java b/app/src/main/java/com/readrops/app/feedsfolders/ManageFeedsFoldersViewModel.java similarity index 98% rename from app/src/main/java/com/readrops/app/viewmodels/ManageFeedsFoldersViewModel.java rename to app/src/main/java/com/readrops/app/feedsfolders/ManageFeedsFoldersViewModel.java index 802006b3..91bd9581 100644 --- a/app/src/main/java/com/readrops/app/viewmodels/ManageFeedsFoldersViewModel.java +++ b/app/src/main/java/com/readrops/app/feedsfolders/ManageFeedsFoldersViewModel.java @@ -1,4 +1,4 @@ -package com.readrops.app.viewmodels; +package com.readrops.app.feedsfolders; import androidx.annotation.NonNull; import androidx.lifecycle.LiveData; diff --git a/app/src/main/java/com/readrops/app/fragments/EditFeedDialogFragment.java b/app/src/main/java/com/readrops/app/feedsfolders/feeds/EditFeedDialogFragment.java similarity index 97% rename from app/src/main/java/com/readrops/app/fragments/EditFeedDialogFragment.java rename to app/src/main/java/com/readrops/app/feedsfolders/feeds/EditFeedDialogFragment.java index 5fc3c2f6..c9819336 100644 --- a/app/src/main/java/com/readrops/app/fragments/EditFeedDialogFragment.java +++ b/app/src/main/java/com/readrops/app/feedsfolders/feeds/EditFeedDialogFragment.java @@ -1,4 +1,4 @@ -package com.readrops.app.fragments; +package com.readrops.app.feedsfolders.feeds; import android.app.AlertDialog; import android.app.Dialog; @@ -14,7 +14,7 @@ import androidx.fragment.app.DialogFragment; import com.google.android.material.textfield.TextInputEditText; import com.readrops.app.R; -import com.readrops.app.viewmodels.ManageFeedsFoldersViewModel; +import com.readrops.app.feedsfolders.ManageFeedsFoldersViewModel; import com.readrops.db.entities.Feed; import com.readrops.db.entities.Folder; import com.readrops.db.entities.account.Account; diff --git a/app/src/main/java/com/readrops/app/fragments/FeedOptionsDialogFragment.kt b/app/src/main/java/com/readrops/app/feedsfolders/feeds/FeedOptionsDialogFragment.kt similarity index 98% rename from app/src/main/java/com/readrops/app/fragments/FeedOptionsDialogFragment.kt rename to app/src/main/java/com/readrops/app/feedsfolders/feeds/FeedOptionsDialogFragment.kt index ac7b9736..eea856e6 100644 --- a/app/src/main/java/com/readrops/app/fragments/FeedOptionsDialogFragment.kt +++ b/app/src/main/java/com/readrops/app/feedsfolders/feeds/FeedOptionsDialogFragment.kt @@ -1,4 +1,4 @@ -package com.readrops.app.fragments +package com.readrops.app.feedsfolders.feeds import android.content.Intent import android.net.Uri diff --git a/app/src/main/java/com/readrops/app/adapters/FeedsAdapter.java b/app/src/main/java/com/readrops/app/feedsfolders/feeds/FeedsAdapter.java similarity index 99% rename from app/src/main/java/com/readrops/app/adapters/FeedsAdapter.java rename to app/src/main/java/com/readrops/app/feedsfolders/feeds/FeedsAdapter.java index 9363b85e..8df74301 100644 --- a/app/src/main/java/com/readrops/app/adapters/FeedsAdapter.java +++ b/app/src/main/java/com/readrops/app/feedsfolders/feeds/FeedsAdapter.java @@ -1,4 +1,4 @@ -package com.readrops.app.adapters; +package com.readrops.app.feedsfolders.feeds; import android.view.LayoutInflater; import android.view.View; diff --git a/app/src/main/java/com/readrops/app/fragments/FeedsFragment.java b/app/src/main/java/com/readrops/app/feedsfolders/feeds/FeedsFragment.java similarity index 97% rename from app/src/main/java/com/readrops/app/fragments/FeedsFragment.java rename to app/src/main/java/com/readrops/app/feedsfolders/feeds/FeedsFragment.java index ff9a31e2..50ac70a5 100644 --- a/app/src/main/java/com/readrops/app/fragments/FeedsFragment.java +++ b/app/src/main/java/com/readrops/app/feedsfolders/feeds/FeedsFragment.java @@ -1,4 +1,4 @@ -package com.readrops.app.fragments; +package com.readrops.app.feedsfolders.feeds; import android.content.res.Resources; @@ -14,11 +14,10 @@ import androidx.recyclerview.widget.LinearLayoutManager; import com.afollestad.materialdialogs.MaterialDialog; import com.readrops.app.R; -import com.readrops.app.adapters.FeedsAdapter; import com.readrops.app.databinding.FragmentFeedsBinding; import com.readrops.app.utils.SharedPreferencesManager; import com.readrops.app.utils.Utils; -import com.readrops.app.viewmodels.ManageFeedsFoldersViewModel; +import com.readrops.app.feedsfolders.ManageFeedsFoldersViewModel; import com.readrops.db.entities.Feed; import com.readrops.db.entities.account.Account; import com.readrops.db.pojo.FeedWithFolder; diff --git a/app/src/main/java/com/readrops/app/fragments/FolderOptionsDialogFragment.kt b/app/src/main/java/com/readrops/app/feedsfolders/folders/FolderOptionsDialogFragment.kt similarity index 97% rename from app/src/main/java/com/readrops/app/fragments/FolderOptionsDialogFragment.kt rename to app/src/main/java/com/readrops/app/feedsfolders/folders/FolderOptionsDialogFragment.kt index ce04a350..853e6ce5 100644 --- a/app/src/main/java/com/readrops/app/fragments/FolderOptionsDialogFragment.kt +++ b/app/src/main/java/com/readrops/app/feedsfolders/folders/FolderOptionsDialogFragment.kt @@ -1,4 +1,4 @@ -package com.readrops.app.fragments +package com.readrops.app.feedsfolders.folders import android.os.Bundle import android.view.LayoutInflater diff --git a/app/src/main/java/com/readrops/app/adapters/FoldersAdapter.java b/app/src/main/java/com/readrops/app/feedsfolders/folders/FoldersAdapter.java similarity index 98% rename from app/src/main/java/com/readrops/app/adapters/FoldersAdapter.java rename to app/src/main/java/com/readrops/app/feedsfolders/folders/FoldersAdapter.java index f4a0f266..85266e92 100644 --- a/app/src/main/java/com/readrops/app/adapters/FoldersAdapter.java +++ b/app/src/main/java/com/readrops/app/feedsfolders/folders/FoldersAdapter.java @@ -1,4 +1,4 @@ -package com.readrops.app.adapters; +package com.readrops.app.feedsfolders.folders; import android.text.TextUtils; import android.view.LayoutInflater; diff --git a/app/src/main/java/com/readrops/app/fragments/FoldersFragment.java b/app/src/main/java/com/readrops/app/feedsfolders/folders/FoldersFragment.java similarity index 97% rename from app/src/main/java/com/readrops/app/fragments/FoldersFragment.java rename to app/src/main/java/com/readrops/app/feedsfolders/folders/FoldersFragment.java index 444c0d0a..0fca4c42 100644 --- a/app/src/main/java/com/readrops/app/fragments/FoldersFragment.java +++ b/app/src/main/java/com/readrops/app/feedsfolders/folders/FoldersFragment.java @@ -1,4 +1,4 @@ -package com.readrops.app.fragments; +package com.readrops.app.feedsfolders.folders; import android.content.res.Resources; @@ -16,11 +16,10 @@ import com.afollestad.materialdialogs.MaterialDialog; import com.readrops.api.utils.exceptions.ConflictException; import com.readrops.api.utils.exceptions.UnknownFormatException; import com.readrops.app.R; -import com.readrops.app.adapters.FoldersAdapter; import com.readrops.app.databinding.FragmentFoldersBinding; import com.readrops.app.utils.SharedPreferencesManager; import com.readrops.app.utils.Utils; -import com.readrops.app.viewmodels.ManageFeedsFoldersViewModel; +import com.readrops.app.feedsfolders.ManageFeedsFoldersViewModel; import com.readrops.db.entities.Folder; import com.readrops.db.entities.account.Account; diff --git a/app/src/main/java/com/readrops/app/activities/ItemActivity.java b/app/src/main/java/com/readrops/app/item/ItemActivity.java similarity index 99% rename from app/src/main/java/com/readrops/app/activities/ItemActivity.java rename to app/src/main/java/com/readrops/app/item/ItemActivity.java index 14a96800..2e7c8057 100644 --- a/app/src/main/java/com/readrops/app/activities/ItemActivity.java +++ b/app/src/main/java/com/readrops/app/item/ItemActivity.java @@ -1,4 +1,4 @@ -package com.readrops.app.activities; +package com.readrops.app.item; import android.Manifest; import android.app.DownloadManager; @@ -37,7 +37,6 @@ import com.readrops.app.utils.GlideRequests; import com.readrops.app.utils.PermissionManager; import com.readrops.app.utils.SharedPreferencesManager; import com.readrops.app.utils.Utils; -import com.readrops.app.viewmodels.ItemViewModel; import com.readrops.db.entities.Item; import com.readrops.db.pojo.ItemWithFeed; diff --git a/app/src/main/java/com/readrops/app/viewmodels/ItemViewModel.java b/app/src/main/java/com/readrops/app/item/ItemViewModel.java similarity index 97% rename from app/src/main/java/com/readrops/app/viewmodels/ItemViewModel.java rename to app/src/main/java/com/readrops/app/item/ItemViewModel.java index a3a94a25..102d66a8 100644 --- a/app/src/main/java/com/readrops/app/viewmodels/ItemViewModel.java +++ b/app/src/main/java/com/readrops/app/item/ItemViewModel.java @@ -1,4 +1,4 @@ -package com.readrops.app.viewmodels; +package com.readrops.app.item; import android.content.Context; import android.graphics.Bitmap; diff --git a/app/src/main/java/com/readrops/app/activities/WebViewActivity.kt b/app/src/main/java/com/readrops/app/item/WebViewActivity.kt similarity index 98% rename from app/src/main/java/com/readrops/app/activities/WebViewActivity.kt rename to app/src/main/java/com/readrops/app/item/WebViewActivity.kt index 2a41a876..a64a2464 100644 --- a/app/src/main/java/com/readrops/app/activities/WebViewActivity.kt +++ b/app/src/main/java/com/readrops/app/item/WebViewActivity.kt @@ -1,4 +1,4 @@ -package com.readrops.app.activities +package com.readrops.app.item import android.annotation.SuppressLint import android.content.Intent @@ -7,7 +7,6 @@ import android.graphics.Bitmap import android.graphics.drawable.ColorDrawable import android.net.Uri import android.os.Bundle -import android.view.LayoutInflater import android.view.Menu import android.view.MenuItem import android.view.View diff --git a/app/src/main/java/com/readrops/app/utils/DrawerManager.java b/app/src/main/java/com/readrops/app/itemslist/DrawerManager.java similarity index 99% rename from app/src/main/java/com/readrops/app/utils/DrawerManager.java rename to app/src/main/java/com/readrops/app/itemslist/DrawerManager.java index a1354f62..57a70ebf 100644 --- a/app/src/main/java/com/readrops/app/utils/DrawerManager.java +++ b/app/src/main/java/com/readrops/app/itemslist/DrawerManager.java @@ -1,4 +1,4 @@ -package com.readrops.app.utils; +package com.readrops.app.itemslist; import android.app.Activity; import android.graphics.drawable.Drawable; diff --git a/app/src/main/java/com/readrops/app/activities/MainActivity.java b/app/src/main/java/com/readrops/app/itemslist/MainActivity.java similarity index 98% rename from app/src/main/java/com/readrops/app/activities/MainActivity.java rename to app/src/main/java/com/readrops/app/itemslist/MainActivity.java index e8dac12f..5c68db04 100644 --- a/app/src/main/java/com/readrops/app/activities/MainActivity.java +++ b/app/src/main/java/com/readrops/app/itemslist/MainActivity.java @@ -1,4 +1,4 @@ -package com.readrops.app.activities; +package com.readrops.app.itemslist; import android.content.Intent; import android.graphics.drawable.Drawable; @@ -34,14 +34,15 @@ import com.mikepenz.materialdrawer.model.PrimaryDrawerItem; import com.mikepenz.materialdrawer.model.SecondaryDrawerItem; import com.mikepenz.materialdrawer.model.interfaces.IDrawerItem; import com.readrops.app.R; -import com.readrops.app.adapters.MainItemListAdapter; +import com.readrops.app.account.AccountTypeListActivity; +import com.readrops.app.addfeed.AddFeedActivity; +import com.readrops.app.item.ItemActivity; +import com.readrops.app.settings.SettingsActivity; import com.readrops.app.databinding.ActivityMainBinding; -import com.readrops.app.utils.DrawerManager; import com.readrops.app.utils.GlideRequests; -import com.readrops.app.utils.ReadropsItemTouchCallback; +import com.readrops.app.utils.customviews.ReadropsItemTouchCallback; import com.readrops.app.utils.SharedPreferencesManager; import com.readrops.app.utils.Utils; -import com.readrops.app.viewmodels.MainViewModel; import com.readrops.db.entities.Feed; import com.readrops.db.entities.Folder; import com.readrops.db.entities.account.Account; diff --git a/app/src/main/java/com/readrops/app/adapters/MainItemListAdapter.java b/app/src/main/java/com/readrops/app/itemslist/MainItemListAdapter.java similarity index 99% rename from app/src/main/java/com/readrops/app/adapters/MainItemListAdapter.java rename to app/src/main/java/com/readrops/app/itemslist/MainItemListAdapter.java index 30520174..9fac99cf 100644 --- a/app/src/main/java/com/readrops/app/adapters/MainItemListAdapter.java +++ b/app/src/main/java/com/readrops/app/itemslist/MainItemListAdapter.java @@ -1,4 +1,4 @@ -package com.readrops.app.adapters; +package com.readrops.app.itemslist; import android.content.Context; import android.content.res.Resources; diff --git a/app/src/main/java/com/readrops/app/viewmodels/MainViewModel.java b/app/src/main/java/com/readrops/app/itemslist/MainViewModel.java similarity index 99% rename from app/src/main/java/com/readrops/app/viewmodels/MainViewModel.java rename to app/src/main/java/com/readrops/app/itemslist/MainViewModel.java index d2071308..9ca634a3 100644 --- a/app/src/main/java/com/readrops/app/viewmodels/MainViewModel.java +++ b/app/src/main/java/com/readrops/app/itemslist/MainViewModel.java @@ -1,4 +1,4 @@ -package com.readrops.app.viewmodels; +package com.readrops.app.itemslist; import androidx.annotation.NonNull; import androidx.lifecycle.LiveData; diff --git a/app/src/main/java/com/readrops/app/activities/NotificationPermissionActivity.kt b/app/src/main/java/com/readrops/app/notifications/NotificationPermissionActivity.kt similarity index 97% rename from app/src/main/java/com/readrops/app/activities/NotificationPermissionActivity.kt rename to app/src/main/java/com/readrops/app/notifications/NotificationPermissionActivity.kt index a0b2ff07..e6c720e5 100644 --- a/app/src/main/java/com/readrops/app/activities/NotificationPermissionActivity.kt +++ b/app/src/main/java/com/readrops/app/notifications/NotificationPermissionActivity.kt @@ -1,4 +1,4 @@ -package com.readrops.app.activities +package com.readrops.app.notifications import android.content.Intent import android.os.Bundle @@ -8,13 +8,12 @@ import androidx.lifecycle.Observer import androidx.recyclerview.widget.LinearLayoutManager import com.afollestad.materialdialogs.MaterialDialog import com.readrops.app.R -import com.readrops.app.adapters.NotificationPermissionListAdapter +import com.readrops.app.settings.SettingsActivity import com.readrops.app.databinding.ActivityNotificationPermissionBinding import com.readrops.app.utils.ReadropsKeys import com.readrops.app.utils.ReadropsKeys.ACCOUNT_ID import com.readrops.app.utils.SharedPreferencesManager import com.readrops.app.utils.Utils -import com.readrops.app.viewmodels.NotificationPermissionViewModel import com.readrops.db.entities.Feed import com.readrops.db.entities.account.Account import io.reactivex.android.schedulers.AndroidSchedulers diff --git a/app/src/main/java/com/readrops/app/adapters/NotificationPermissionListAdapter.kt b/app/src/main/java/com/readrops/app/notifications/NotificationPermissionListAdapter.kt similarity index 97% rename from app/src/main/java/com/readrops/app/adapters/NotificationPermissionListAdapter.kt rename to app/src/main/java/com/readrops/app/notifications/NotificationPermissionListAdapter.kt index 146ba029..dade0c3a 100644 --- a/app/src/main/java/com/readrops/app/adapters/NotificationPermissionListAdapter.kt +++ b/app/src/main/java/com/readrops/app/notifications/NotificationPermissionListAdapter.kt @@ -1,4 +1,4 @@ -package com.readrops.app.adapters +package com.readrops.app.notifications import android.view.LayoutInflater import android.view.ViewGroup @@ -8,7 +8,6 @@ import androidx.recyclerview.widget.RecyclerView import com.bumptech.glide.load.engine.DiskCacheStrategy import com.readrops.app.R import com.readrops.app.databinding.NotificationPermissionLayoutBinding -import com.readrops.app.utils.GlideApp import com.readrops.app.utils.GlideRequests import com.readrops.db.entities.Feed import org.koin.core.KoinComponent diff --git a/app/src/main/java/com/readrops/app/viewmodels/NotificationPermissionViewModel.kt b/app/src/main/java/com/readrops/app/notifications/NotificationPermissionViewModel.kt similarity index 96% rename from app/src/main/java/com/readrops/app/viewmodels/NotificationPermissionViewModel.kt rename to app/src/main/java/com/readrops/app/notifications/NotificationPermissionViewModel.kt index 1e2fd243..d555f8a0 100644 --- a/app/src/main/java/com/readrops/app/viewmodels/NotificationPermissionViewModel.kt +++ b/app/src/main/java/com/readrops/app/notifications/NotificationPermissionViewModel.kt @@ -1,4 +1,4 @@ -package com.readrops.app.viewmodels +package com.readrops.app.notifications import androidx.lifecycle.LiveData import androidx.lifecycle.ViewModel diff --git a/app/src/main/java/com/readrops/app/utils/SyncResultAnalyser.kt b/app/src/main/java/com/readrops/app/notifications/sync/SyncResultAnalyser.kt similarity index 97% rename from app/src/main/java/com/readrops/app/utils/SyncResultAnalyser.kt rename to app/src/main/java/com/readrops/app/notifications/sync/SyncResultAnalyser.kt index a95c37dd..9b90df50 100644 --- a/app/src/main/java/com/readrops/app/utils/SyncResultAnalyser.kt +++ b/app/src/main/java/com/readrops/app/notifications/sync/SyncResultAnalyser.kt @@ -1,4 +1,4 @@ -package com.readrops.app.utils +package com.readrops.app.notifications.sync import android.content.Context import androidx.core.content.ContextCompat @@ -9,6 +9,8 @@ import com.readrops.db.entities.Feed import com.readrops.db.entities.Item import com.readrops.db.entities.account.Account import com.readrops.api.services.SyncResult +import com.readrops.app.utils.GlideRequests +import com.readrops.app.utils.Utils import org.koin.core.KoinComponent import org.koin.core.get diff --git a/app/src/main/java/com/readrops/app/utils/SyncResultDebugData.kt b/app/src/main/java/com/readrops/app/notifications/sync/SyncResultDebugData.kt similarity index 98% rename from app/src/main/java/com/readrops/app/utils/SyncResultDebugData.kt rename to app/src/main/java/com/readrops/app/notifications/sync/SyncResultDebugData.kt index e80de538..9931db0b 100644 --- a/app/src/main/java/com/readrops/app/utils/SyncResultDebugData.kt +++ b/app/src/main/java/com/readrops/app/notifications/sync/SyncResultDebugData.kt @@ -1,6 +1,5 @@ -package com.readrops.app.utils +package com.readrops.app.notifications.sync -import android.content.Context import com.readrops.api.services.SyncResult import com.readrops.db.Database import com.readrops.db.entities.Item diff --git a/app/src/main/java/com/readrops/app/utils/SyncResultNotifContent.kt b/app/src/main/java/com/readrops/app/notifications/sync/SyncResultNotifContent.kt similarity index 85% rename from app/src/main/java/com/readrops/app/utils/SyncResultNotifContent.kt rename to app/src/main/java/com/readrops/app/notifications/sync/SyncResultNotifContent.kt index 3552e1c6..0e1891cf 100644 --- a/app/src/main/java/com/readrops/app/utils/SyncResultNotifContent.kt +++ b/app/src/main/java/com/readrops/app/notifications/sync/SyncResultNotifContent.kt @@ -1,4 +1,4 @@ -package com.readrops.app.utils +package com.readrops.app.notifications.sync import android.graphics.Bitmap import com.readrops.db.entities.Item diff --git a/app/src/main/java/com/readrops/app/utils/SyncWorker.kt b/app/src/main/java/com/readrops/app/notifications/sync/SyncWorker.kt similarity index 97% rename from app/src/main/java/com/readrops/app/utils/SyncWorker.kt rename to app/src/main/java/com/readrops/app/notifications/sync/SyncWorker.kt index e788690b..acd187f8 100644 --- a/app/src/main/java/com/readrops/app/utils/SyncWorker.kt +++ b/app/src/main/java/com/readrops/app/notifications/sync/SyncWorker.kt @@ -1,4 +1,4 @@ -package com.readrops.app.utils +package com.readrops.app.notifications.sync import android.app.PendingIntent import android.content.BroadcastReceiver @@ -12,8 +12,10 @@ import androidx.work.WorkerParameters import com.readrops.api.services.SyncResult import com.readrops.app.R import com.readrops.app.ReadropsApp -import com.readrops.app.activities.MainActivity +import com.readrops.app.itemslist.MainActivity import com.readrops.app.repositories.ARepository +import com.readrops.app.utils.ReadropsKeys +import com.readrops.app.utils.SharedPreferencesManager import com.readrops.db.Database import com.readrops.db.entities.Item import com.readrops.db.entities.account.Account diff --git a/app/src/main/java/com/readrops/app/repositories/ARepository.java b/app/src/main/java/com/readrops/app/repositories/ARepository.java index 47425232..b20261c1 100644 --- a/app/src/main/java/com/readrops/app/repositories/ARepository.java +++ b/app/src/main/java/com/readrops/app/repositories/ARepository.java @@ -9,8 +9,8 @@ import androidx.annotation.Nullable; import com.readrops.api.services.Credentials; import com.readrops.api.services.SyncResult; import com.readrops.api.utils.AuthInterceptor; -import com.readrops.app.utils.FeedInsertionResult; -import com.readrops.app.utils.ParsingResult; +import com.readrops.app.addfeed.FeedInsertionResult; +import com.readrops.app.addfeed.ParsingResult; import com.readrops.app.utils.feedscolors.FeedColorsKt; import com.readrops.app.utils.feedscolors.FeedsColorsIntentService; import com.readrops.db.Database; diff --git a/app/src/main/java/com/readrops/app/repositories/FreshRSSRepository.java b/app/src/main/java/com/readrops/app/repositories/FreshRSSRepository.java index 0ab8a89c..77fcd385 100644 --- a/app/src/main/java/com/readrops/app/repositories/FreshRSSRepository.java +++ b/app/src/main/java/com/readrops/app/repositories/FreshRSSRepository.java @@ -10,8 +10,8 @@ import androidx.annotation.Nullable; import com.readrops.api.services.SyncType; import com.readrops.api.services.freshrss.FreshRSSDataSource; import com.readrops.api.services.freshrss.FreshRSSSyncData; -import com.readrops.app.utils.FeedInsertionResult; -import com.readrops.app.utils.ParsingResult; +import com.readrops.app.addfeed.FeedInsertionResult; +import com.readrops.app.addfeed.ParsingResult; import com.readrops.app.utils.Utils; import com.readrops.db.Database; import com.readrops.db.entities.Feed; diff --git a/app/src/main/java/com/readrops/app/repositories/LocalFeedRepository.java b/app/src/main/java/com/readrops/app/repositories/LocalFeedRepository.java index 874644fe..63ffddf6 100644 --- a/app/src/main/java/com/readrops/app/repositories/LocalFeedRepository.java +++ b/app/src/main/java/com/readrops/app/repositories/LocalFeedRepository.java @@ -12,8 +12,8 @@ import com.readrops.api.services.SyncResult; import com.readrops.api.utils.ApiUtils; import com.readrops.api.utils.exceptions.ParseException; import com.readrops.api.utils.exceptions.UnknownFormatException; -import com.readrops.app.utils.FeedInsertionResult; -import com.readrops.app.utils.ParsingResult; +import com.readrops.app.addfeed.FeedInsertionResult; +import com.readrops.app.addfeed.ParsingResult; import com.readrops.app.utils.SharedPreferencesManager; import com.readrops.app.utils.Utils; import com.readrops.db.Database; diff --git a/app/src/main/java/com/readrops/app/repositories/NextNewsRepository.java b/app/src/main/java/com/readrops/app/repositories/NextNewsRepository.java index 18adc41a..8d850a91 100644 --- a/app/src/main/java/com/readrops/app/repositories/NextNewsRepository.java +++ b/app/src/main/java/com/readrops/app/repositories/NextNewsRepository.java @@ -13,8 +13,8 @@ import com.readrops.api.services.nextcloudnews.NextNewsDataSource; import com.readrops.api.services.nextcloudnews.NextNewsSyncData; import com.readrops.api.services.nextcloudnews.json.NextNewsUser; import com.readrops.api.utils.exceptions.UnknownFormatException; -import com.readrops.app.utils.FeedInsertionResult; -import com.readrops.app.utils.ParsingResult; +import com.readrops.app.addfeed.FeedInsertionResult; +import com.readrops.app.addfeed.ParsingResult; import com.readrops.app.utils.Utils; import com.readrops.db.Database; import com.readrops.db.entities.Feed; diff --git a/app/src/main/java/com/readrops/app/fragments/settings/AccountSettingsFragment.java b/app/src/main/java/com/readrops/app/settings/AccountSettingsFragment.java similarity index 97% rename from app/src/main/java/com/readrops/app/fragments/settings/AccountSettingsFragment.java rename to app/src/main/java/com/readrops/app/settings/AccountSettingsFragment.java index b0f1cc95..767c5241 100644 --- a/app/src/main/java/com/readrops/app/fragments/settings/AccountSettingsFragment.java +++ b/app/src/main/java/com/readrops/app/settings/AccountSettingsFragment.java @@ -1,4 +1,4 @@ -package com.readrops.app.fragments.settings; +package com.readrops.app.settings; import android.Manifest; @@ -24,14 +24,14 @@ import com.readrops.api.opml.OPMLHelper; import com.readrops.api.opml.OPMLParser; import com.readrops.app.R; import com.readrops.app.ReadropsApp; -import com.readrops.app.activities.AddAccountActivity; -import com.readrops.app.activities.ManageFeedsFoldersActivity; -import com.readrops.app.activities.NotificationPermissionActivity; +import com.readrops.app.account.AddAccountActivity; +import com.readrops.app.feedsfolders.ManageFeedsFoldersActivity; +import com.readrops.app.notifications.NotificationPermissionActivity; import com.readrops.app.utils.FileUtils; import com.readrops.app.utils.PermissionManager; import com.readrops.app.utils.SharedPreferencesManager; import com.readrops.app.utils.Utils; -import com.readrops.app.viewmodels.AccountViewModel; +import com.readrops.app.account.AccountViewModel; import com.readrops.db.entities.account.Account; import com.readrops.db.entities.account.AccountType; diff --git a/app/src/main/java/com/readrops/app/activities/SettingsActivity.java b/app/src/main/java/com/readrops/app/settings/SettingsActivity.java similarity index 91% rename from app/src/main/java/com/readrops/app/activities/SettingsActivity.java rename to app/src/main/java/com/readrops/app/settings/SettingsActivity.java index 0a259894..06b0a639 100644 --- a/app/src/main/java/com/readrops/app/activities/SettingsActivity.java +++ b/app/src/main/java/com/readrops/app/settings/SettingsActivity.java @@ -1,4 +1,4 @@ -package com.readrops.app.activities; +package com.readrops.app.settings; import android.os.Bundle; import android.view.MenuItem; @@ -8,8 +8,6 @@ import androidx.fragment.app.Fragment; import com.readrops.app.R; import com.readrops.db.entities.account.Account; -import com.readrops.app.fragments.settings.AccountSettingsFragment; -import com.readrops.app.fragments.settings.SettingsFragment; import static com.readrops.app.utils.ReadropsKeys.ACCOUNT; import static com.readrops.app.utils.ReadropsKeys.SETTINGS; diff --git a/app/src/main/java/com/readrops/app/fragments/settings/SettingsFragment.java b/app/src/main/java/com/readrops/app/settings/SettingsFragment.java similarity index 98% rename from app/src/main/java/com/readrops/app/fragments/settings/SettingsFragment.java rename to app/src/main/java/com/readrops/app/settings/SettingsFragment.java index 4a28bad7..c0a02dcd 100644 --- a/app/src/main/java/com/readrops/app/fragments/settings/SettingsFragment.java +++ b/app/src/main/java/com/readrops/app/settings/SettingsFragment.java @@ -1,4 +1,4 @@ -package com.readrops.app.fragments.settings; +package com.readrops.app.settings; import android.content.Intent; import android.os.Bundle; @@ -15,7 +15,7 @@ import androidx.work.PeriodicWorkRequest; import androidx.work.WorkManager; import com.readrops.app.R; -import com.readrops.app.utils.SyncWorker; +import com.readrops.app.notifications.sync.SyncWorker; import com.readrops.app.utils.feedscolors.FeedsColorsIntentService; import com.readrops.db.Database; diff --git a/app/src/main/java/com/readrops/app/utils/HtmlParser.java b/app/src/main/java/com/readrops/app/utils/HtmlParser.java index 180ff2bd..9964aa9c 100644 --- a/app/src/main/java/com/readrops/app/utils/HtmlParser.java +++ b/app/src/main/java/com/readrops/app/utils/HtmlParser.java @@ -7,6 +7,7 @@ import androidx.annotation.Nullable; import com.readrops.api.localfeed.LocalRSSHelper; import com.readrops.api.utils.ApiUtils; +import com.readrops.app.addfeed.ParsingResult; import org.jsoup.Jsoup; import org.jsoup.nodes.Document; diff --git a/app/src/main/java/com/readrops/app/utils/EmptyListView.kt b/app/src/main/java/com/readrops/app/utils/customviews/EmptyListView.kt similarity index 94% rename from app/src/main/java/com/readrops/app/utils/EmptyListView.kt rename to app/src/main/java/com/readrops/app/utils/customviews/EmptyListView.kt index dadd5c20..c353c825 100644 --- a/app/src/main/java/com/readrops/app/utils/EmptyListView.kt +++ b/app/src/main/java/com/readrops/app/utils/customviews/EmptyListView.kt @@ -1,4 +1,4 @@ -package com.readrops.app.utils +package com.readrops.app.utils.customviews import android.content.Context import android.util.AttributeSet diff --git a/app/src/main/java/com/readrops/app/utils/ReadropsItemTouchCallback.kt b/app/src/main/java/com/readrops/app/utils/customviews/ReadropsItemTouchCallback.kt similarity index 99% rename from app/src/main/java/com/readrops/app/utils/ReadropsItemTouchCallback.kt rename to app/src/main/java/com/readrops/app/utils/customviews/ReadropsItemTouchCallback.kt index 07357416..8c61fb14 100644 --- a/app/src/main/java/com/readrops/app/utils/ReadropsItemTouchCallback.kt +++ b/app/src/main/java/com/readrops/app/utils/customviews/ReadropsItemTouchCallback.kt @@ -1,4 +1,4 @@ -package com.readrops.app.utils +package com.readrops.app.utils.customviews import android.content.Context import android.graphics.Canvas diff --git a/app/src/main/java/com/readrops/app/utils/ReadropsWebView.java b/app/src/main/java/com/readrops/app/utils/customviews/ReadropsWebView.java similarity index 97% rename from app/src/main/java/com/readrops/app/utils/ReadropsWebView.java rename to app/src/main/java/com/readrops/app/utils/customviews/ReadropsWebView.java index 554503d8..60d35341 100644 --- a/app/src/main/java/com/readrops/app/utils/ReadropsWebView.java +++ b/app/src/main/java/com/readrops/app/utils/customviews/ReadropsWebView.java @@ -1,4 +1,4 @@ -package com.readrops.app.utils; +package com.readrops.app.utils.customviews; import android.annotation.SuppressLint; import android.content.Context; @@ -12,6 +12,7 @@ import androidx.annotation.ColorInt; import androidx.annotation.Nullable; import com.readrops.app.R; +import com.readrops.app.utils.Utils; import com.readrops.db.pojo.ItemWithFeed; import org.jsoup.Jsoup; diff --git a/app/src/main/res/layout/activity_account_type_list.xml b/app/src/main/res/layout/activity_account_type_list.xml index 32d7fb03..8ce3e3a1 100644 --- a/app/src/main/res/layout/activity_account_type_list.xml +++ b/app/src/main/res/layout/activity_account_type_list.xml @@ -5,7 +5,7 @@ xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" - tools:context=".activities.AccountTypeListActivity"> + tools:context=".account.AccountTypeListActivity"> + tools:context=".account.AddAccountActivity"> + tools:context=".addfeed.AddFeedActivity"> + tools:context=".item.ItemActivity"> - + tools:context=".itemslist.MainActivity"> - diff --git a/app/src/main/res/layout/activity_notification_permission.xml b/app/src/main/res/layout/activity_notification_permission.xml index b8a52082..fa78aa28 100644 --- a/app/src/main/res/layout/activity_notification_permission.xml +++ b/app/src/main/res/layout/activity_notification_permission.xml @@ -5,7 +5,7 @@ xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" - tools:context="com.readrops.app.activities.NotificationPermissionActivity"> + tools:context="com.readrops.app.notifications.NotificationPermissionActivity"> + tools:context=".settings.SettingsActivity"> + tools:context=".SplashActivity"> \ No newline at end of file diff --git a/app/src/main/res/layout/activity_web_view.xml b/app/src/main/res/layout/activity_web_view.xml index 0c79eee6..2854e005 100644 --- a/app/src/main/res/layout/activity_web_view.xml +++ b/app/src/main/res/layout/activity_web_view.xml @@ -2,7 +2,7 @@ diff --git a/app/src/main/res/layout/fragment_feeds.xml b/app/src/main/res/layout/fragment_feeds.xml index 487b0244..3358c261 100644 --- a/app/src/main/res/layout/fragment_feeds.xml +++ b/app/src/main/res/layout/fragment_feeds.xml @@ -6,7 +6,7 @@ android:id="@+id/feeds_root" android:layout_width="match_parent" android:layout_height="match_parent" - tools:context=".fragments.FeedsFragment"> + tools:context=".feedsfolders.feeds.FeedsFragment"> - + tools:context=".feedsfolders.folders.FoldersFragment"> - Date: Sat, 24 Oct 2020 19:24:19 +0200 Subject: [PATCH 102/187] Replace Single by Completable for login call --- .../app/account/AccountViewModel.java | 2 +- .../app/account/AddAccountActivity.java | 55 ++++++------------- .../app/repositories/ARepository.java | 3 +- .../app/repositories/FreshRSSRepository.java | 10 ++-- .../app/repositories/LocalFeedRepository.java | 3 +- .../app/repositories/NextNewsRepository.java | 10 ++-- 6 files changed, 32 insertions(+), 51 deletions(-) diff --git a/app/src/main/java/com/readrops/app/account/AccountViewModel.java b/app/src/main/java/com/readrops/app/account/AccountViewModel.java index 353e1cea..c846e37d 100644 --- a/app/src/main/java/com/readrops/app/account/AccountViewModel.java +++ b/app/src/main/java/com/readrops/app/account/AccountViewModel.java @@ -42,7 +42,7 @@ public class AccountViewModel extends ViewModel { () -> DefinitionParametersKt.parametersOf(account)); } - public Single login(Account account, boolean insert) { + public Completable login(Account account, boolean insert) { return repository.login(account, insert); } diff --git a/app/src/main/java/com/readrops/app/account/AddAccountActivity.java b/app/src/main/java/com/readrops/app/account/AddAccountActivity.java index 051949f7..d6cb63ea 100644 --- a/app/src/main/java/com/readrops/app/account/AddAccountActivity.java +++ b/app/src/main/java/com/readrops/app/account/AddAccountActivity.java @@ -2,6 +2,7 @@ package com.readrops.app.account; import android.content.Intent; import android.os.Bundle; +import android.util.Log; import android.util.Patterns; import android.view.KeyEvent; import android.view.MenuItem; @@ -19,9 +20,7 @@ import com.readrops.db.entities.account.AccountType; import org.koin.androidx.viewmodel.compat.ViewModelCompat; -import io.reactivex.Completable; import io.reactivex.CompletableObserver; -import io.reactivex.SingleObserver; import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.disposables.Disposable; import io.reactivex.schedulers.Schedulers; @@ -32,6 +31,8 @@ import static com.readrops.app.utils.ReadropsKeys.EDIT_ACCOUNT; public class AddAccountActivity extends AppCompatActivity { + private static final String TAG = AddAccountActivity.class.getSimpleName(); + private ActivityAddAccountBinding binding; private AccountViewModel viewModel; @@ -101,7 +102,7 @@ public class AddAccountActivity extends AppCompatActivity { viewModel.login(account, true) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) - .subscribe(new SingleObserver() { + .subscribe(new CompletableObserver() { @Override public void onSubscribe(Disposable d) { @@ -110,33 +111,25 @@ public class AddAccountActivity extends AppCompatActivity { } @Override - public void onSuccess(Boolean success) { - binding.addAccountLoading.setVisibility(View.GONE); + public void onComplete() { + saveLoginPassword(account); - if (success) { - saveLoginPassword(account); - - if (forwardResult) { - Intent intent = new Intent(); - intent.putExtra(ACCOUNT, account); - setResult(RESULT_OK, intent); - finish(); - - } else { - Intent intent = new Intent(getApplicationContext(), MainActivity.class); - intent.putExtra(ACCOUNT, account); - startActivity(intent); - } - - finish(); + if (forwardResult) { + Intent intent = new Intent(); + intent.putExtra(ACCOUNT, account); + setResult(RESULT_OK, intent); } else { - binding.addAccountValidate.setEnabled(true); - Utils.showSnackbar(binding.addAccountRoot, getString(R.string.login_failed)); + Intent intent = new Intent(getApplicationContext(), MainActivity.class); + intent.putExtra(ACCOUNT, account); + startActivity(intent); } + + finish(); } @Override public void onError(Throwable e) { + Log.d(TAG, e.getMessage()); binding.addAccountLoading.setVisibility(View.GONE); binding.addAccountValidate.setEnabled(true); @@ -198,20 +191,8 @@ public class AddAccountActivity extends AppCompatActivity { private void updateAccount() { viewModel.login(accountToEdit, false) .doOnError(throwable -> Utils.showSnackbar(binding.addAccountRoot, throwable.getMessage())) - .flatMapCompletable(b -> { - if (b) { - saveLoginPassword(accountToEdit); - return viewModel.update(accountToEdit); - } else { - runOnUiThread(() -> { - binding.addAccountLoading.setVisibility(View.GONE); - binding.addAccountValidate.setEnabled(true); - Utils.showSnackbar(binding.addAccountRoot, getString(R.string.login_failed)); - }); - - return Completable.never(); - } - }) + .doAfterTerminate(() -> saveLoginPassword(accountToEdit)) + .andThen(viewModel.update(accountToEdit)) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(new CompletableObserver() { diff --git a/app/src/main/java/com/readrops/app/repositories/ARepository.java b/app/src/main/java/com/readrops/app/repositories/ARepository.java index b20261c1..f01864d2 100644 --- a/app/src/main/java/com/readrops/app/repositories/ARepository.java +++ b/app/src/main/java/com/readrops/app/repositories/ARepository.java @@ -54,8 +54,7 @@ public abstract class ARepository { .setCredentials(account != null && !account.isLocal() ? Credentials.toCredentials(account) : null); } - // TODO : replace Single by Completable - public abstract Single login(Account account, boolean insert); + public abstract Completable login(Account account, boolean insert); public abstract Observable sync(List feeds); diff --git a/app/src/main/java/com/readrops/app/repositories/FreshRSSRepository.java b/app/src/main/java/com/readrops/app/repositories/FreshRSSRepository.java index 77fcd385..7b4cfdeb 100644 --- a/app/src/main/java/com/readrops/app/repositories/FreshRSSRepository.java +++ b/app/src/main/java/com/readrops/app/repositories/FreshRSSRepository.java @@ -42,7 +42,7 @@ public class FreshRSSRepository extends ARepository { } @Override - public Single login(Account account, boolean insert) { + public Completable login(Account account, boolean insert) { setCredentials(account); return dataSource.login(account.getLogin(), account.getPassword()) @@ -57,19 +57,19 @@ public class FreshRSSRepository extends ARepository { return dataSource.getUserInfo(); }) - .flatMap(userInfo -> { + .flatMapCompletable(userInfo -> { account.setDisplayedName(userInfo.getUserName()); if (insert) { return database.accountDao().insert(account) - .flatMap(id -> { + .flatMapCompletable(id -> { account.setId(id.intValue()); - return Single.just(true); + return Completable.complete(); }); } - return Single.just(true); + return Completable.complete(); }); } diff --git a/app/src/main/java/com/readrops/app/repositories/LocalFeedRepository.java b/app/src/main/java/com/readrops/app/repositories/LocalFeedRepository.java index 63ffddf6..724c7afa 100644 --- a/app/src/main/java/com/readrops/app/repositories/LocalFeedRepository.java +++ b/app/src/main/java/com/readrops/app/repositories/LocalFeedRepository.java @@ -29,6 +29,7 @@ import java.util.Collection; import java.util.Collections; import java.util.List; +import io.reactivex.Completable; import io.reactivex.Observable; import io.reactivex.Single; import kotlin.Pair; @@ -48,7 +49,7 @@ public class LocalFeedRepository extends ARepository { } @Override - public Single login(Account account, boolean insert) { + public Completable login(Account account, boolean insert) { return null; } diff --git a/app/src/main/java/com/readrops/app/repositories/NextNewsRepository.java b/app/src/main/java/com/readrops/app/repositories/NextNewsRepository.java index 8d850a91..3cdb7dd8 100644 --- a/app/src/main/java/com/readrops/app/repositories/NextNewsRepository.java +++ b/app/src/main/java/com/readrops/app/repositories/NextNewsRepository.java @@ -46,7 +46,7 @@ public class NextNewsRepository extends ARepository { } @Override - public Single login(Account account, boolean insert) { + public Completable login(Account account, boolean insert) { setCredentials(account); return Single.create(emitter -> { NextNewsUser user = dataSource.login(); @@ -56,19 +56,19 @@ public class NextNewsRepository extends ARepository { } else { emitter.onError(new Exception("Login failed. Please check your credentials and your Nextcloud News setup.")); } - }).flatMap(user -> { + }).flatMapCompletable(user -> { account.setDisplayedName(user.getDisplayName()); account.setCurrentAccount(true); if (insert) { return database.accountDao().insert(account) - .flatMap(id -> { + .flatMapCompletable(id -> { account.setId(id.intValue()); - return Single.just(true); + return Completable.complete(); }); } - return Single.just(true); + return Completable.complete(); }); } From 39e613e7e68116227a2ab5c44b9f1da055992e45 Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Sun, 25 Oct 2020 11:56:20 +0100 Subject: [PATCH 103/187] Fix url being rewritten for not freshrss/nextcloud news http calls --- .../java/com/readrops/api/localfeed/LocalRSSDataSource.kt | 6 +++++- app/src/main/java/com/readrops/app/utils/HtmlParser.java | 2 ++ app/src/main/java/com/readrops/app/utils/Utils.java | 2 ++ 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/api/src/main/java/com/readrops/api/localfeed/LocalRSSDataSource.kt b/api/src/main/java/com/readrops/api/localfeed/LocalRSSDataSource.kt index 4a7cfd46..6916809e 100644 --- a/api/src/main/java/com/readrops/api/localfeed/LocalRSSDataSource.kt +++ b/api/src/main/java/com/readrops/api/localfeed/LocalRSSDataSource.kt @@ -5,6 +5,7 @@ import androidx.annotation.WorkerThread import com.readrops.api.localfeed.json.JSONFeedAdapter import com.readrops.api.localfeed.json.JSONItemsAdapter import com.readrops.api.utils.ApiUtils +import com.readrops.api.utils.AuthInterceptor import com.readrops.api.utils.exceptions.ParseException import com.readrops.api.utils.exceptions.UnknownFormatException import com.readrops.db.entities.Feed @@ -16,12 +17,14 @@ import okhttp3.OkHttpClient import okhttp3.Request import okhttp3.Response import okio.Buffer +import org.koin.core.KoinComponent +import org.koin.core.get import java.io.ByteArrayInputStream import java.io.IOException import java.io.InputStream import java.net.HttpURLConnection -class LocalRSSDataSource(private val httpClient: OkHttpClient) { +class LocalRSSDataSource(private val httpClient: OkHttpClient): KoinComponent { /** * Query RSS url @@ -32,6 +35,7 @@ class LocalRSSDataSource(private val httpClient: OkHttpClient) { @Throws(ParseException::class, UnknownFormatException::class, NetworkErrorException::class, IOException::class) @WorkerThread fun queryRSSResource(url: String, headers: Headers?): Pair>? { + get().credentials = null val response = queryUrl(url, headers) return when { diff --git a/app/src/main/java/com/readrops/app/utils/HtmlParser.java b/app/src/main/java/com/readrops/app/utils/HtmlParser.java index 9964aa9c..e7ef597a 100644 --- a/app/src/main/java/com/readrops/app/utils/HtmlParser.java +++ b/app/src/main/java/com/readrops/app/utils/HtmlParser.java @@ -7,6 +7,7 @@ import androidx.annotation.Nullable; import com.readrops.api.localfeed.LocalRSSHelper; import com.readrops.api.utils.ApiUtils; +import com.readrops.api.utils.AuthInterceptor; import com.readrops.app.addfeed.ParsingResult; import org.jsoup.Jsoup; @@ -87,6 +88,7 @@ public final class HtmlParser { try { Response response = KoinJavaComponent.get(OkHttpClient.class) .newCall(new Request.Builder().url(url).build()).execute(); + KoinJavaComponent.get(AuthInterceptor.class).setCredentials(null); if (response.header("Content-Type").contains(ApiUtils.HTML_CONTENT_TYPE)) { String body = response.body().string(); diff --git a/app/src/main/java/com/readrops/app/utils/Utils.java b/app/src/main/java/com/readrops/app/utils/Utils.java index aad043b1..1c3ecb28 100644 --- a/app/src/main/java/com/readrops/app/utils/Utils.java +++ b/app/src/main/java/com/readrops/app/utils/Utils.java @@ -15,6 +15,7 @@ import androidx.annotation.ColorInt; import androidx.annotation.NonNull; import com.google.android.material.snackbar.Snackbar; +import com.readrops.api.utils.AuthInterceptor; import org.koin.java.KoinJavaComponent; @@ -36,6 +37,7 @@ public final class Utils { public static Bitmap getImageFromUrl(String url) { try { Request request = new Request.Builder().url(url).build(); + KoinJavaComponent.get(AuthInterceptor.class).setCredentials(null); Response response = KoinJavaComponent.get(OkHttpClient.class).newCall(request).execute(); From 86c58b9280f7bb1f5f087d260e544d1469135804 Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Sun, 25 Oct 2020 12:49:19 +0100 Subject: [PATCH 104/187] Wrap Freshrss and Nextcloud News adapters code in a try/catch to throw ParseException --- .../freshrss/adapters/FreshRSSFeedsAdapter.kt | 50 ++++++++------ .../adapters/FreshRSSFoldersAdapter.kt | 57 +++++++++------- .../freshrss/adapters/FreshRSSItemsAdapter.kt | 28 +++++--- .../adapters/NextNewsFeedsAdapter.kt | 19 ++++-- .../adapters/NextNewsFoldersAdapter.kt | 39 ++++++----- .../adapters/NextNewsItemsAdapter.kt | 67 ++++++++++--------- 6 files changed, 146 insertions(+), 114 deletions(-) diff --git a/api/src/main/java/com/readrops/api/services/freshrss/adapters/FreshRSSFeedsAdapter.kt b/api/src/main/java/com/readrops/api/services/freshrss/adapters/FreshRSSFeedsAdapter.kt index e0c19853..365d7c71 100644 --- a/api/src/main/java/com/readrops/api/services/freshrss/adapters/FreshRSSFeedsAdapter.kt +++ b/api/src/main/java/com/readrops/api/services/freshrss/adapters/FreshRSSFeedsAdapter.kt @@ -1,6 +1,7 @@ package com.readrops.api.services.freshrss.adapters import android.annotation.SuppressLint +import com.readrops.api.utils.exceptions.ParseException import com.readrops.db.entities.Feed import com.squareup.moshi.FromJson import com.squareup.moshi.JsonReader @@ -16,36 +17,40 @@ class FreshRSSFeedsAdapter { fun fromJson(reader: JsonReader): List { val feeds = mutableListOf() - reader.beginObject() - reader.nextName() // "subscriptions", beginning of the feed array - reader.beginArray() - - while (reader.hasNext()) { + return try { reader.beginObject() + reader.nextName() // "subscriptions", beginning of the feed array + reader.beginArray() - val feed = Feed() while (reader.hasNext()) { - with(feed) { - when (reader.selectName(NAMES)) { - 0 -> name = reader.nextString() - 1 -> url = reader.nextString() - 2 -> siteUrl = reader.nextString() - 3 -> iconUrl = reader.nextString() - 4 -> remoteId = reader.nextString() - 5 -> remoteFolderId = getCategoryId(reader) - else -> reader.skipValue() + reader.beginObject() + + val feed = Feed() + while (reader.hasNext()) { + with(feed) { + when (reader.selectName(NAMES)) { + 0 -> name = reader.nextString() + 1 -> url = reader.nextString() + 2 -> siteUrl = reader.nextString() + 3 -> iconUrl = reader.nextString() + 4 -> remoteId = reader.nextString() + 5 -> remoteFolderId = getCategoryId(reader) + else -> reader.skipValue() + } } } + + feeds += feed + reader.endObject() } - feeds += feed + reader.endArray() reader.endObject() + + feeds + } catch (e: Exception) { + throw ParseException(e.message) } - - reader.endArray() - reader.endObject() - - return feeds } private fun getCategoryId(reader: JsonReader): String? { @@ -72,6 +77,7 @@ class FreshRSSFeedsAdapter { } companion object { - val NAMES: JsonReader.Options = JsonReader.Options.of("title", "url", "htmlUrl", "iconUrl", "id", "categories") + val NAMES: JsonReader.Options = JsonReader.Options.of("title", "url", "htmlUrl", + "iconUrl", "id", "categories") } } \ No newline at end of file diff --git a/api/src/main/java/com/readrops/api/services/freshrss/adapters/FreshRSSFoldersAdapter.kt b/api/src/main/java/com/readrops/api/services/freshrss/adapters/FreshRSSFoldersAdapter.kt index 8574f990..20c687c3 100644 --- a/api/src/main/java/com/readrops/api/services/freshrss/adapters/FreshRSSFoldersAdapter.kt +++ b/api/src/main/java/com/readrops/api/services/freshrss/adapters/FreshRSSFoldersAdapter.kt @@ -1,6 +1,7 @@ package com.readrops.api.services.freshrss.adapters import android.annotation.SuppressLint +import com.readrops.api.utils.exceptions.ParseException import com.readrops.db.entities.Folder import com.squareup.moshi.FromJson import com.squareup.moshi.JsonReader @@ -17,42 +18,46 @@ class FreshRSSFoldersAdapter { fun fromJson(reader: JsonReader): List { val folders = mutableListOf() - reader.beginObject() - reader.nextName() // "tags", beginning of folder array - reader.beginArray() - - while (reader.hasNext()) { + return try { reader.beginObject() - - val folder = Folder() - var type: String? = null + reader.nextName() // "tags", beginning of folder array + reader.beginArray() while (reader.hasNext()) { - with(folder) { - when (reader.selectName(NAMES)) { - 0 -> { - val id = reader.nextString() - name = StringTokenizer(id, "/") - .toList() - .last() as String - remoteId = id + reader.beginObject() + + val folder = Folder() + var type: String? = null + + while (reader.hasNext()) { + with(folder) { + when (reader.selectName(NAMES)) { + 0 -> { + val id = reader.nextString() + name = StringTokenizer(id, "/") + .toList() + .last() as String + remoteId = id + } + 1 -> type = reader.nextString() + else -> reader.skipValue() } - 1 -> type = reader.nextString() - else -> reader.skipValue() } } + + if (type == "folder") // add only folders and avoid tags + folders += folder + + reader.endObject() } - if (type == "folder") // add only folders and avoid tags - folders += folder - + reader.endArray() reader.endObject() + + folders + } catch (e: Exception) { + throw ParseException(e.message) } - - reader.endArray() - reader.endObject() - - return folders } companion object { diff --git a/api/src/main/java/com/readrops/api/services/freshrss/adapters/FreshRSSItemsAdapter.kt b/api/src/main/java/com/readrops/api/services/freshrss/adapters/FreshRSSItemsAdapter.kt index f6ef5f95..bb89f88c 100644 --- a/api/src/main/java/com/readrops/api/services/freshrss/adapters/FreshRSSItemsAdapter.kt +++ b/api/src/main/java/com/readrops/api/services/freshrss/adapters/FreshRSSItemsAdapter.kt @@ -3,6 +3,7 @@ package com.readrops.api.services.freshrss.adapters import android.util.TimingLogger import com.readrops.db.entities.Item import com.readrops.api.services.freshrss.FreshRSSDataSource.GOOGLE_READ +import com.readrops.api.utils.exceptions.ParseException import com.squareup.moshi.JsonAdapter import com.squareup.moshi.JsonReader import com.squareup.moshi.JsonWriter @@ -19,17 +20,21 @@ class FreshRSSItemsAdapter : JsonAdapter>() { val logger = TimingLogger(TAG, "item parsing") val items = mutableListOf() - reader.beginObject() - while (reader.hasNext()) { - if (reader.nextName() == "items") parseItems(reader, items) else reader.skipValue() + return try { + reader.beginObject() + while (reader.hasNext()) { + if (reader.nextName() == "items") parseItems(reader, items) else reader.skipValue() + } + + reader.endObject() + + logger.addSplit("item parsing done") + logger.dumpToLog() + + items + } catch (e: Exception) { + throw ParseException(e.message) } - - reader.endObject() - - logger.addSplit("item parsing done") - logger.dumpToLog() - - return items } private fun parseItems(reader: JsonReader, items: MutableList) { @@ -127,7 +132,8 @@ class FreshRSSItemsAdapter : JsonAdapter>() { } companion object { - val NAMES: JsonReader.Options = JsonReader.Options.of("id", "published", "title", "summary", "alternate", "categories", "origin", "author") + val NAMES: JsonReader.Options = JsonReader.Options.of("id", "published", "title", + "summary", "alternate", "categories", "origin", "author") val TAG = FreshRSSItemsAdapter::class.java.simpleName } diff --git a/api/src/main/java/com/readrops/api/services/nextcloudnews/adapters/NextNewsFeedsAdapter.kt b/api/src/main/java/com/readrops/api/services/nextcloudnews/adapters/NextNewsFeedsAdapter.kt index ba1c34e0..d7720f75 100644 --- a/api/src/main/java/com/readrops/api/services/nextcloudnews/adapters/NextNewsFeedsAdapter.kt +++ b/api/src/main/java/com/readrops/api/services/nextcloudnews/adapters/NextNewsFeedsAdapter.kt @@ -1,6 +1,7 @@ package com.readrops.api.services.nextcloudnews.adapters import android.annotation.SuppressLint +import com.readrops.api.utils.exceptions.ParseException import com.readrops.db.entities.Feed import com.readrops.api.utils.extensions.nextNullableString import com.squareup.moshi.FromJson @@ -17,15 +18,19 @@ class NextNewsFeedsAdapter { fun fromJson(reader: JsonReader): List { val feeds = mutableListOf() - reader.beginObject() + return try { + reader.beginObject() - while (reader.hasNext()) { - if (reader.nextName() == "feeds") parseFeeds(reader, feeds) else reader.skipValue() + while (reader.hasNext()) { + if (reader.nextName() == "feeds") parseFeeds(reader, feeds) else reader.skipValue() + } + + reader.endObject() + + feeds + } catch (e: Exception) { + throw ParseException(e.message) } - - reader.endObject() - - return feeds } private fun parseFeeds(reader: JsonReader, feeds: MutableList) { diff --git a/api/src/main/java/com/readrops/api/services/nextcloudnews/adapters/NextNewsFoldersAdapter.kt b/api/src/main/java/com/readrops/api/services/nextcloudnews/adapters/NextNewsFoldersAdapter.kt index e3f70f7b..ceefc80a 100644 --- a/api/src/main/java/com/readrops/api/services/nextcloudnews/adapters/NextNewsFoldersAdapter.kt +++ b/api/src/main/java/com/readrops/api/services/nextcloudnews/adapters/NextNewsFoldersAdapter.kt @@ -1,6 +1,7 @@ package com.readrops.api.services.nextcloudnews.adapters import android.annotation.SuppressLint +import com.readrops.api.utils.exceptions.ParseException import com.readrops.db.entities.Folder import com.squareup.moshi.FromJson import com.squareup.moshi.JsonReader @@ -16,32 +17,36 @@ class NextNewsFoldersAdapter { fun fromJson(reader: JsonReader): List { val folders = mutableListOf() - reader.beginObject() - reader.nextName() // "folders", beginning of folders array - reader.beginArray() - - while (reader.hasNext()) { - val folder = Folder() + return try { reader.beginObject() + reader.nextName() // "folders", beginning of folders array + reader.beginArray() while (reader.hasNext()) { - with(folder) { - when (reader.selectName(NAMES)) { - 0 -> remoteId = reader.nextInt().toString() - 1 -> name = reader.nextString() - else -> reader.skipValue() + val folder = Folder() + reader.beginObject() + + while (reader.hasNext()) { + with(folder) { + when (reader.selectName(NAMES)) { + 0 -> remoteId = reader.nextInt().toString() + 1 -> name = reader.nextString() + else -> reader.skipValue() + } } } + + folders += folder + reader.endObject() } - folders += folder + reader.endArray() reader.endObject() + + folders + } catch (e: Exception) { + throw ParseException(e.message) } - - reader.endArray() - reader.endObject() - - return folders } companion object { diff --git a/api/src/main/java/com/readrops/api/services/nextcloudnews/adapters/NextNewsItemsAdapter.kt b/api/src/main/java/com/readrops/api/services/nextcloudnews/adapters/NextNewsItemsAdapter.kt index 9599e2c3..25fdf91e 100644 --- a/api/src/main/java/com/readrops/api/services/nextcloudnews/adapters/NextNewsItemsAdapter.kt +++ b/api/src/main/java/com/readrops/api/services/nextcloudnews/adapters/NextNewsItemsAdapter.kt @@ -3,6 +3,7 @@ package com.readrops.api.services.nextcloudnews.adapters import android.annotation.SuppressLint import com.readrops.db.entities.Item import com.readrops.api.utils.ApiUtils +import com.readrops.api.utils.exceptions.ParseException import com.readrops.api.utils.extensions.nextNullableString import com.squareup.moshi.JsonAdapter import com.squareup.moshi.JsonReader @@ -21,46 +22,50 @@ class NextNewsItemsAdapter : JsonAdapter>() { override fun fromJson(reader: JsonReader): List { val items = mutableListOf() - reader.beginObject() - reader.nextName() // "items", beginning of items array - reader.beginArray() - - while (reader.hasNext()) { - val item = Item() + return try { reader.beginObject() - - var enclosureMime: String? = null - var enclosureLink: String? = null + reader.nextName() // "items", beginning of items array + reader.beginArray() while (reader.hasNext()) { - with(item) { - when (reader.selectName(NAMES)) { - 0 -> remoteId = reader.nextInt().toString() - 1 -> link = reader.nextNullableString() - 2 -> title = reader.nextString() - 3 -> author = reader.nextString() - 4 -> pubDate = LocalDateTime(reader.nextLong() * 1000L, DateTimeZone.getDefault()) - 5 -> content = reader.nextString() - 6 -> enclosureMime = reader.nextNullableString() - 7 -> enclosureLink = reader.nextNullableString() - 8 -> feedRemoteId = reader.nextInt().toString() - 9 -> isRead = !reader.nextBoolean() - else -> reader.skipValue() + val item = Item() + reader.beginObject() + + var enclosureMime: String? = null + var enclosureLink: String? = null + + while (reader.hasNext()) { + with(item) { + when (reader.selectName(NAMES)) { + 0 -> remoteId = reader.nextInt().toString() + 1 -> link = reader.nextNullableString() + 2 -> title = reader.nextString() + 3 -> author = reader.nextString() + 4 -> pubDate = LocalDateTime(reader.nextLong() * 1000L, DateTimeZone.getDefault()) + 5 -> content = reader.nextString() + 6 -> enclosureMime = reader.nextNullableString() + 7 -> enclosureLink = reader.nextNullableString() + 8 -> feedRemoteId = reader.nextInt().toString() + 9 -> isRead = !reader.nextBoolean() + else -> reader.skipValue() + } } } + + if (enclosureMime != null && ApiUtils.isMimeImage(enclosureMime!!)) + item.imageLink = enclosureLink + + items += item + reader.endObject() } - if (enclosureMime != null && ApiUtils.isMimeImage(enclosureMime!!)) - item.imageLink = enclosureLink - - items += item + reader.endArray() reader.endObject() + + items + } catch (e: Exception) { + throw ParseException(e.message) } - - reader.endArray() - reader.endObject() - - return items } companion object { From 537490252b965ea540308c49746b7ffe9feb847c Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Sun, 25 Oct 2020 12:58:29 +0100 Subject: [PATCH 105/187] Fix HtmlParser tests --- app/src/test/java/com/readrops/app/HtmlParserTest.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/src/test/java/com/readrops/app/HtmlParserTest.kt b/app/src/test/java/com/readrops/app/HtmlParserTest.kt index 08808695..28c560b0 100644 --- a/app/src/test/java/com/readrops/app/HtmlParserTest.kt +++ b/app/src/test/java/com/readrops/app/HtmlParserTest.kt @@ -1,5 +1,6 @@ package com.readrops.app +import com.readrops.api.utils.AuthInterceptor import com.readrops.app.utils.HtmlParser import com.readrops.app.addfeed.ParsingResult import junit.framework.TestCase @@ -15,7 +16,8 @@ class HtmlParserTest { @get:Rule val koinTestRule = KoinTestRule.create { modules(module { - single { OkHttpClient() } + single { OkHttpClient.Builder().addInterceptor(get()).build() } + single { AuthInterceptor() } }) } From 847a9d2004eab3c818418505cde09357d2fcbb1e Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Sun, 25 Oct 2020 14:03:49 +0100 Subject: [PATCH 106/187] Fix LocalRSSDataSource tests --- api/build.gradle | 1 + .../api/localfeed/LocalRSSDataSourceTest.kt | 19 +++++++++++++++---- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/api/build.gradle b/api/build.gradle index 4fa10611..28b127dd 100644 --- a/api/build.gradle +++ b/api/build.gradle @@ -49,6 +49,7 @@ dependencies { androidTestImplementation 'androidx.test:rules:1.3.0' androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0' androidTestImplementation 'com.squareup.okhttp3:mockwebserver:4.9.0' + androidTestImplementation "org.koin:koin-test:2.1.6" testImplementation "org.koin:koin-test:2.1.6" implementation 'com.gitlab.mvysny.konsume-xml:konsume-xml:0.12' diff --git a/api/src/androidTest/java/com/readrops/api/localfeed/LocalRSSDataSourceTest.kt b/api/src/androidTest/java/com/readrops/api/localfeed/LocalRSSDataSourceTest.kt index 34fa8826..bda2241a 100644 --- a/api/src/androidTest/java/com/readrops/api/localfeed/LocalRSSDataSourceTest.kt +++ b/api/src/androidTest/java/com/readrops/api/localfeed/LocalRSSDataSourceTest.kt @@ -4,31 +4,42 @@ import android.accounts.NetworkErrorException import android.content.Context import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.platform.app.InstrumentationRegistry +import com.readrops.api.apiModule import com.readrops.api.utils.ApiUtils import com.readrops.api.utils.exceptions.ParseException import com.readrops.api.utils.exceptions.UnknownFormatException import junit.framework.TestCase.* import okhttp3.Headers import okhttp3.HttpUrl -import okhttp3.OkHttpClient import okhttp3.mockwebserver.MockResponse import okhttp3.mockwebserver.MockWebServer import okio.Buffer import org.junit.After import org.junit.Before +import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith +import org.koin.android.ext.koin.androidContext +import org.koin.test.KoinTest +import org.koin.test.KoinTestRule +import org.koin.test.inject import java.net.HttpURLConnection @RunWith(AndroidJUnit4::class) -class LocalRSSDataSourceTest { +class LocalRSSDataSourceTest : KoinTest { - private val context: Context = InstrumentationRegistry.getInstrumentation().context + private val context: Context by inject() private lateinit var url: HttpUrl private val mockServer: MockWebServer = MockWebServer() - private val localRSSDataSource = LocalRSSDataSource(OkHttpClient()) + private val localRSSDataSource by inject() + + @get:Rule + val koinTestRule = KoinTestRule.create { + androidContext(InstrumentationRegistry.getInstrumentation().context) + modules(apiModule) + } @Before fun before() { From 363ccad5afd77a6b56fb425ae53a03ca982535c5 Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Sun, 25 Oct 2020 15:16:12 +0100 Subject: [PATCH 107/187] Add fields to Item for starred items --- db/schemas/com.readrops.db.Database/3.json | 451 ++++++++++++++++++ .../com/readrops/db/DatabaseMigrationsTest.kt | 7 +- .../main/java/com/readrops/db/Database.java | 12 +- .../java/com/readrops/db/entities/Item.java | 21 + 4 files changed, 489 insertions(+), 2 deletions(-) create mode 100644 db/schemas/com.readrops.db.Database/3.json diff --git a/db/schemas/com.readrops.db.Database/3.json b/db/schemas/com.readrops.db.Database/3.json new file mode 100644 index 00000000..bb0455d5 --- /dev/null +++ b/db/schemas/com.readrops.db.Database/3.json @@ -0,0 +1,451 @@ +{ + "formatVersion": 1, + "database": { + "version": 3, + "identityHash": "474b2c463c0a4bc9f0a8b69feb2feeec", + "entities": [ + { + "tableName": "Feed", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT, `description` TEXT, `url` TEXT, `siteUrl` TEXT, `lastUpdated` TEXT, `text_color` INTEGER NOT NULL, `background_color` INTEGER NOT NULL, `icon_url` TEXT, `etag` TEXT, `last_modified` TEXT, `folder_id` INTEGER, `remoteId` TEXT, `account_id` INTEGER NOT NULL, `notification_enabled` INTEGER NOT NULL DEFAULT 1, FOREIGN KEY(`folder_id`) REFERENCES `Folder`(`id`) ON UPDATE NO ACTION ON DELETE SET NULL , FOREIGN KEY(`account_id`) REFERENCES `Account`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "siteUrl", + "columnName": "siteUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastUpdated", + "columnName": "lastUpdated", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "textColor", + "columnName": "text_color", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "backgroundColor", + "columnName": "background_color", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "iconUrl", + "columnName": "icon_url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "etag", + "columnName": "etag", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastModified", + "columnName": "last_modified", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "folderId", + "columnName": "folder_id", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "remoteId", + "columnName": "remoteId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "accountId", + "columnName": "account_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationEnabled", + "columnName": "notification_enabled", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_Feed_folder_id", + "unique": false, + "columnNames": [ + "folder_id" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_Feed_folder_id` ON `${TABLE_NAME}` (`folder_id`)" + }, + { + "name": "index_Feed_account_id", + "unique": false, + "columnNames": [ + "account_id" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_Feed_account_id` ON `${TABLE_NAME}` (`account_id`)" + } + ], + "foreignKeys": [ + { + "table": "Folder", + "onDelete": "SET NULL", + "onUpdate": "NO ACTION", + "columns": [ + "folder_id" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "Account", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "account_id" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "Item", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `title` TEXT, `description` TEXT, `clean_description` TEXT, `link` TEXT, `image_link` TEXT, `author` TEXT, `pub_date` INTEGER, `content` TEXT, `feed_id` INTEGER NOT NULL, `guid` TEXT, `read_time` REAL NOT NULL, `read` INTEGER NOT NULL, `read_changed` INTEGER NOT NULL, `starred` INTEGER NOT NULL, `starred_changed` INTEGER NOT NULL, `read_it_later` INTEGER NOT NULL, `remoteId` TEXT, FOREIGN KEY(`feed_id`) REFERENCES `Feed`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "cleanDescription", + "columnName": "clean_description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "link", + "columnName": "link", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "imageLink", + "columnName": "image_link", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "author", + "columnName": "author", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "pubDate", + "columnName": "pub_date", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "feedId", + "columnName": "feed_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "guid", + "columnName": "guid", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "readTime", + "columnName": "read_time", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "read", + "columnName": "read", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "readChanged", + "columnName": "read_changed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "starred", + "columnName": "starred", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "starredChanged", + "columnName": "starred_changed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "readItLater", + "columnName": "read_it_later", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "remoteId", + "columnName": "remoteId", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_Item_feed_id", + "unique": false, + "columnNames": [ + "feed_id" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_Item_feed_id` ON `${TABLE_NAME}` (`feed_id`)" + }, + { + "name": "index_Item_guid", + "unique": false, + "columnNames": [ + "guid" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_Item_guid` ON `${TABLE_NAME}` (`guid`)" + }, + { + "name": "index_Item_starred_changed", + "unique": false, + "columnNames": [ + "starred_changed" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_Item_starred_changed` ON `${TABLE_NAME}` (`starred_changed`)" + } + ], + "foreignKeys": [ + { + "table": "Feed", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "feed_id" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "Folder", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT, `remoteId` TEXT, `account_id` INTEGER NOT NULL, FOREIGN KEY(`account_id`) REFERENCES `Account`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "remoteId", + "columnName": "remoteId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "accountId", + "columnName": "account_id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_Folder_account_id", + "unique": false, + "columnNames": [ + "account_id" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_Folder_account_id` ON `${TABLE_NAME}` (`account_id`)" + } + ], + "foreignKeys": [ + { + "table": "Account", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "account_id" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "Account", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `url` TEXT, `account_name` TEXT, `displayed_name` TEXT, `account_type` INTEGER, `last_modified` INTEGER NOT NULL, `current_account` INTEGER NOT NULL, `token` TEXT, `writeToken` TEXT, `notifications_enabled` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "accountName", + "columnName": "account_name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "displayedName", + "columnName": "displayed_name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "accountType", + "columnName": "account_type", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "lastModified", + "columnName": "last_modified", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "currentAccount", + "columnName": "current_account", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "token", + "columnName": "token", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "writeToken", + "columnName": "writeToken", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "notificationsEnabled", + "columnName": "notifications_enabled", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '474b2c463c0a4bc9f0a8b69feb2feeec')" + ] + } +} \ No newline at end of file diff --git a/db/src/androidTest/java/com/readrops/db/DatabaseMigrationsTest.kt b/db/src/androidTest/java/com/readrops/db/DatabaseMigrationsTest.kt index 038d02fc..5280220c 100644 --- a/db/src/androidTest/java/com/readrops/db/DatabaseMigrationsTest.kt +++ b/db/src/androidTest/java/com/readrops/db/DatabaseMigrationsTest.kt @@ -22,9 +22,14 @@ class DatabaseMigrationsTest { ) @Test - @Throws(IOException::class) fun migrate1To2() { helper.createDatabase(testDb, 1).close() helper.runMigrationsAndValidate(testDb, 2, true, Database.MIGRATION_1_2).close() } + + @Test + fun migrate2to3() { + helper.createDatabase(testDb, 2).close() + helper.runMigrationsAndValidate(testDb, 3, true, Database.MIGRATION_2_3).close() + } } \ No newline at end of file diff --git a/db/src/main/java/com/readrops/db/Database.java b/db/src/main/java/com/readrops/db/Database.java index 3d23536a..9f5c1fa4 100644 --- a/db/src/main/java/com/readrops/db/Database.java +++ b/db/src/main/java/com/readrops/db/Database.java @@ -16,7 +16,7 @@ import com.readrops.db.entities.Item; import com.readrops.db.entities.account.Account; -@androidx.room.Database(entities = {Feed.class, Item.class, Folder.class, Account.class}, version = 2) +@androidx.room.Database(entities = {Feed.class, Item.class, Folder.class, Account.class}, version = 3) @TypeConverters({Converters.class}) public abstract class Database extends RoomDatabase { @@ -36,4 +36,14 @@ public abstract class Database extends RoomDatabase { database.execSQL("Alter Table Feed Add Column notification_enabled INTEGER Not Null Default 1"); } }; + + public static final Migration MIGRATION_2_3 = new Migration(2, 3) { + @Override + public void migrate(@NonNull SupportSQLiteDatabase database) { + database.execSQL("Alter Table Item Add Column starred INTEGER Not Null Default 0"); + database.execSQL("Alter Table Item Add Column starred_changed INTEGER Not Null Default 0"); + + database.execSQL("CREATE INDEX `index_Item_starred_changed` ON `Item` (`starred_changed`);"); + } + }; } \ No newline at end of file diff --git a/db/src/main/java/com/readrops/db/entities/Item.java b/db/src/main/java/com/readrops/db/entities/Item.java index a56f01a7..3194f505 100644 --- a/db/src/main/java/com/readrops/db/entities/Item.java +++ b/db/src/main/java/com/readrops/db/entities/Item.java @@ -50,6 +50,11 @@ public class Item implements Comparable { @ColumnInfo(name = "read_changed") private boolean readChanged; + private boolean starred; + + @ColumnInfo(name = "starred_changed", index = true) + private boolean starredChanged; + @ColumnInfo(name = "read_it_later") private boolean readItLater; @@ -181,6 +186,22 @@ public class Item implements Comparable { this.readChanged = readChanged; } + public boolean isStarred() { + return starred; + } + + public void setStarred(boolean starred) { + this.starred = starred; + } + + public boolean isStarredChanged() { + return starredChanged; + } + + public void setStarredChanged(boolean starredChanged) { + this.starredChanged = starredChanged; + } + public boolean isReadItLater() { return readItLater; } From 9ceb5e6dd7edb3883b8725e8a9b5fd8848870fb9 Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Sun, 25 Oct 2020 19:14:01 +0100 Subject: [PATCH 108/187] Add star/unstar api calls for Nextcloud News --- .../nextcloudnews/NextNewsDataSource.java | 62 ++++++++++++------- .../nextcloudnews/NextNewsService.java | 5 +- .../nextcloudnews/NextNewsSyncData.java | 14 +++-- .../adapters/NextNewsItemsAdapter.kt | 7 ++- .../app/repositories/NextNewsRepository.java | 19 +++++- .../java/com/readrops/db/dao/ItemDao.java | 12 +++- .../java/com/readrops/db/pojo/StarItem.kt | 7 +++ 7 files changed, 91 insertions(+), 35 deletions(-) create mode 100644 db/src/main/java/com/readrops/db/pojo/StarItem.kt diff --git a/api/src/main/java/com/readrops/api/services/nextcloudnews/NextNewsDataSource.java b/api/src/main/java/com/readrops/api/services/nextcloudnews/NextNewsDataSource.java index eaa6d30c..4bb96f1d 100644 --- a/api/src/main/java/com/readrops/api/services/nextcloudnews/NextNewsDataSource.java +++ b/api/src/main/java/com/readrops/api/services/nextcloudnews/NextNewsDataSource.java @@ -14,9 +14,11 @@ import com.readrops.api.utils.exceptions.UnknownFormatException; import com.readrops.db.entities.Feed; import com.readrops.db.entities.Folder; import com.readrops.db.entities.Item; +import com.readrops.db.pojo.StarItem; import java.io.IOException; import java.util.ArrayList; +import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -125,27 +127,11 @@ public class NextNewsDataSource { } private void putModifiedItems(NextNewsSyncData data, SyncResult syncResult) throws IOException { - if (!data.getReadItems().isEmpty()) { - Map> itemIdsMap = new HashMap<>(); - itemIdsMap.put("items", data.getReadItems()); + setReadState(data.getReadItems(), syncResult, StateType.READ); + setReadState(data.getUnreadItems(), syncResult, StateType.UNREAD); - Response readItemsResponse = api.setArticlesState(StateType.READ.name().toLowerCase(), - itemIdsMap).execute(); - - if (!readItemsResponse.isSuccessful()) - syncResult.setError(true); - } - - if (!data.getUnreadItems().isEmpty()) { - Map> itemIdsMap = new HashMap<>(); - itemIdsMap.put("items", data.getUnreadItems()); - - Response unreadItemsResponse = api.setArticlesState(StateType.UNREAD.toString().toLowerCase(), - itemIdsMap).execute(); - - if (!unreadItemsResponse.isSuccessful()) - syncResult.setError(true); - } + setStarState(data.getStarredItems(), syncResult, StateType.STAR); + setStarState(data.getUnstarredItems(), syncResult, StateType.UNSTAR); } public List createFolder(Folder folder) throws IOException, UnknownFormatException, ConflictException { @@ -236,10 +222,42 @@ public class NextNewsDataSource { return false; } + private void setReadState(List items, SyncResult syncResult, StateType stateType) throws IOException { + if (!items.isEmpty()) { + Map> itemIdsMap = new HashMap<>(); + itemIdsMap.put("items", items); + + Response readItemsResponse = api.setReadState(stateType.name().toLowerCase(), + itemIdsMap).execute(); + + if (!readItemsResponse.isSuccessful()) + syncResult.setError(true); + } + } + + private void setStarState(List items, SyncResult syncResult, StateType stateType) throws IOException { + if (!items.isEmpty()) { + List> body = new ArrayList<>(); + for (StarItem item : items) { + Map itemBody = new HashMap<>(); + itemBody.put("feedId", item.getFeedRemoteId()); + itemBody.put("guidHash", item.getGuidHash()); + + body.add(itemBody); + } + + Response response = api.setStarState(stateType.name().toLowerCase(), + Collections.singletonMap("items", body)).execute(); + if (!response.isSuccessful()) { + syncResult.setError(true); + } + } + } + public enum StateType { READ, UNREAD, - STARRED, - UNSTARRED + STAR, + UNSTAR } } diff --git a/api/src/main/java/com/readrops/api/services/nextcloudnews/NextNewsService.java b/api/src/main/java/com/readrops/api/services/nextcloudnews/NextNewsService.java index ad37b4fa..a4d4d11c 100644 --- a/api/src/main/java/com/readrops/api/services/nextcloudnews/NextNewsService.java +++ b/api/src/main/java/com/readrops/api/services/nextcloudnews/NextNewsService.java @@ -38,7 +38,10 @@ public interface NextNewsService { Call> getNewItems(@Query("lastModified") long lastModified, @Query("type") int type); @PUT("items/{stateType}/multiple") - Call setArticlesState(@Path("stateType") String stateType, @Body Map> itemIdsMap); + Call setReadState(@Path("stateType") String stateType, @Body Map> itemIdsMap); + + @PUT("items/{starType}/multiple") + Call setStarState(@Path("starType") String starType, @Body Map>> body); @POST("feeds") Call> createFeed(@Query("url") String url, @Query("folderId") int folderId); diff --git a/api/src/main/java/com/readrops/api/services/nextcloudnews/NextNewsSyncData.java b/api/src/main/java/com/readrops/api/services/nextcloudnews/NextNewsSyncData.java index cc1fc8ba..0e59a982 100644 --- a/api/src/main/java/com/readrops/api/services/nextcloudnews/NextNewsSyncData.java +++ b/api/src/main/java/com/readrops/api/services/nextcloudnews/NextNewsSyncData.java @@ -1,5 +1,7 @@ package com.readrops.api.services.nextcloudnews; +import com.readrops.db.pojo.StarItem; + import java.util.ArrayList; import java.util.List; @@ -9,9 +11,9 @@ public class NextNewsSyncData { private List readItems; - private List starredItems; + private List starredItems; - private List unstarredItems; + private List unstarredItems; private long lastModified; @@ -38,19 +40,19 @@ public class NextNewsSyncData { this.readItems = readItems; } - public List getStarredItems() { + public List getStarredItems() { return starredItems; } - public void setStarredItems(List starredItems) { + public void setStarredItems(List starredItems) { this.starredItems = starredItems; } - public List getUnstarredItems() { + public List getUnstarredItems() { return unstarredItems; } - public void setUnstarredItems(List unstarredItems) { + public void setUnstarredItems(List unstarredItems) { this.unstarredItems = unstarredItems; } diff --git a/api/src/main/java/com/readrops/api/services/nextcloudnews/adapters/NextNewsItemsAdapter.kt b/api/src/main/java/com/readrops/api/services/nextcloudnews/adapters/NextNewsItemsAdapter.kt index 25fdf91e..2667b46f 100644 --- a/api/src/main/java/com/readrops/api/services/nextcloudnews/adapters/NextNewsItemsAdapter.kt +++ b/api/src/main/java/com/readrops/api/services/nextcloudnews/adapters/NextNewsItemsAdapter.kt @@ -4,6 +4,7 @@ import android.annotation.SuppressLint import com.readrops.db.entities.Item import com.readrops.api.utils.ApiUtils import com.readrops.api.utils.exceptions.ParseException +import com.readrops.api.utils.extensions.nextNonEmptyString import com.readrops.api.utils.extensions.nextNullableString import com.squareup.moshi.JsonAdapter import com.squareup.moshi.JsonReader @@ -46,7 +47,9 @@ class NextNewsItemsAdapter : JsonAdapter>() { 6 -> enclosureMime = reader.nextNullableString() 7 -> enclosureLink = reader.nextNullableString() 8 -> feedRemoteId = reader.nextInt().toString() - 9 -> isRead = !reader.nextBoolean() + 9 -> isRead = !reader.nextBoolean() // the negation is important here + 10 -> isStarred = reader.nextBoolean() + 11 -> guid = reader.nextNullableString() else -> reader.skipValue() } } @@ -70,6 +73,6 @@ class NextNewsItemsAdapter : JsonAdapter>() { companion object { val NAMES: JsonReader.Options = JsonReader.Options.of("id", "url", "title", "author", - "pubDate", "body", "enclosureMime", "enclosureLink", "feedId", "unread") + "pubDate", "body", "enclosureMime", "enclosureLink", "feedId", "unread", "starred", "guidHash") } } \ No newline at end of file diff --git a/app/src/main/java/com/readrops/app/repositories/NextNewsRepository.java b/app/src/main/java/com/readrops/app/repositories/NextNewsRepository.java index 3cdb7dd8..96789171 100644 --- a/app/src/main/java/com/readrops/app/repositories/NextNewsRepository.java +++ b/app/src/main/java/com/readrops/app/repositories/NextNewsRepository.java @@ -2,6 +2,7 @@ package com.readrops.app.repositories; import android.content.Context; import android.database.sqlite.SQLiteConstraintException; +import android.util.Log; import android.util.TimingLogger; import androidx.annotation.NonNull; @@ -88,8 +89,12 @@ public class NextNewsRepository extends ARepository { if (syncType == SyncType.CLASSIC_SYNC) { syncData.setLastModified(account.getLastModified() / 1000L); + syncData.setReadItems(database.itemDao().getReadChanges(account.getId())); syncData.setUnreadItems(database.itemDao().getUnreadChanges(account.getId())); + + syncData.setStarredItems(database.itemDao().getStarChanges(account.getId())); + syncData.setUnstarredItems(database.itemDao().getUnstarChanges(account.getId())); } TimingLogger timings = new TimingLogger(TAG, "nextcloud news " + syncType.name().toLowerCase()); @@ -111,14 +116,22 @@ public class NextNewsRepository extends ARepository { account.setLastModified(lastModified); database.accountDao().updateLastModified(account.getId(), lastModified); - database.itemDao().resetReadChanges(account.getId()); + + if (!syncData.getReadItems().isEmpty() || !syncData.getUnreadItems().isEmpty()) { + database.itemDao().resetReadChanges(account.getId()); + } + + if (!syncData.getStarredItems().isEmpty() || !syncData.getUnstarredItems().isEmpty()) { + database.itemDao().resetStarChanges(account.getId()); + } emitter.onComplete(); - } else + } else { emitter.onError(new Throwable()); + } } catch (Exception e) { - e.printStackTrace(); + Log.d(TAG, "sync: " + e.getMessage()); emitter.onError(e); } }); diff --git a/db/src/main/java/com/readrops/db/dao/ItemDao.java b/db/src/main/java/com/readrops/db/dao/ItemDao.java index b0eb1f45..439b4e85 100644 --- a/db/src/main/java/com/readrops/db/dao/ItemDao.java +++ b/db/src/main/java/com/readrops/db/dao/ItemDao.java @@ -13,6 +13,7 @@ import com.readrops.db.entities.Feed; import com.readrops.db.entities.Folder; import com.readrops.db.entities.Item; import com.readrops.db.pojo.ItemWithFeed; +import com.readrops.db.pojo.StarItem; import java.util.List; @@ -40,7 +41,7 @@ public interface ItemDao extends BaseDao { * Set an item read or unread * * @param itemId id of the item to update - * @param read 1 for read, 0 for unread + * @param read 1 for read, 0 for unread * @param readChanged */ @Query("Update Item Set read_changed = :readChanged, read = :read Where id = :itemId") @@ -70,9 +71,18 @@ public interface ItemDao extends BaseDao { @Query("Select Item.remoteId From Item Inner Join Feed On Item.feed_id = Feed.id Where read_changed = 1 And read = 0 And account_id = :accountId") List getUnreadChanges(int accountId); + @Query("Select Item.guid, Feed.remoteId as feedRemoteId From Item Inner Join Feed On Item.feed_id = Feed.id Where starred_changed = 1 And starred = 1 And account_id = :accountId") + List getStarChanges(int accountId); + + @Query("Select Item.guid, Feed.remoteId as feedRemoteId From Item Inner Join Feed On Item.feed_id = Feed.id Where starred_changed = 1 And starred = 0 And account_id = :accountId") + List getUnstarChanges(int accountId); + @Query("Update Item set read_changed = 0 Where feed_id in (Select id From Feed Where account_id = :accountId)") void resetReadChanges(int accountId); + @Query("Update Item set starred_changed = 0 Where feed_id in (Select id From Feed Where account_id = :accountId)") + void resetStarChanges(int accountId); + @Query("Update Item set read = :read Where remoteId = :remoteId") void setReadState(String remoteId, boolean read); } diff --git a/db/src/main/java/com/readrops/db/pojo/StarItem.kt b/db/src/main/java/com/readrops/db/pojo/StarItem.kt new file mode 100644 index 00000000..a58ba616 --- /dev/null +++ b/db/src/main/java/com/readrops/db/pojo/StarItem.kt @@ -0,0 +1,7 @@ +package com.readrops.db.pojo + +import androidx.room.ColumnInfo + + +data class StarItem(@ColumnInfo val feedRemoteId: String, + @ColumnInfo(name = "guid") val guidHash: String) \ No newline at end of file From 04e2f3eadb9400eae2e482e98f8285aed9dcd683 Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Sun, 25 Oct 2020 21:27:11 +0100 Subject: [PATCH 109/187] Add star/unstar api calls for FreshRSS --- .../services/freshrss/FreshRSSDataSource.java | 64 ++++++++++++++++--- .../services/freshrss/FreshRSSService.java | 2 +- .../services/freshrss/FreshRSSSyncData.java | 20 ++++++ .../freshrss/adapters/FreshRSSItemsAdapter.kt | 12 ++-- .../app/repositories/FreshRSSRepository.java | 12 +++- .../java/com/readrops/db/dao/ItemDao.java | 6 ++ 6 files changed, 99 insertions(+), 17 deletions(-) diff --git a/api/src/main/java/com/readrops/api/services/freshrss/FreshRSSDataSource.java b/api/src/main/java/com/readrops/api/services/freshrss/FreshRSSDataSource.java index ac4a8b56..5667c17e 100644 --- a/api/src/main/java/com/readrops/api/services/freshrss/FreshRSSDataSource.java +++ b/api/src/main/java/com/readrops/api/services/freshrss/FreshRSSDataSource.java @@ -24,6 +24,7 @@ public class FreshRSSDataSource { private static final int MAX_ITEMS = 5000; public static final String GOOGLE_READ = "user/-/state/com.google/read"; + public static final String GOOGLE_STARRED = "user/-/state/com.google/starred"; private static final String FEED_PREFIX = "feed/"; @@ -87,6 +88,7 @@ public class FreshRSSDataSource { SyncResult syncResult = new SyncResult(); return setItemsReadState(syncData, writeToken) + .andThen(setItemsStarState(syncData, writeToken)) .andThen(getFolders() .flatMap(freshRSSFolders -> { syncResult.setFolders(freshRSSFolders); @@ -149,10 +151,27 @@ public class FreshRSSDataSource { * @return Completable */ public Completable markItemsReadUnread(boolean read, @NonNull List itemIds, @NonNull String token) { - if (read) - return api.setItemsReadState(token, GOOGLE_READ, null, itemIds); - else - return api.setItemsReadState(token, null, GOOGLE_READ, itemIds); + if (read) { + return api.setItemsState(token, GOOGLE_READ, null, itemIds); + } else { + return api.setItemsState(token, null, GOOGLE_READ, itemIds); + } + } + + /** + * Mark items as starred or unstarred + * + * @param starred true for starred, false for unstarred + * @param itemIds items ids to mark + * @param token token for modifications + * @return Completable + */ + public Completable markItemsStarredUnstarred(boolean starred, @NonNull List itemIds, @NonNull String token) { + if (starred) { + return api.setItemsState(token, GOOGLE_STARRED, null, itemIds); + } else { + return api.setItemsState(token, null, GOOGLE_STARRED, itemIds); + } } /** @@ -225,7 +244,7 @@ public class FreshRSSDataSource { } /** - * Set the state of items + * Set items star state * * @param syncData data containing items to mark * @param token token for modifications @@ -233,17 +252,44 @@ public class FreshRSSDataSource { */ private Completable setItemsReadState(@NonNull FreshRSSSyncData syncData, @NonNull String token) { Completable readItemsCompletable; - if (syncData.getReadItemsIds().isEmpty()) + if (syncData.getReadItemsIds().isEmpty()) { readItemsCompletable = Completable.complete(); - else + } else { readItemsCompletable = markItemsReadUnread(true, syncData.getReadItemsIds(), token); + } Completable unreadItemsCompletable; - if (syncData.getUnreadItemsIds().isEmpty()) + if (syncData.getUnreadItemsIds().isEmpty()) { unreadItemsCompletable = Completable.complete(); - else + } else { unreadItemsCompletable = markItemsReadUnread(false, syncData.getUnreadItemsIds(), token); + } return readItemsCompletable.concatWith(unreadItemsCompletable); } + + /** + * Set items star state + * + * @param syncData data containing items to mark + * @param token token for modifications + * @return A concatenation of two completable (starred and unstarred completable) + */ + private Completable setItemsStarState(@NonNull FreshRSSSyncData syncData, @NonNull String token) { + Completable starredItemsCompletable; + if (syncData.getStarredItemsIds().isEmpty()) { + starredItemsCompletable = Completable.complete(); + } else { + starredItemsCompletable = markItemsStarredUnstarred(true, syncData.getStarredItemsIds(), token); + } + + Completable unstarredItemsCompletable; + if (syncData.getUnstarredItemsIds().isEmpty()) { + unstarredItemsCompletable = Completable.complete(); + } else { + unstarredItemsCompletable = markItemsStarredUnstarred(false, syncData.getUnstarredItemsIds(), token); + } + + return starredItemsCompletable.concatWith(unstarredItemsCompletable); + } } diff --git a/api/src/main/java/com/readrops/api/services/freshrss/FreshRSSService.java b/api/src/main/java/com/readrops/api/services/freshrss/FreshRSSService.java index 321a4164..a812b237 100644 --- a/api/src/main/java/com/readrops/api/services/freshrss/FreshRSSService.java +++ b/api/src/main/java/com/readrops/api/services/freshrss/FreshRSSService.java @@ -42,7 +42,7 @@ public interface FreshRSSService { @FormUrlEncoded @POST("reader/api/0/edit-tag") - Completable setItemsReadState(@Field("T") String token, @Field("a") String readAction, @Field("r") String unreadAction, @Field("i") List itemIds); + Completable setItemsState(@Field("T") String token, @Field("a") String addAction, @Field("r") String removeAction, @Field("i") List itemIds); @FormUrlEncoded @POST("reader/api/0/subscription/edit") diff --git a/api/src/main/java/com/readrops/api/services/freshrss/FreshRSSSyncData.java b/api/src/main/java/com/readrops/api/services/freshrss/FreshRSSSyncData.java index 072b11bf..dde0c8b2 100644 --- a/api/src/main/java/com/readrops/api/services/freshrss/FreshRSSSyncData.java +++ b/api/src/main/java/com/readrops/api/services/freshrss/FreshRSSSyncData.java @@ -11,6 +11,10 @@ public class FreshRSSSyncData { private List unreadItemsIds; + private List starredItemsIds; + + private List unstarredItemsIds; + public FreshRSSSyncData() { readItemsIds = new ArrayList<>(); unreadItemsIds = new ArrayList<>(); @@ -39,4 +43,20 @@ public class FreshRSSSyncData { public void setUnreadItemsIds(List unreadItemsIds) { this.unreadItemsIds = unreadItemsIds; } + + public List getStarredItemsIds() { + return starredItemsIds; + } + + public void setStarredItemsIds(List starredItemsIds) { + this.starredItemsIds = starredItemsIds; + } + + public List getUnstarredItemsIds() { + return unstarredItemsIds; + } + + public void setUnstarredItemsIds(List unstarredItemsIds) { + this.unstarredItemsIds = unstarredItemsIds; + } } diff --git a/api/src/main/java/com/readrops/api/services/freshrss/adapters/FreshRSSItemsAdapter.kt b/api/src/main/java/com/readrops/api/services/freshrss/adapters/FreshRSSItemsAdapter.kt index bb89f88c..a68f0f88 100644 --- a/api/src/main/java/com/readrops/api/services/freshrss/adapters/FreshRSSItemsAdapter.kt +++ b/api/src/main/java/com/readrops/api/services/freshrss/adapters/FreshRSSItemsAdapter.kt @@ -1,9 +1,10 @@ package com.readrops.api.services.freshrss.adapters import android.util.TimingLogger -import com.readrops.db.entities.Item import com.readrops.api.services.freshrss.FreshRSSDataSource.GOOGLE_READ +import com.readrops.api.services.freshrss.FreshRSSDataSource.GOOGLE_STARRED import com.readrops.api.utils.exceptions.ParseException +import com.readrops.db.entities.Item import com.squareup.moshi.JsonAdapter import com.squareup.moshi.JsonReader import com.squareup.moshi.JsonWriter @@ -53,7 +54,7 @@ class FreshRSSItemsAdapter : JsonAdapter>() { 2 -> title = reader.nextString() 3 -> content = getContent(reader) 4 -> link = getLink(reader) - 5 -> isRead = getReadState(reader) + 5 -> getStates(reader, this) 6 -> feedRemoteId = getRemoteFeedId(reader) 7 -> author = reader.nextString() else -> reader.skipValue() @@ -102,18 +103,17 @@ class FreshRSSItemsAdapter : JsonAdapter>() { return href } - private fun getReadState(reader: JsonReader): Boolean { - var isRead = false + private fun getStates(reader: JsonReader, item: Item) { reader.beginArray() while (reader.hasNext()) { when (reader.nextString()) { - GOOGLE_READ -> isRead = true + GOOGLE_READ -> item.isRead = true + GOOGLE_STARRED -> item.isStarred = true } } reader.endArray() - return isRead } private fun getRemoteFeedId(reader: JsonReader): String? { diff --git a/app/src/main/java/com/readrops/app/repositories/FreshRSSRepository.java b/app/src/main/java/com/readrops/app/repositories/FreshRSSRepository.java index 7b4cfdeb..41c39236 100644 --- a/app/src/main/java/com/readrops/app/repositories/FreshRSSRepository.java +++ b/app/src/main/java/com/readrops/app/repositories/FreshRSSRepository.java @@ -91,6 +91,9 @@ public class FreshRSSRepository extends ARepository { syncData.setReadItemsIds(database.itemDao().getReadChanges(account.getId())); syncData.setUnreadItemsIds(database.itemDao().getUnreadChanges(account.getId())); + syncData.setStarredItemsIds(database.itemDao().getFreshRSSStarChanges(account.getId())); + syncData.setUnstarredItemsIds(database.itemDao().getFreshRSSUnstarChanges(account.getId())); + emitter.onSuccess(syncData); }).flatMap(syncData1 -> dataSource.sync(syncType, syncData1, account.getWriteToken())) .flatMapObservable(syncResult -> { @@ -107,7 +110,14 @@ public class FreshRSSRepository extends ARepository { account.setLastModified(newLastModified); database.accountDao().updateLastModified(account.getId(), newLastModified); - database.itemDao().resetReadChanges(account.getId()); + if (!syncData.getReadItemsIds().isEmpty() || !syncData.getUnreadItemsIds().isEmpty()) { + database.itemDao().resetReadChanges(account.getId()); + } + + if (!syncData.getStarredItemsIds().isEmpty() || !syncData.getUnstarredItemsIds().isEmpty()) { + database.itemDao().resetStarChanges(account.getId()); + } + logger.addSplit("reset read changes"); logger.dumpToLog(); diff --git a/db/src/main/java/com/readrops/db/dao/ItemDao.java b/db/src/main/java/com/readrops/db/dao/ItemDao.java index 439b4e85..3b59efbb 100644 --- a/db/src/main/java/com/readrops/db/dao/ItemDao.java +++ b/db/src/main/java/com/readrops/db/dao/ItemDao.java @@ -71,6 +71,12 @@ public interface ItemDao extends BaseDao { @Query("Select Item.remoteId From Item Inner Join Feed On Item.feed_id = Feed.id Where read_changed = 1 And read = 0 And account_id = :accountId") List getUnreadChanges(int accountId); + @Query("Select Item.remoteId From Item Inner Join Feed On Item.feed_id = Feed.id Where starred_changed = 1 And starred = 1 And account_id = :accountId") + List getFreshRSSStarChanges(int accountId); + + @Query("Select Item.remoteId From Item Inner Join Feed On Item.feed_id = Feed.id Where starred_changed = 1 And starred = 0 And account_id = :accountId") + List getFreshRSSUnstarChanges(int accountId); + @Query("Select Item.guid, Feed.remoteId as feedRemoteId From Item Inner Join Feed On Item.feed_id = Feed.id Where starred_changed = 1 And starred = 1 And account_id = :accountId") List getStarChanges(int accountId); From 9406416ce7793bef1c7bff2de7c68913173c7fb5 Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Mon, 26 Oct 2020 14:14:50 +0100 Subject: [PATCH 110/187] Add drawer favorites item to display starred/favorites items --- .../readrops/app/itemslist/DrawerManager.java | 8 ++ .../readrops/app/itemslist/MainActivity.java | 7 +- .../readrops/app/itemslist/MainViewModel.java | 43 ++++--- app/src/main/res/drawable/ic_star.xml | 5 + app/src/main/res/values-fr/strings.xml | 1 + app/src/main/res/values/strings.xml | 1 + .../readrops/db/ItemsListQueryBuilder.java | 114 ------------------ .../java/com/readrops/db/ItemsQueryBuilder.kt | 62 ++++++++++ .../com/readrops/db/filters/FilterType.java | 1 + 9 files changed, 105 insertions(+), 137 deletions(-) create mode 100644 app/src/main/res/drawable/ic_star.xml delete mode 100644 db/src/main/java/com/readrops/db/ItemsListQueryBuilder.java create mode 100644 db/src/main/java/com/readrops/db/ItemsQueryBuilder.kt diff --git a/app/src/main/java/com/readrops/app/itemslist/DrawerManager.java b/app/src/main/java/com/readrops/app/itemslist/DrawerManager.java index 57a70ebf..53be84eb 100644 --- a/app/src/main/java/com/readrops/app/itemslist/DrawerManager.java +++ b/app/src/main/java/com/readrops/app/itemslist/DrawerManager.java @@ -41,6 +41,7 @@ public class DrawerManager { public static final int ARTICLES_ITEM_ID = -5; public static final int READ_LATER_ID = -6; + public static final int STARS_ID = -10; public static final int ADD_ACCOUNT_ID = -4; public static final int ABOUT_ID = -7; public static final int SETTINGS_ID = -8; @@ -208,6 +209,12 @@ public class DrawerManager { .withSelectable(true) .withIdentifier(READ_LATER_ID); + PrimaryDrawerItem favorites = new PrimaryDrawerItem() + .withName(R.string.favorites) + .withIcon(R.drawable.ic_star) + .withSelectable(true) + .withIdentifier(STARS_ID); + PrimaryDrawerItem aboutItem = new PrimaryDrawerItem() .withName(R.string.about) .withIcon(R.drawable.ic_about_grey) @@ -224,6 +231,7 @@ public class DrawerManager { drawer.addStickyFooterItem(aboutItem); drawer.addItem(articles); + drawer.addItem(favorites); drawer.addItem(toReadLater); drawer.addItem(new DividerDrawerItem()); } diff --git a/app/src/main/java/com/readrops/app/itemslist/MainActivity.java b/app/src/main/java/com/readrops/app/itemslist/MainActivity.java index 5c68db04..0647787b 100644 --- a/app/src/main/java/com/readrops/app/itemslist/MainActivity.java +++ b/app/src/main/java/com/readrops/app/itemslist/MainActivity.java @@ -120,9 +120,6 @@ public class MainActivity extends AppCompatActivity implements SwipeRefreshLayou viewModel = ViewModelCompat.getViewModel(this, MainViewModel.class); - viewModel.setShowReadItems(SharedPreferencesManager.readBoolean( - SharedPreferencesManager.SharedPrefKey.SHOW_READ_ARTICLES)); - viewModel.getItemsWithFeed().observe(this, itemWithFeeds -> { allItems = itemWithFeeds; @@ -256,6 +253,10 @@ public class MainActivity extends AppCompatActivity implements SwipeRefreshLayou viewModel.setFilterType(FilterType.READ_IT_LATER_FILTER); viewModel.invalidate(); break; + case DrawerManager.STARS_ID: + viewModel.setFilterType(FilterType.STARS_FILTER); + viewModel.invalidate(); + break; case DrawerManager.ABOUT_ID: startAboutActivity(); break; diff --git a/app/src/main/java/com/readrops/app/itemslist/MainViewModel.java b/app/src/main/java/com/readrops/app/itemslist/MainViewModel.java index 9ca634a3..211540e7 100644 --- a/app/src/main/java/com/readrops/app/itemslist/MainViewModel.java +++ b/app/src/main/java/com/readrops/app/itemslist/MainViewModel.java @@ -8,8 +8,10 @@ import androidx.paging.LivePagedListBuilder; import androidx.paging.PagedList; import com.readrops.app.repositories.ARepository; +import com.readrops.app.utils.SharedPreferencesManager; import com.readrops.db.Database; -import com.readrops.db.ItemsListQueryBuilder; +import com.readrops.db.ItemsQueryBuilder; +import com.readrops.db.QueryFilters; import com.readrops.db.RoomFactoryWrapper; import com.readrops.db.entities.Feed; import com.readrops.db.entities.Folder; @@ -39,19 +41,18 @@ public class MainViewModel extends ViewModel { private ARepository repository; private final Database database; - private final ItemsListQueryBuilder queryBuilder; + private final QueryFilters queryFilters; private Account currentAccount; private List accounts; public MainViewModel(@NonNull Database database) { - queryBuilder = new ItemsListQueryBuilder(); - - queryBuilder.setFilterType(FilterType.NO_FILTER); - queryBuilder.setSortType(ListSortType.NEWEST_TO_OLDEST); - this.database = database; itemsWithFeed = new MediatorLiveData<>(); + + queryFilters = new QueryFilters(); + queryFilters.setShowReadItems(SharedPreferencesManager.readBoolean( + SharedPreferencesManager.SharedPrefKey.SHOW_READ_ARTICLES)); } //region main query @@ -62,10 +63,12 @@ public class MainViewModel extends ViewModel { } private void buildPagedList() { - if (lastFetch != null) + if (lastFetch != null) { itemsWithFeed.removeSource(lastFetch); + } - lastFetch = new LivePagedListBuilder<>(new RoomFactoryWrapper<>(database.itemDao().selectAll(queryBuilder.getQuery())), + lastFetch = new LivePagedListBuilder<>(new RoomFactoryWrapper<>(database.itemDao() + .selectAll(ItemsQueryBuilder.buildQuery(queryFilters))), new PagedList.Config.Builder() .setPageSize(100) .setPrefetchDistance(150) @@ -81,31 +84,31 @@ public class MainViewModel extends ViewModel { } public void setShowReadItems(boolean showReadItems) { - queryBuilder.setShowReadItems(showReadItems); + queryFilters.setShowReadItems(showReadItems); } public boolean showReadItems() { - return queryBuilder.showReadItems(); + return queryFilters.getShowReadItems(); } public void setFilterType(FilterType filterType) { - queryBuilder.setFilterType(filterType); + queryFilters.setFilterType(filterType); } public FilterType getFilterType() { - return queryBuilder.getFilterType(); + return queryFilters.getFilterType(); } public void setSortType(ListSortType sortType) { - queryBuilder.setSortType(sortType); + queryFilters.setSortType(sortType); } public ListSortType getSortType() { - return queryBuilder.getSortType(); + return queryFilters.getSortType(); } public void setFilterFeedId(int filterFeedId) { - queryBuilder.setFilterFeedId(filterFeedId); + queryFilters.setFilterFeedId(filterFeedId); } public MediatorLiveData> getItemsWithFeed() { @@ -160,7 +163,7 @@ public class MainViewModel extends ViewModel { public void setCurrentAccount(Account currentAccount) { this.currentAccount = currentAccount; setRepository(); - queryBuilder.setAccountId(currentAccount.getId()); + queryFilters.setAccountId(currentAccount.getId()); buildPagedList(); // set the new account as the current one @@ -191,7 +194,7 @@ public class MainViewModel extends ViewModel { currentAccountExists = true; setRepository(); - queryBuilder.setAccountId(currentAccount.getId()); + queryFilters.setAccountId(currentAccount.getId()); buildPagedList(); break; } @@ -230,8 +233,8 @@ public class MainViewModel extends ViewModel { } public Completable setAllItemsReadState(boolean read) { - if (queryBuilder.getFilterType() == FilterType.FEED_FILTER) - return repository.setAllFeedItemsReadState(queryBuilder.getFilterFeedId(), read); + if (queryFilters.getFilterType() == FilterType.FEED_FILTER) + return repository.setAllFeedItemsReadState(queryFilters.getFilterFeedId(), read); else return repository.setAllItemsReadState(read); } diff --git a/app/src/main/res/drawable/ic_star.xml b/app/src/main/res/drawable/ic_star.xml new file mode 100644 index 00000000..c172fe62 --- /dev/null +++ b/app/src/main/res/drawable/ic_star.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index cc89f85b..c370ce6a 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -133,5 +133,6 @@ Votre mot de passe d\'API (Configuration > Profil) Synchroniser Vue navigateur + Favoris \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 1e1f835e..eac89635 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -139,4 +139,5 @@ Show caption Synchronize Navigator view + Favorites diff --git a/db/src/main/java/com/readrops/db/ItemsListQueryBuilder.java b/db/src/main/java/com/readrops/db/ItemsListQueryBuilder.java deleted file mode 100644 index 60a2ca9a..00000000 --- a/db/src/main/java/com/readrops/db/ItemsListQueryBuilder.java +++ /dev/null @@ -1,114 +0,0 @@ -package com.readrops.db; - -import androidx.sqlite.db.SupportSQLiteQuery; -import androidx.sqlite.db.SupportSQLiteQueryBuilder; - -import com.readrops.db.filters.FilterType; -import com.readrops.db.filters.ListSortType; - -public class ItemsListQueryBuilder { - - private String[] columns = {"Item.id", "title", "clean_description", "image_link", "pub_date", "read", - "read_changed", "read_it_later", "Feed.name", "text_color", "background_color", "icon_url", "read_time", "Item.remoteId", - "Feed.id as feedId", "Feed.account_id", "Folder.id as folder_id", "Folder.name as folder_name"}; - - private String SELECT_ALL_JOIN = "Item INNER JOIN Feed on Item.feed_id = Feed.id " + - "LEFT JOIN Folder on Feed.folder_id = Folder.id"; - - private String ORDER_BY_ASC = "Item.id DESC"; - - private String ORDER_BY_DESC = "pub_date ASC"; - - private SupportSQLiteQueryBuilder queryBuilder; - - private boolean showReadItems; - private int filterFeedId; - private int accountId; - - private FilterType filterType; - private ListSortType sortType; - - public ItemsListQueryBuilder() { - queryBuilder = SupportSQLiteQueryBuilder.builder(SELECT_ALL_JOIN); - } - - private String buildWhereClause() { - StringBuilder stringBuilder = new StringBuilder(80); - - stringBuilder.append("Feed.account_id = ").append(accountId).append(" And "); - - if (!showReadItems) - stringBuilder.append("read = 0 And "); - - switch (filterType) { - case FEED_FILTER: - stringBuilder.append("feed_id = ").append(filterFeedId).append(" And read_it_later = 0"); - break; - case READ_IT_LATER_FILTER: - stringBuilder.append("read_it_later = 1"); - break; - case NO_FILTER: - stringBuilder.append("read_it_later = 0"); - break; - default: - stringBuilder.append("read_it_later = 0"); - break; - } - - return stringBuilder.toString(); - } - - - public SupportSQLiteQuery getQuery() { - queryBuilder.columns(columns); - - queryBuilder.selection(buildWhereClause(), new String[0]); - - if (sortType == ListSortType.NEWEST_TO_OLDEST) - queryBuilder.orderBy(ORDER_BY_ASC); - else - queryBuilder.orderBy(ORDER_BY_DESC); - - return queryBuilder.create(); - } - - public boolean showReadItems() { - return showReadItems; - } - - public void setShowReadItems(boolean showReadItems) { - this.showReadItems = showReadItems; - } - - public int getFilterFeedId() { - return filterFeedId; - } - - public void setFilterFeedId(int filterFeedId) { - this.filterFeedId = filterFeedId; - } - - public FilterType getFilterType() { - return filterType; - } - - public void setFilterType(FilterType filterType) { - this.filterType = filterType; - } - - public ListSortType getSortType() { - return sortType; - } - - public void setSortType(ListSortType sortType) { - this.sortType = sortType; - } - - public int getAccountId() { - return accountId; - } - - public void setAccountId(int accountId) { - this.accountId = accountId; - } -} diff --git a/db/src/main/java/com/readrops/db/ItemsQueryBuilder.kt b/db/src/main/java/com/readrops/db/ItemsQueryBuilder.kt new file mode 100644 index 00000000..d7c4cd2a --- /dev/null +++ b/db/src/main/java/com/readrops/db/ItemsQueryBuilder.kt @@ -0,0 +1,62 @@ +package com.readrops.db + +import androidx.sqlite.db.SupportSQLiteQuery +import androidx.sqlite.db.SupportSQLiteQueryBuilder +import com.readrops.db.filters.FilterType +import com.readrops.db.filters.ListSortType + +object ItemsQueryBuilder { + + private val COLUMNS = arrayOf("Item.id", "title", "clean_description", "image_link", "pub_date", "read", + "read_changed", "read_it_later", "Feed.name", "text_color", "background_color", "icon_url", "read_time", "Item.remoteId", + "Feed.id as feedId", "Feed.account_id", "Folder.id as folder_id", "Folder.name as folder_name") + + private const val SELECT_ALL_JOIN = "Item INNER JOIN Feed on Item.feed_id = Feed.id " + + "LEFT JOIN Folder on Feed.folder_id = Folder.id" + + private const val ORDER_BY_ASC = "Item.id DESC" + + private const val ORDER_BY_DESC = "pub_date ASC" + + + @JvmStatic + fun buildQuery(queryFilters: QueryFilters): SupportSQLiteQuery { + if (queryFilters.accountId == 0) + throw IllegalArgumentException("AccountId must be greater than 0") + + if (queryFilters.filterType == FilterType.FEED_FILTER && queryFilters.filterFeedId == 0) + throw IllegalArgumentException("FeedId must be greater than 0 if current filter is FEED_FILTER") + + return SupportSQLiteQueryBuilder.builder(SELECT_ALL_JOIN).run { + columns(COLUMNS) + selection(buildWhereClause(queryFilters), null) + orderBy(if (queryFilters.sortType == ListSortType.NEWEST_TO_OLDEST) ORDER_BY_ASC else ORDER_BY_DESC) + + create() + } + } + + private fun buildWhereClause(queryFilters: QueryFilters): String? { + return StringBuilder(500).run { + append("Feed.account_id = ${queryFilters.accountId} And ") + + if (!queryFilters.showReadItems) append("read = 0 And ") + + when (queryFilters.filterType) { + FilterType.FEED_FILTER -> append("feed_id = ${queryFilters.filterFeedId} And read_it_later = 0") + FilterType.READ_IT_LATER_FILTER -> append("read_it_later = 1") + FilterType.STARS_FILTER -> append("starred = 1 And read_it_later = 0") + else -> append("read_it_later = 0") + } + + toString() + } + + } +} + +class QueryFilters(var showReadItems: Boolean = true, + var filterFeedId: Int = 0, + var accountId: Int = 0, + var filterType: FilterType = FilterType.NO_FILTER, + var sortType: ListSortType = ListSortType.NEWEST_TO_OLDEST) \ No newline at end of file diff --git a/db/src/main/java/com/readrops/db/filters/FilterType.java b/db/src/main/java/com/readrops/db/filters/FilterType.java index 7ed9eb9f..a3388429 100644 --- a/db/src/main/java/com/readrops/db/filters/FilterType.java +++ b/db/src/main/java/com/readrops/db/filters/FilterType.java @@ -3,5 +3,6 @@ package com.readrops.db.filters; public enum FilterType { FEED_FILTER, READ_IT_LATER_FILTER, + STARS_FILTER, NO_FILTER } From 4460420f8a07cccec4cc8fb2c5d74d2daafa43ca Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Mon, 26 Oct 2020 14:17:20 +0100 Subject: [PATCH 111/187] Add tests for ItemsQueryBuilder --- .../java/com/readrops/db/ExampleUnitTest.java | 17 ----- .../com/readrops/db/ItemsQueryBuilderTest.kt | 73 +++++++++++++++++++ 2 files changed, 73 insertions(+), 17 deletions(-) delete mode 100644 db/src/test/java/com/readrops/db/ExampleUnitTest.java create mode 100644 db/src/test/java/com/readrops/db/ItemsQueryBuilderTest.kt diff --git a/db/src/test/java/com/readrops/db/ExampleUnitTest.java b/db/src/test/java/com/readrops/db/ExampleUnitTest.java deleted file mode 100644 index f8cec603..00000000 --- a/db/src/test/java/com/readrops/db/ExampleUnitTest.java +++ /dev/null @@ -1,17 +0,0 @@ -package com.readrops.db; - -import org.junit.Test; - -import static org.junit.Assert.*; - -/** - * Example local unit test, which will execute on the development machine (host). - * - * @see Testing documentation - */ -public class ExampleUnitTest { - @Test - public void addition_isCorrect() { - assertEquals(4, 2 + 2); - } -} \ No newline at end of file diff --git a/db/src/test/java/com/readrops/db/ItemsQueryBuilderTest.kt b/db/src/test/java/com/readrops/db/ItemsQueryBuilderTest.kt new file mode 100644 index 00000000..b61ce808 --- /dev/null +++ b/db/src/test/java/com/readrops/db/ItemsQueryBuilderTest.kt @@ -0,0 +1,73 @@ +package com.readrops.db + +import com.readrops.db.filters.FilterType +import com.readrops.db.filters.ListSortType +import junit.framework.TestCase.assertFalse +import junit.framework.TestCase.assertTrue +import org.junit.Test + +class ItemsQueryBuilderTest { + + @Test + fun noFilterDefaultSortCaseTest() { + val queryFilters = QueryFilters(accountId = 1) + + val query = ItemsQueryBuilder.buildQuery(queryFilters).sql + + assertTrue(query.contains("Feed.account_id = 1")) + assertTrue(query.contains("read_it_later = 0")) + assertTrue(query.contains("Item.id DESC")) + + assertFalse(query.contains("read = 0 And")) + } + + @Test + fun feedFilterCaseTest() { + val queryFilters = QueryFilters(accountId = 1, filterType = FilterType.FEED_FILTER, + filterFeedId = 15) + + val query = ItemsQueryBuilder.buildQuery(queryFilters).sql + + assertTrue(query.contains("feed_id = 15 And read_it_later = 0")) + } + + @Test + fun readLaterFilterCaseTest() { + val queryFilters = QueryFilters(accountId = 1, filterType = FilterType.READ_IT_LATER_FILTER) + + val query = ItemsQueryBuilder.buildQuery(queryFilters).sql + assertTrue(query.contains("read_it_later = 1")) + } + + @Test + fun starsFilterCaseTest() { + val queryFilters = QueryFilters(accountId = 1, filterType = FilterType.STARS_FILTER) + + val query = ItemsQueryBuilder.buildQuery(queryFilters).sql + assertTrue(query.contains("starred = 1 And read_it_later = 0")) + } + + @Test + fun oldestSortCaseTest() { + val queryFilters = QueryFilters(accountId = 1, sortType = ListSortType.OLDEST_TO_NEWEST, + showReadItems = false) + + val query = ItemsQueryBuilder.buildQuery(queryFilters).sql + + assertTrue(query.contains("read = 0 And ")) + assertTrue(query.contains("pub_date ASC")) + } + + @Test(expected = IllegalArgumentException::class) + fun accountIdExceptionTest() { + val queryFilters = QueryFilters() + + ItemsQueryBuilder.buildQuery(queryFilters) + } + + @Test(expected = IllegalArgumentException::class) + fun filterFeedIdExceptionTest() { + val queryFilters = QueryFilters(accountId = 1, filterType = FilterType.FEED_FILTER) + ItemsQueryBuilder.buildQuery(queryFilters) + } +} \ No newline at end of file From 97ae58305c391ede930a43daac53c3de01839b44 Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Mon, 26 Oct 2020 22:02:39 +0100 Subject: [PATCH 112/187] Add action to star/fav an item --- .../com/readrops/app/item/ItemActivity.java | 39 ++++++++++++++++++- .../com/readrops/app/item/ItemViewModel.java | 5 +++ app/src/main/res/drawable/ic_empty_star.xml | 5 +++ app/src/main/res/layout/activity_item.xml | 13 ++++++- .../java/com/readrops/db/dao/ItemDao.java | 7 +++- 5 files changed, 65 insertions(+), 4 deletions(-) create mode 100644 app/src/main/res/drawable/ic_empty_star.xml diff --git a/app/src/main/java/com/readrops/app/item/ItemActivity.java b/app/src/main/java/com/readrops/app/item/ItemActivity.java index 2e7c8057..22277997 100644 --- a/app/src/main/java/com/readrops/app/item/ItemActivity.java +++ b/app/src/main/java/com/readrops/app/item/ItemActivity.java @@ -46,6 +46,9 @@ import org.koin.java.KoinJavaComponent; import java.util.regex.Matcher; import java.util.regex.Pattern; +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.schedulers.Schedulers; + import static com.readrops.app.utils.ReadropsKeys.ACTION_BAR_COLOR; import static com.readrops.app.utils.ReadropsKeys.IMAGE_URL; import static com.readrops.app.utils.ReadropsKeys.ITEM_ID; @@ -66,6 +69,8 @@ public class ItemActivity extends AppCompatActivity { private String urlToDownload; private String imageTitle; + private boolean uiBinded; + @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -112,14 +117,44 @@ public class ItemActivity extends AppCompatActivity { })); viewModel = ViewModelCompat.getViewModel(this, ItemViewModel.class); - viewModel.getItemById(itemId).observe(this, this::bindUI); + viewModel.getItemById(itemId).observe(this, itemWithFeed1 -> { + if (!uiBinded) { + bindUI(itemWithFeed1); + uiBinded = true; + } + }); + binding.activityItemFab.setOnClickListener(v -> openInNavigator()); + + binding.itemStarFab.setOnClickListener(v -> { + Item item = itemWithFeed.getItem(); + + if (item.isStarred()) { + binding.itemStarFab.setImageResource(R.drawable.ic_empty_star); + } else { + binding.itemStarFab.setImageResource(R.drawable.ic_star); + } + + item.setStarred(!item.isStarred()); + item.setStarredChanged(!item.isStarredChanged()); + + viewModel.setStarState(item.getId(), item.isStarred(), item.isStarredChanged()) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .doOnError(throwable -> Utils.showSnackbar(binding.itemRoot, throwable.getMessage())) + .subscribe(); + }); + } private void bindUI(ItemWithFeed itemWithFeed) { this.itemWithFeed = itemWithFeed; Item item = itemWithFeed.getItem(); + if (item.isStarred()) { + binding.itemStarFab.setImageResource(R.drawable.ic_star); + } + binding.activityItemDate.setText(DateUtils.formattedDateTimeByLocal(item.getPubDate())); if (item.getImageLink() == null) @@ -165,6 +200,7 @@ public class ItemActivity extends AppCompatActivity { getWindow().setStatusBarColor(itemWithFeed.getBgColor()); binding.activityItemFab.setBackgroundTintList(ColorStateList.valueOf(itemWithFeed.getBgColor())); + binding.itemStarFab.setBackgroundTintList(ColorStateList.valueOf(itemWithFeed.getBgColor())); } else if (itemWithFeed.getColor() != 0) { binding.collapsingLayout.setBackgroundColor(itemWithFeed.getColor()); binding.collapsingLayout.setContentScrimColor(itemWithFeed.getColor()); @@ -172,6 +208,7 @@ public class ItemActivity extends AppCompatActivity { getWindow().setStatusBarColor(itemWithFeed.getColor()); binding.activityItemFab.setBackgroundTintList(ColorStateList.valueOf(itemWithFeed.getColor())); + binding.itemStarFab.setBackgroundTintList(ColorStateList.valueOf(itemWithFeed.getColor())); } binding.itemWebview.setItem(itemWithFeed); diff --git a/app/src/main/java/com/readrops/app/item/ItemViewModel.java b/app/src/main/java/com/readrops/app/item/ItemViewModel.java index 102d66a8..8fa785a9 100644 --- a/app/src/main/java/com/readrops/app/item/ItemViewModel.java +++ b/app/src/main/java/com/readrops/app/item/ItemViewModel.java @@ -17,6 +17,8 @@ import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStream; +import io.reactivex.Completable; + public class ItemViewModel extends ViewModel { private final Database database; @@ -29,6 +31,9 @@ public class ItemViewModel extends ViewModel { return database.itemDao().getItemById(id); } + public Completable setStarState(int itemId, boolean starred, boolean starredChanged) { + return database.itemDao().setStarState(itemId, starred, starredChanged); + } public Uri saveImageInCache(Bitmap bitmap, Context context) throws IOException { File imagesFolder = new File(context.getCacheDir().getAbsolutePath(), "images"); diff --git a/app/src/main/res/drawable/ic_empty_star.xml b/app/src/main/res/drawable/ic_empty_star.xml new file mode 100644 index 00000000..26a48774 --- /dev/null +++ b/app/src/main/res/drawable/ic_empty_star.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/layout/activity_item.xml b/app/src/main/res/layout/activity_item.xml index abcc6c68..c7249ef3 100644 --- a/app/src/main/res/layout/activity_item.xml +++ b/app/src/main/res/layout/activity_item.xml @@ -54,7 +54,8 @@ android:focusable="true" android:src="@drawable/ic_open_in_browser_white" app:layout_anchor="@id/app_bar_layout" - app:layout_anchorGravity="bottom|right|end" /> + app:layout_anchorGravity="bottom|end" /> + + + \ No newline at end of file diff --git a/db/src/main/java/com/readrops/db/dao/ItemDao.java b/db/src/main/java/com/readrops/db/dao/ItemDao.java index 3b59efbb..b3cfe561 100644 --- a/db/src/main/java/com/readrops/db/dao/ItemDao.java +++ b/db/src/main/java/com/readrops/db/dao/ItemDao.java @@ -60,8 +60,8 @@ public interface ItemDao extends BaseDao { int getUnreadCount(int feedId); @SuppressWarnings(RoomWarnings.CURSOR_MISMATCH) - @Query("Select title, Item.description, content, link, pub_date, image_link, author, read, text_color, " + - "background_color, read_time, Feed.name, Feed.id as feedId, siteUrl, Folder.id as folder_id, " + + @Query("Select Item.id, title, Item.description, content, link, pub_date, image_link, author, read, text_color, " + + "background_color, read_time, starred, Feed.name, Feed.id as feedId, siteUrl, Folder.id as folder_id, " + "Folder.name as folder_name from Item Inner Join Feed On Item.feed_id = Feed.id Left Join Folder on Folder.id = Feed.folder_id Where Item.id = :id") LiveData getItemById(int id); @@ -91,4 +91,7 @@ public interface ItemDao extends BaseDao { @Query("Update Item set read = :read Where remoteId = :remoteId") void setReadState(String remoteId, boolean read); + + @Query("Update Item set starred = :starred, starred_changed = :starredChanged Where id = :itemId") + Completable setStarState(int itemId, boolean starred, boolean starredChanged); } From 81de8985c8e8ba0b9cafe86d8b03972ff52d60fa Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Tue, 27 Oct 2020 21:29:19 +0100 Subject: [PATCH 113/187] Sync Nextcloud News items star state --- .../com/readrops/app/repositories/FreshRSSRepository.java | 2 +- .../com/readrops/app/repositories/NextNewsRepository.java | 2 +- db/src/main/java/com/readrops/db/dao/ItemDao.java | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/com/readrops/app/repositories/FreshRSSRepository.java b/app/src/main/java/com/readrops/app/repositories/FreshRSSRepository.java index 41c39236..053ab00a 100644 --- a/app/src/main/java/com/readrops/app/repositories/FreshRSSRepository.java +++ b/app/src/main/java/com/readrops/app/repositories/FreshRSSRepository.java @@ -220,7 +220,7 @@ public class FreshRSSRepository extends ARepository { int feedId = database.feedDao().getFeedIdByRemoteId(item.getFeedRemoteId(), account.getId()); if (!initialSync && feedId > 0 && database.itemDao().remoteItemExists(item.getRemoteId(), feedId)) { - database.itemDao().setReadState(item.getRemoteId(), item.isRead()); + database.itemDao().setReadAndStarState(item.getRemoteId(), item.isRead(), item.isStarred()); continue; } diff --git a/app/src/main/java/com/readrops/app/repositories/NextNewsRepository.java b/app/src/main/java/com/readrops/app/repositories/NextNewsRepository.java index 96789171..302e8eb1 100644 --- a/app/src/main/java/com/readrops/app/repositories/NextNewsRepository.java +++ b/app/src/main/java/com/readrops/app/repositories/NextNewsRepository.java @@ -301,7 +301,7 @@ public class NextNewsRepository extends ARepository { //if the item already exists, update only its read state if (!initialSync && feedId > 0 && database.itemDao().remoteItemExists(String.valueOf(item.getRemoteId()), feedId)) { - database.itemDao().setReadState(item.getRemoteId(), item.isRead()); + database.itemDao().setReadAndStarState(item.getRemoteId(), item.isRead(), item.isStarred()); continue; } diff --git a/db/src/main/java/com/readrops/db/dao/ItemDao.java b/db/src/main/java/com/readrops/db/dao/ItemDao.java index b3cfe561..0a451e1e 100644 --- a/db/src/main/java/com/readrops/db/dao/ItemDao.java +++ b/db/src/main/java/com/readrops/db/dao/ItemDao.java @@ -89,8 +89,8 @@ public interface ItemDao extends BaseDao { @Query("Update Item set starred_changed = 0 Where feed_id in (Select id From Feed Where account_id = :accountId)") void resetStarChanges(int accountId); - @Query("Update Item set read = :read Where remoteId = :remoteId") - void setReadState(String remoteId, boolean read); + @Query("Update Item set read = :read, starred = :starred Where remoteId = :remoteId") + void setReadAndStarState(String remoteId, boolean read, boolean starred); @Query("Update Item set starred = :starred, starred_changed = :starredChanged Where id = :itemId") Completable setStarState(int itemId, boolean starred, boolean starredChanged); From da6b68fd2ef0976e388b0031161feab9e56cecf3 Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Tue, 27 Oct 2020 21:30:26 +0100 Subject: [PATCH 114/187] Use a different retrofit builder for each service --- .../main/java/com/readrops/api/ApiModule.kt | 23 ++++++++++--------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/api/src/main/java/com/readrops/api/ApiModule.kt b/api/src/main/java/com/readrops/api/ApiModule.kt index 81d9b800..677dc8ad 100644 --- a/api/src/main/java/com/readrops/api/ApiModule.kt +++ b/api/src/main/java/com/readrops/api/ApiModule.kt @@ -23,6 +23,8 @@ import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory import retrofit2.converter.moshi.MoshiConverterFactory import java.util.concurrent.TimeUnit +const val BASE_URL = "https://baseurl.com" + val apiModule = module { single(createdAtStart = true) { @@ -38,7 +40,7 @@ val apiModule = module { //region freshrss single { - FreshRSSDataSource(get()) + FreshRSSDataSource(get()) } single { @@ -47,7 +49,10 @@ val apiModule = module { } single(named("freshrssRetrofit")) { - get() + Retrofit.Builder() // url will be set dynamically in an interceptor + .baseUrl(BASE_URL) + .addCallAdapterFactory(RxJava2CallAdapterFactory.create()) + .client(get()) .addConverterFactory(MoshiConverterFactory.create(get(named("freshrssMoshi")))) .build() } @@ -65,7 +70,7 @@ val apiModule = module { //region nextcloud news single { - NextNewsDataSource(get()) + NextNewsDataSource(get()) } single { @@ -74,7 +79,10 @@ val apiModule = module { } single(named("nextcloudNewsRetrofit")) { - get() + Retrofit.Builder() // url will be set dynamically in an interceptor + .baseUrl(BASE_URL) + .addCallAdapterFactory(RxJava2CallAdapterFactory.create()) + .client(get()) .addConverterFactory(MoshiConverterFactory.create(get(named("nextcloudNewsMoshi")))) .build() } @@ -89,13 +97,6 @@ val apiModule = module { //endregion nextcloud news - single { - Retrofit.Builder() // url will be set dynamically in an interceptor - .baseUrl("https://baseurl.com") - .addCallAdapterFactory(RxJava2CallAdapterFactory.create()) - .client(get()) - } - single { AuthInterceptor() } From c449a9426005b39ee1bda1b49836258c52354650 Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Tue, 27 Oct 2020 21:33:49 +0100 Subject: [PATCH 115/187] Add missing migration to database builder --- db/src/main/java/com/readrops/db/DbModule.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/db/src/main/java/com/readrops/db/DbModule.kt b/db/src/main/java/com/readrops/db/DbModule.kt index 031f9361..dcf264ff 100644 --- a/db/src/main/java/com/readrops/db/DbModule.kt +++ b/db/src/main/java/com/readrops/db/DbModule.kt @@ -8,6 +8,7 @@ val dbModule = module { single(createdAtStart = true) { Room.databaseBuilder(get(), Database::class.java, "readrops-db") .addMigrations(Database.MIGRATION_1_2) + .addMigrations(Database.MIGRATION_2_3) .build() } } \ No newline at end of file From cfa764e78bfd6120f25932b1c48975b80cbf1ce2 Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Mon, 2 Nov 2020 22:35:57 +0100 Subject: [PATCH 116/187] Sync FreshRSS items star state --- .../main/java/com/readrops/api/ApiModule.kt | 2 + .../com/readrops/api/services/SyncResult.kt | 4 ++ .../services/freshrss/FreshRSSDataSource.java | 40 ++++++++++++--- .../services/freshrss/FreshRSSService.java | 10 +++- .../adapters/FreshRSSItemsIdsAdapter.kt | 51 +++++++++++++++++++ .../adapters/FreshRSSItemsIdsAdapterTest.kt | 30 +++++++++++ .../freshrss/adapters/items_starred_ids.json | 19 +++++++ .../app/repositories/FreshRSSRepository.java | 22 +++++++- .../java/com/readrops/db/dao/ItemDao.java | 6 +++ 9 files changed, 173 insertions(+), 11 deletions(-) create mode 100644 api/src/main/java/com/readrops/api/services/freshrss/adapters/FreshRSSItemsIdsAdapter.kt create mode 100644 api/src/test/java/com/readrops/api/services/freshrss/adapters/FreshRSSItemsIdsAdapterTest.kt create mode 100644 api/src/test/resources/services/freshrss/adapters/items_starred_ids.json diff --git a/api/src/main/java/com/readrops/api/ApiModule.kt b/api/src/main/java/com/readrops/api/ApiModule.kt index 677dc8ad..eecfff74 100644 --- a/api/src/main/java/com/readrops/api/ApiModule.kt +++ b/api/src/main/java/com/readrops/api/ApiModule.kt @@ -6,6 +6,7 @@ import com.readrops.api.services.freshrss.FreshRSSService import com.readrops.api.services.freshrss.adapters.FreshRSSFeedsAdapter import com.readrops.api.services.freshrss.adapters.FreshRSSFoldersAdapter import com.readrops.api.services.freshrss.adapters.FreshRSSItemsAdapter +import com.readrops.api.services.freshrss.adapters.FreshRSSItemsIdsAdapter import com.readrops.api.services.nextcloudnews.NextNewsDataSource import com.readrops.api.services.nextcloudnews.NextNewsService import com.readrops.api.services.nextcloudnews.adapters.NextNewsFeedsAdapter @@ -60,6 +61,7 @@ val apiModule = module { single(named("freshrssMoshi")) { Moshi.Builder() .add(Types.newParameterizedType(List::class.java, Item::class.java), FreshRSSItemsAdapter()) + .add(Types.newParameterizedType(List::class.java, String::class.java), FreshRSSItemsIdsAdapter()) .add(FreshRSSFeedsAdapter()) .add(FreshRSSFoldersAdapter()) .build() diff --git a/api/src/main/java/com/readrops/api/services/SyncResult.kt b/api/src/main/java/com/readrops/api/services/SyncResult.kt index daf97a5d..2c14fed3 100644 --- a/api/src/main/java/com/readrops/api/services/SyncResult.kt +++ b/api/src/main/java/com/readrops/api/services/SyncResult.kt @@ -8,9 +8,13 @@ class SyncResult { var items: List = mutableListOf() + var starredItems: List = mutableListOf() + var feeds: List = listOf() var folders: List = listOf() + var starredIds: List? = null + var isError: Boolean = false } diff --git a/api/src/main/java/com/readrops/api/services/freshrss/FreshRSSDataSource.java b/api/src/main/java/com/readrops/api/services/freshrss/FreshRSSDataSource.java index 5667c17e..f1415008 100644 --- a/api/src/main/java/com/readrops/api/services/freshrss/FreshRSSDataSource.java +++ b/api/src/main/java/com/readrops/api/services/freshrss/FreshRSSDataSource.java @@ -11,6 +11,8 @@ import com.readrops.db.entities.Folder; import com.readrops.db.entities.Item; import java.io.StringReader; +import java.util.Arrays; +import java.util.Collections; import java.util.List; import java.util.Properties; @@ -22,9 +24,11 @@ import okhttp3.RequestBody; public class FreshRSSDataSource { private static final int MAX_ITEMS = 5000; + private static final int MAX_STARRED_ITEMS = 999; public static final String GOOGLE_READ = "user/-/state/com.google/read"; public static final String GOOGLE_STARRED = "user/-/state/com.google/starred"; + public static final String GOOGLE_READING_LIST = "user/-/state/com.google/reading-list"; private static final String FEED_PREFIX = "feed/"; @@ -99,14 +103,22 @@ public class FreshRSSDataSource { syncResult.setFeeds(freshRSSFeeds); if (syncType == SyncType.INITIAL_SYNC) { - return getItems(GOOGLE_READ, MAX_ITEMS, null); + return getItems(Arrays.asList(GOOGLE_READ, GOOGLE_STARRED), MAX_ITEMS, null); } else { - return getItems(null, MAX_ITEMS, syncData.getLastModified()); + return getItems(Collections.singletonList(GOOGLE_STARRED), MAX_ITEMS, syncData.getLastModified()); } }) .flatMap(freshRSSItems -> { syncResult.setItems(freshRSSItems); + return getStarredItems(MAX_STARRED_ITEMS); + }).flatMap(starredItems -> { + syncResult.setStarredItems(starredItems); + + return getItemsIds(null, GOOGLE_STARRED, MAX_STARRED_ITEMS); + }).flatMap(starredIds -> { + syncResult.setStarredIds(starredIds); + return Single.just(syncResult); })); } @@ -132,13 +144,27 @@ public class FreshRSSDataSource { /** * Fetch the items * - * @param excludeTarget type of items to exclude (currently only read items) - * @param max max number of items to fetch - * @param lastModified fetch only items created after this timestamp + * @param excludeTargets type of items to exclude (read items and starred items) + * @param max max number of items to fetch + * @param lastModified fetch only items created after this timestamp * @return the items */ - public Single> getItems(@Nullable String excludeTarget, int max, @Nullable Long lastModified) { - return api.getItems(excludeTarget, max, lastModified); + public Single> getItems(@Nullable List excludeTargets, int max, @Nullable Long lastModified) { + return api.getItems(excludeTargets, max, lastModified); + } + + /** + * Fetch starred items + * + * @param max max number of items to fetch + * @return items + */ + public Single> getStarredItems(int max) { + return api.getStarredItems(max); + } + + public Single> getItemsIds(String excludeTarget, String includeTarget, int max) { + return api.getItemsIds(excludeTarget, includeTarget, max); } diff --git a/api/src/main/java/com/readrops/api/services/freshrss/FreshRSSService.java b/api/src/main/java/com/readrops/api/services/freshrss/FreshRSSService.java index a812b237..43184f7f 100644 --- a/api/src/main/java/com/readrops/api/services/freshrss/FreshRSSService.java +++ b/api/src/main/java/com/readrops/api/services/freshrss/FreshRSSService.java @@ -1,9 +1,9 @@ package com.readrops.api.services.freshrss; +import com.readrops.api.services.freshrss.json.FreshRSSUserInfo; import com.readrops.db.entities.Feed; import com.readrops.db.entities.Folder; import com.readrops.db.entities.Item; -import com.readrops.api.services.freshrss.json.FreshRSSUserInfo; import java.util.List; @@ -35,7 +35,13 @@ public interface FreshRSSService { Single> getFeeds(); @GET("reader/api/0/stream/contents/user/-/state/com.google/reading-list") - Single> getItems(@Query("xt") String excludeTarget, @Query("n") int max, @Query("ot") Long lastModified); + Single> getItems(@Query("xt") List excludeTarget, @Query("n") int max, @Query("ot") Long lastModified); + + @GET("reader/api/0/stream/contents/user/-/state/com.google/starred") + Single> getStarredItems(@Query("n") int max); + + @GET("reader/api/0/stream/items/ids") + Single> getItemsIds(@Query("xt") String excludeTarget, @Query("s") String includeTarget, @Query("n") int max); @GET("reader/api/0/tag/list?output=json") Single> getFolders(); diff --git a/api/src/main/java/com/readrops/api/services/freshrss/adapters/FreshRSSItemsIdsAdapter.kt b/api/src/main/java/com/readrops/api/services/freshrss/adapters/FreshRSSItemsIdsAdapter.kt new file mode 100644 index 00000000..c03dca06 --- /dev/null +++ b/api/src/main/java/com/readrops/api/services/freshrss/adapters/FreshRSSItemsIdsAdapter.kt @@ -0,0 +1,51 @@ +package com.readrops.api.services.freshrss.adapters + +import android.annotation.SuppressLint +import com.readrops.api.utils.exceptions.ParseException +import com.readrops.api.utils.extensions.nextNonEmptyString +import com.squareup.moshi.JsonAdapter +import com.squareup.moshi.JsonReader +import com.squareup.moshi.JsonWriter + +class FreshRSSItemsIdsAdapter : JsonAdapter>() { + + override fun toJson(writer: JsonWriter, value: List?) { + // not useful here + } + + @SuppressLint("CheckResult") + override fun fromJson(reader: JsonReader): List? = with(reader) { + val ids = arrayListOf() + + return try { + beginObject() + nextName() + beginArray() + + while (hasNext()) { + beginObject() + + when (nextName()) { + "id" -> { + val value = nextNonEmptyString() + ids += "tag:google.com,2005:reader/item/${ + value.toLong() + .toString(16).padStart(value.length, '0') + }" + } + else -> skipValue() + } + + endObject() + } + + endArray() + endObject() + + ids + } catch (e: Exception) { + throw ParseException(e.message) + } + } + +} \ No newline at end of file diff --git a/api/src/test/java/com/readrops/api/services/freshrss/adapters/FreshRSSItemsIdsAdapterTest.kt b/api/src/test/java/com/readrops/api/services/freshrss/adapters/FreshRSSItemsIdsAdapterTest.kt new file mode 100644 index 00000000..7831c213 --- /dev/null +++ b/api/src/test/java/com/readrops/api/services/freshrss/adapters/FreshRSSItemsIdsAdapterTest.kt @@ -0,0 +1,30 @@ +package com.readrops.api.services.freshrss.adapters + +import com.squareup.moshi.Moshi +import com.squareup.moshi.Types +import junit.framework.TestCase.assertEquals +import okio.Buffer +import org.junit.Test + +class FreshRSSItemsIdsAdapterTest { + + private val adapter = Moshi.Builder() + .add(Types.newParameterizedType(List::class.java, String::class.java), FreshRSSItemsIdsAdapter()) + .build() + .adapter>(Types.newParameterizedType(List::class.java, String::class.java)) + + @Test + fun validIdsTest() { + val stream = javaClass.classLoader!!.getResourceAsStream("services/freshrss/adapters/items_starred_ids.json") + + val ids = adapter.fromJson(Buffer().readFrom(stream))!! + + assertEquals(ids, listOf( + "tag:google.com,2005:reader/item/0005b2c17277b383", + "tag:google.com,2005:reader/item/0005b2c12d328ae4", + "tag:google.com,2005:reader/item/0005b2c0781d0737", + "tag:google.com,2005:reader/item/0005b2bf3852c293", + "tag:google.com,2005:reader/item/0005b2bebeed9f7f" + )) + } +} \ No newline at end of file diff --git a/api/src/test/resources/services/freshrss/adapters/items_starred_ids.json b/api/src/test/resources/services/freshrss/adapters/items_starred_ids.json new file mode 100644 index 00000000..29031ef3 --- /dev/null +++ b/api/src/test/resources/services/freshrss/adapters/items_starred_ids.json @@ -0,0 +1,19 @@ +{ + "itemRefs": [ + { + "id": "1603918802432899" + }, + { + "id": "1603917640272612" + }, + { + "id": "1603914602186551" + }, + { + "id": "1603909236998803" + }, + { + "id": "1603907200327551" + } + ] +} \ No newline at end of file diff --git a/app/src/main/java/com/readrops/app/repositories/FreshRSSRepository.java b/app/src/main/java/com/readrops/app/repositories/FreshRSSRepository.java index 053ab00a..9d755076 100644 --- a/app/src/main/java/com/readrops/app/repositories/FreshRSSRepository.java +++ b/app/src/main/java/com/readrops/app/repositories/FreshRSSRepository.java @@ -107,6 +107,10 @@ public class FreshRSSRepository extends ARepository { insertItems(syncResult.getItems(), syncType == SyncType.INITIAL_SYNC); logger.addSplit("items insertion"); + insertItems(syncResult.getStarredItems(), syncType == SyncType.INITIAL_SYNC); + + updateItemsStarState(syncResult.getStarredIds()); + account.setLastModified(newLastModified); database.accountDao().updateLastModified(account.getId(), newLastModified); @@ -216,6 +220,8 @@ public class FreshRSSRepository extends ARepository { } private void insertItems(List items, boolean initialSync) { + List itemsToInsert = new ArrayList<>(); + for (Item item : items) { int feedId = database.feedDao().getFeedIdByRemoteId(item.getFeedRemoteId(), account.getId()); @@ -226,9 +232,21 @@ public class FreshRSSRepository extends ARepository { item.setFeedId(feedId); item.setReadTime(Utils.readTimeFromString(item.getContent())); + itemsToInsert.add(item); } - Collections.sort(items, Item::compareTo); - database.itemDao().insert(items); + if (!itemsToInsert.isEmpty()) { + Collections.sort(itemsToInsert, Item::compareTo); + database.itemDao().insert(itemsToInsert); + } } + + private void updateItemsStarState(List itemsIds) { + if (itemsIds != null && !itemsIds.isEmpty()) { + database.itemDao().unstarItems(itemsIds, account.getId()); + database.itemDao().starItems(itemsIds, account.getId()); + } + } + + } diff --git a/db/src/main/java/com/readrops/db/dao/ItemDao.java b/db/src/main/java/com/readrops/db/dao/ItemDao.java index 0a451e1e..1dc6d0c2 100644 --- a/db/src/main/java/com/readrops/db/dao/ItemDao.java +++ b/db/src/main/java/com/readrops/db/dao/ItemDao.java @@ -94,4 +94,10 @@ public interface ItemDao extends BaseDao { @Query("Update Item set starred = :starred, starred_changed = :starredChanged Where id = :itemId") Completable setStarState(int itemId, boolean starred, boolean starredChanged); + + @Query("Update Item set starred = 1 Where remoteId In (:ids) And feed_id In (Select id From Feed Where account_id = :accountId)") + void starItems(List ids, int accountId); + + @Query("Update Item set starred = 0 Where remoteId Not In (:ids) And feed_id In (Select id From Feed Where account_id = :accountId)") + void unstarItems(List ids, int accountId); } From f2daac5068048d2b3ad07a3b039757717fd10e82 Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Tue, 3 Nov 2020 21:13:38 +0100 Subject: [PATCH 117/187] Move Niddler instantiation in api module --- api/build.gradle | 3 +++ .../main/java/com/readrops/api/ApiModule.kt | 18 ++++++++++++++++-- app/build.gradle | 3 --- .../com/readrops/app/ReadropsDebugApp.java | 14 -------------- 4 files changed, 19 insertions(+), 19 deletions(-) diff --git a/api/build.gradle b/api/build.gradle index 28b127dd..9e6a554e 100644 --- a/api/build.gradle +++ b/api/build.gradle @@ -76,4 +76,7 @@ dependencies { api 'io.reactivex.rxjava2:rxandroid:2.1.1' api 'org.jsoup:jsoup:1.13.1' + + debugImplementation 'com.icapps.niddler:niddler:1.2.0' + releaseImplementation 'com.icapps.niddler:niddler-noop:1.2.0' } diff --git a/api/src/main/java/com/readrops/api/ApiModule.kt b/api/src/main/java/com/readrops/api/ApiModule.kt index eecfff74..3e6491af 100644 --- a/api/src/main/java/com/readrops/api/ApiModule.kt +++ b/api/src/main/java/com/readrops/api/ApiModule.kt @@ -1,5 +1,8 @@ package com.readrops.api +import com.icapps.niddler.core.AndroidNiddler +import com.icapps.niddler.core.Niddler +import com.icapps.niddler.interceptor.okhttp.NiddlerOkHttpInterceptor import com.readrops.api.localfeed.LocalRSSDataSource import com.readrops.api.services.freshrss.FreshRSSDataSource import com.readrops.api.services.freshrss.FreshRSSService @@ -33,6 +36,7 @@ val apiModule = module { .callTimeout(1, TimeUnit.MINUTES) .readTimeout(1, TimeUnit.HOURS) .addInterceptor(get()) + .addInterceptor(NiddlerOkHttpInterceptor(get(), "niddler")) .build() } @@ -99,7 +103,17 @@ val apiModule = module { //endregion nextcloud news - single { - AuthInterceptor() + single { AuthInterceptor() } + + single { + val niddler = AndroidNiddler.Builder() + .setNiddlerInformation(AndroidNiddler.fromApplication(get())) + .setPort(0) + .setMaxStackTraceSize(10) + .build() + + niddler.attachToApplication(get()) + + niddler.apply { start() } } } \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle index 4ece006c..a05d59bd 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -98,7 +98,4 @@ dependencies { debugImplementation 'com.facebook.flipper:flipper:0.30.1' debugImplementation 'com.facebook.soloader:soloader:0.9.0' debugImplementation 'com.facebook.flipper:flipper-network-plugin:0.30.1' - - debugImplementation 'com.icapps.niddler:niddler:1.2.0' - releaseImplementation 'com.icapps.niddler:niddler-noop:1.2.0' } diff --git a/app/src/debug/java/com/readrops/app/ReadropsDebugApp.java b/app/src/debug/java/com/readrops/app/ReadropsDebugApp.java index 673e3081..2ef2a039 100644 --- a/app/src/debug/java/com/readrops/app/ReadropsDebugApp.java +++ b/app/src/debug/java/com/readrops/app/ReadropsDebugApp.java @@ -16,7 +16,6 @@ import com.facebook.flipper.plugins.navigation.NavigationFlipperPlugin; import com.facebook.flipper.plugins.network.NetworkFlipperPlugin; import com.facebook.flipper.plugins.sharedpreferences.SharedPreferencesFlipperPlugin; import com.facebook.soloader.SoLoader; -import com.icapps.niddler.core.AndroidNiddler; public class ReadropsDebugApp extends ReadropsApp implements Configuration.Provider { @@ -26,7 +25,6 @@ public class ReadropsDebugApp extends ReadropsApp implements Configuration.Provi SoLoader.init(this, false); initFlipper(); - //initNiddler(); } private void initFlipper() { @@ -46,18 +44,6 @@ public class ReadropsDebugApp extends ReadropsApp implements Configuration.Provi } } - private void initNiddler() { - AndroidNiddler niddler = new AndroidNiddler.Builder() - .setNiddlerInformation(AndroidNiddler.fromApplication(this)) - .setPort(0) - .setMaxStackTraceSize(10) - .build(); - - niddler.attachToApplication(this); - - niddler.start(); - } - @NonNull @Override public Configuration getWorkManagerConfiguration() { From e548b44ac83eadcf70cd3a49fba2f1196afcc4ef Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Sat, 7 Nov 2020 13:43:24 +0100 Subject: [PATCH 118/187] Fix LocalRSSDataSource tests --- api/build.gradle | 4 +-- .../api/localfeed/LocalRSSDataSourceTest.kt | 17 +++++++++-- .../main/java/com/readrops/api/ApiModule.kt | 30 ++++--------------- .../main/java/com/readrops/app/AppModule.kt | 14 +++++++++ 4 files changed, 37 insertions(+), 28 deletions(-) diff --git a/api/build.gradle b/api/build.gradle index 9e6a554e..48edd34c 100644 --- a/api/build.gradle +++ b/api/build.gradle @@ -77,6 +77,6 @@ dependencies { api 'io.reactivex.rxjava2:rxandroid:2.1.1' api 'org.jsoup:jsoup:1.13.1' - debugImplementation 'com.icapps.niddler:niddler:1.2.0' - releaseImplementation 'com.icapps.niddler:niddler-noop:1.2.0' + debugApi 'com.icapps.niddler:niddler:1.2.0' + releaseApi 'com.icapps.niddler:niddler-noop:1.2.0' } diff --git a/api/src/androidTest/java/com/readrops/api/localfeed/LocalRSSDataSourceTest.kt b/api/src/androidTest/java/com/readrops/api/localfeed/LocalRSSDataSourceTest.kt index bda2241a..f23d00d5 100644 --- a/api/src/androidTest/java/com/readrops/api/localfeed/LocalRSSDataSourceTest.kt +++ b/api/src/androidTest/java/com/readrops/api/localfeed/LocalRSSDataSourceTest.kt @@ -4,13 +4,16 @@ import android.accounts.NetworkErrorException import android.content.Context import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.platform.app.InstrumentationRegistry +import com.icapps.niddler.interceptor.okhttp.NiddlerOkHttpInterceptor import com.readrops.api.apiModule import com.readrops.api.utils.ApiUtils +import com.readrops.api.utils.AuthInterceptor import com.readrops.api.utils.exceptions.ParseException import com.readrops.api.utils.exceptions.UnknownFormatException import junit.framework.TestCase.* import okhttp3.Headers import okhttp3.HttpUrl +import okhttp3.OkHttpClient import okhttp3.mockwebserver.MockResponse import okhttp3.mockwebserver.MockWebServer import okio.Buffer @@ -20,16 +23,18 @@ import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import org.koin.android.ext.koin.androidContext +import org.koin.dsl.module import org.koin.test.KoinTest import org.koin.test.KoinTestRule import org.koin.test.inject import java.net.HttpURLConnection +import java.util.concurrent.TimeUnit @RunWith(AndroidJUnit4::class) class LocalRSSDataSourceTest : KoinTest { - private val context: Context by inject() + private val context by inject() private lateinit var url: HttpUrl private val mockServer: MockWebServer = MockWebServer() @@ -38,7 +43,15 @@ class LocalRSSDataSourceTest : KoinTest { @get:Rule val koinTestRule = KoinTestRule.create { androidContext(InstrumentationRegistry.getInstrumentation().context) - modules(apiModule) + modules(apiModule, module { + single(override = true) { + OkHttpClient.Builder() + .callTimeout(1, TimeUnit.MINUTES) + .readTimeout(1, TimeUnit.HOURS) + .addInterceptor(get()) + .build() + } + }) } @Before diff --git a/api/src/main/java/com/readrops/api/ApiModule.kt b/api/src/main/java/com/readrops/api/ApiModule.kt index 3e6491af..da607bad 100644 --- a/api/src/main/java/com/readrops/api/ApiModule.kt +++ b/api/src/main/java/com/readrops/api/ApiModule.kt @@ -1,7 +1,5 @@ package com.readrops.api -import com.icapps.niddler.core.AndroidNiddler -import com.icapps.niddler.core.Niddler import com.icapps.niddler.interceptor.okhttp.NiddlerOkHttpInterceptor import com.readrops.api.localfeed.LocalRSSDataSource import com.readrops.api.services.freshrss.FreshRSSDataSource @@ -40,13 +38,13 @@ val apiModule = module { .build() } + single { AuthInterceptor() } + single { LocalRSSDataSource(get()) } //region freshrss - single { - FreshRSSDataSource(get()) - } + single { FreshRSSDataSource(get()) } single { get(named("freshrssRetrofit")) @@ -54,7 +52,7 @@ val apiModule = module { } single(named("freshrssRetrofit")) { - Retrofit.Builder() // url will be set dynamically in an interceptor + Retrofit.Builder() // url will be set dynamically in AuthInterceptor .baseUrl(BASE_URL) .addCallAdapterFactory(RxJava2CallAdapterFactory.create()) .client(get()) @@ -75,9 +73,7 @@ val apiModule = module { //region nextcloud news - single { - NextNewsDataSource(get()) - } + single { NextNewsDataSource(get()) } single { get(named("nextcloudNewsRetrofit")) @@ -85,7 +81,7 @@ val apiModule = module { } single(named("nextcloudNewsRetrofit")) { - Retrofit.Builder() // url will be set dynamically in an interceptor + Retrofit.Builder() // url will be set dynamically in AuthInterceptor .baseUrl(BASE_URL) .addCallAdapterFactory(RxJava2CallAdapterFactory.create()) .client(get()) @@ -102,18 +98,4 @@ val apiModule = module { } //endregion nextcloud news - - single { AuthInterceptor() } - - single { - val niddler = AndroidNiddler.Builder() - .setNiddlerInformation(AndroidNiddler.fromApplication(get())) - .setPort(0) - .setMaxStackTraceSize(10) - .build() - - niddler.attachToApplication(get()) - - niddler.apply { start() } - } } \ No newline at end of file diff --git a/app/src/main/java/com/readrops/app/AppModule.kt b/app/src/main/java/com/readrops/app/AppModule.kt index 0202b6ba..bd947d82 100644 --- a/app/src/main/java/com/readrops/app/AppModule.kt +++ b/app/src/main/java/com/readrops/app/AppModule.kt @@ -1,6 +1,8 @@ package com.readrops.app import androidx.preference.PreferenceManager +import com.icapps.niddler.core.AndroidNiddler +import com.icapps.niddler.core.Niddler import com.readrops.app.account.AccountViewModel import com.readrops.app.addfeed.AddFeedsViewModel import com.readrops.app.feedsfolders.ManageFeedsFoldersViewModel @@ -56,4 +58,16 @@ val appModule = module { single { GlideApp.with(androidApplication()) } single { PreferenceManager.getDefaultSharedPreferences(androidContext()) } + + single { + val niddler = AndroidNiddler.Builder() + .setNiddlerInformation(AndroidNiddler.fromApplication(get())) + .setPort(0) + .setMaxStackTraceSize(10) + .build() + + niddler.attachToApplication(get()) + + niddler.apply { start() } + } } \ No newline at end of file From 36ac0c72475e1c1cbade786d78b09f6e96be3921 Mon Sep 17 00:00:00 2001 From: shunf4 Date: Sat, 7 Nov 2020 21:04:17 +0800 Subject: [PATCH 119/187] fix: fix "You can't request ViewModel before onCreate call error" --- .../app/notifications/NotificationPermissionActivity.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/readrops/app/notifications/NotificationPermissionActivity.kt b/app/src/main/java/com/readrops/app/notifications/NotificationPermissionActivity.kt index e6c720e5..46230e4e 100644 --- a/app/src/main/java/com/readrops/app/notifications/NotificationPermissionActivity.kt +++ b/app/src/main/java/com/readrops/app/notifications/NotificationPermissionActivity.kt @@ -3,6 +3,7 @@ package com.readrops.app.notifications import android.content.Intent import android.os.Bundle import android.view.MenuItem +import androidx.activity.viewModels import androidx.appcompat.app.AppCompatActivity import androidx.lifecycle.Observer import androidx.recyclerview.widget.LinearLayoutManager @@ -23,7 +24,7 @@ import org.koin.androidx.viewmodel.ext.android.getViewModel class NotificationPermissionActivity : AppCompatActivity() { private lateinit var binding: ActivityNotificationPermissionBinding - private val viewModel = getViewModel() + private lateinit var viewModel: NotificationPermissionViewModel private var adapter: NotificationPermissionListAdapter? = null private var isFirstCheck = true @@ -40,6 +41,7 @@ class NotificationPermissionActivity : AppCompatActivity() { val accountId = intent.getIntExtra(ACCOUNT_ID, 0) + viewModel = getViewModel() viewModel.getAccount(accountId).observe(this, Observer { account -> viewModel.account = account From 3c97441d3620ae102de7c1f97fd4d4d97a69e6c7 Mon Sep 17 00:00:00 2001 From: shunf4 Date: Mon, 9 Nov 2020 02:08:02 +0800 Subject: [PATCH 120/187] fix: set updating to false when refresh failed --- app/src/main/java/com/readrops/app/itemslist/MainActivity.java | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/java/com/readrops/app/itemslist/MainActivity.java b/app/src/main/java/com/readrops/app/itemslist/MainActivity.java index 0647787b..c10d54f6 100644 --- a/app/src/main/java/com/readrops/app/itemslist/MainActivity.java +++ b/app/src/main/java/com/readrops/app/itemslist/MainActivity.java @@ -619,6 +619,7 @@ public class MainActivity extends AppCompatActivity implements SwipeRefreshLayou Utils.showSnackbar(binding.mainRoot, e.getMessage()); drawerManager.enableAccountSelection(); + updating = false; } @Override From a8e3a689d511f1c33e6b980e0ecf638c6d4a6adb Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Mon, 9 Nov 2020 22:00:55 +0100 Subject: [PATCH 121/187] Move api androidTest/ tests to test/ It's useless to make an instrumented test if Context is only used to retrieve a resource file. It can be done with classLoader --- api/build.gradle | 2 +- .../com/readrops/api/utils/AuthInterceptor.kt | 11 ++--- .../test/java/com/readrops/api/TestUtils.kt | 9 ++++ .../api/localfeed/LocalRSSDataSourceTest.kt | 20 +++------ .../readrops/api/localfeed/XmlAdapterTest.kt | 12 +++-- .../api/localfeed/atom/ATOMFeedAdapterTest.kt | 10 +---- .../localfeed/atom/ATOMItemsAdapterTest.kt | 37 ++++++++------- .../api/localfeed/json/JSONFeedAdapterTest.kt | 12 ++--- .../localfeed/json/JSONItemsAdapterTest.kt | 39 ++++++++-------- .../api/localfeed/rss1/RSS1FeedAdapterTest.kt | 10 +---- .../localfeed/rss1/RSS1ItemsAdapterTest.kt | 39 ++++++++-------- .../api/localfeed/rss2/RSS2FeedAdapterTest.kt | 11 ++--- .../localfeed/rss2/RSS2ItemsAdapterTest.kt | 45 ++++++++++--------- .../readrops/api/utils/AuthInterceptorTest.kt | 0 .../api/utils/JsonReaderExtensionsTest.kt | 2 +- .../resources}/localfeed/atom/atom_feed.xml | 0 .../atom/atom_feed_no_url_siteurl.xml | 0 .../resources}/localfeed/atom/atom_items.xml | 0 .../localfeed/atom/atom_items_no_date.xml | 0 .../localfeed/atom/atom_items_no_link.xml | 0 .../localfeed/atom/atom_items_no_title.xml | 0 .../resources}/localfeed/json/json_feed.json | 0 .../localfeed/json/json_items_no_date.json | 0 .../localfeed/json/json_items_no_link.json | 0 .../localfeed/json/json_items_no_title.json | 0 .../json/json_items_other_cases.json | 0 .../resources}/localfeed/rss1/rss1_feed.xml | 0 .../rss1/rss1_feed_no_url_siteurl.xml | 0 .../localfeed/rss1/rss1_items_no_date.xml | 0 .../localfeed/rss1/rss1_items_no_link.xml | 0 .../localfeed/rss1/rss1_items_no_title.xml | 0 .../rss1/rss1_items_special_cases.xml | 0 .../localfeed/rss2/rss_feed_special_cases.xml | 0 .../localfeed/rss2/rss_full_feed.xml | 0 .../localfeed/rss2/rss_items_enclosure.xml | 0 .../rss2/rss_items_media_content.xml | 0 .../localfeed/rss2/rss_items_media_group.xml | 0 .../localfeed/rss2/rss_items_no_date.xml | 0 .../localfeed/rss2/rss_items_no_link.xml | 0 .../localfeed/rss2/rss_items_no_title.xml | 0 .../rss2/rss_items_other_namespaces.xml | 0 .../resources}/localfeed/rss_feed.xml | 0 42 files changed, 125 insertions(+), 134 deletions(-) create mode 100644 api/src/test/java/com/readrops/api/TestUtils.kt rename api/src/{androidTest => test}/java/com/readrops/api/localfeed/LocalRSSDataSourceTest.kt (88%) rename api/src/{androidTest => test}/java/com/readrops/api/localfeed/atom/ATOMFeedAdapterTest.kt (61%) rename api/src/{androidTest => test}/java/com/readrops/api/localfeed/atom/ATOMItemsAdapterTest.kt (51%) rename api/src/{androidTest => test}/java/com/readrops/api/localfeed/json/JSONFeedAdapterTest.kt (63%) rename api/src/{androidTest => test}/java/com/readrops/api/localfeed/json/JSONItemsAdapterTest.kt (59%) rename api/src/{androidTest => test}/java/com/readrops/api/localfeed/rss1/RSS1FeedAdapterTest.kt (57%) rename api/src/{androidTest => test}/java/com/readrops/api/localfeed/rss1/RSS1ItemsAdapterTest.kt (60%) rename api/src/{androidTest => test}/java/com/readrops/api/localfeed/rss2/RSS2FeedAdapterTest.kt (60%) rename api/src/{androidTest => test}/java/com/readrops/api/localfeed/rss2/RSS2ItemsAdapterTest.kt (57%) rename api/src/{androidTest => test}/java/com/readrops/api/utils/AuthInterceptorTest.kt (100%) rename api/src/{androidTest/assets => test/resources}/localfeed/atom/atom_feed.xml (100%) rename api/src/{androidTest/assets => test/resources}/localfeed/atom/atom_feed_no_url_siteurl.xml (100%) rename api/src/{androidTest/assets => test/resources}/localfeed/atom/atom_items.xml (100%) rename api/src/{androidTest/assets => test/resources}/localfeed/atom/atom_items_no_date.xml (100%) rename api/src/{androidTest/assets => test/resources}/localfeed/atom/atom_items_no_link.xml (100%) rename api/src/{androidTest/assets => test/resources}/localfeed/atom/atom_items_no_title.xml (100%) rename api/src/{androidTest/assets => test/resources}/localfeed/json/json_feed.json (100%) rename api/src/{androidTest/assets => test/resources}/localfeed/json/json_items_no_date.json (100%) rename api/src/{androidTest/assets => test/resources}/localfeed/json/json_items_no_link.json (100%) rename api/src/{androidTest/assets => test/resources}/localfeed/json/json_items_no_title.json (100%) rename api/src/{androidTest/assets => test/resources}/localfeed/json/json_items_other_cases.json (100%) rename api/src/{androidTest/assets => test/resources}/localfeed/rss1/rss1_feed.xml (100%) rename api/src/{androidTest/assets => test/resources}/localfeed/rss1/rss1_feed_no_url_siteurl.xml (100%) rename api/src/{androidTest/assets => test/resources}/localfeed/rss1/rss1_items_no_date.xml (100%) rename api/src/{androidTest/assets => test/resources}/localfeed/rss1/rss1_items_no_link.xml (100%) rename api/src/{androidTest/assets => test/resources}/localfeed/rss1/rss1_items_no_title.xml (100%) rename api/src/{androidTest/assets => test/resources}/localfeed/rss1/rss1_items_special_cases.xml (100%) rename api/src/{androidTest/assets => test/resources}/localfeed/rss2/rss_feed_special_cases.xml (100%) rename api/src/{androidTest/assets => test/resources}/localfeed/rss2/rss_full_feed.xml (100%) rename api/src/{androidTest/assets => test/resources}/localfeed/rss2/rss_items_enclosure.xml (100%) rename api/src/{androidTest/assets => test/resources}/localfeed/rss2/rss_items_media_content.xml (100%) rename api/src/{androidTest/assets => test/resources}/localfeed/rss2/rss_items_media_group.xml (100%) rename api/src/{androidTest/assets => test/resources}/localfeed/rss2/rss_items_no_date.xml (100%) rename api/src/{androidTest/assets => test/resources}/localfeed/rss2/rss_items_no_link.xml (100%) rename api/src/{androidTest/assets => test/resources}/localfeed/rss2/rss_items_no_title.xml (100%) rename api/src/{androidTest/assets => test/resources}/localfeed/rss2/rss_items_other_namespaces.xml (100%) rename api/src/{androidTest/assets => test/resources}/localfeed/rss_feed.xml (100%) diff --git a/api/build.gradle b/api/build.gradle index 48edd34c..5a7c8119 100644 --- a/api/build.gradle +++ b/api/build.gradle @@ -48,7 +48,7 @@ dependencies { androidTestImplementation 'androidx.test:runner:1.3.0' androidTestImplementation 'androidx.test:rules:1.3.0' androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0' - androidTestImplementation 'com.squareup.okhttp3:mockwebserver:4.9.0' + testImplementation 'com.squareup.okhttp3:mockwebserver:4.9.0' androidTestImplementation "org.koin:koin-test:2.1.6" testImplementation "org.koin:koin-test:2.1.6" diff --git a/api/src/main/java/com/readrops/api/utils/AuthInterceptor.kt b/api/src/main/java/com/readrops/api/utils/AuthInterceptor.kt index 76828f4b..9c0e6797 100644 --- a/api/src/main/java/com/readrops/api/utils/AuthInterceptor.kt +++ b/api/src/main/java/com/readrops/api/utils/AuthInterceptor.kt @@ -1,14 +1,9 @@ package com.readrops.api.utils -import android.net.Uri import com.readrops.api.services.Credentials -import com.readrops.api.services.freshrss.FreshRSSService -import com.readrops.api.services.nextcloudnews.NextNewsService -import com.readrops.db.entities.account.Account -import com.readrops.db.entities.account.AccountType import okhttp3.Interceptor import okhttp3.Response -import java.lang.IllegalArgumentException +import java.net.URI class AuthInterceptor(var credentials: Credentials? = null) : Interceptor { @@ -18,11 +13,11 @@ class AuthInterceptor(var credentials: Credentials? = null) : Interceptor { if (credentials != null) { if (credentials!!.url != null) { - val uri = Uri.parse(credentials!!.url) + val uri = URI.create(credentials!!.url) urlBuilder .scheme(uri.scheme!!) .host(uri.host!!) - .encodedPath(uri.encodedPath + chain.request().url.encodedPath) + .encodedPath(uri.path + chain.request().url.encodedPath) } if (credentials!!.authorization != null) { diff --git a/api/src/test/java/com/readrops/api/TestUtils.kt b/api/src/test/java/com/readrops/api/TestUtils.kt new file mode 100644 index 00000000..dac26c4c --- /dev/null +++ b/api/src/test/java/com/readrops/api/TestUtils.kt @@ -0,0 +1,9 @@ +package com.readrops.api + +import java.io.InputStream + +object TestUtils { + + fun loadResource(path: String): InputStream = + javaClass.classLoader?.getResourceAsStream(path)!! +} \ No newline at end of file diff --git a/api/src/androidTest/java/com/readrops/api/localfeed/LocalRSSDataSourceTest.kt b/api/src/test/java/com/readrops/api/localfeed/LocalRSSDataSourceTest.kt similarity index 88% rename from api/src/androidTest/java/com/readrops/api/localfeed/LocalRSSDataSourceTest.kt rename to api/src/test/java/com/readrops/api/localfeed/LocalRSSDataSourceTest.kt index f23d00d5..495ba5e3 100644 --- a/api/src/androidTest/java/com/readrops/api/localfeed/LocalRSSDataSourceTest.kt +++ b/api/src/test/java/com/readrops/api/localfeed/LocalRSSDataSourceTest.kt @@ -1,10 +1,7 @@ package com.readrops.api.localfeed import android.accounts.NetworkErrorException -import android.content.Context -import androidx.test.ext.junit.runners.AndroidJUnit4 -import androidx.test.platform.app.InstrumentationRegistry -import com.icapps.niddler.interceptor.okhttp.NiddlerOkHttpInterceptor +import com.readrops.api.TestUtils import com.readrops.api.apiModule import com.readrops.api.utils.ApiUtils import com.readrops.api.utils.AuthInterceptor @@ -21,8 +18,6 @@ import org.junit.After import org.junit.Before import org.junit.Rule import org.junit.Test -import org.junit.runner.RunWith -import org.koin.android.ext.koin.androidContext import org.koin.dsl.module import org.koin.test.KoinTest import org.koin.test.KoinTestRule @@ -31,10 +26,8 @@ import java.net.HttpURLConnection import java.util.concurrent.TimeUnit -@RunWith(AndroidJUnit4::class) class LocalRSSDataSourceTest : KoinTest { - private val context by inject() private lateinit var url: HttpUrl private val mockServer: MockWebServer = MockWebServer() @@ -42,7 +35,6 @@ class LocalRSSDataSourceTest : KoinTest { @get:Rule val koinTestRule = KoinTestRule.create { - androidContext(InstrumentationRegistry.getInstrumentation().context) modules(apiModule, module { single(override = true) { OkHttpClient.Builder() @@ -67,7 +59,7 @@ class LocalRSSDataSourceTest : KoinTest { @Test fun successfulQueryTest() { - val stream = context.resources.assets.open("localfeed/rss_feed.xml") + val stream = TestUtils.loadResource("localfeed/rss_feed.xml") mockServer.enqueue(MockResponse().setResponseCode(HttpURLConnection.HTTP_OK) .addHeader(ApiUtils.CONTENT_TYPE_HEADER, "application/xml; charset=UTF-8") @@ -91,7 +83,7 @@ class LocalRSSDataSourceTest : KoinTest { @Test fun headersTest() { - val stream = context.resources.assets.open("localfeed/rss_feed.xml") + val stream = TestUtils.loadResource("localfeed/rss_feed.xml") mockServer.enqueue(MockResponse().setResponseCode(HttpURLConnection.HTTP_OK) .addHeader("Content-Type", "application/rss+xml; charset=UTF-8") @@ -108,7 +100,7 @@ class LocalRSSDataSourceTest : KoinTest { @Test fun jsonFeedTest() { - val stream = context.resources.assets.open("localfeed/json/json_feed.json") + val stream = TestUtils.loadResource("localfeed/json/json_feed.json") mockServer.enqueue(MockResponse().setResponseCode(HttpURLConnection.HTTP_OK) .addHeader(ApiUtils.CONTENT_TYPE_HEADER, "application/feed+json") @@ -122,7 +114,7 @@ class LocalRSSDataSourceTest : KoinTest { @Test fun specialCasesAtomTest() { - val stream = context.resources.assets.open("localfeed/atom/atom_feed_no_url_siteurl.xml") + val stream = TestUtils.loadResource("localfeed/atom/atom_feed_no_url_siteurl.xml") mockServer.enqueue(MockResponse().setResponseCode(HttpURLConnection.HTTP_OK) .addHeader(ApiUtils.CONTENT_TYPE_HEADER, "application/atom+xml") @@ -136,7 +128,7 @@ class LocalRSSDataSourceTest : KoinTest { @Test fun specialCasesRSS1Test() { - val stream = context.resources.assets.open("localfeed/rss1/rss1_feed_no_url_siteurl.xml") + val stream = TestUtils.loadResource("localfeed/rss1/rss1_feed_no_url_siteurl.xml") mockServer.enqueue(MockResponse().setResponseCode(HttpURLConnection.HTTP_OK) .addHeader(ApiUtils.CONTENT_TYPE_HEADER, "application/rdf+xml") diff --git a/api/src/test/java/com/readrops/api/localfeed/XmlAdapterTest.kt b/api/src/test/java/com/readrops/api/localfeed/XmlAdapterTest.kt index 64e73791..bc3f729d 100644 --- a/api/src/test/java/com/readrops/api/localfeed/XmlAdapterTest.kt +++ b/api/src/test/java/com/readrops/api/localfeed/XmlAdapterTest.kt @@ -7,18 +7,23 @@ import com.readrops.api.localfeed.rss1.RSS1ItemsAdapter import com.readrops.api.localfeed.rss2.RSS2FeedAdapter import com.readrops.api.localfeed.rss2.RSS2ItemsAdapter import junit.framework.TestCase.assertTrue -import org.junit.Assert +import org.junit.Rule import org.junit.Test +import org.junit.rules.ExpectedException class XmlAdapterTest { + @get:Rule + val expectedException: ExpectedException = ExpectedException.none() + @Test fun xmlFeedAdapterFactoryTest() { assertTrue(XmlAdapter.xmlFeedAdapterFactory(LocalRSSHelper.RSSType.RSS_1) is RSS1FeedAdapter) assertTrue(XmlAdapter.xmlFeedAdapterFactory(LocalRSSHelper.RSSType.RSS_2) is RSS2FeedAdapter) assertTrue(XmlAdapter.xmlFeedAdapterFactory(LocalRSSHelper.RSSType.ATOM) is ATOMFeedAdapter) - Assert.assertThrows(IllegalArgumentException::class.java) { XmlAdapter.xmlFeedAdapterFactory(LocalRSSHelper.RSSType.UNKNOWN) } + expectedException.expect(IllegalArgumentException::class.java) + XmlAdapter.xmlFeedAdapterFactory(LocalRSSHelper.RSSType.UNKNOWN) } @Test @@ -27,6 +32,7 @@ class XmlAdapterTest { assertTrue(XmlAdapter.xmlItemsAdapterFactory(LocalRSSHelper.RSSType.RSS_2) is RSS2ItemsAdapter) assertTrue(XmlAdapter.xmlItemsAdapterFactory(LocalRSSHelper.RSSType.ATOM) is ATOMItemsAdapter) - Assert.assertThrows(IllegalArgumentException::class.java) { XmlAdapter.xmlItemsAdapterFactory(LocalRSSHelper.RSSType.UNKNOWN) } + expectedException.expect(IllegalArgumentException::class.java) + XmlAdapter.xmlItemsAdapterFactory(LocalRSSHelper.RSSType.UNKNOWN) } } \ No newline at end of file diff --git a/api/src/androidTest/java/com/readrops/api/localfeed/atom/ATOMFeedAdapterTest.kt b/api/src/test/java/com/readrops/api/localfeed/atom/ATOMFeedAdapterTest.kt similarity index 61% rename from api/src/androidTest/java/com/readrops/api/localfeed/atom/ATOMFeedAdapterTest.kt rename to api/src/test/java/com/readrops/api/localfeed/atom/ATOMFeedAdapterTest.kt index 57711b22..18cb0686 100644 --- a/api/src/androidTest/java/com/readrops/api/localfeed/atom/ATOMFeedAdapterTest.kt +++ b/api/src/test/java/com/readrops/api/localfeed/atom/ATOMFeedAdapterTest.kt @@ -1,22 +1,16 @@ package com.readrops.api.localfeed.atom -import android.content.Context -import androidx.test.ext.junit.runners.AndroidJUnit4 -import androidx.test.platform.app.InstrumentationRegistry +import com.readrops.api.TestUtils import junit.framework.TestCase.assertEquals import org.junit.Test -import org.junit.runner.RunWith -@RunWith(AndroidJUnit4::class) class ATOMFeedAdapterTest { - private val context: Context = InstrumentationRegistry.getInstrumentation().context - private val adapter = ATOMFeedAdapter() @Test fun normalCasesTest() { - val stream = context.assets.open("localfeed/atom/atom_feed.xml") + val stream = TestUtils.loadResource("localfeed/atom/atom_feed.xml") val feed = adapter.fromXml(stream) diff --git a/api/src/androidTest/java/com/readrops/api/localfeed/atom/ATOMItemsAdapterTest.kt b/api/src/test/java/com/readrops/api/localfeed/atom/ATOMItemsAdapterTest.kt similarity index 51% rename from api/src/androidTest/java/com/readrops/api/localfeed/atom/ATOMItemsAdapterTest.kt rename to api/src/test/java/com/readrops/api/localfeed/atom/ATOMItemsAdapterTest.kt index b737272a..6b014126 100644 --- a/api/src/androidTest/java/com/readrops/api/localfeed/atom/ATOMItemsAdapterTest.kt +++ b/api/src/test/java/com/readrops/api/localfeed/atom/ATOMItemsAdapterTest.kt @@ -1,25 +1,24 @@ package com.readrops.api.localfeed.atom -import android.content.Context -import androidx.test.ext.junit.runners.AndroidJUnit4 -import androidx.test.platform.app.InstrumentationRegistry +import com.readrops.api.TestUtils import com.readrops.api.utils.DateUtils import com.readrops.api.utils.exceptions.ParseException -import junit.framework.TestCase.* -import org.junit.Assert +import junit.framework.TestCase.assertEquals +import junit.framework.TestCase.assertNotNull +import org.junit.Rule import org.junit.Test -import org.junit.runner.RunWith +import org.junit.rules.ExpectedException -@RunWith(AndroidJUnit4::class) class ATOMItemsAdapterTest { - private val context: Context = InstrumentationRegistry.getInstrumentation().context - private val adapter = ATOMItemsAdapter() + @get:Rule + val expectedException: ExpectedException = ExpectedException.none() + @Test fun normalCasesTest() { - val stream = context.resources.assets.open("localfeed/atom/atom_items.xml") + val stream = TestUtils.loadResource("localfeed/atom/atom_items.xml") val items = adapter.fromXml(stream) val item = items[0] @@ -36,7 +35,7 @@ class ATOMItemsAdapterTest { @Test fun noDateTest() { - val stream = context.resources.assets.open("localfeed/atom/atom_items_no_date.xml") + val stream = TestUtils.loadResource("localfeed/atom/atom_items_no_date.xml") val item = adapter.fromXml(stream).first() assertNotNull(item.pubDate) @@ -44,18 +43,22 @@ class ATOMItemsAdapterTest { @Test fun noTitleTest() { - val stream = context.resources.assets.open("localfeed/atom/atom_items_no_title.xml") + val stream = TestUtils.loadResource("localfeed/atom/atom_items_no_title.xml") - val exception = Assert.assertThrows(ParseException::class.java) { adapter.fromXml(stream) } - assertTrue(exception.message!!.contains("Item title is required")) + expectedException.expect(ParseException::class.java) + expectedException.expectMessage("Item title is required") + + adapter.fromXml(stream) } @Test fun noLinkTest() { - val stream = context.resources.assets.open("localfeed/atom/atom_items_no_link.xml") + val stream = TestUtils.loadResource("localfeed/atom/atom_items_no_link.xml") - val exception = Assert.assertThrows(ParseException::class.java) { adapter.fromXml(stream) } - assertTrue(exception.message!!.contains("Item link is required")) + expectedException.expect(ParseException::class.java) + expectedException.expectMessage("Item link is required") + + adapter.fromXml(stream) } } \ No newline at end of file diff --git a/api/src/androidTest/java/com/readrops/api/localfeed/json/JSONFeedAdapterTest.kt b/api/src/test/java/com/readrops/api/localfeed/json/JSONFeedAdapterTest.kt similarity index 63% rename from api/src/androidTest/java/com/readrops/api/localfeed/json/JSONFeedAdapterTest.kt rename to api/src/test/java/com/readrops/api/localfeed/json/JSONFeedAdapterTest.kt index def7cd0c..bece7e27 100644 --- a/api/src/androidTest/java/com/readrops/api/localfeed/json/JSONFeedAdapterTest.kt +++ b/api/src/test/java/com/readrops/api/localfeed/json/JSONFeedAdapterTest.kt @@ -1,28 +1,22 @@ package com.readrops.api.localfeed.json -import android.content.Context -import androidx.test.ext.junit.runners.AndroidJUnit4 -import androidx.test.platform.app.InstrumentationRegistry +import com.readrops.api.TestUtils import com.readrops.db.entities.Feed import com.squareup.moshi.Moshi import junit.framework.TestCase.assertEquals import okio.Buffer import org.junit.Test -import org.junit.runner.RunWith -@RunWith(AndroidJUnit4::class) class JSONFeedAdapterTest { - private val context: Context = InstrumentationRegistry.getInstrumentation().context - private val adapter = Moshi.Builder() .add(JSONFeedAdapter()) .build() - .adapter(Feed::class.java) + .adapter(Feed::class.java) @Test fun normalCasesTest() { - val stream = context.assets.open("localfeed/json/json_feed.json") + val stream = TestUtils.loadResource("localfeed/json/json_feed.json") val feed = adapter.fromJson(Buffer().readFrom(stream))!! diff --git a/api/src/androidTest/java/com/readrops/api/localfeed/json/JSONItemsAdapterTest.kt b/api/src/test/java/com/readrops/api/localfeed/json/JSONItemsAdapterTest.kt similarity index 59% rename from api/src/androidTest/java/com/readrops/api/localfeed/json/JSONItemsAdapterTest.kt rename to api/src/test/java/com/readrops/api/localfeed/json/JSONItemsAdapterTest.kt index 4b95a665..518520c2 100644 --- a/api/src/androidTest/java/com/readrops/api/localfeed/json/JSONItemsAdapterTest.kt +++ b/api/src/test/java/com/readrops/api/localfeed/json/JSONItemsAdapterTest.kt @@ -1,33 +1,32 @@ package com.readrops.api.localfeed.json -import android.content.Context -import androidx.test.ext.junit.runners.AndroidJUnit4 -import androidx.test.platform.app.InstrumentationRegistry +import com.readrops.api.TestUtils import com.readrops.api.utils.DateUtils import com.readrops.api.utils.exceptions.ParseException import com.readrops.db.entities.Item import com.squareup.moshi.Moshi import com.squareup.moshi.Types -import junit.framework.TestCase.* +import junit.framework.TestCase.assertEquals +import junit.framework.TestCase.assertNotNull import okio.Buffer -import org.junit.Assert +import org.junit.Rule import org.junit.Test -import org.junit.runner.RunWith +import org.junit.rules.ExpectedException -@RunWith(AndroidJUnit4::class) class JSONItemsAdapterTest { - private val context: Context = InstrumentationRegistry.getInstrumentation().context - private val adapter = Moshi.Builder() .add(Types.newParameterizedType(List::class.java, Item::class.java), JSONItemsAdapter()) .build() .adapter>(Types.newParameterizedType(List::class.java, Item::class.java)) + @get:Rule + val expectedException: ExpectedException = ExpectedException.none() + @Test fun normalCasesTest() { - val stream = context.resources.assets.open("localfeed/json/json_feed.json") + val stream = TestUtils.loadResource("localfeed/json/json_feed.json") val items = adapter.fromJson(Buffer().readFrom(stream))!! val item = items.first() @@ -43,7 +42,7 @@ class JSONItemsAdapterTest { @Test fun otherCasesTest() { - val stream = context.resources.assets.open("localfeed/json/json_items_other_cases.json") + val stream = TestUtils.loadResource("localfeed/json/json_items_other_cases.json") val item = adapter.fromJson(Buffer().readFrom(stream))!!.first() @@ -55,7 +54,7 @@ class JSONItemsAdapterTest { @Test fun nullDateTest() { - val stream = context.resources.assets.open("localfeed/json/json_items_no_date.json") + val stream = TestUtils.loadResource("localfeed/json/json_items_no_date.json") val item = adapter.fromJson(Buffer().readFrom(stream))!!.first() assertNotNull(item.pubDate) @@ -63,18 +62,22 @@ class JSONItemsAdapterTest { @Test fun nullTitleTest() { - val stream = context.resources.assets.open("localfeed/json/json_items_no_title.json") + val stream = TestUtils.loadResource("localfeed/json/json_items_no_title.json") - val exception = Assert.assertThrows(ParseException::class.java) { adapter.fromJson(Buffer().readFrom(stream)) } - assertTrue(exception.message!!.contains("Item title is required")) + expectedException.expect(ParseException::class.java) + expectedException.expectMessage("Item title is required") + + adapter.fromJson(Buffer().readFrom(stream)) } @Test fun nullLinkTest() { - val stream = context.resources.assets.open("localfeed/json/json_items_no_link.json") + val stream = TestUtils.loadResource("localfeed/json/json_items_no_link.json") - val exception = Assert.assertThrows(ParseException::class.java) { adapter.fromJson(Buffer().readFrom(stream)) } - assertTrue(exception.message!!.contains("Item link is required")) + expectedException.expect(ParseException::class.java) + expectedException.expectMessage("Item link is required") + + adapter.fromJson(Buffer().readFrom(stream)) } } \ No newline at end of file diff --git a/api/src/androidTest/java/com/readrops/api/localfeed/rss1/RSS1FeedAdapterTest.kt b/api/src/test/java/com/readrops/api/localfeed/rss1/RSS1FeedAdapterTest.kt similarity index 57% rename from api/src/androidTest/java/com/readrops/api/localfeed/rss1/RSS1FeedAdapterTest.kt rename to api/src/test/java/com/readrops/api/localfeed/rss1/RSS1FeedAdapterTest.kt index 47d023b0..ddee8ead 100644 --- a/api/src/androidTest/java/com/readrops/api/localfeed/rss1/RSS1FeedAdapterTest.kt +++ b/api/src/test/java/com/readrops/api/localfeed/rss1/RSS1FeedAdapterTest.kt @@ -1,22 +1,16 @@ package com.readrops.api.localfeed.rss1 -import android.content.Context -import androidx.test.ext.junit.runners.AndroidJUnit4 -import androidx.test.platform.app.InstrumentationRegistry +import com.readrops.api.TestUtils import junit.framework.Assert.assertEquals import org.junit.Test -import org.junit.runner.RunWith -@RunWith(AndroidJUnit4::class) class RSS1FeedAdapterTest { - private val context: Context = InstrumentationRegistry.getInstrumentation().context - private val adapter = RSS1FeedAdapter() @Test fun normalCaseTest() { - val stream = context.resources.assets.open("localfeed/rss1/rss1_feed.xml") + val stream = TestUtils.loadResource("localfeed/rss1/rss1_feed.xml") val feed = adapter.fromXml(stream) diff --git a/api/src/androidTest/java/com/readrops/api/localfeed/rss1/RSS1ItemsAdapterTest.kt b/api/src/test/java/com/readrops/api/localfeed/rss1/RSS1ItemsAdapterTest.kt similarity index 60% rename from api/src/androidTest/java/com/readrops/api/localfeed/rss1/RSS1ItemsAdapterTest.kt rename to api/src/test/java/com/readrops/api/localfeed/rss1/RSS1ItemsAdapterTest.kt index 99ce97f4..7efda78f 100644 --- a/api/src/androidTest/java/com/readrops/api/localfeed/rss1/RSS1ItemsAdapterTest.kt +++ b/api/src/test/java/com/readrops/api/localfeed/rss1/RSS1ItemsAdapterTest.kt @@ -1,25 +1,24 @@ package com.readrops.api.localfeed.rss1 -import android.content.Context -import androidx.test.ext.junit.runners.AndroidJUnit4 -import androidx.test.platform.app.InstrumentationRegistry +import com.readrops.api.TestUtils import com.readrops.api.utils.DateUtils import com.readrops.api.utils.exceptions.ParseException -import junit.framework.TestCase.* -import org.junit.Assert +import junit.framework.TestCase.assertEquals +import junit.framework.TestCase.assertNotNull +import org.junit.Rule import org.junit.Test -import org.junit.runner.RunWith +import org.junit.rules.ExpectedException -@RunWith(AndroidJUnit4::class) class RSS1ItemsAdapterTest { - private val context: Context = InstrumentationRegistry.getInstrumentation().context - private val adapter = RSS1ItemsAdapter() + @get:Rule + val expectedException: ExpectedException = ExpectedException.none() + @Test fun normalCasesTest() { - val stream = context.resources.assets.open("localfeed/rss1/rss1_feed.xml") + val stream = TestUtils.loadResource("localfeed/rss1/rss1_feed.xml") val items = adapter.fromXml(stream) val item = items.first() @@ -38,7 +37,7 @@ class RSS1ItemsAdapterTest { @Test fun specialCasesTest() { - val stream = context.resources.assets.open("localfeed/rss1/rss1_items_special_cases.xml") + val stream = TestUtils.loadResource("localfeed/rss1/rss1_items_special_cases.xml") val item = adapter.fromXml(stream).first() @@ -49,7 +48,7 @@ class RSS1ItemsAdapterTest { @Test fun nullDateTest() { - val stream = context.resources.assets.open("localfeed/rss1/rss1_items_no_date.xml") + val stream = TestUtils.loadResource("localfeed/rss1/rss1_items_no_date.xml") val item = adapter.fromXml(stream).first() assertNotNull(item.pubDate) @@ -57,17 +56,21 @@ class RSS1ItemsAdapterTest { @Test fun nullTitleTest() { - val stream = context.resources.assets.open("localfeed/rss1/rss1_items_no_title.xml") + val stream = TestUtils.loadResource("localfeed/rss1/rss1_items_no_title.xml") - val exception = Assert.assertThrows(ParseException::class.java) { adapter.fromXml(stream) } - assertTrue(exception.message!!.contains("Item title is required")) + expectedException.expect(ParseException::class.java) + expectedException.expectMessage("Item title is required") + + adapter.fromXml(stream) } @Test fun nullLinkTest() { - val stream = context.resources.assets.open("localfeed/rss1/rss1_items_no_link.xml") + val stream = TestUtils.loadResource("localfeed/rss1/rss1_items_no_link.xml") - val exception = Assert.assertThrows(ParseException::class.java) { adapter.fromXml(stream) } - assertTrue(exception.message!!.contains("RSS1 link or about element is required")) + expectedException.expect(ParseException::class.java) + expectedException.expectMessage("RSS1 link or about element is required") + + adapter.fromXml(stream) } } \ No newline at end of file diff --git a/api/src/androidTest/java/com/readrops/api/localfeed/rss2/RSS2FeedAdapterTest.kt b/api/src/test/java/com/readrops/api/localfeed/rss2/RSS2FeedAdapterTest.kt similarity index 60% rename from api/src/androidTest/java/com/readrops/api/localfeed/rss2/RSS2FeedAdapterTest.kt rename to api/src/test/java/com/readrops/api/localfeed/rss2/RSS2FeedAdapterTest.kt index 3368c0e6..c16a1581 100644 --- a/api/src/androidTest/java/com/readrops/api/localfeed/rss2/RSS2FeedAdapterTest.kt +++ b/api/src/test/java/com/readrops/api/localfeed/rss2/RSS2FeedAdapterTest.kt @@ -1,23 +1,18 @@ package com.readrops.api.localfeed.rss2 -import android.content.Context -import androidx.test.ext.junit.runners.AndroidJUnit4 -import androidx.test.platform.app.InstrumentationRegistry +import com.readrops.api.TestUtils import com.readrops.api.utils.exceptions.ParseException import junit.framework.TestCase.assertEquals import org.junit.Test -import org.junit.runner.RunWith -@RunWith(AndroidJUnit4::class) class RSS2FeedAdapterTest { - private val context: Context = InstrumentationRegistry.getInstrumentation().context private val adapter = RSS2FeedAdapter() @Test fun normalCasesTest() { - val stream = context.resources.assets.open("localfeed/rss2/rss_full_feed.xml") + val stream = TestUtils.loadResource("localfeed/rss2/rss_full_feed.xml") val feed = adapter.fromXml(stream) @@ -30,7 +25,7 @@ class RSS2FeedAdapterTest { @Test(expected = ParseException::class) fun nullTitleTest() { - val stream = context.resources.assets.open("localfeed/rss2/rss_feed_special_cases.xml") + val stream = TestUtils.loadResource("localfeed/rss2/rss_feed_special_cases.xml") adapter.fromXml(stream) } } \ No newline at end of file diff --git a/api/src/androidTest/java/com/readrops/api/localfeed/rss2/RSS2ItemsAdapterTest.kt b/api/src/test/java/com/readrops/api/localfeed/rss2/RSS2ItemsAdapterTest.kt similarity index 57% rename from api/src/androidTest/java/com/readrops/api/localfeed/rss2/RSS2ItemsAdapterTest.kt rename to api/src/test/java/com/readrops/api/localfeed/rss2/RSS2ItemsAdapterTest.kt index 4154f924..49862135 100644 --- a/api/src/androidTest/java/com/readrops/api/localfeed/rss2/RSS2ItemsAdapterTest.kt +++ b/api/src/test/java/com/readrops/api/localfeed/rss2/RSS2ItemsAdapterTest.kt @@ -1,25 +1,24 @@ package com.readrops.api.localfeed.rss2 -import android.content.Context -import androidx.test.ext.junit.runners.AndroidJUnit4 -import androidx.test.platform.app.InstrumentationRegistry +import com.readrops.api.TestUtils import com.readrops.api.utils.DateUtils import com.readrops.api.utils.exceptions.ParseException -import junit.framework.TestCase.* -import org.junit.Assert +import junit.framework.TestCase.assertEquals +import junit.framework.TestCase.assertNotNull +import org.junit.Rule import org.junit.Test -import org.junit.runner.RunWith +import org.junit.rules.ExpectedException -@RunWith(AndroidJUnit4::class) class RSS2ItemsAdapterTest { - private val context: Context = InstrumentationRegistry.getInstrumentation().context - private val adapter = RSS2ItemsAdapter() + @get:Rule + val expectedException: ExpectedException = ExpectedException.none() + @Test fun normalCasesTest() { - val stream = context.resources.assets.open("localfeed/rss_feed.xml") + val stream = TestUtils.loadResource("localfeed/rss_feed.xml") val items = adapter.fromXml(stream) val item = items.first() @@ -35,7 +34,7 @@ class RSS2ItemsAdapterTest { @Test fun otherNamespacesTest() { - val stream = context.resources.assets.open("localfeed/rss2/rss_items_other_namespaces.xml") + val stream = TestUtils.loadResource("localfeed/rss2/rss_items_other_namespaces.xml") val item = adapter.fromXml(stream).first() assertEquals(item.guid, "guid") @@ -46,7 +45,7 @@ class RSS2ItemsAdapterTest { @Test fun noDateTest() { - val stream = context.resources.assets.open("localfeed/rss2/rss_items_no_date.xml") + val stream = TestUtils.loadResource("localfeed/rss2/rss_items_no_date.xml") val item = adapter.fromXml(stream).first() assertNotNull(item.pubDate) @@ -54,23 +53,27 @@ class RSS2ItemsAdapterTest { @Test fun noTitleTest() { - val stream = context.resources.assets.open("localfeed/rss2/rss_items_no_title.xml") + val stream = TestUtils.loadResource("localfeed/rss2/rss_items_no_title.xml") - val exception = Assert.assertThrows(ParseException::class.java) { adapter.fromXml(stream) } - assertTrue(exception.message!!.contains("Item title is required")) + expectedException.expect(ParseException::class.java) + expectedException.expectMessage("Item title is required") + + adapter.fromXml(stream) } @Test fun noLinkTest() { - val stream = context.resources.assets.open("localfeed/rss2/rss_items_no_link.xml") + val stream = TestUtils.loadResource("localfeed/rss2/rss_items_no_link.xml") - val exception = Assert.assertThrows(ParseException::class.java) { adapter.fromXml(stream) } - assertTrue(exception.message!!.contains("Item link is required")) + expectedException.expect(ParseException::class.java) + expectedException.expectMessage("Item link is required") + + adapter.fromXml(stream) } @Test fun enclosureTest() { - val stream = context.resources.assets.open("localfeed/rss2/rss_items_enclosure.xml") + val stream = TestUtils.loadResource("localfeed/rss2/rss_items_enclosure.xml") val item = adapter.fromXml(stream).first() assertEquals(item.imageLink, "https://image1.jpg") @@ -78,7 +81,7 @@ class RSS2ItemsAdapterTest { @Test fun mediaContentTest() { - val stream = context.resources.assets.open("localfeed/rss2/rss_items_media_content.xml") + val stream = TestUtils.loadResource("localfeed/rss2/rss_items_media_content.xml") val items = adapter.fromXml(stream) assertEquals(items.first().imageLink, "https://image1.jpg") @@ -87,7 +90,7 @@ class RSS2ItemsAdapterTest { @Test fun mediaGroupTest() { - val stream = context.resources.assets.open("localfeed/rss2/rss_items_media_group.xml") + val stream = TestUtils.loadResource("localfeed/rss2/rss_items_media_group.xml") val item = adapter.fromXml(stream).first() assertEquals(item.imageLink, "https://image1.jpg") diff --git a/api/src/androidTest/java/com/readrops/api/utils/AuthInterceptorTest.kt b/api/src/test/java/com/readrops/api/utils/AuthInterceptorTest.kt similarity index 100% rename from api/src/androidTest/java/com/readrops/api/utils/AuthInterceptorTest.kt rename to api/src/test/java/com/readrops/api/utils/AuthInterceptorTest.kt diff --git a/api/src/test/java/com/readrops/api/utils/JsonReaderExtensionsTest.kt b/api/src/test/java/com/readrops/api/utils/JsonReaderExtensionsTest.kt index fb27e07a..92b95a8f 100644 --- a/api/src/test/java/com/readrops/api/utils/JsonReaderExtensionsTest.kt +++ b/api/src/test/java/com/readrops/api/utils/JsonReaderExtensionsTest.kt @@ -67,7 +67,7 @@ class JsonReaderExtensionsTest { reader.beginObject() reader.nextName() - assertEquals(reader.nextNullableString(), "value") + assertEquals(reader.nextNonEmptyString(), "value") reader.endObject() } diff --git a/api/src/androidTest/assets/localfeed/atom/atom_feed.xml b/api/src/test/resources/localfeed/atom/atom_feed.xml similarity index 100% rename from api/src/androidTest/assets/localfeed/atom/atom_feed.xml rename to api/src/test/resources/localfeed/atom/atom_feed.xml diff --git a/api/src/androidTest/assets/localfeed/atom/atom_feed_no_url_siteurl.xml b/api/src/test/resources/localfeed/atom/atom_feed_no_url_siteurl.xml similarity index 100% rename from api/src/androidTest/assets/localfeed/atom/atom_feed_no_url_siteurl.xml rename to api/src/test/resources/localfeed/atom/atom_feed_no_url_siteurl.xml diff --git a/api/src/androidTest/assets/localfeed/atom/atom_items.xml b/api/src/test/resources/localfeed/atom/atom_items.xml similarity index 100% rename from api/src/androidTest/assets/localfeed/atom/atom_items.xml rename to api/src/test/resources/localfeed/atom/atom_items.xml diff --git a/api/src/androidTest/assets/localfeed/atom/atom_items_no_date.xml b/api/src/test/resources/localfeed/atom/atom_items_no_date.xml similarity index 100% rename from api/src/androidTest/assets/localfeed/atom/atom_items_no_date.xml rename to api/src/test/resources/localfeed/atom/atom_items_no_date.xml diff --git a/api/src/androidTest/assets/localfeed/atom/atom_items_no_link.xml b/api/src/test/resources/localfeed/atom/atom_items_no_link.xml similarity index 100% rename from api/src/androidTest/assets/localfeed/atom/atom_items_no_link.xml rename to api/src/test/resources/localfeed/atom/atom_items_no_link.xml diff --git a/api/src/androidTest/assets/localfeed/atom/atom_items_no_title.xml b/api/src/test/resources/localfeed/atom/atom_items_no_title.xml similarity index 100% rename from api/src/androidTest/assets/localfeed/atom/atom_items_no_title.xml rename to api/src/test/resources/localfeed/atom/atom_items_no_title.xml diff --git a/api/src/androidTest/assets/localfeed/json/json_feed.json b/api/src/test/resources/localfeed/json/json_feed.json similarity index 100% rename from api/src/androidTest/assets/localfeed/json/json_feed.json rename to api/src/test/resources/localfeed/json/json_feed.json diff --git a/api/src/androidTest/assets/localfeed/json/json_items_no_date.json b/api/src/test/resources/localfeed/json/json_items_no_date.json similarity index 100% rename from api/src/androidTest/assets/localfeed/json/json_items_no_date.json rename to api/src/test/resources/localfeed/json/json_items_no_date.json diff --git a/api/src/androidTest/assets/localfeed/json/json_items_no_link.json b/api/src/test/resources/localfeed/json/json_items_no_link.json similarity index 100% rename from api/src/androidTest/assets/localfeed/json/json_items_no_link.json rename to api/src/test/resources/localfeed/json/json_items_no_link.json diff --git a/api/src/androidTest/assets/localfeed/json/json_items_no_title.json b/api/src/test/resources/localfeed/json/json_items_no_title.json similarity index 100% rename from api/src/androidTest/assets/localfeed/json/json_items_no_title.json rename to api/src/test/resources/localfeed/json/json_items_no_title.json diff --git a/api/src/androidTest/assets/localfeed/json/json_items_other_cases.json b/api/src/test/resources/localfeed/json/json_items_other_cases.json similarity index 100% rename from api/src/androidTest/assets/localfeed/json/json_items_other_cases.json rename to api/src/test/resources/localfeed/json/json_items_other_cases.json diff --git a/api/src/androidTest/assets/localfeed/rss1/rss1_feed.xml b/api/src/test/resources/localfeed/rss1/rss1_feed.xml similarity index 100% rename from api/src/androidTest/assets/localfeed/rss1/rss1_feed.xml rename to api/src/test/resources/localfeed/rss1/rss1_feed.xml diff --git a/api/src/androidTest/assets/localfeed/rss1/rss1_feed_no_url_siteurl.xml b/api/src/test/resources/localfeed/rss1/rss1_feed_no_url_siteurl.xml similarity index 100% rename from api/src/androidTest/assets/localfeed/rss1/rss1_feed_no_url_siteurl.xml rename to api/src/test/resources/localfeed/rss1/rss1_feed_no_url_siteurl.xml diff --git a/api/src/androidTest/assets/localfeed/rss1/rss1_items_no_date.xml b/api/src/test/resources/localfeed/rss1/rss1_items_no_date.xml similarity index 100% rename from api/src/androidTest/assets/localfeed/rss1/rss1_items_no_date.xml rename to api/src/test/resources/localfeed/rss1/rss1_items_no_date.xml diff --git a/api/src/androidTest/assets/localfeed/rss1/rss1_items_no_link.xml b/api/src/test/resources/localfeed/rss1/rss1_items_no_link.xml similarity index 100% rename from api/src/androidTest/assets/localfeed/rss1/rss1_items_no_link.xml rename to api/src/test/resources/localfeed/rss1/rss1_items_no_link.xml diff --git a/api/src/androidTest/assets/localfeed/rss1/rss1_items_no_title.xml b/api/src/test/resources/localfeed/rss1/rss1_items_no_title.xml similarity index 100% rename from api/src/androidTest/assets/localfeed/rss1/rss1_items_no_title.xml rename to api/src/test/resources/localfeed/rss1/rss1_items_no_title.xml diff --git a/api/src/androidTest/assets/localfeed/rss1/rss1_items_special_cases.xml b/api/src/test/resources/localfeed/rss1/rss1_items_special_cases.xml similarity index 100% rename from api/src/androidTest/assets/localfeed/rss1/rss1_items_special_cases.xml rename to api/src/test/resources/localfeed/rss1/rss1_items_special_cases.xml diff --git a/api/src/androidTest/assets/localfeed/rss2/rss_feed_special_cases.xml b/api/src/test/resources/localfeed/rss2/rss_feed_special_cases.xml similarity index 100% rename from api/src/androidTest/assets/localfeed/rss2/rss_feed_special_cases.xml rename to api/src/test/resources/localfeed/rss2/rss_feed_special_cases.xml diff --git a/api/src/androidTest/assets/localfeed/rss2/rss_full_feed.xml b/api/src/test/resources/localfeed/rss2/rss_full_feed.xml similarity index 100% rename from api/src/androidTest/assets/localfeed/rss2/rss_full_feed.xml rename to api/src/test/resources/localfeed/rss2/rss_full_feed.xml diff --git a/api/src/androidTest/assets/localfeed/rss2/rss_items_enclosure.xml b/api/src/test/resources/localfeed/rss2/rss_items_enclosure.xml similarity index 100% rename from api/src/androidTest/assets/localfeed/rss2/rss_items_enclosure.xml rename to api/src/test/resources/localfeed/rss2/rss_items_enclosure.xml diff --git a/api/src/androidTest/assets/localfeed/rss2/rss_items_media_content.xml b/api/src/test/resources/localfeed/rss2/rss_items_media_content.xml similarity index 100% rename from api/src/androidTest/assets/localfeed/rss2/rss_items_media_content.xml rename to api/src/test/resources/localfeed/rss2/rss_items_media_content.xml diff --git a/api/src/androidTest/assets/localfeed/rss2/rss_items_media_group.xml b/api/src/test/resources/localfeed/rss2/rss_items_media_group.xml similarity index 100% rename from api/src/androidTest/assets/localfeed/rss2/rss_items_media_group.xml rename to api/src/test/resources/localfeed/rss2/rss_items_media_group.xml diff --git a/api/src/androidTest/assets/localfeed/rss2/rss_items_no_date.xml b/api/src/test/resources/localfeed/rss2/rss_items_no_date.xml similarity index 100% rename from api/src/androidTest/assets/localfeed/rss2/rss_items_no_date.xml rename to api/src/test/resources/localfeed/rss2/rss_items_no_date.xml diff --git a/api/src/androidTest/assets/localfeed/rss2/rss_items_no_link.xml b/api/src/test/resources/localfeed/rss2/rss_items_no_link.xml similarity index 100% rename from api/src/androidTest/assets/localfeed/rss2/rss_items_no_link.xml rename to api/src/test/resources/localfeed/rss2/rss_items_no_link.xml diff --git a/api/src/androidTest/assets/localfeed/rss2/rss_items_no_title.xml b/api/src/test/resources/localfeed/rss2/rss_items_no_title.xml similarity index 100% rename from api/src/androidTest/assets/localfeed/rss2/rss_items_no_title.xml rename to api/src/test/resources/localfeed/rss2/rss_items_no_title.xml diff --git a/api/src/androidTest/assets/localfeed/rss2/rss_items_other_namespaces.xml b/api/src/test/resources/localfeed/rss2/rss_items_other_namespaces.xml similarity index 100% rename from api/src/androidTest/assets/localfeed/rss2/rss_items_other_namespaces.xml rename to api/src/test/resources/localfeed/rss2/rss_items_other_namespaces.xml diff --git a/api/src/androidTest/assets/localfeed/rss_feed.xml b/api/src/test/resources/localfeed/rss_feed.xml similarity index 100% rename from api/src/androidTest/assets/localfeed/rss_feed.xml rename to api/src/test/resources/localfeed/rss_feed.xml From 1cba1b6e17a415c819474635b0f166a4c6d1f234 Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Wed, 11 Nov 2020 22:22:07 +0100 Subject: [PATCH 122/187] Accept null value for Nextcloud News feed attribute folderId --- .../adapters/NextNewsFeedsAdapter.kt | 19 ++++--- .../utils/extensions/JsonReaderExtensions.kt | 5 +- .../adapters/NextNewsFeedsAdapterTest.kt | 39 +++++++++++++++ .../services/nextcloudnews/feeds.json | 49 +++++++++++++++++++ 4 files changed, 103 insertions(+), 9 deletions(-) create mode 100644 api/src/test/java/com/readrops/api/services/nextcloudnews/adapters/NextNewsFeedsAdapterTest.kt create mode 100644 api/src/test/resources/services/nextcloudnews/feeds.json diff --git a/api/src/main/java/com/readrops/api/services/nextcloudnews/adapters/NextNewsFeedsAdapter.kt b/api/src/main/java/com/readrops/api/services/nextcloudnews/adapters/NextNewsFeedsAdapter.kt index d7720f75..259dadb3 100644 --- a/api/src/main/java/com/readrops/api/services/nextcloudnews/adapters/NextNewsFeedsAdapter.kt +++ b/api/src/main/java/com/readrops/api/services/nextcloudnews/adapters/NextNewsFeedsAdapter.kt @@ -2,8 +2,10 @@ package com.readrops.api.services.nextcloudnews.adapters import android.annotation.SuppressLint import com.readrops.api.utils.exceptions.ParseException -import com.readrops.db.entities.Feed +import com.readrops.api.utils.extensions.nextNonEmptyString +import com.readrops.api.utils.extensions.nextNullableInt import com.readrops.api.utils.extensions.nextNullableString +import com.readrops.db.entities.Feed import com.squareup.moshi.FromJson import com.squareup.moshi.JsonReader import com.squareup.moshi.ToJson @@ -43,13 +45,13 @@ class NextNewsFeedsAdapter { while (reader.hasNext()) { with(feed) { when (reader.selectName(NAMES)) { - 0 -> remoteId = reader.nextString() - 1 -> url = reader.nextString() - 2 -> name = reader.nextString() - 3 -> iconUrl = reader.nextString() + 0 -> remoteId = reader.nextNonEmptyString() + 1 -> url = reader.nextNonEmptyString() + 2 -> name = reader.nextNonEmptyString() + 3 -> iconUrl = reader.nextNullableString() 4 -> { - val nextInt = reader.nextInt() - remoteFolderId = if (nextInt > 0) nextInt.toString() else null + val nextInt = reader.nextNullableInt() + remoteFolderId = if (nextInt != null && nextInt > 0) nextInt.toString() else null } 5 -> siteUrl = reader.nextNullableString() else -> reader.skipValue() @@ -65,6 +67,7 @@ class NextNewsFeedsAdapter { } companion object { - val NAMES: JsonReader.Options = JsonReader.Options.of("id", "url", "title", "faviconLink", "folderId", "link") + val NAMES: JsonReader.Options = JsonReader.Options.of("id", "url", "title", + "faviconLink", "folderId", "link") } } \ No newline at end of file diff --git a/api/src/main/java/com/readrops/api/utils/extensions/JsonReaderExtensions.kt b/api/src/main/java/com/readrops/api/utils/extensions/JsonReaderExtensions.kt index d06ca5dd..9eb49679 100644 --- a/api/src/main/java/com/readrops/api/utils/extensions/JsonReaderExtensions.kt +++ b/api/src/main/java/com/readrops/api/utils/extensions/JsonReaderExtensions.kt @@ -9,4 +9,7 @@ fun JsonReader.nextNullableString(): String? = fun JsonReader.nextNonEmptyString(): String { val text = nextString() return if (text.isNotEmpty()) text.trim() else throw ParseException("Json value can't be null") -} \ No newline at end of file +} + +fun JsonReader.nextNullableInt(): Int? = + if (peek() != JsonReader.Token.NULL) nextInt() else nextNull() diff --git a/api/src/test/java/com/readrops/api/services/nextcloudnews/adapters/NextNewsFeedsAdapterTest.kt b/api/src/test/java/com/readrops/api/services/nextcloudnews/adapters/NextNewsFeedsAdapterTest.kt new file mode 100644 index 00000000..0cf0a93c --- /dev/null +++ b/api/src/test/java/com/readrops/api/services/nextcloudnews/adapters/NextNewsFeedsAdapterTest.kt @@ -0,0 +1,39 @@ +package com.readrops.api.services.nextcloudnews.adapters + +import com.readrops.api.TestUtils +import com.readrops.db.entities.Feed +import com.squareup.moshi.Moshi +import com.squareup.moshi.Types +import okio.Buffer +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNull +import org.junit.Test + +class NextNewsFeedsAdapterTest { + + private val adapter = Moshi.Builder() + .add(NextNewsFeedsAdapter()) + .build() + .adapter>(Types.newParameterizedType(List::class.java, Feed::class.java)) + + @Test + fun validFeedsTest() { + val stream = TestUtils.loadResource("services/nextcloudnews/feeds.json") + + val feeds = adapter.fromJson(Buffer().readFrom(stream))!! + val feed1 = feeds[0] + + assertEquals(feed1.name, "Krebs on Security") + assertEquals(feed1.url, "https://krebsonsecurity.com/feed/") + assertEquals(feed1.siteUrl, "https://krebsonsecurity.com/") + assertEquals(feed1.remoteId, "3") + assertNull(feed1.remoteFolderId) + assertEquals(feed1.iconUrl, "https://krebsonsecurity.com/favicon.ico") + + val feed2 = feeds[1] + assertNull(feed2.remoteFolderId) + + val feed3 = feeds[2] + assertEquals(feed3.remoteFolderId, "5") + } +} \ No newline at end of file diff --git a/api/src/test/resources/services/nextcloudnews/feeds.json b/api/src/test/resources/services/nextcloudnews/feeds.json new file mode 100644 index 00000000..a7c5793b --- /dev/null +++ b/api/src/test/resources/services/nextcloudnews/feeds.json @@ -0,0 +1,49 @@ +{ + "feeds": [ + { + "id": 3, + "url": "https://krebsonsecurity.com/feed/", + "title": "Krebs on Security", + "faviconLink": "https://krebsonsecurity.com/favicon.ico", + "added": 1490999780, + "folderId": null, + "unreadCount": 1, + "ordering": 0, + "link": "https://krebsonsecurity.com/", + "pinned": false, + "updateErrorCount": 0, + "lastUpdateError": null, + "items": [] + }, + { + "id": 3, + "url": "https://krebsonsecurity.com/feed/", + "title": "Krebs on Security", + "faviconLink": "https://krebsonsecurity.com/favicon.ico", + "added": 1490999780, + "folderId": 0, + "unreadCount": 1, + "ordering": 0, + "link": "https://krebsonsecurity.com/", + "pinned": false, + "updateErrorCount": 0, + "lastUpdateError": null, + "items": [] + }, + { + "id": 3, + "url": "https://krebsonsecurity.com/feed/", + "title": "Krebs on Security", + "faviconLink": "https://krebsonsecurity.com/favicon.ico", + "added": 1490999780, + "folderId": 5, + "unreadCount": 1, + "ordering": 0, + "link": "https://krebsonsecurity.com/", + "pinned": false, + "updateErrorCount": 0, + "lastUpdateError": null, + "items": [] + } + ] +} From f2607c307c067062c1383510e0e10a4462de3517 Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Sun, 15 Nov 2020 19:13:21 +0100 Subject: [PATCH 123/187] Replace Nextcloud News /user depreciated api call by /ocs/v1.php/cloud/users/{userId} --- .../readrops/api/services/Credentials.java | 6 ++- .../nextcloudnews/NextNewsDataSource.java | 17 +++++--- .../nextcloudnews/NextNewsService.java | 9 +++-- .../adapters/NextNewsFeedsAdapter.kt | 6 ++- .../adapters/NextNewsUserAdapter.kt | 31 +++++++++++++++ .../adapters/NextNewsFeedsAdapterTest.kt | 1 + .../adapters/NextNewsUserAdapterTest.kt | 17 ++++++++ .../services/nextcloudnews/feeds.json | 2 +- .../resources/services/nextcloudnews/user.xml | 39 +++++++++++++++++++ .../app/repositories/NextNewsRepository.java | 20 ++++++---- 10 files changed, 128 insertions(+), 20 deletions(-) create mode 100644 api/src/main/java/com/readrops/api/services/nextcloudnews/adapters/NextNewsUserAdapter.kt create mode 100644 api/src/test/java/com/readrops/api/services/nextcloudnews/adapters/NextNewsUserAdapterTest.kt create mode 100644 api/src/test/resources/services/nextcloudnews/user.xml diff --git a/api/src/main/java/com/readrops/api/services/Credentials.java b/api/src/main/java/com/readrops/api/services/Credentials.java index b77df749..755ff918 100644 --- a/api/src/main/java/com/readrops/api/services/Credentials.java +++ b/api/src/main/java/com/readrops/api/services/Credentials.java @@ -11,7 +11,7 @@ public abstract class Credentials { private final String authorization; - private final String url; + private String url; public Credentials(String authorization, String url) { this.authorization = authorization; @@ -26,6 +26,10 @@ public abstract class Credentials { return url; } + public void setUrl(String url) { + this.url = url; + } + public static Credentials toCredentials(Account account) { String endPoint = getEndPoint(account.getAccountType()); diff --git a/api/src/main/java/com/readrops/api/services/nextcloudnews/NextNewsDataSource.java b/api/src/main/java/com/readrops/api/services/nextcloudnews/NextNewsDataSource.java index 4bb96f1d..24875679 100644 --- a/api/src/main/java/com/readrops/api/services/nextcloudnews/NextNewsDataSource.java +++ b/api/src/main/java/com/readrops/api/services/nextcloudnews/NextNewsDataSource.java @@ -7,9 +7,9 @@ import androidx.annotation.Nullable; import com.readrops.api.services.SyncResult; import com.readrops.api.services.SyncType; -import com.readrops.api.services.nextcloudnews.json.NextNewsUser; -import com.readrops.api.utils.exceptions.ConflictException; +import com.readrops.api.services.nextcloudnews.adapters.NextNewsUserAdapter; import com.readrops.api.utils.ApiUtils; +import com.readrops.api.utils.exceptions.ConflictException; import com.readrops.api.utils.exceptions.UnknownFormatException; import com.readrops.db.entities.Feed; import com.readrops.db.entities.Folder; @@ -23,6 +23,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import okhttp3.ResponseBody; import retrofit2.Response; public class NextNewsDataSource { @@ -38,13 +39,17 @@ public class NextNewsDataSource { } @Nullable - public NextNewsUser login() throws IOException { - Response response = api.getUser().execute(); + public String login(String user) throws IOException { + Response response = api.getUser(user).execute(); - if (!response.isSuccessful()) + if (!response.isSuccessful()) { return null; + } - return response.body(); + String displayName = new NextNewsUserAdapter().fromXml(response.body().byteStream()); + response.body().close(); + + return displayName; } @Nullable diff --git a/api/src/main/java/com/readrops/api/services/nextcloudnews/NextNewsService.java b/api/src/main/java/com/readrops/api/services/nextcloudnews/NextNewsService.java index a4d4d11c..cf1ce098 100644 --- a/api/src/main/java/com/readrops/api/services/nextcloudnews/NextNewsService.java +++ b/api/src/main/java/com/readrops/api/services/nextcloudnews/NextNewsService.java @@ -3,7 +3,6 @@ package com.readrops.api.services.nextcloudnews; import com.readrops.db.entities.Feed; import com.readrops.db.entities.Folder; import com.readrops.db.entities.Item; -import com.readrops.api.services.nextcloudnews.json.NextNewsUser; import java.util.List; import java.util.Map; @@ -13,6 +12,7 @@ import retrofit2.Call; import retrofit2.http.Body; import retrofit2.http.DELETE; import retrofit2.http.GET; +import retrofit2.http.Headers; import retrofit2.http.POST; import retrofit2.http.PUT; import retrofit2.http.Path; @@ -21,9 +21,10 @@ import retrofit2.http.Query; public interface NextNewsService { String END_POINT = "/index.php/apps/news/api/v1-2/"; - - @GET("user") - Call getUser(); + + @GET("/ocs/v1.php/cloud/users/{userId}") + @Headers("OCS-APIRequest: true") + Call getUser(@Path("userId") String userId); @GET("folders") Call> getFolders(); diff --git a/api/src/main/java/com/readrops/api/services/nextcloudnews/adapters/NextNewsFeedsAdapter.kt b/api/src/main/java/com/readrops/api/services/nextcloudnews/adapters/NextNewsFeedsAdapter.kt index 259dadb3..456bb0e9 100644 --- a/api/src/main/java/com/readrops/api/services/nextcloudnews/adapters/NextNewsFeedsAdapter.kt +++ b/api/src/main/java/com/readrops/api/services/nextcloudnews/adapters/NextNewsFeedsAdapter.kt @@ -1,6 +1,7 @@ package com.readrops.api.services.nextcloudnews.adapters import android.annotation.SuppressLint +import androidx.core.net.toUri import com.readrops.api.utils.exceptions.ParseException import com.readrops.api.utils.extensions.nextNonEmptyString import com.readrops.api.utils.extensions.nextNullableInt @@ -9,6 +10,7 @@ import com.readrops.db.entities.Feed import com.squareup.moshi.FromJson import com.squareup.moshi.JsonReader import com.squareup.moshi.ToJson +import java.net.URI class NextNewsFeedsAdapter { @@ -47,7 +49,7 @@ class NextNewsFeedsAdapter { when (reader.selectName(NAMES)) { 0 -> remoteId = reader.nextNonEmptyString() 1 -> url = reader.nextNonEmptyString() - 2 -> name = reader.nextNonEmptyString() + 2 -> name = reader.nextNullableString() 3 -> iconUrl = reader.nextNullableString() 4 -> { val nextInt = reader.nextNullableInt() @@ -59,6 +61,8 @@ class NextNewsFeedsAdapter { } } + if (feed.name == null) feed.name = URI.create(feed.url).host + feeds += feed reader.endObject() } diff --git a/api/src/main/java/com/readrops/api/services/nextcloudnews/adapters/NextNewsUserAdapter.kt b/api/src/main/java/com/readrops/api/services/nextcloudnews/adapters/NextNewsUserAdapter.kt new file mode 100644 index 00000000..992db5ba --- /dev/null +++ b/api/src/main/java/com/readrops/api/services/nextcloudnews/adapters/NextNewsUserAdapter.kt @@ -0,0 +1,31 @@ +package com.readrops.api.services.nextcloudnews.adapters + +import com.gitlab.mvysny.konsumexml.allChildrenAutoIgnore +import com.gitlab.mvysny.konsumexml.konsumeXml +import com.readrops.api.localfeed.XmlAdapter +import com.readrops.api.utils.exceptions.ParseException +import com.readrops.api.utils.extensions.nonNullText +import java.io.InputStream + +class NextNewsUserAdapter : XmlAdapter { + + override fun fromXml(inputStream: InputStream): String { + val konsumer = inputStream.konsumeXml() + var displayName: String? = null + + return try { + konsumer.child("ocs") { + allChildrenAutoIgnore("data") { + allChildrenAutoIgnore("displayname") { + displayName = nonNullText() + } + } + } + + konsumer.close() + displayName!! + } catch (e: Exception) { + throw ParseException(e.message) + } + } +} \ No newline at end of file diff --git a/api/src/test/java/com/readrops/api/services/nextcloudnews/adapters/NextNewsFeedsAdapterTest.kt b/api/src/test/java/com/readrops/api/services/nextcloudnews/adapters/NextNewsFeedsAdapterTest.kt index 0cf0a93c..01a7c4ff 100644 --- a/api/src/test/java/com/readrops/api/services/nextcloudnews/adapters/NextNewsFeedsAdapterTest.kt +++ b/api/src/test/java/com/readrops/api/services/nextcloudnews/adapters/NextNewsFeedsAdapterTest.kt @@ -34,6 +34,7 @@ class NextNewsFeedsAdapterTest { assertNull(feed2.remoteFolderId) val feed3 = feeds[2] + assertEquals(feed3.name, "krebsonsecurity.com") assertEquals(feed3.remoteFolderId, "5") } } \ No newline at end of file diff --git a/api/src/test/java/com/readrops/api/services/nextcloudnews/adapters/NextNewsUserAdapterTest.kt b/api/src/test/java/com/readrops/api/services/nextcloudnews/adapters/NextNewsUserAdapterTest.kt new file mode 100644 index 00000000..6638f16b --- /dev/null +++ b/api/src/test/java/com/readrops/api/services/nextcloudnews/adapters/NextNewsUserAdapterTest.kt @@ -0,0 +1,17 @@ +package com.readrops.api.services.nextcloudnews.adapters + +import com.readrops.api.TestUtils +import org.junit.Assert.assertEquals +import org.junit.Test + +class NextNewsUserAdapterTest { + + private val adapter = NextNewsUserAdapter() + + @Test + fun validXmlTest() { + val stream = TestUtils.loadResource("services/nextcloudnews/user.xml") + + assertEquals(adapter.fromXml(stream), "Shinokuni") + } +} \ No newline at end of file diff --git a/api/src/test/resources/services/nextcloudnews/feeds.json b/api/src/test/resources/services/nextcloudnews/feeds.json index a7c5793b..8663bb33 100644 --- a/api/src/test/resources/services/nextcloudnews/feeds.json +++ b/api/src/test/resources/services/nextcloudnews/feeds.json @@ -33,7 +33,7 @@ { "id": 3, "url": "https://krebsonsecurity.com/feed/", - "title": "Krebs on Security", + "title": "", "faviconLink": "https://krebsonsecurity.com/favicon.ico", "added": 1490999780, "folderId": 5, diff --git a/api/src/test/resources/services/nextcloudnews/user.xml b/api/src/test/resources/services/nextcloudnews/user.xml new file mode 100644 index 00000000..4a661401 --- /dev/null +++ b/api/src/test/resources/services/nextcloudnews/user.xml @@ -0,0 +1,39 @@ + + + ok + 100 + OK + + + + + 1 + /opt/nextcloud/data/Shinokuni + Shinokuni + 1605289415000 + Database + + + 6215059179 + 15259777301 + 21474836480 + 71.06 + 21474836480 + + email@email.org + Shinokuni + phone +
+ https://url.com + + + admin + + fr + fr_FR + + 1 + 1 + +
+
diff --git a/app/src/main/java/com/readrops/app/repositories/NextNewsRepository.java b/app/src/main/java/com/readrops/app/repositories/NextNewsRepository.java index 302e8eb1..60400780 100644 --- a/app/src/main/java/com/readrops/app/repositories/NextNewsRepository.java +++ b/app/src/main/java/com/readrops/app/repositories/NextNewsRepository.java @@ -11,8 +11,9 @@ import androidx.annotation.Nullable; import com.readrops.api.services.SyncResult; import com.readrops.api.services.SyncType; import com.readrops.api.services.nextcloudnews.NextNewsDataSource; +import com.readrops.api.services.nextcloudnews.NextNewsService; import com.readrops.api.services.nextcloudnews.NextNewsSyncData; -import com.readrops.api.services.nextcloudnews.json.NextNewsUser; +import com.readrops.api.utils.AuthInterceptor; import com.readrops.api.utils.exceptions.UnknownFormatException; import com.readrops.app.addfeed.FeedInsertionResult; import com.readrops.app.addfeed.ParsingResult; @@ -24,6 +25,7 @@ import com.readrops.db.entities.Item; import com.readrops.db.entities.account.Account; import org.joda.time.LocalDateTime; +import org.koin.java.KoinJavaComponent; import java.io.IOException; import java.util.ArrayList; @@ -49,16 +51,20 @@ public class NextNewsRepository extends ARepository { @Override public Completable login(Account account, boolean insert) { setCredentials(account); - return Single.create(emitter -> { - NextNewsUser user = dataSource.login(); + return Single.create(emitter -> { + // workaround to have the Nextcloud API call working, as I don't know how to do otherwise + AuthInterceptor authInterceptor = KoinJavaComponent.get(AuthInterceptor.class); + authInterceptor.getCredentials().setUrl(authInterceptor.getCredentials().getUrl().replace(NextNewsService.END_POINT, "")); - if (user != null) { - emitter.onSuccess(user); + String displayName = dataSource.login(account.getLogin()); + + if (displayName != null) { + emitter.onSuccess(displayName); } else { emitter.onError(new Exception("Login failed. Please check your credentials and your Nextcloud News setup.")); } - }).flatMapCompletable(user -> { - account.setDisplayedName(user.getDisplayName()); + }).flatMapCompletable(displayName -> { + account.setDisplayedName(displayName); account.setCurrentAccount(true); if (insert) { From 5f3c7b969ac2f6fe43c9e4bbad27d95875d482f2 Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Sun, 22 Nov 2020 19:22:16 +0100 Subject: [PATCH 124/187] Update android emulator runner build step --- .github/workflows/android.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml index a1070e53..72b9c21e 100644 --- a/.github/workflows/android.yml +++ b/.github/workflows/android.yml @@ -22,7 +22,7 @@ jobs: - name: Build with Gradle run: ./gradlew clean build - name: Android Emulator Runner - uses: ReactiveCircus/android-emulator-runner@v2.5.0 + uses: ReactiveCircus/android-emulator-runner@v2 with: api-level: 29 script: ./gradlew connectedCheck From f6f65d2048e2f93b40b4ed6ee87089b570ca85d8 Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Sun, 22 Nov 2020 21:38:28 +0100 Subject: [PATCH 125/187] Update android gradle plugin --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 665e510e..3ccdde58 100644 --- a/build.gradle +++ b/build.gradle @@ -8,7 +8,7 @@ buildscript { jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:4.1.0' + classpath 'com.android.tools.build:gradle:4.1.1' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" } } From 6647a9bb2c5f0cc2806b6dcc1806df81382bc5d6 Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Sun, 22 Nov 2020 23:20:48 +0100 Subject: [PATCH 126/187] Generate buildConfig only for app module --- app/build.gradle | 1 + gradle.properties | 5 +---- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 2bae9f74..07f303c5 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -50,6 +50,7 @@ android { buildFeatures { viewBinding true + buildConfig true } } diff --git a/gradle.properties b/gradle.properties index 3313ccbc..7363cef4 100644 --- a/gradle.properties +++ b/gradle.properties @@ -12,9 +12,6 @@ org.gradle.jvmargs=-Xmx1536m android.databinding.incremental=true kapt.incremental.apt=true org.gradle.parallel=true -# When configured, Gradle will run in incubating parallel mode. -# This option should only be used with decoupled projects. More details, visit -# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects -# org.gradle.parallel=true +android.defaults.buildfeatures.buildconfig=false From 7270ed0f855da50112d118b5e5ad9ead674a62de Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Sun, 22 Nov 2020 23:24:50 +0100 Subject: [PATCH 127/187] Remove unused class --- .../services/nextcloudnews/json/NextNewsUser.kt | 14 -------------- 1 file changed, 14 deletions(-) delete mode 100644 api/src/main/java/com/readrops/api/services/nextcloudnews/json/NextNewsUser.kt diff --git a/api/src/main/java/com/readrops/api/services/nextcloudnews/json/NextNewsUser.kt b/api/src/main/java/com/readrops/api/services/nextcloudnews/json/NextNewsUser.kt deleted file mode 100644 index bc50ae6f..00000000 --- a/api/src/main/java/com/readrops/api/services/nextcloudnews/json/NextNewsUser.kt +++ /dev/null @@ -1,14 +0,0 @@ -package com.readrops.api.services.nextcloudnews.json - -import com.squareup.moshi.JsonClass - -@JsonClass(generateAdapter = true) -data class NextNewsUser(val userId: String, - val displayName: String, - val lastLoginTimestamp: Long, - val avatar: Avatar?) { - - @JsonClass(generateAdapter = true) - data class Avatar(val data: String, - val mime: String) -} \ No newline at end of file From c3a94b900369568dcca5f8282410cf7ec0b35319 Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Tue, 24 Nov 2020 23:23:26 +0100 Subject: [PATCH 128/187] Update kotlin gradle plugin --- api/build.gradle | 1 - app/build.gradle | 1 - build.gradle | 2 +- db/build.gradle | 1 - gradle.properties | 1 + 5 files changed, 2 insertions(+), 4 deletions(-) diff --git a/api/build.gradle b/api/build.gradle index 5a7c8119..3f609172 100644 --- a/api/build.gradle +++ b/api/build.gradle @@ -1,6 +1,5 @@ apply plugin: 'com.android.library' apply plugin: 'kotlin-android' -apply plugin: 'kotlin-android-extensions' apply plugin: 'kotlin-kapt' android { diff --git a/app/build.gradle b/app/build.gradle index 07f303c5..9969a26c 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,6 +1,5 @@ apply plugin: 'com.android.application' apply plugin: 'kotlin-android' -apply plugin: 'kotlin-android-extensions' apply plugin: 'kotlin-kapt' android { diff --git a/build.gradle b/build.gradle index 3ccdde58..40a2ddf6 100644 --- a/build.gradle +++ b/build.gradle @@ -1,7 +1,7 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. buildscript { - ext.kotlin_version = '1.4.10' + ext.kotlin_version = '1.4.20' repositories { google() diff --git a/db/build.gradle b/db/build.gradle index 435172a6..9630383f 100644 --- a/db/build.gradle +++ b/db/build.gradle @@ -1,6 +1,5 @@ apply plugin: 'com.android.library' apply plugin: 'kotlin-android' -apply plugin: 'kotlin-android-extensions' apply plugin: 'kotlin-kapt' android { diff --git a/gradle.properties b/gradle.properties index 7363cef4..70ede94b 100644 --- a/gradle.properties +++ b/gradle.properties @@ -12,6 +12,7 @@ org.gradle.jvmargs=-Xmx1536m android.databinding.incremental=true kapt.incremental.apt=true org.gradle.parallel=true +kotlin.parallel.tasks.in.project=true android.defaults.buildfeatures.buildconfig=false From daca5ff4c8e8902a4f84c7ce93a7285ae28cbe90 Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Fri, 18 Dec 2020 17:51:50 +0100 Subject: [PATCH 129/187] Update kotlin gradle plugin --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 40a2ddf6..9c4c269d 100644 --- a/build.gradle +++ b/build.gradle @@ -1,7 +1,7 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. buildscript { - ext.kotlin_version = '1.4.20' + ext.kotlin_version = '1.4.21' repositories { google() From 009dda1d00d7148a38b357d91d80225247cbba51 Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Fri, 18 Dec 2020 18:16:23 +0100 Subject: [PATCH 130/187] Fix FreshRSS items ids parsing --- .../services/freshrss/adapters/FreshRSSItemsIdsAdapter.kt | 7 +++++++ .../services/freshrss/adapters/items_starred_ids.json | 3 ++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/api/src/main/java/com/readrops/api/services/freshrss/adapters/FreshRSSItemsIdsAdapter.kt b/api/src/main/java/com/readrops/api/services/freshrss/adapters/FreshRSSItemsIdsAdapter.kt index c03dca06..988507c1 100644 --- a/api/src/main/java/com/readrops/api/services/freshrss/adapters/FreshRSSItemsIdsAdapter.kt +++ b/api/src/main/java/com/readrops/api/services/freshrss/adapters/FreshRSSItemsIdsAdapter.kt @@ -40,6 +40,13 @@ class FreshRSSItemsIdsAdapter : JsonAdapter>() { } endArray() + + // skip continuation + if (hasNext()) { + skipName() + skipValue() + } + endObject() ids diff --git a/api/src/test/resources/services/freshrss/adapters/items_starred_ids.json b/api/src/test/resources/services/freshrss/adapters/items_starred_ids.json index 29031ef3..743b02a3 100644 --- a/api/src/test/resources/services/freshrss/adapters/items_starred_ids.json +++ b/api/src/test/resources/services/freshrss/adapters/items_starred_ids.json @@ -15,5 +15,6 @@ { "id": "1603907200327551" } - ] + ], + "continuation": 1600675234695337 } \ No newline at end of file From fbb4eeee95d238c5a65e96903c03bad9e0e4b741 Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Fri, 18 Dec 2020 18:16:48 +0100 Subject: [PATCH 131/187] Log splash exceptions --- app/src/main/java/com/readrops/app/SplashActivity.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/readrops/app/SplashActivity.java b/app/src/main/java/com/readrops/app/SplashActivity.java index 6af8cfb3..c28cd93c 100644 --- a/app/src/main/java/com/readrops/app/SplashActivity.java +++ b/app/src/main/java/com/readrops/app/SplashActivity.java @@ -2,6 +2,7 @@ package com.readrops.app; import android.content.Intent; import android.os.Bundle; +import android.util.Log; import androidx.appcompat.app.AppCompatActivity; @@ -17,6 +18,8 @@ import io.reactivex.schedulers.Schedulers; public class SplashActivity extends AppCompatActivity { + private static final String TAG = SplashActivity.class.getSimpleName(); + private AccountViewModel viewModel; @Override @@ -46,7 +49,7 @@ public class SplashActivity extends AppCompatActivity { @Override public void onError(Throwable e) { - + Log.d(TAG, e.getMessage()); } }); From 03c806b9a5edc0fd800239be63d08ac812fd2792 Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Mon, 28 Dec 2020 15:32:21 +0100 Subject: [PATCH 132/187] Add new tables for better FreshRSS synchronization speed --- build.gradle | 1 + db/build.gradle | 3 + db/schemas/com.readrops.db.Database/3.json | 256 +++++++++++++++++- .../main/java/com/readrops/db/Database.java | 49 ---- db/src/main/java/com/readrops/db/Database.kt | 26 ++ db/src/main/java/com/readrops/db/DbModule.kt | 3 +- .../java/com/readrops/db/dao/ItemDao.java | 11 +- .../java/com/readrops/db/dao/ItemsIdsDao.kt | 25 ++ .../com/readrops/db/dao/StarredItemDao.kt | 23 ++ .../java/com/readrops/db/entities/ItemsIds.kt | 17 ++ .../com/readrops/db/entities/StarredItem.kt | 35 +++ 11 files changed, 392 insertions(+), 57 deletions(-) delete mode 100644 db/src/main/java/com/readrops/db/Database.java create mode 100644 db/src/main/java/com/readrops/db/Database.kt create mode 100644 db/src/main/java/com/readrops/db/dao/ItemsIdsDao.kt create mode 100644 db/src/main/java/com/readrops/db/dao/StarredItemDao.kt create mode 100644 db/src/main/java/com/readrops/db/entities/ItemsIds.kt create mode 100644 db/src/main/java/com/readrops/db/entities/StarredItem.kt diff --git a/build.gradle b/build.gradle index 9c4c269d..998346e8 100644 --- a/build.gradle +++ b/build.gradle @@ -18,6 +18,7 @@ allprojects { google() jcenter() mavenCentral() + maven { url 'https://jitpack.io' } } afterEvaluate { tasks.withType(JavaCompile.class) { diff --git a/db/build.gradle b/db/build.gradle index 9630383f..854a67c2 100644 --- a/db/build.gradle +++ b/db/build.gradle @@ -67,6 +67,9 @@ dependencies { implementation 'androidx.room:room-rxjava2:2.2.5' androidTestImplementation "androidx.room:room-testing:2.2.5" + implementation 'com.github.MatrixDev.Roomigrant:RoomigrantLib:0.2.0' + kapt 'com.github.MatrixDev.Roomigrant:RoomigrantCompiler:0.2.0' + api 'androidx.paging:paging-runtime:2.1.2' api 'androidx.paging:paging-common:2.1.2' diff --git a/db/schemas/com.readrops.db.Database/3.json b/db/schemas/com.readrops.db.Database/3.json index bb0455d5..16a36b30 100644 --- a/db/schemas/com.readrops.db.Database/3.json +++ b/db/schemas/com.readrops.db.Database/3.json @@ -2,7 +2,7 @@ "formatVersion": 1, "database": { "version": 3, - "identityHash": "474b2c463c0a4bc9f0a8b69feb2feeec", + "identityHash": "c5754bffea62bb91c7d79c091c61aca8", "entities": [ { "tableName": "Feed", @@ -440,12 +440,264 @@ }, "indices": [], "foreignKeys": [] + }, + { + "tableName": "UnreadItemsIds", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `remote_id` TEXT NOT NULL, `account_id` INTEGER NOT NULL, FOREIGN KEY(`account_id`) REFERENCES `Account`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "remoteId", + "columnName": "remote_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "accountId", + "columnName": "account_id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_UnreadItemsIds_remote_id", + "unique": false, + "columnNames": [ + "remote_id" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_UnreadItemsIds_remote_id` ON `${TABLE_NAME}` (`remote_id`)" + }, + { + "name": "index_UnreadItemsIds_account_id", + "unique": false, + "columnNames": [ + "account_id" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_UnreadItemsIds_account_id` ON `${TABLE_NAME}` (`account_id`)" + } + ], + "foreignKeys": [ + { + "table": "Account", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "account_id" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "ReadStarStateChange", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `read_change` INTEGER NOT NULL, `star_change` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "readChange", + "columnName": "read_change", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "starChange", + "columnName": "star_change", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "StarredItem", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `title` TEXT, `description` TEXT, `clean_description` TEXT, `link` TEXT, `image_link` TEXT, `author` TEXT, `pub_date` INTEGER, `content` TEXT, `feed_id` INTEGER NOT NULL, `guid` TEXT, `read_time` REAL NOT NULL, `read` INTEGER NOT NULL, `read_changed` INTEGER NOT NULL, `starred` INTEGER NOT NULL, `starred_changed` INTEGER NOT NULL, `read_it_later` INTEGER NOT NULL, `remoteId` TEXT, FOREIGN KEY(`feed_id`) REFERENCES `Feed`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "cleanDescription", + "columnName": "clean_description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "link", + "columnName": "link", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "imageLink", + "columnName": "image_link", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "author", + "columnName": "author", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "pubDate", + "columnName": "pub_date", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "feedId", + "columnName": "feed_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "guid", + "columnName": "guid", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "readTime", + "columnName": "read_time", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "read", + "columnName": "read", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "readChanged", + "columnName": "read_changed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "starred", + "columnName": "starred", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "starredChanged", + "columnName": "starred_changed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "readItLater", + "columnName": "read_it_later", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "remoteId", + "columnName": "remoteId", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_StarredItem_feed_id", + "unique": false, + "columnNames": [ + "feed_id" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_StarredItem_feed_id` ON `${TABLE_NAME}` (`feed_id`)" + }, + { + "name": "index_StarredItem_guid", + "unique": false, + "columnNames": [ + "guid" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_StarredItem_guid` ON `${TABLE_NAME}` (`guid`)" + }, + { + "name": "index_StarredItem_starred_changed", + "unique": false, + "columnNames": [ + "starred_changed" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_StarredItem_starred_changed` ON `${TABLE_NAME}` (`starred_changed`)" + } + ], + "foreignKeys": [ + { + "table": "Feed", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "feed_id" + ], + "referencedColumns": [ + "id" + ] + } + ] } ], "views": [], "setupQueries": [ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", - "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '474b2c463c0a4bc9f0a8b69feb2feeec')" + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'c5754bffea62bb91c7d79c091c61aca8')" ] } } \ No newline at end of file diff --git a/db/src/main/java/com/readrops/db/Database.java b/db/src/main/java/com/readrops/db/Database.java deleted file mode 100644 index 9f5c1fa4..00000000 --- a/db/src/main/java/com/readrops/db/Database.java +++ /dev/null @@ -1,49 +0,0 @@ -package com.readrops.db; - -import androidx.annotation.NonNull; -import androidx.room.RoomDatabase; -import androidx.room.TypeConverters; -import androidx.room.migration.Migration; -import androidx.sqlite.db.SupportSQLiteDatabase; - -import com.readrops.db.dao.AccountDao; -import com.readrops.db.dao.FeedDao; -import com.readrops.db.dao.FolderDao; -import com.readrops.db.dao.ItemDao; -import com.readrops.db.entities.Feed; -import com.readrops.db.entities.Folder; -import com.readrops.db.entities.Item; -import com.readrops.db.entities.account.Account; - - -@androidx.room.Database(entities = {Feed.class, Item.class, Folder.class, Account.class}, version = 3) -@TypeConverters({Converters.class}) -public abstract class Database extends RoomDatabase { - - public abstract FeedDao feedDao(); - - public abstract ItemDao itemDao(); - - public abstract FolderDao folderDao(); - - public abstract AccountDao accountDao(); - - public static final Migration MIGRATION_1_2 = new Migration(1, 2) { - @Override - public void migrate(@NonNull SupportSQLiteDatabase database) { - database.execSQL("Alter Table Account Add Column notifications_enabled INTEGER Not Null Default 0"); - - database.execSQL("Alter Table Feed Add Column notification_enabled INTEGER Not Null Default 1"); - } - }; - - public static final Migration MIGRATION_2_3 = new Migration(2, 3) { - @Override - public void migrate(@NonNull SupportSQLiteDatabase database) { - database.execSQL("Alter Table Item Add Column starred INTEGER Not Null Default 0"); - database.execSQL("Alter Table Item Add Column starred_changed INTEGER Not Null Default 0"); - - database.execSQL("CREATE INDEX `index_Item_starred_changed` ON `Item` (`starred_changed`);"); - } - }; -} \ No newline at end of file diff --git a/db/src/main/java/com/readrops/db/Database.kt b/db/src/main/java/com/readrops/db/Database.kt new file mode 100644 index 00000000..4482c2f2 --- /dev/null +++ b/db/src/main/java/com/readrops/db/Database.kt @@ -0,0 +1,26 @@ +package com.readrops.db + +import androidx.room.Database +import androidx.room.RoomDatabase +import androidx.room.TypeConverters +import com.readrops.db.dao.* +import com.readrops.db.entities.* +import com.readrops.db.entities.account.Account +import dev.matrix.roomigrant.GenerateRoomMigrations + +@Database(entities = [Feed::class, Item::class, Folder::class, Account::class, UnreadItemsIds::class, ReadStarStateChange::class, StarredItem::class], version = 3) +@TypeConverters(Converters::class) +@GenerateRoomMigrations +abstract class Database : RoomDatabase() { + abstract fun feedDao(): FeedDao + + abstract fun itemDao(): ItemDao + + abstract fun folderDao(): FolderDao + + abstract fun accountDao(): AccountDao + + abstract fun itemsIdsDao(): ItemsIdsDao + + abstract fun starredItemDao(): StarredItemDao +} \ No newline at end of file diff --git a/db/src/main/java/com/readrops/db/DbModule.kt b/db/src/main/java/com/readrops/db/DbModule.kt index dcf264ff..b8a55249 100644 --- a/db/src/main/java/com/readrops/db/DbModule.kt +++ b/db/src/main/java/com/readrops/db/DbModule.kt @@ -7,8 +7,7 @@ val dbModule = module { single(createdAtStart = true) { Room.databaseBuilder(get(), Database::class.java, "readrops-db") - .addMigrations(Database.MIGRATION_1_2) - .addMigrations(Database.MIGRATION_2_3) + .addMigrations(*Database_Migrations.build()) .build() } } \ No newline at end of file diff --git a/db/src/main/java/com/readrops/db/dao/ItemDao.java b/db/src/main/java/com/readrops/db/dao/ItemDao.java index 1dc6d0c2..7b1efd53 100644 --- a/db/src/main/java/com/readrops/db/dao/ItemDao.java +++ b/db/src/main/java/com/readrops/db/dao/ItemDao.java @@ -7,6 +7,7 @@ import androidx.room.Dao; import androidx.room.Query; import androidx.room.RawQuery; import androidx.room.RoomWarnings; +import androidx.room.Transaction; import androidx.sqlite.db.SupportSQLiteQuery; import com.readrops.db.entities.Feed; @@ -95,9 +96,11 @@ public interface ItemDao extends BaseDao { @Query("Update Item set starred = :starred, starred_changed = :starredChanged Where id = :itemId") Completable setStarState(int itemId, boolean starred, boolean starredChanged); - @Query("Update Item set starred = 1 Where remoteId In (:ids) And feed_id In (Select id From Feed Where account_id = :accountId)") - void starItems(List ids, int accountId); + @Transaction + @Query("Update Item set read = 0 Where Item.remoteId In (Select remote_id From UnreadItemsIds) And feed_id In (Select id From Feed Where account_id = :accountId)") + void updateUnreadState(int accountId); - @Query("Update Item set starred = 0 Where remoteId Not In (:ids) And feed_id In (Select id From Feed Where account_id = :accountId)") - void unstarItems(List ids, int accountId); + @Transaction + @Query("Update Item set read = 1 Where Item.remoteId Not In (Select remote_id From UnreadItemsIds) And feed_id In (Select id From Feed Where account_id = :accountId)") + void updateReadState(int accountId); } diff --git a/db/src/main/java/com/readrops/db/dao/ItemsIdsDao.kt b/db/src/main/java/com/readrops/db/dao/ItemsIdsDao.kt new file mode 100644 index 00000000..76662dfe --- /dev/null +++ b/db/src/main/java/com/readrops/db/dao/ItemsIdsDao.kt @@ -0,0 +1,25 @@ +package com.readrops.db.dao + +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.Query +import com.readrops.db.entities.ReadStarStateChange +import com.readrops.db.entities.UnreadItemsIds + +@Dao +interface ItemsIdsDao { + + @Insert + fun insertUnreadItemsIds(unreadItemsIds: List) + + @Insert + fun insertReadStarStateChange(readStarStateChange: ReadStarStateChange) + + @Query("Delete From UnreadItemsIds Where account_id = :accountId") + fun deleteUnreadItemsIds(accountId: Int) + + @Query("Delete From ReadStarStateChange") + fun deleteReadStarStateChanges() + + +} \ No newline at end of file diff --git a/db/src/main/java/com/readrops/db/dao/StarredItemDao.kt b/db/src/main/java/com/readrops/db/dao/StarredItemDao.kt new file mode 100644 index 00000000..bf78d748 --- /dev/null +++ b/db/src/main/java/com/readrops/db/dao/StarredItemDao.kt @@ -0,0 +1,23 @@ +package com.readrops.db.dao + +import androidx.paging.DataSource +import androidx.room.Dao +import androidx.room.Query +import androidx.room.RawQuery +import androidx.sqlite.db.SupportSQLiteQuery +import com.readrops.db.entities.Feed +import com.readrops.db.entities.Folder +import com.readrops.db.entities.Item +import com.readrops.db.entities.StarredItem +import com.readrops.db.pojo.ItemWithFeed + +@Dao +interface StarredItemDao : BaseDao { + + @Query("Delete From StarredItem Where feed_id In (Select feed_id From Feed Where account_id = :accountId)") + fun deleteStarredItems(accountId: Int) + + @RawQuery(observedEntities = [StarredItem::class, Folder::class, Feed::class]) + fun selectAll(query: SupportSQLiteQuery?): DataSource.Factory? + +} \ No newline at end of file diff --git a/db/src/main/java/com/readrops/db/entities/ItemsIds.kt b/db/src/main/java/com/readrops/db/entities/ItemsIds.kt new file mode 100644 index 00000000..07f8cf66 --- /dev/null +++ b/db/src/main/java/com/readrops/db/entities/ItemsIds.kt @@ -0,0 +1,17 @@ +package com.readrops.db.entities + +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.ForeignKey +import androidx.room.PrimaryKey +import com.readrops.db.entities.account.Account + +@Entity(foreignKeys = [ForeignKey(entity = Account::class, parentColumns = ["id"], + childColumns = ["account_id"], onDelete = ForeignKey.CASCADE)]) +data class UnreadItemsIds(@PrimaryKey(autoGenerate = true) val id: Int = 0, + @ColumnInfo(name = "remote_id", index = true) val remoteId: String, + @ColumnInfo(name = "account_id", index = true) val accountId: Int) +@Entity +data class ReadStarStateChange(@PrimaryKey val id: Int = 0, + @ColumnInfo(name = "read_change") val readChange: Boolean = false, + @ColumnInfo(name = "star_change") val starChange: Boolean = false) \ No newline at end of file diff --git a/db/src/main/java/com/readrops/db/entities/StarredItem.kt b/db/src/main/java/com/readrops/db/entities/StarredItem.kt new file mode 100644 index 00000000..aed88c15 --- /dev/null +++ b/db/src/main/java/com/readrops/db/entities/StarredItem.kt @@ -0,0 +1,35 @@ +package com.readrops.db.entities + +import androidx.room.Entity +import androidx.room.ForeignKey +import androidx.room.Ignore + +@Entity(foreignKeys = [ForeignKey(entity = Feed::class, parentColumns = ["id"], childColumns = ["feed_id"], + onDelete = ForeignKey.CASCADE)], inheritSuperIndices = true) +class StarredItem() : Item() { + + // TODO really hacky, should be replaced by something better + @Ignore + constructor(item: Item) : this() { + id = item.id + title = item.title + description = item.description + cleanDescription = item.cleanDescription + link = item.link + imageLink = item.imageLink + author = item.author + pubDate = item.pubDate + content = item.content + feedId = item.feedId + guid = item.guid + readTime = item.readTime + isRead = item.isRead + isReadChanged = item.isReadChanged + isStarred = true // important here for the items query compatibility + isStarredChanged = item.isStarredChanged + isReadItLater = item.isReadItLater + remoteId = item.remoteId + feedRemoteId = item.feedRemoteId + } + +} \ No newline at end of file From 0c9c601d4186fed5fff21a3e06b6d28bcea7dd6a Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Mon, 28 Dec 2020 15:48:32 +0100 Subject: [PATCH 133/187] Add new synchronization process for FreshRSS with read state and starred items sync --- .../com/readrops/api/services/SyncResult.kt | 21 ++--- .../services/freshrss/FreshRSSDataSource.java | 11 +-- .../app/repositories/FreshRSSRepository.java | 81 ++++++++++++------- 3 files changed, 66 insertions(+), 47 deletions(-) diff --git a/api/src/main/java/com/readrops/api/services/SyncResult.kt b/api/src/main/java/com/readrops/api/services/SyncResult.kt index 2c14fed3..a409526a 100644 --- a/api/src/main/java/com/readrops/api/services/SyncResult.kt +++ b/api/src/main/java/com/readrops/api/services/SyncResult.kt @@ -4,17 +4,10 @@ import com.readrops.db.entities.Feed import com.readrops.db.entities.Folder import com.readrops.db.entities.Item -class SyncResult { - - var items: List = mutableListOf() - - var starredItems: List = mutableListOf() - - var feeds: List = listOf() - - var folders: List = listOf() - - var starredIds: List? = null - - var isError: Boolean = false -} +class SyncResult(var items: List = mutableListOf(), + var starredItems: List = mutableListOf(), + var feeds: List = listOf(), + var folders: List = listOf(), + var unreadIds: List? = null, + var isError: Boolean = false +) diff --git a/api/src/main/java/com/readrops/api/services/freshrss/FreshRSSDataSource.java b/api/src/main/java/com/readrops/api/services/freshrss/FreshRSSDataSource.java index f1415008..267e6bcb 100644 --- a/api/src/main/java/com/readrops/api/services/freshrss/FreshRSSDataSource.java +++ b/api/src/main/java/com/readrops/api/services/freshrss/FreshRSSDataSource.java @@ -24,7 +24,8 @@ import okhttp3.RequestBody; public class FreshRSSDataSource { private static final int MAX_ITEMS = 5000; - private static final int MAX_STARRED_ITEMS = 999; + private static final int MAX_UNREAD_ITEMS_IDS = 5000; + private static final int MAX_STARRED_ITEMS = 1000; public static final String GOOGLE_READ = "user/-/state/com.google/read"; public static final String GOOGLE_STARRED = "user/-/state/com.google/starred"; @@ -111,14 +112,14 @@ public class FreshRSSDataSource { .flatMap(freshRSSItems -> { syncResult.setItems(freshRSSItems); + return getItemsIds(GOOGLE_READ, GOOGLE_READING_LIST, MAX_UNREAD_ITEMS_IDS); + }).flatMap(unreadItemsIds -> { + syncResult.setUnreadIds(unreadItemsIds); + return getStarredItems(MAX_STARRED_ITEMS); }).flatMap(starredItems -> { syncResult.setStarredItems(starredItems); - return getItemsIds(null, GOOGLE_STARRED, MAX_STARRED_ITEMS); - }).flatMap(starredIds -> { - syncResult.setStarredIds(starredIds); - return Single.just(syncResult); })); } diff --git a/app/src/main/java/com/readrops/app/repositories/FreshRSSRepository.java b/app/src/main/java/com/readrops/app/repositories/FreshRSSRepository.java index 9d755076..45e69e8b 100644 --- a/app/src/main/java/com/readrops/app/repositories/FreshRSSRepository.java +++ b/app/src/main/java/com/readrops/app/repositories/FreshRSSRepository.java @@ -17,13 +17,18 @@ import com.readrops.db.Database; import com.readrops.db.entities.Feed; import com.readrops.db.entities.Folder; import com.readrops.db.entities.Item; +import com.readrops.db.entities.StarredItem; +import com.readrops.db.entities.UnreadItemsIds; import com.readrops.db.entities.account.Account; import org.joda.time.DateTime; import java.util.ArrayList; import java.util.Collections; +import java.util.HashMap; import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; import io.reactivex.Completable; import io.reactivex.Observable; @@ -104,25 +109,18 @@ public class FreshRSSRepository extends ARepository { insertFeeds(syncResult.getFeeds()); logger.addSplit("feeds insertion"); - insertItems(syncResult.getItems(), syncType == SyncType.INITIAL_SYNC); + insertItems(syncResult.getItems()); logger.addSplit("items insertion"); - insertItems(syncResult.getStarredItems(), syncType == SyncType.INITIAL_SYNC); + insertStarredItems(syncResult.getStarredItems()); + logger.addSplit("starred items insertion"); - updateItemsStarState(syncResult.getStarredIds()); + insertUnreadItemsIds(syncResult.getUnreadIds()); + logger.addSplit("insert and update items ids"); account.setLastModified(newLastModified); database.accountDao().updateLastModified(account.getId(), newLastModified); - if (!syncData.getReadItemsIds().isEmpty() || !syncData.getUnreadItemsIds().isEmpty()) { - database.itemDao().resetReadChanges(account.getId()); - } - - if (!syncData.getStarredItemsIds().isEmpty() || !syncData.getUnstarredItemsIds().isEmpty()) { - database.itemDao().resetStarChanges(account.getId()); - } - - logger.addSplit("reset read changes"); logger.dumpToLog(); this.syncResult = syncResult; @@ -199,9 +197,7 @@ public class FreshRSSRepository extends ARepository { } private void insertFeeds(List freshRSSFeeds) { - for (Feed feed : freshRSSFeeds) { - feed.setAccountId(account.getId()); - } + freshRSSFeeds.stream().forEach(feed -> feed.setAccountId(account.getId())); List insertedFeedsIds = database.feedDao().feedsUpsert(freshRSSFeeds, account); @@ -212,22 +208,22 @@ public class FreshRSSRepository extends ARepository { } private void insertFolders(List freshRSSFolders) { - for (Folder folder : freshRSSFolders) { - folder.setAccountId(account.getId()); - } + freshRSSFolders.stream().forEach(folder -> folder.setAccountId(account.getId())); database.folderDao().foldersUpsert(freshRSSFolders, account); } - private void insertItems(List items, boolean initialSync) { + private void insertItems(List items) { List itemsToInsert = new ArrayList<>(); + Map itemsFeedsIds = new HashMap<>(); for (Item item : items) { - int feedId = database.feedDao().getFeedIdByRemoteId(item.getFeedRemoteId(), account.getId()); - - if (!initialSync && feedId > 0 && database.itemDao().remoteItemExists(item.getRemoteId(), feedId)) { - database.itemDao().setReadAndStarState(item.getRemoteId(), item.isRead(), item.isStarred()); - continue; + Integer feedId; + if (itemsFeedsIds.containsKey(item.getFeedRemoteId())) { + feedId = itemsFeedsIds.get(item.getFeedRemoteId()); + } else { + feedId = database.feedDao().getFeedIdByRemoteId(item.getFeedRemoteId(), account.getId()); + itemsFeedsIds.put(item.getFeedRemoteId(), feedId); } item.setFeedId(feedId); @@ -241,12 +237,41 @@ public class FreshRSSRepository extends ARepository { } } - private void updateItemsStarState(List itemsIds) { - if (itemsIds != null && !itemsIds.isEmpty()) { - database.itemDao().unstarItems(itemsIds, account.getId()); - database.itemDao().starItems(itemsIds, account.getId()); + private void insertStarredItems(List items) { + List starredItems = items.stream().map(StarredItem::new).collect(Collectors.toList()); + + List itemsToInsert = new ArrayList<>(); + Map itemsFeedsIds = new HashMap<>(); + + for (StarredItem item : starredItems) { + int feedId; + + if (itemsFeedsIds.containsKey(item.getFeedRemoteId())) { + feedId = itemsFeedsIds.get(item.getFeedRemoteId()); + } else { + feedId = database.feedDao().getFeedIdByRemoteId(item.getFeedRemoteId(), account.getId()); + itemsFeedsIds.put(item.getFeedRemoteId(), feedId); + } + + item.setFeedId(feedId); + item.setReadTime(Utils.readTimeFromString(item.getContent())); + itemsToInsert.add(item); + } + + if (!itemsToInsert.isEmpty()) { + Collections.sort(itemsToInsert, Item::compareTo); + + database.starredItemDao().deleteStarredItems(account.getId()); + database.starredItemDao().insert(itemsToInsert); } } + private void insertUnreadItemsIds(List ids) { + database.itemsIdsDao().deleteUnreadItemsIds(account.getId()); + database.itemsIdsDao().insertUnreadItemsIds(ids.stream().map(id -> + new UnreadItemsIds(0, id, account.getId())).collect(Collectors.toList())); + database.itemDao().updateUnreadState(account.getId()); + database.itemDao().updateReadState(account.getId()); + } } From 0f924d4a95d3cfda7782d2492dbaac1d11cdc1db Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Mon, 28 Dec 2020 15:52:38 +0100 Subject: [PATCH 134/187] Use a different request for starred items when option is to true --- .../readrops/app/itemslist/MainViewModel.java | 11 ++++- .../java/com/readrops/db/ItemsQueryBuilder.kt | 45 ++++++++++++++----- .../db/entities/account/AccountConfig.java | 16 +++++++ .../com/readrops/db/ItemsQueryBuilderTest.kt | 14 +++--- 4 files changed, 66 insertions(+), 20 deletions(-) diff --git a/app/src/main/java/com/readrops/app/itemslist/MainViewModel.java b/app/src/main/java/com/readrops/app/itemslist/MainViewModel.java index 211540e7..9a5ff1bb 100644 --- a/app/src/main/java/com/readrops/app/itemslist/MainViewModel.java +++ b/app/src/main/java/com/readrops/app/itemslist/MainViewModel.java @@ -6,6 +6,7 @@ import androidx.lifecycle.MediatorLiveData; import androidx.lifecycle.ViewModel; import androidx.paging.LivePagedListBuilder; import androidx.paging.PagedList; +import androidx.sqlite.db.SupportSQLiteQuery; import com.readrops.app.repositories.ARepository; import com.readrops.app.utils.SharedPreferencesManager; @@ -67,8 +68,16 @@ public class MainViewModel extends ViewModel { itemsWithFeed.removeSource(lastFetch); } + SupportSQLiteQuery query; + + if (queryFilters.getFilterType() == FilterType.STARS_FILTER && currentAccount.getAccountType().getAccountConfig().isUseStarredItems()) { + query = ItemsQueryBuilder.buildStarredItemsQuery(queryFilters); + } else { + query = ItemsQueryBuilder.buildItemsQuery(queryFilters); + } + lastFetch = new LivePagedListBuilder<>(new RoomFactoryWrapper<>(database.itemDao() - .selectAll(ItemsQueryBuilder.buildQuery(queryFilters))), + .selectAll(query)), new PagedList.Config.Builder() .setPageSize(100) .setPrefetchDistance(150) diff --git a/db/src/main/java/com/readrops/db/ItemsQueryBuilder.kt b/db/src/main/java/com/readrops/db/ItemsQueryBuilder.kt index d7c4cd2a..70660ec4 100644 --- a/db/src/main/java/com/readrops/db/ItemsQueryBuilder.kt +++ b/db/src/main/java/com/readrops/db/ItemsQueryBuilder.kt @@ -7,36 +7,44 @@ import com.readrops.db.filters.ListSortType object ItemsQueryBuilder { - private val COLUMNS = arrayOf("Item.id", "title", "clean_description", "image_link", "pub_date", "read", - "read_changed", "read_it_later", "Feed.name", "text_color", "background_color", "icon_url", "read_time", "Item.remoteId", + private val COLUMNS = arrayOf("title", "clean_description", "image_link", "pub_date", "read", + "read_changed", "read_it_later", "Feed.name", "text_color", "background_color", "icon_url", "read_time", "Feed.id as feedId", "Feed.account_id", "Folder.id as folder_id", "Folder.name as folder_name") + private val ITEM_COLUMNS = arrayOf(".id", ".remoteId") + private const val SELECT_ALL_JOIN = "Item INNER JOIN Feed on Item.feed_id = Feed.id " + "LEFT JOIN Folder on Feed.folder_id = Folder.id" - private const val ORDER_BY_ASC = "Item.id DESC" + private const val ORDER_BY_ASC = ".id DESC" private const val ORDER_BY_DESC = "pub_date ASC" + @JvmStatic + fun buildItemsQuery(queryFilters: QueryFilters): SupportSQLiteQuery = + buildQuery(queryFilters, false) @JvmStatic - fun buildQuery(queryFilters: QueryFilters): SupportSQLiteQuery { - if (queryFilters.accountId == 0) + fun buildStarredItemsQuery(queryFilters: QueryFilters): SupportSQLiteQuery = + buildQuery(queryFilters, true) + + private fun buildQuery(queryFilters: QueryFilters, starQuery: Boolean): SupportSQLiteQuery = with(queryFilters) { + if (accountId == 0) throw IllegalArgumentException("AccountId must be greater than 0") - if (queryFilters.filterType == FilterType.FEED_FILTER && queryFilters.filterFeedId == 0) + if (filterType == FilterType.FEED_FILTER && filterFeedId == 0) throw IllegalArgumentException("FeedId must be greater than 0 if current filter is FEED_FILTER") - return SupportSQLiteQueryBuilder.builder(SELECT_ALL_JOIN).run { - columns(COLUMNS) - selection(buildWhereClause(queryFilters), null) - orderBy(if (queryFilters.sortType == ListSortType.NEWEST_TO_OLDEST) ORDER_BY_ASC else ORDER_BY_DESC) + SupportSQLiteQueryBuilder.builder(if (starQuery) SELECT_ALL_JOIN.replace("Item", "StarredItem") else SELECT_ALL_JOIN).run { + columns(COLUMNS.plus(buildItemColumns(starQuery))) + selection(buildWhereClause(this@with), null) + orderBy(if (sortType == ListSortType.NEWEST_TO_OLDEST) buildOrderByAsc(starQuery) else ORDER_BY_DESC) create() } } - private fun buildWhereClause(queryFilters: QueryFilters): String? { + private fun buildWhereClause(queryFilters: QueryFilters): String { return StringBuilder(500).run { append("Feed.account_id = ${queryFilters.accountId} And ") @@ -51,8 +59,21 @@ object ItemsQueryBuilder { toString() } - } + + private fun buildItemColumns(starQuery: Boolean): Array { + val columns = arrayListOf() + + for (column in ITEM_COLUMNS) { + columns += if (starQuery) "StarredItem$column" else "Item$column" + } + + return columns.toTypedArray() + } + + private fun buildOrderByAsc(starQuery: Boolean): String = + if (starQuery) "StarredItem$ORDER_BY_ASC" else "Item$ORDER_BY_ASC" + } class QueryFilters(var showReadItems: Boolean = true, diff --git a/db/src/main/java/com/readrops/db/entities/account/AccountConfig.java b/db/src/main/java/com/readrops/db/entities/account/AccountConfig.java index b05c8bf7..2c086408 100644 --- a/db/src/main/java/com/readrops/db/entities/account/AccountConfig.java +++ b/db/src/main/java/com/readrops/db/entities/account/AccountConfig.java @@ -6,18 +6,21 @@ public class AccountConfig { .setFeedUrlEditable(true) .setFolderCreation(true) .setNoFolderCase(false) + .setUseStarredItems(false) .build(); public static final AccountConfig NEXTNEWS = new AccountConfigBuilder() .setFeedUrlEditable(false) .setFolderCreation(true) .setNoFolderCase(false) + .setUseStarredItems(false) .build(); public static final AccountConfig FRESHRSS = new AccountConfigBuilder() .setFeedUrlEditable(false) .setFolderCreation(false) .setNoFolderCase(true) + .setUseStarredItems(true) .build(); private boolean feedUrlEditable; @@ -26,6 +29,8 @@ public class AccountConfig { private boolean noFolderCase; + private boolean useStarredItems; + public boolean isFeedUrlEditable() { return feedUrlEditable; } @@ -38,16 +43,22 @@ public class AccountConfig { return noFolderCase; } + public boolean isUseStarredItems() { + return useStarredItems; + } + public AccountConfig(AccountConfigBuilder builder) { this.feedUrlEditable = builder.feedUrlEditable; this.folderCreation = builder.folderCreation; this.noFolderCase = builder.noFolderCase; + this.useStarredItems = builder.useStarredItems; } public static class AccountConfigBuilder { private boolean feedUrlEditable; private boolean folderCreation; private boolean noFolderCase; + private boolean useStarredItems; public AccountConfigBuilder setFeedUrlEditable(boolean feedUrlEditable) { this.feedUrlEditable = feedUrlEditable; @@ -64,6 +75,11 @@ public class AccountConfig { return this; } + public AccountConfigBuilder setUseStarredItems(boolean useStarredItems) { + this.useStarredItems = useStarredItems; + return this; + } + public AccountConfig build() { return new AccountConfig(this); } diff --git a/db/src/test/java/com/readrops/db/ItemsQueryBuilderTest.kt b/db/src/test/java/com/readrops/db/ItemsQueryBuilderTest.kt index b61ce808..fd16b23b 100644 --- a/db/src/test/java/com/readrops/db/ItemsQueryBuilderTest.kt +++ b/db/src/test/java/com/readrops/db/ItemsQueryBuilderTest.kt @@ -12,7 +12,7 @@ class ItemsQueryBuilderTest { fun noFilterDefaultSortCaseTest() { val queryFilters = QueryFilters(accountId = 1) - val query = ItemsQueryBuilder.buildQuery(queryFilters).sql + val query = ItemsQueryBuilder.buildItemsQuery(queryFilters).sql assertTrue(query.contains("Feed.account_id = 1")) assertTrue(query.contains("read_it_later = 0")) @@ -26,7 +26,7 @@ class ItemsQueryBuilderTest { val queryFilters = QueryFilters(accountId = 1, filterType = FilterType.FEED_FILTER, filterFeedId = 15) - val query = ItemsQueryBuilder.buildQuery(queryFilters).sql + val query = ItemsQueryBuilder.buildItemsQuery(queryFilters).sql assertTrue(query.contains("feed_id = 15 And read_it_later = 0")) } @@ -35,7 +35,7 @@ class ItemsQueryBuilderTest { fun readLaterFilterCaseTest() { val queryFilters = QueryFilters(accountId = 1, filterType = FilterType.READ_IT_LATER_FILTER) - val query = ItemsQueryBuilder.buildQuery(queryFilters).sql + val query = ItemsQueryBuilder.buildItemsQuery(queryFilters).sql assertTrue(query.contains("read_it_later = 1")) } @@ -43,7 +43,7 @@ class ItemsQueryBuilderTest { fun starsFilterCaseTest() { val queryFilters = QueryFilters(accountId = 1, filterType = FilterType.STARS_FILTER) - val query = ItemsQueryBuilder.buildQuery(queryFilters).sql + val query = ItemsQueryBuilder.buildItemsQuery(queryFilters).sql assertTrue(query.contains("starred = 1 And read_it_later = 0")) } @@ -52,7 +52,7 @@ class ItemsQueryBuilderTest { val queryFilters = QueryFilters(accountId = 1, sortType = ListSortType.OLDEST_TO_NEWEST, showReadItems = false) - val query = ItemsQueryBuilder.buildQuery(queryFilters).sql + val query = ItemsQueryBuilder.buildItemsQuery(queryFilters).sql assertTrue(query.contains("read = 0 And ")) assertTrue(query.contains("pub_date ASC")) @@ -62,12 +62,12 @@ class ItemsQueryBuilderTest { fun accountIdExceptionTest() { val queryFilters = QueryFilters() - ItemsQueryBuilder.buildQuery(queryFilters) + ItemsQueryBuilder.buildItemsQuery(queryFilters) } @Test(expected = IllegalArgumentException::class) fun filterFeedIdExceptionTest() { val queryFilters = QueryFilters(accountId = 1, filterType = FilterType.FEED_FILTER) - ItemsQueryBuilder.buildQuery(queryFilters) + ItemsQueryBuilder.buildItemsQuery(queryFilters) } } \ No newline at end of file From d6f98b0d085f92852cc07988b5d6771abdff858d Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Mon, 28 Dec 2020 16:02:08 +0100 Subject: [PATCH 135/187] Delete unused tests --- .../com/readrops/db/DatabaseMigrationsTest.kt | 35 ------------------- 1 file changed, 35 deletions(-) delete mode 100644 db/src/androidTest/java/com/readrops/db/DatabaseMigrationsTest.kt diff --git a/db/src/androidTest/java/com/readrops/db/DatabaseMigrationsTest.kt b/db/src/androidTest/java/com/readrops/db/DatabaseMigrationsTest.kt deleted file mode 100644 index 5280220c..00000000 --- a/db/src/androidTest/java/com/readrops/db/DatabaseMigrationsTest.kt +++ /dev/null @@ -1,35 +0,0 @@ -package com.readrops.db - -import androidx.room.testing.MigrationTestHelper -import androidx.sqlite.db.framework.FrameworkSQLiteOpenHelperFactory -import androidx.test.ext.junit.runners.AndroidJUnit4 -import androidx.test.platform.app.InstrumentationRegistry -import org.junit.Rule -import org.junit.Test -import org.junit.runner.RunWith -import java.io.IOException - -@RunWith(AndroidJUnit4::class) -class DatabaseMigrationsTest { - - private val testDb = "migration-test" - - @get:Rule - val helper = MigrationTestHelper( - InstrumentationRegistry.getInstrumentation(), - "com.readrops.db.Database/", - FrameworkSQLiteOpenHelperFactory() - ) - - @Test - fun migrate1To2() { - helper.createDatabase(testDb, 1).close() - helper.runMigrationsAndValidate(testDb, 2, true, Database.MIGRATION_1_2).close() - } - - @Test - fun migrate2to3() { - helper.createDatabase(testDb, 2).close() - helper.runMigrationsAndValidate(testDb, 3, true, Database.MIGRATION_2_3).close() - } -} \ No newline at end of file From 5589ce871ae4f7828f4eeea80fe3280416cda31c Mon Sep 17 00:00:00 2001 From: zmni Date: Sun, 21 Feb 2021 21:16:07 +0700 Subject: [PATCH 136/187] Create Indonesian translation --- app/src/main/res/values-in/strings.xml | 143 +++++++++++++++++++++++++ 1 file changed, 143 insertions(+) create mode 100644 app/src/main/res/values-in/strings.xml diff --git a/app/src/main/res/values-in/strings.xml b/app/src/main/res/values-in/strings.xml new file mode 100644 index 00000000..4e9a3b09 --- /dev/null +++ b/app/src/main/res/values-in/strings.xml @@ -0,0 +1,143 @@ + + Readrops + + To read + Non read articles + Buka menu + Tutup menu + Tambah feed + Tambah folder + Pengaturan + Tentang + Tambah feed + Url feed + Validasi + Ruas tidak boleh kosong + URL tidak valid + Url feed tidak ditemukan + Galat koneksi + Hos tidak diketahui + oleh %1$s + %1$s mnt + Kurang dari 1 menit + 1 mnt + · + Bagikan Artikel + Buka url + Tambah folder + Folder feed + Nama feed + Sunting feed + Folder + Tidak ada folder + Batal + Hapus feed ? + Muat + Memperbarui feed : %1$s + Hasil + Feed %1$s berhasil ditambahkan + Jaringan terputus saat mengakses feed %1$s + Terjadi kesalahan saat mengurai feed %1$s + Kesalahan format untuk feed %1$s + Kesalahan tidak diketahui untuk feed %1$s + Artikel + Baca nanti + Tampilkan artikel dibaca + Filter + Tandai belum dibaca + Tandai dibaca + Pilih semua + Url akun + Nama akun + Masuk + Sandi + Ini adalah sandi API FreshRSS Anda (Konfigurasi > Profil) + Pengaturan akun + Tambah akun + Tidak ada feed + Pilih akun + Feed dan folder + Akun + Kelola feed dan folder + Folder + Feed + Sunting folder + Hapus folder ? + Hapus akun + Hapus akun ? + Feed %1$s telah dihapus + Feed %1$s tidak ada pada server + Telah terjadi keslahan + Folder sudah ada + Format tidak valid untuk folder baru + Folder tidak ada pada server + + Kredensial + Terbaru > terlama + Terlama > terbaru + Gagal masuk. Silakan periksa kredensial Anda + Akun baru + Aplikasi dirilis dengan lisensi GPLv3 + https://github.com/readrops/Readrops + Jumlah maksimum item per feed + Tidak terbatas + Lokal + %1$s feed + %1$s feed + Hapus +
%2$s]]>
+ Tidak ada item + Feed tidak ditemukan + Galat feed %1$s + Ambil warna feed + Warna Feed + Global + Muat ulang warna feed + Buka item di + Webview + Peramban eksternal + Aktualisasikan + Bagikan url + Impor/Ekspor OPML + Memproses berkas OPML + Proses ini bisa memakan waktu lama karena feed harus diproses satu persatu. + Telah terjadi kesalahan saat memproses berkas + Impor OPML + Ekspor OPML + Subscriptions + Ekspor berkas OPML membutuhkan izin akses penyimpanan eksternal + Coba lagi + Perizinan + Atau + Opsi Gambar + Unduh gambar + Bagikan gambar + Tema + Terang + Gelap + Ekspor feed dan folder + Feed baru + Untuk mengunduh gambar, dibutuhkan izin akses penyimpanan + Sinkronisasi otomatis + Manual + 30 mnt + 1 jam + 2 jam + 3 jam + 6 jam + 12 jam + Sehari sekali + Sinkronisasi akun + %1$s artikel baru + Aktifkan notifikasi + Notifikasi + Aktifkan semua notifikasi feed + Sinkronisasi otomatis dinonaktifkan + Agar bisa ditampilkan, notifikasi membutuhkan sinkronisasi otomatis untuk diaktifkan.\nApakah Anda ingin membuka pengaturan ? + Buka + Kembali + Tampilkan takarir + Sinkronkan + Navigator view + Favorit +
From fa7b7346cd5b1ac36a150c3c08aa37db46ba8966 Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Wed, 14 Apr 2021 17:58:33 +0200 Subject: [PATCH 137/187] Improve some adapters syntax --- .../com/readrops/api/localfeed/XmlAdapter.kt | 29 ++++--- .../api/localfeed/atom/ATOMFeedAdapter.kt | 8 +- .../api/localfeed/json/JSONFeedAdapter.kt | 34 ++++---- .../api/localfeed/json/JSONItemsAdapter.kt | 77 +++++++++---------- .../api/localfeed/rss2/RSS2ItemsAdapter.kt | 22 +++--- 5 files changed, 80 insertions(+), 90 deletions(-) diff --git a/api/src/main/java/com/readrops/api/localfeed/XmlAdapter.kt b/api/src/main/java/com/readrops/api/localfeed/XmlAdapter.kt index 798e476d..732d495a 100644 --- a/api/src/main/java/com/readrops/api/localfeed/XmlAdapter.kt +++ b/api/src/main/java/com/readrops/api/localfeed/XmlAdapter.kt @@ -11,27 +11,24 @@ import com.readrops.db.entities.Item import java.io.InputStream interface XmlAdapter { - + fun fromXml(inputStream: InputStream): T companion object { - fun xmlFeedAdapterFactory(type: LocalRSSHelper.RSSType): XmlAdapter { - return when (type) { - LocalRSSHelper.RSSType.RSS_1 -> RSS1FeedAdapter() - LocalRSSHelper.RSSType.RSS_2 -> RSS2FeedAdapter() - LocalRSSHelper.RSSType.ATOM -> ATOMFeedAdapter() - else -> throw IllegalArgumentException("Unknown RSS type : $type") - } + fun xmlFeedAdapterFactory(type: LocalRSSHelper.RSSType): XmlAdapter = when (type) { + LocalRSSHelper.RSSType.RSS_1 -> RSS1FeedAdapter() + LocalRSSHelper.RSSType.RSS_2 -> RSS2FeedAdapter() + LocalRSSHelper.RSSType.ATOM -> ATOMFeedAdapter() + else -> throw IllegalArgumentException("Unknown RSS type : $type") } - fun xmlItemsAdapterFactory(type: LocalRSSHelper.RSSType): XmlAdapter> { - return when (type) { - LocalRSSHelper.RSSType.RSS_1 -> RSS1ItemsAdapter() - LocalRSSHelper.RSSType.RSS_2 -> RSS2ItemsAdapter() - LocalRSSHelper.RSSType.ATOM -> ATOMItemsAdapter() - else -> throw IllegalArgumentException("Unknown RSS type : $type") - } - } + fun xmlItemsAdapterFactory(type: LocalRSSHelper.RSSType): XmlAdapter> = + when (type) { + LocalRSSHelper.RSSType.RSS_1 -> RSS1ItemsAdapter() + LocalRSSHelper.RSSType.RSS_2 -> RSS2ItemsAdapter() + LocalRSSHelper.RSSType.ATOM -> ATOMItemsAdapter() + else -> throw IllegalArgumentException("Unknown RSS type : $type") + } const val AUTHORS_MAX = 4 } diff --git a/api/src/main/java/com/readrops/api/localfeed/atom/ATOMFeedAdapter.kt b/api/src/main/java/com/readrops/api/localfeed/atom/ATOMFeedAdapter.kt index 4e88f5c8..7497083c 100644 --- a/api/src/main/java/com/readrops/api/localfeed/atom/ATOMFeedAdapter.kt +++ b/api/src/main/java/com/readrops/api/localfeed/atom/ATOMFeedAdapter.kt @@ -37,13 +37,13 @@ class ATOMFeedAdapter : XmlAdapter { } } - private fun parseLink(konsume: Konsumer, feed: Feed) { - val rel = konsume.attributes.getValueOpt("rel") + private fun parseLink(konsume: Konsumer, feed: Feed) = with(konsume) { + val rel = attributes.getValueOpt("rel") if (rel == "self") - feed.url = konsume.attributes["href"] + feed.url = attributes["href"] else if (rel == "alternate") - feed.siteUrl = konsume.attributes["href"] + feed.siteUrl = attributes["href"] } companion object { diff --git a/api/src/main/java/com/readrops/api/localfeed/json/JSONFeedAdapter.kt b/api/src/main/java/com/readrops/api/localfeed/json/JSONFeedAdapter.kt index 80086873..f95772eb 100644 --- a/api/src/main/java/com/readrops/api/localfeed/json/JSONFeedAdapter.kt +++ b/api/src/main/java/com/readrops/api/localfeed/json/JSONFeedAdapter.kt @@ -14,28 +14,26 @@ class JSONFeedAdapter { fun toJson(feed: Feed) = "" @FromJson - fun fromJson(reader: JsonReader): Feed { - return try { - val feed = Feed() - reader.beginObject() + fun fromJson(reader: JsonReader): Feed = try { + val feed = Feed() + reader.beginObject() - while (reader.hasNext()) { - with(feed) { - when (reader.selectName(names)) { - 0 -> name = reader.nextNonEmptyString() - 1 -> siteUrl = reader.nextNullableString() - 2 -> url = reader.nextNullableString() - 3 -> description = reader.nextNullableString() - else -> reader.skipValue() - } + while (reader.hasNext()) { + with(feed) { + when (reader.selectName(names)) { + 0 -> name = reader.nextNonEmptyString() + 1 -> siteUrl = reader.nextNullableString() + 2 -> url = reader.nextNullableString() + 3 -> description = reader.nextNullableString() + else -> reader.skipValue() } } - - reader.endObject() - feed - } catch (e: Exception) { - throw ParseException(e.message) } + + reader.endObject() + feed + } catch (e: Exception) { + throw ParseException(e.message) } companion object { diff --git a/api/src/main/java/com/readrops/api/localfeed/json/JSONItemsAdapter.kt b/api/src/main/java/com/readrops/api/localfeed/json/JSONItemsAdapter.kt index 71e26506..e4ca8f2f 100644 --- a/api/src/main/java/com/readrops/api/localfeed/json/JSONItemsAdapter.kt +++ b/api/src/main/java/com/readrops/api/localfeed/json/JSONItemsAdapter.kt @@ -17,48 +17,46 @@ class JSONItemsAdapter : JsonAdapter>() { // not useful } - override fun fromJson(reader: JsonReader): List { - return try { - val items = arrayListOf() - reader.beginObject() - - while (reader.hasNext()) { - when (reader.nextName()) { - "items" -> parseItems(reader, items) - else -> reader.skipValue() - } - } - - items - } catch (e: Exception) { - throw ParseException(e.message) - } - } - - private fun parseItems(reader: JsonReader, items: MutableList) { - reader.beginArray() + override fun fromJson(reader: JsonReader): List = try { + val items = arrayListOf() + reader.beginObject() while (reader.hasNext()) { - reader.beginObject() + when (reader.nextName()) { + "items" -> parseItems(reader, items) + else -> reader.skipValue() + } + } + + items + } catch (e: Exception) { + throw ParseException(e.message) + } + + private fun parseItems(reader: JsonReader, items: MutableList) = with(reader) { + beginArray() + + while (hasNext()) { + beginObject() val item = Item() var contentText: String? = null var contentHtml: String? = null - while (reader.hasNext()) { + while (hasNext()) { with(item) { - when (reader.selectName(names)) { - 0 -> guid = reader.nextNonEmptyString() - 1 -> link = reader.nextNonEmptyString() - 2 -> title = reader.nextNonEmptyString() - 3 -> contentHtml = reader.nextNullableString() - 4 -> contentText = reader.nextNullableString() - 5 -> description = reader.nextNullableString() - 6 -> imageLink = reader.nextNullableString() - 7 -> pubDate = DateUtils.parse(reader.nextNullableString()) + when (selectName(names)) { + 0 -> guid = nextNonEmptyString() + 1 -> link = nextNonEmptyString() + 2 -> title = nextNonEmptyString() + 3 -> contentHtml = nextNullableString() + 4 -> contentText = nextNullableString() + 5 -> description = nextNullableString() + 6 -> imageLink = nextNullableString() + 7 -> pubDate = DateUtils.parse(nextNullableString()) 8 -> author = parseAuthor(reader) // jsonfeed 1.0 9 -> author = parseAuthors(reader) // jsonfeed 1.1 - else -> reader.skipValue() + else -> skipValue() } } } @@ -67,11 +65,11 @@ class JSONItemsAdapter : JsonAdapter>() { item.content = if (contentHtml != null) contentHtml else contentText if (item.pubDate == null) item.pubDate = LocalDateTime.now() - reader.endObject() + endObject() items += item } - reader.endArray() + endArray() } private fun parseAuthor(reader: JsonReader): String? { @@ -94,7 +92,7 @@ class JSONItemsAdapter : JsonAdapter>() { reader.beginArray() while (reader.hasNext()) { - authors.add(parseAuthor(reader)) + authors += parseAuthor(reader) } reader.endArray() @@ -103,11 +101,10 @@ class JSONItemsAdapter : JsonAdapter>() { authors.filterNotNull().joinToString(limit = AUTHORS_MAX) else null } - private fun validateItem(item: Item) { - when { - item.title == null -> throw ParseException("Item title is required") - item.link == null -> throw ParseException("Item link is required") - } + private fun validateItem(item: Item): Boolean = when { + item.title == null -> throw ParseException("Item title is required") + item.link == null -> throw ParseException("Item link is required") + else -> true } companion object { diff --git a/api/src/main/java/com/readrops/api/localfeed/rss2/RSS2ItemsAdapter.kt b/api/src/main/java/com/readrops/api/localfeed/rss2/RSS2ItemsAdapter.kt index 3d37adbe..1f73875a 100644 --- a/api/src/main/java/com/readrops/api/localfeed/rss2/RSS2ItemsAdapter.kt +++ b/api/src/main/java/com/readrops/api/localfeed/rss2/RSS2ItemsAdapter.kt @@ -58,10 +58,10 @@ class RSS2ItemsAdapter : XmlAdapter> { } } - private fun parseEnclosure(konsumer: Konsumer, item: Item) { - if (konsumer.attributes.getValueOpt("type") != null - && ApiUtils.isMimeImage(konsumer.attributes["type"]) && item.imageLink == null) - item.imageLink = konsumer.attributes.getValueOpt("url") + private fun parseEnclosure(konsumer: Konsumer, item: Item) = with(konsumer) { + if (attributes.getValueOpt("type") != null + && ApiUtils.isMimeImage(attributes["type"]) && item.imageLink == null) + item.imageLink = attributes.getValueOpt("url") } private fun isMediumImage(konsumer: Konsumer) = with(konsumer) { @@ -88,15 +88,13 @@ class RSS2ItemsAdapter : XmlAdapter> { } } - private fun finalizeItem(item: Item, creators: List) { - item.apply { - validateItem(this) + private fun finalizeItem(item: Item, creators: List) = with(item) { + validateItem(this) - if (pubDate == null) pubDate = LocalDateTime.now() - if (guid == null) guid = link - if (author == null && creators.filterNotNull().isNotEmpty()) - author = creators.filterNotNull().joinToString(limit = AUTHORS_MAX) - } + if (pubDate == null) pubDate = LocalDateTime.now() + if (guid == null) guid = link + if (author == null && creators.filterNotNull().isNotEmpty()) + author = creators.filterNotNull().joinToString(limit = AUTHORS_MAX) } private fun validateItem(item: Item) { From 18c9d22b35e83a5718ff03ab4c08db41d8f312ec Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Wed, 14 Apr 2021 18:42:41 +0200 Subject: [PATCH 138/187] Update gradle and kotlin plugins --- build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index 998346e8..ed832462 100644 --- a/build.gradle +++ b/build.gradle @@ -1,14 +1,14 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. buildscript { - ext.kotlin_version = '1.4.21' + ext.kotlin_version = '1.4.32' repositories { google() jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:4.1.1' + classpath 'com.android.tools.build:gradle:4.1.3' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" } } From 2d6e798301d85ab173437c703b38b41bba204e4e Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Wed, 14 Apr 2021 18:49:28 +0200 Subject: [PATCH 139/187] Disable badly written test --- app/src/test/java/com/readrops/app/HtmlParserTest.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/src/test/java/com/readrops/app/HtmlParserTest.kt b/app/src/test/java/com/readrops/app/HtmlParserTest.kt index 28c560b0..32c766ea 100644 --- a/app/src/test/java/com/readrops/app/HtmlParserTest.kt +++ b/app/src/test/java/com/readrops/app/HtmlParserTest.kt @@ -21,6 +21,7 @@ class HtmlParserTest { }) } +/* @Test fun getFeedLinkTest() { val url = "https://github.com/readrops/Readrops" @@ -32,6 +33,7 @@ class HtmlParserTest { val parsingResultList1 = HtmlParser.getFeedLink(url) Assert.assertEquals(parsingResultList, parsingResultList1) } +*/ @Test fun getFaviconLinkTest() { From 2c2df55970371f29c0d762d349b779b90503196b Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Fri, 16 Apr 2021 21:42:25 +0200 Subject: [PATCH 140/187] Update item state local management with new db requests --- .../com/readrops/app/item/ItemActivity.java | 4 +- .../com/readrops/app/item/ItemViewModel.java | 8 ++- .../readrops/app/itemslist/MainActivity.java | 22 +++++--- .../readrops/app/itemslist/MainViewModel.java | 13 +++-- .../app/notifications/sync/SyncWorker.kt | 2 +- .../app/repositories/ARepository.java | 17 ++++-- .../app/repositories/NextNewsRepository.java | 4 +- db/schemas/com.readrops.db.Database/3.json | 56 ++++--------------- .../java/com/readrops/db/ItemsQueryBuilder.kt | 45 ++++++++------- .../java/com/readrops/db/dao/ItemDao.java | 45 ++++----------- .../java/com/readrops/db/dao/ItemsIdsDao.kt | 56 ++++++++++++++++++- .../java/com/readrops/db/entities/Item.java | 22 -------- .../java/com/readrops/db/entities/ItemsIds.kt | 18 ++++-- .../com/readrops/db/entities/StarredItem.kt | 2 - .../com/readrops/db/pojo/ItemReadStarState.kt | 9 +++ 15 files changed, 163 insertions(+), 160 deletions(-) create mode 100644 db/src/main/java/com/readrops/db/pojo/ItemReadStarState.kt diff --git a/app/src/main/java/com/readrops/app/item/ItemActivity.java b/app/src/main/java/com/readrops/app/item/ItemActivity.java index 22277997..9e9ed6fb 100644 --- a/app/src/main/java/com/readrops/app/item/ItemActivity.java +++ b/app/src/main/java/com/readrops/app/item/ItemActivity.java @@ -136,9 +136,7 @@ public class ItemActivity extends AppCompatActivity { } item.setStarred(!item.isStarred()); - item.setStarredChanged(!item.isStarredChanged()); - - viewModel.setStarState(item.getId(), item.isStarred(), item.isStarredChanged()) + viewModel.setStarState(item) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .doOnError(throwable -> Utils.showSnackbar(binding.itemRoot, throwable.getMessage())) diff --git a/app/src/main/java/com/readrops/app/item/ItemViewModel.java b/app/src/main/java/com/readrops/app/item/ItemViewModel.java index 8fa785a9..2c249c04 100644 --- a/app/src/main/java/com/readrops/app/item/ItemViewModel.java +++ b/app/src/main/java/com/readrops/app/item/ItemViewModel.java @@ -9,9 +9,13 @@ import androidx.core.content.FileProvider; import androidx.lifecycle.LiveData; import androidx.lifecycle.ViewModel; +import com.readrops.app.repositories.ARepository; import com.readrops.db.Database; +import com.readrops.db.entities.Item; import com.readrops.db.pojo.ItemWithFeed; +import org.koin.java.KoinJavaComponent; + import java.io.File; import java.io.FileOutputStream; import java.io.IOException; @@ -31,8 +35,8 @@ public class ItemViewModel extends ViewModel { return database.itemDao().getItemById(id); } - public Completable setStarState(int itemId, boolean starred, boolean starredChanged) { - return database.itemDao().setStarState(itemId, starred, starredChanged); + public Completable setStarState(Item item) { + return KoinJavaComponent.get(ARepository.class).setItemStarState(item); } public Uri saveImageInCache(Bitmap bitmap, Context context) throws IOException { diff --git a/app/src/main/java/com/readrops/app/itemslist/MainActivity.java b/app/src/main/java/com/readrops/app/itemslist/MainActivity.java index c10d54f6..d988bdae 100644 --- a/app/src/main/java/com/readrops/app/itemslist/MainActivity.java +++ b/app/src/main/java/com/readrops/app/itemslist/MainActivity.java @@ -36,15 +36,16 @@ import com.mikepenz.materialdrawer.model.interfaces.IDrawerItem; import com.readrops.app.R; import com.readrops.app.account.AccountTypeListActivity; import com.readrops.app.addfeed.AddFeedActivity; +import com.readrops.app.databinding.ActivityMainBinding; import com.readrops.app.item.ItemActivity; import com.readrops.app.settings.SettingsActivity; -import com.readrops.app.databinding.ActivityMainBinding; import com.readrops.app.utils.GlideRequests; -import com.readrops.app.utils.customviews.ReadropsItemTouchCallback; import com.readrops.app.utils.SharedPreferencesManager; import com.readrops.app.utils.Utils; +import com.readrops.app.utils.customviews.ReadropsItemTouchCallback; import com.readrops.db.entities.Feed; import com.readrops.db.entities.Folder; +import com.readrops.db.entities.Item; import com.readrops.db.entities.account.Account; import com.readrops.db.filters.FilterType; import com.readrops.db.filters.ListSortType; @@ -230,7 +231,11 @@ public class MainActivity extends AppCompatActivity implements SwipeRefreshLayou startActivity(itemIntent); - viewModel.setItemReadState(intent.getIntExtra(ITEM_ID, 0), true, true) + Item item = new Item(); + item.setId(intent.getIntExtra(ITEM_ID, 0)); + item.setRead(true); + + viewModel.setItemReadState(item) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .doOnError(throwable -> Utils.showSnackbar(binding.mainRoot, throwable.getMessage())) @@ -314,13 +319,13 @@ public class MainActivity extends AppCompatActivity implements SwipeRefreshLayou intent.putExtra(IMAGE_URL, itemWithFeed.getItem().getImageLink()); startActivityForResult(intent, ITEM_REQUEST); - viewModel.setItemReadState(itemWithFeed, true) + itemWithFeed.getItem().setRead(true); + viewModel.setItemReadState(itemWithFeed) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .doOnError(throwable -> Utils.showSnackbar(binding.mainRoot, throwable.getMessage())) .subscribe(); - itemWithFeed.getItem().setRead(true); adapter.notifyItemChanged(position, itemWithFeed); updateDrawerFeeds(); } else { @@ -412,14 +417,13 @@ public class MainActivity extends AppCompatActivity implements SwipeRefreshLayou if (direction == ItemTouchHelper.LEFT) { // set item read state ItemWithFeed itemWithFeed = adapter.getItemWithFeed(viewHolder.getAdapterPosition()); - viewModel.setItemReadState(itemWithFeed, !itemWithFeed.getItem().isRead()) + itemWithFeed.getItem().setRead(!itemWithFeed.getItem().isRead()); + viewModel.setItemReadState(itemWithFeed) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .doOnError(throwable -> Utils.showSnackbar(binding.mainRoot, throwable.getMessage())) .subscribe(); - itemWithFeed.getItem().setRead(!itemWithFeed.getItem().isRead()); - adapter.notifyItemChanged(viewHolder.getAdapterPosition()); } else { // add item to read it later section viewModel.setItemReadItLater((int) adapter.getItemId(viewHolder.getAdapterPosition())) @@ -497,7 +501,7 @@ public class MainActivity extends AppCompatActivity implements SwipeRefreshLayou allItemsSelected = false; } else { - viewModel.setItemsReadState(adapter.getSelectedItems(), read) + viewModel.setItemsReadState(adapter.getSelectedItems()) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .doOnError(throwable -> Utils.showSnackbar(binding.mainRoot, throwable.getMessage())) diff --git a/app/src/main/java/com/readrops/app/itemslist/MainViewModel.java b/app/src/main/java/com/readrops/app/itemslist/MainViewModel.java index 9a5ff1bb..c6f9ab1a 100644 --- a/app/src/main/java/com/readrops/app/itemslist/MainViewModel.java +++ b/app/src/main/java/com/readrops/app/itemslist/MainViewModel.java @@ -16,6 +16,7 @@ import com.readrops.db.QueryFilters; import com.readrops.db.RoomFactoryWrapper; import com.readrops.db.entities.Feed; import com.readrops.db.entities.Folder; +import com.readrops.db.entities.Item; import com.readrops.db.entities.account.Account; import com.readrops.db.filters.FilterType; import com.readrops.db.filters.ListSortType; @@ -223,19 +224,19 @@ public class MainViewModel extends ViewModel { //region Item read state - public Completable setItemReadState(ItemWithFeed itemWithFeed, boolean read) { - return repository.setItemReadState(itemWithFeed.getItem(), read); + public Completable setItemReadState(ItemWithFeed itemWithFeed) { + return repository.setItemReadState(itemWithFeed.getItem()); } - public Completable setItemReadState(int itemId, boolean read, boolean readChanged) { - return repository.setItemReadState(itemId, read, readChanged); + public Completable setItemReadState(Item item) { + return repository.setItemReadState(item); } - public Completable setItemsReadState(List items, boolean read) { + public Completable setItemsReadState(List items) { List completableList = new ArrayList<>(); for (ItemWithFeed itemWithFeed : items) { - completableList.add(setItemReadState(itemWithFeed, read)); + completableList.add(setItemReadState(itemWithFeed)); } return Completable.concat(completableList); diff --git a/app/src/main/java/com/readrops/app/notifications/sync/SyncWorker.kt b/app/src/main/java/com/readrops/app/notifications/sync/SyncWorker.kt index acd187f8..9ac3da1c 100644 --- a/app/src/main/java/com/readrops/app/notifications/sync/SyncWorker.kt +++ b/app/src/main/java/com/readrops/app/notifications/sync/SyncWorker.kt @@ -149,7 +149,7 @@ class SyncWorker(context: Context, parameters: WorkerParameters) : Worker(contex val itemId = intent?.getIntExtra(ReadropsKeys.ITEM_ID, 0)!! with(get()) { - itemDao().setReadState(itemId, true, true) + itemDao().setReadState(itemId, true) .subscribeOn(Schedulers.io()) .subscribe() } diff --git a/app/src/main/java/com/readrops/app/repositories/ARepository.java b/app/src/main/java/com/readrops/app/repositories/ARepository.java index f01864d2..6fbafa66 100644 --- a/app/src/main/java/com/readrops/app/repositories/ARepository.java +++ b/app/src/main/java/com/readrops/app/repositories/ARepository.java @@ -17,6 +17,7 @@ import com.readrops.db.Database; import com.readrops.db.entities.Feed; import com.readrops.db.entities.Folder; import com.readrops.db.entities.Item; +import com.readrops.db.entities.ReadStarStateChange; import com.readrops.db.entities.account.Account; import org.koin.java.KoinJavaComponent; @@ -113,12 +114,10 @@ public abstract class ARepository { return database.folderDao().delete(folder); } - public Completable setItemReadState(Item item, boolean read) { - return setItemReadState(item.getId(), read, !item.isReadChanged()); - } - - public Completable setItemReadState(int itemId, boolean read, boolean readChanged) { - return database.itemDao().setReadState(itemId, read, readChanged); + public Completable setItemReadState(Item item) { + return database.itemDao().setReadState(item.getId(), item.isRead()) + .andThen(database.itemsIdsDao().upsertReadStarStateChange(new ReadStarStateChange(item.getId(), + true, false, account.getId()))); } public Completable setAllItemsReadState(boolean read) { @@ -129,6 +128,12 @@ public abstract class ARepository { return database.itemDao().setAllFeedItemsReadState(feedId, read ? 1 : 0); } + public Completable setItemStarState(Item item) { + return database.itemDao().setStarState(item.getId(), item.isStarred()) + .andThen(database.itemsIdsDao().upsertReadStarStateChange(new ReadStarStateChange(item.getId(), + false, true, account.getId()))); + } + public Single getFeedCount(int accountId) { return database.feedDao().getFeedCount(accountId); } diff --git a/app/src/main/java/com/readrops/app/repositories/NextNewsRepository.java b/app/src/main/java/com/readrops/app/repositories/NextNewsRepository.java index 60400780..7da5c170 100644 --- a/app/src/main/java/com/readrops/app/repositories/NextNewsRepository.java +++ b/app/src/main/java/com/readrops/app/repositories/NextNewsRepository.java @@ -124,11 +124,11 @@ public class NextNewsRepository extends ARepository { database.accountDao().updateLastModified(account.getId(), lastModified); if (!syncData.getReadItems().isEmpty() || !syncData.getUnreadItems().isEmpty()) { - database.itemDao().resetReadChanges(account.getId()); + } if (!syncData.getStarredItems().isEmpty() || !syncData.getUnstarredItems().isEmpty()) { - database.itemDao().resetStarChanges(account.getId()); + } emitter.onComplete(); diff --git a/db/schemas/com.readrops.db.Database/3.json b/db/schemas/com.readrops.db.Database/3.json index 16a36b30..aa6d7e4b 100644 --- a/db/schemas/com.readrops.db.Database/3.json +++ b/db/schemas/com.readrops.db.Database/3.json @@ -2,7 +2,7 @@ "formatVersion": 1, "database": { "version": 3, - "identityHash": "c5754bffea62bb91c7d79c091c61aca8", + "identityHash": "be089eb90d96a8582a76c667963886cc", "entities": [ { "tableName": "Feed", @@ -151,7 +151,7 @@ }, { "tableName": "Item", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `title` TEXT, `description` TEXT, `clean_description` TEXT, `link` TEXT, `image_link` TEXT, `author` TEXT, `pub_date` INTEGER, `content` TEXT, `feed_id` INTEGER NOT NULL, `guid` TEXT, `read_time` REAL NOT NULL, `read` INTEGER NOT NULL, `read_changed` INTEGER NOT NULL, `starred` INTEGER NOT NULL, `starred_changed` INTEGER NOT NULL, `read_it_later` INTEGER NOT NULL, `remoteId` TEXT, FOREIGN KEY(`feed_id`) REFERENCES `Feed`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `title` TEXT, `description` TEXT, `clean_description` TEXT, `link` TEXT, `image_link` TEXT, `author` TEXT, `pub_date` INTEGER, `content` TEXT, `feed_id` INTEGER NOT NULL, `guid` TEXT, `read_time` REAL NOT NULL, `read` INTEGER NOT NULL, `starred` INTEGER NOT NULL, `read_it_later` INTEGER NOT NULL, `remoteId` TEXT, FOREIGN KEY(`feed_id`) REFERENCES `Feed`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", "fields": [ { "fieldPath": "id", @@ -231,24 +231,12 @@ "affinity": "INTEGER", "notNull": true }, - { - "fieldPath": "readChanged", - "columnName": "read_changed", - "affinity": "INTEGER", - "notNull": true - }, { "fieldPath": "starred", "columnName": "starred", "affinity": "INTEGER", "notNull": true }, - { - "fieldPath": "starredChanged", - "columnName": "starred_changed", - "affinity": "INTEGER", - "notNull": true - }, { "fieldPath": "readItLater", "columnName": "read_it_later", @@ -284,14 +272,6 @@ "guid" ], "createSql": "CREATE INDEX IF NOT EXISTS `index_Item_guid` ON `${TABLE_NAME}` (`guid`)" - }, - { - "name": "index_Item_starred_changed", - "unique": false, - "columnNames": [ - "starred_changed" - ], - "createSql": "CREATE INDEX IF NOT EXISTS `index_Item_starred_changed` ON `${TABLE_NAME}` (`starred_changed`)" } ], "foreignKeys": [ @@ -504,7 +484,7 @@ }, { "tableName": "ReadStarStateChange", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `read_change` INTEGER NOT NULL, `star_change` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `read_change` INTEGER NOT NULL, `star_change` INTEGER NOT NULL, `account_id` INTEGER NOT NULL, PRIMARY KEY(`id`))", "fields": [ { "fieldPath": "id", @@ -523,6 +503,12 @@ "columnName": "star_change", "affinity": "INTEGER", "notNull": true + }, + { + "fieldPath": "accountId", + "columnName": "account_id", + "affinity": "INTEGER", + "notNull": true } ], "primaryKey": { @@ -536,7 +522,7 @@ }, { "tableName": "StarredItem", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `title` TEXT, `description` TEXT, `clean_description` TEXT, `link` TEXT, `image_link` TEXT, `author` TEXT, `pub_date` INTEGER, `content` TEXT, `feed_id` INTEGER NOT NULL, `guid` TEXT, `read_time` REAL NOT NULL, `read` INTEGER NOT NULL, `read_changed` INTEGER NOT NULL, `starred` INTEGER NOT NULL, `starred_changed` INTEGER NOT NULL, `read_it_later` INTEGER NOT NULL, `remoteId` TEXT, FOREIGN KEY(`feed_id`) REFERENCES `Feed`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `title` TEXT, `description` TEXT, `clean_description` TEXT, `link` TEXT, `image_link` TEXT, `author` TEXT, `pub_date` INTEGER, `content` TEXT, `feed_id` INTEGER NOT NULL, `guid` TEXT, `read_time` REAL NOT NULL, `read` INTEGER NOT NULL, `starred` INTEGER NOT NULL, `read_it_later` INTEGER NOT NULL, `remoteId` TEXT, FOREIGN KEY(`feed_id`) REFERENCES `Feed`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", "fields": [ { "fieldPath": "id", @@ -616,24 +602,12 @@ "affinity": "INTEGER", "notNull": true }, - { - "fieldPath": "readChanged", - "columnName": "read_changed", - "affinity": "INTEGER", - "notNull": true - }, { "fieldPath": "starred", "columnName": "starred", "affinity": "INTEGER", "notNull": true }, - { - "fieldPath": "starredChanged", - "columnName": "starred_changed", - "affinity": "INTEGER", - "notNull": true - }, { "fieldPath": "readItLater", "columnName": "read_it_later", @@ -669,14 +643,6 @@ "guid" ], "createSql": "CREATE INDEX IF NOT EXISTS `index_StarredItem_guid` ON `${TABLE_NAME}` (`guid`)" - }, - { - "name": "index_StarredItem_starred_changed", - "unique": false, - "columnNames": [ - "starred_changed" - ], - "createSql": "CREATE INDEX IF NOT EXISTS `index_StarredItem_starred_changed` ON `${TABLE_NAME}` (`starred_changed`)" } ], "foreignKeys": [ @@ -697,7 +663,7 @@ "views": [], "setupQueries": [ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", - "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'c5754bffea62bb91c7d79c091c61aca8')" + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'be089eb90d96a8582a76c667963886cc')" ] } } \ No newline at end of file diff --git a/db/src/main/java/com/readrops/db/ItemsQueryBuilder.kt b/db/src/main/java/com/readrops/db/ItemsQueryBuilder.kt index 70660ec4..712fa4e1 100644 --- a/db/src/main/java/com/readrops/db/ItemsQueryBuilder.kt +++ b/db/src/main/java/com/readrops/db/ItemsQueryBuilder.kt @@ -7,14 +7,16 @@ import com.readrops.db.filters.ListSortType object ItemsQueryBuilder { - private val COLUMNS = arrayOf("title", "clean_description", "image_link", "pub_date", "read", - "read_changed", "read_it_later", "Feed.name", "text_color", "background_color", "icon_url", "read_time", - "Feed.id as feedId", "Feed.account_id", "Folder.id as folder_id", "Folder.name as folder_name") + private val COLUMNS = arrayOf("title", "clean_description", "image_link", "pub_date", + "read_it_later", "Feed.name", "text_color", "background_color", "icon_url", "read_time", + "Feed.id as feedId", "Feed.account_id", "Folder.id as folder_id", "Folder.name as folder_name", + "case When UnreadItemsIds.remote_id is NULL Then 1 else 0 End read") private val ITEM_COLUMNS = arrayOf(".id", ".remoteId") private const val SELECT_ALL_JOIN = "Item INNER JOIN Feed on Item.feed_id = Feed.id " + - "LEFT JOIN Folder on Feed.folder_id = Folder.id" + "LEFT JOIN Folder on Feed.folder_id = Folder.id LEFT JOIN UnreadItemsIds On " + + "Item.remoteId = UnreadItemsIds.remote_id" private const val ORDER_BY_ASC = ".id DESC" @@ -44,21 +46,20 @@ object ItemsQueryBuilder { } } - private fun buildWhereClause(queryFilters: QueryFilters): String { - return StringBuilder(500).run { - append("Feed.account_id = ${queryFilters.accountId} And ") + private fun buildWhereClause(queryFilters: QueryFilters): String = StringBuilder(500).run { + append("Feed.account_id = ${queryFilters.accountId} And " + + "UnreadItemsIds.account_id = ${queryFilters.accountId} Or UnreadItemsIds.account_id is NULL And ") - if (!queryFilters.showReadItems) append("read = 0 And ") + if (!queryFilters.showReadItems) append("read = 0 And ") - when (queryFilters.filterType) { - FilterType.FEED_FILTER -> append("feed_id = ${queryFilters.filterFeedId} And read_it_later = 0") - FilterType.READ_IT_LATER_FILTER -> append("read_it_later = 1") - FilterType.STARS_FILTER -> append("starred = 1 And read_it_later = 0") - else -> append("read_it_later = 0") - } - - toString() + when (queryFilters.filterType) { + FilterType.FEED_FILTER -> append("feed_id = ${queryFilters.filterFeedId} And read_it_later = 0") + FilterType.READ_IT_LATER_FILTER -> append("read_it_later = 1") + FilterType.STARS_FILTER -> append("starred = 1 And read_it_later = 0") + else -> append("read_it_later = 0") } + + toString() } private fun buildItemColumns(starQuery: Boolean): Array { @@ -76,8 +77,10 @@ object ItemsQueryBuilder { } -class QueryFilters(var showReadItems: Boolean = true, - var filterFeedId: Int = 0, - var accountId: Int = 0, - var filterType: FilterType = FilterType.NO_FILTER, - var sortType: ListSortType = ListSortType.NEWEST_TO_OLDEST) \ No newline at end of file +class QueryFilters( + var showReadItems: Boolean = true, + var filterFeedId: Int = 0, + var accountId: Int = 0, + var filterType: FilterType = FilterType.NO_FILTER, + var sortType: ListSortType = ListSortType.NEWEST_TO_OLDEST, +) \ No newline at end of file diff --git a/db/src/main/java/com/readrops/db/dao/ItemDao.java b/db/src/main/java/com/readrops/db/dao/ItemDao.java index 7b1efd53..71eca34e 100644 --- a/db/src/main/java/com/readrops/db/dao/ItemDao.java +++ b/db/src/main/java/com/readrops/db/dao/ItemDao.java @@ -7,12 +7,12 @@ import androidx.room.Dao; import androidx.room.Query; import androidx.room.RawQuery; import androidx.room.RoomWarnings; -import androidx.room.Transaction; import androidx.sqlite.db.SupportSQLiteQuery; import com.readrops.db.entities.Feed; import com.readrops.db.entities.Folder; import com.readrops.db.entities.Item; +import com.readrops.db.entities.UnreadItemsIds; import com.readrops.db.pojo.ItemWithFeed; import com.readrops.db.pojo.StarItem; @@ -23,7 +23,7 @@ import io.reactivex.Completable; @Dao public interface ItemDao extends BaseDao { - @RawQuery(observedEntities = {Item.class, Folder.class, Feed.class}) + @RawQuery(observedEntities = {Item.class, Folder.class, Feed.class, UnreadItemsIds.class}) DataSource.Factory selectAll(SupportSQLiteQuery query); @Query("Select * From Item Where id = :itemId") @@ -43,15 +43,14 @@ public interface ItemDao extends BaseDao { * * @param itemId id of the item to update * @param read 1 for read, 0 for unread - * @param readChanged */ - @Query("Update Item Set read_changed = :readChanged, read = :read Where id = :itemId") - Completable setReadState(int itemId, boolean read, boolean readChanged); + @Query("Update Item Set read = :read Where id = :itemId") + Completable setReadState(int itemId, boolean read); - @Query("Update Item set read_changed = 1, read = :readState Where feed_id In (Select id From Feed Where account_id = :accountId)") + @Query("Update Item set read = :readState Where feed_id In (Select id From Feed Where account_id = :accountId)") Completable setAllItemsReadState(int readState, int accountId); - @Query("Update Item set read_changed = 1, read = :readState Where feed_id = :feedId") + @Query("Update Item set read = :readState Where feed_id = :feedId") Completable setAllFeedItemsReadState(int feedId, int readState); @Query("Update Item set read_it_later = 1 Where id = :itemId") @@ -66,41 +65,21 @@ public interface ItemDao extends BaseDao { "Folder.name as folder_name from Item Inner Join Feed On Item.feed_id = Feed.id Left Join Folder on Folder.id = Feed.folder_id Where Item.id = :id") LiveData getItemById(int id); - @Query("Select Item.remoteId From Item Inner Join Feed On Item.feed_id = Feed.id Where read_changed = 1 And read = 1 And account_id = :accountId") + @Query("Select Item.remoteId From Item Inner Join Feed On Item.feed_id = Feed.id Where read = 1 And account_id = :accountId") List getReadChanges(int accountId); - @Query("Select Item.remoteId From Item Inner Join Feed On Item.feed_id = Feed.id Where read_changed = 1 And read = 0 And account_id = :accountId") + @Query("Select Item.remoteId From Item Inner Join Feed On Item.feed_id = Feed.id Where read = 0 And account_id = :accountId") List getUnreadChanges(int accountId); - @Query("Select Item.remoteId From Item Inner Join Feed On Item.feed_id = Feed.id Where starred_changed = 1 And starred = 1 And account_id = :accountId") - List getFreshRSSStarChanges(int accountId); - - @Query("Select Item.remoteId From Item Inner Join Feed On Item.feed_id = Feed.id Where starred_changed = 1 And starred = 0 And account_id = :accountId") - List getFreshRSSUnstarChanges(int accountId); - - @Query("Select Item.guid, Feed.remoteId as feedRemoteId From Item Inner Join Feed On Item.feed_id = Feed.id Where starred_changed = 1 And starred = 1 And account_id = :accountId") + @Query("Select Item.guid, Feed.remoteId as feedRemoteId From Item Inner Join Feed On Item.feed_id = Feed.id Where starred = 1 And account_id = :accountId") List getStarChanges(int accountId); - @Query("Select Item.guid, Feed.remoteId as feedRemoteId From Item Inner Join Feed On Item.feed_id = Feed.id Where starred_changed = 1 And starred = 0 And account_id = :accountId") + @Query("Select Item.guid, Feed.remoteId as feedRemoteId From Item Inner Join Feed On Item.feed_id = Feed.id Where starred = 0 And account_id = :accountId") List getUnstarChanges(int accountId); - @Query("Update Item set read_changed = 0 Where feed_id in (Select id From Feed Where account_id = :accountId)") - void resetReadChanges(int accountId); - - @Query("Update Item set starred_changed = 0 Where feed_id in (Select id From Feed Where account_id = :accountId)") - void resetStarChanges(int accountId); - @Query("Update Item set read = :read, starred = :starred Where remoteId = :remoteId") void setReadAndStarState(String remoteId, boolean read, boolean starred); - @Query("Update Item set starred = :starred, starred_changed = :starredChanged Where id = :itemId") - Completable setStarState(int itemId, boolean starred, boolean starredChanged); - - @Transaction - @Query("Update Item set read = 0 Where Item.remoteId In (Select remote_id From UnreadItemsIds) And feed_id In (Select id From Feed Where account_id = :accountId)") - void updateUnreadState(int accountId); - - @Transaction - @Query("Update Item set read = 1 Where Item.remoteId Not In (Select remote_id From UnreadItemsIds) And feed_id In (Select id From Feed Where account_id = :accountId)") - void updateReadState(int accountId); + @Query("Update Item set starred = :starred Where id = :itemId") + Completable setStarState(int itemId, boolean starred); } diff --git a/db/src/main/java/com/readrops/db/dao/ItemsIdsDao.kt b/db/src/main/java/com/readrops/db/dao/ItemsIdsDao.kt index 76662dfe..dff975fb 100644 --- a/db/src/main/java/com/readrops/db/dao/ItemsIdsDao.kt +++ b/db/src/main/java/com/readrops/db/dao/ItemsIdsDao.kt @@ -1,10 +1,13 @@ package com.readrops.db.dao import androidx.room.Dao +import androidx.room.Delete import androidx.room.Insert import androidx.room.Query import com.readrops.db.entities.ReadStarStateChange import com.readrops.db.entities.UnreadItemsIds +import com.readrops.db.pojo.ItemReadStarState +import io.reactivex.Completable @Dao interface ItemsIdsDao { @@ -12,14 +15,63 @@ interface ItemsIdsDao { @Insert fun insertUnreadItemsIds(unreadItemsIds: List) + @Insert + fun insertUnreadItemId(unreadItemId: UnreadItemsIds) + + @Query("Delete From UnreadItemsIds Where remote_id = :remoteId And account_id = :accountId") + fun deleteUnreadItemId(remoteId: String, accountId: Int) + + fun upsertUnreadItemId(unreadItemId: UnreadItemsIds) = Completable.create { + if (unreadItemIdExists(unreadItemId.remoteId, unreadItemId.accountId)) { + deleteUnreadItemId(unreadItemId.remoteId, unreadItemId.accountId) + } else { + insertUnreadItemId(unreadItemId) + } + + it.onComplete() + } + + @Query("Select case When Exists (Select remote_id, account_id From UnreadItemsIds Where remote_id = :remoteId And account_id = :accountId) Then 1 else 0 End") + fun unreadItemIdExists(remoteId: String, accountId: Int): Boolean + @Insert fun insertReadStarStateChange(readStarStateChange: ReadStarStateChange) + @Delete + fun deleteReadStarStateChange(readStarStateChange: ReadStarStateChange) + @Query("Delete From UnreadItemsIds Where account_id = :accountId") fun deleteUnreadItemsIds(accountId: Int) - @Query("Delete From ReadStarStateChange") - fun deleteReadStarStateChanges() + @Query("Delete From ReadStarStateChange Where account_id = :accountId") + fun deleteReadStarStateChanges(accountId: Int) + @Query("Delete From ReadStarStateChange Where account_id = :accountId") + fun deleteStateChanges(accountId: Int) + @Query("Select case When UnreadItemsIds.remote_id is NULL Then 1 else 0 End read, Item.remoteId, ReadStarStateChange.read_change, Item.starred, ReadStarStateChange.star_change " + + "From ReadStarStateChange Inner Join Item On ReadStarStateChange.id = Item.id " + + "Left Join UnreadItemsIds On UnreadItemsIds.remote_id = Item.remoteId Where ReadStarStateChange.account_id = :accountId") + fun getItemStateChanges(accountId: Int): List + + @Query("Select StarredItem.remoteId, Case When StarredItem.read = 1 then 0 else 1 end read, StarredItem.starred, ReadStarStateChange.read_change, " + + "ReadStarStateChange.star_change From StarredItem Inner Join ReadStarStateChange On StarredItem.id = ReadStarStateChange.id Where account_id = :accountId") + fun getStarredItemStateChanges(accountId: Int): List + + fun upsertReadStarStateChange(readStarStateChange: ReadStarStateChange) = Completable.create { + if (readStarStateChange.readChange && readStateChangeExists(readStarStateChange.id) || + readStarStateChange.starChange && starStateChangeExists(readStarStateChange.id)) { + deleteReadStarStateChange(readStarStateChange) + } else { + insertReadStarStateChange(readStarStateChange) + } + + it.onComplete() + } + + @Query("Select Case When :itemId In (Select id From ReadStarStateChange Where read_change = 1) Then 1 Else 0 End") + fun readStateChangeExists(itemId: Int): Boolean + + @Query("Select Case When :itemId In (Select id From ReadStarStateChange Where star_change = 1) Then 1 Else 0 End") + fun starStateChangeExists(itemId: Int): Boolean } \ No newline at end of file diff --git a/db/src/main/java/com/readrops/db/entities/Item.java b/db/src/main/java/com/readrops/db/entities/Item.java index 3194f505..b722574d 100644 --- a/db/src/main/java/com/readrops/db/entities/Item.java +++ b/db/src/main/java/com/readrops/db/entities/Item.java @@ -47,14 +47,8 @@ public class Item implements Comparable { private boolean read; - @ColumnInfo(name = "read_changed") - private boolean readChanged; - private boolean starred; - @ColumnInfo(name = "starred_changed", index = true) - private boolean starredChanged; - @ColumnInfo(name = "read_it_later") private boolean readItLater; @@ -178,14 +172,6 @@ public class Item implements Comparable { this.read = read; } - public boolean isReadChanged() { - return readChanged; - } - - public void setReadChanged(boolean readChanged) { - this.readChanged = readChanged; - } - public boolean isStarred() { return starred; } @@ -194,14 +180,6 @@ public class Item implements Comparable { this.starred = starred; } - public boolean isStarredChanged() { - return starredChanged; - } - - public void setStarredChanged(boolean starredChanged) { - this.starredChanged = starredChanged; - } - public boolean isReadItLater() { return readItLater; } diff --git a/db/src/main/java/com/readrops/db/entities/ItemsIds.kt b/db/src/main/java/com/readrops/db/entities/ItemsIds.kt index 07f8cf66..4568ab0e 100644 --- a/db/src/main/java/com/readrops/db/entities/ItemsIds.kt +++ b/db/src/main/java/com/readrops/db/entities/ItemsIds.kt @@ -8,10 +8,16 @@ import com.readrops.db.entities.account.Account @Entity(foreignKeys = [ForeignKey(entity = Account::class, parentColumns = ["id"], childColumns = ["account_id"], onDelete = ForeignKey.CASCADE)]) -data class UnreadItemsIds(@PrimaryKey(autoGenerate = true) val id: Int = 0, - @ColumnInfo(name = "remote_id", index = true) val remoteId: String, - @ColumnInfo(name = "account_id", index = true) val accountId: Int) +data class UnreadItemsIds( + @PrimaryKey(autoGenerate = true) val id: Int = 0, + @ColumnInfo(name = "remote_id", index = true) val remoteId: String, + @ColumnInfo(name = "account_id", index = true) val accountId: Int, +) + @Entity -data class ReadStarStateChange(@PrimaryKey val id: Int = 0, - @ColumnInfo(name = "read_change") val readChange: Boolean = false, - @ColumnInfo(name = "star_change") val starChange: Boolean = false) \ No newline at end of file +data class ReadStarStateChange( + @PrimaryKey val id: Int = 0, + @ColumnInfo(name = "read_change") val readChange: Boolean = false, + @ColumnInfo(name = "star_change") val starChange: Boolean = false, + @ColumnInfo(name = "account_id") val accountId: Int, +) \ No newline at end of file diff --git a/db/src/main/java/com/readrops/db/entities/StarredItem.kt b/db/src/main/java/com/readrops/db/entities/StarredItem.kt index aed88c15..a77dc2ba 100644 --- a/db/src/main/java/com/readrops/db/entities/StarredItem.kt +++ b/db/src/main/java/com/readrops/db/entities/StarredItem.kt @@ -24,9 +24,7 @@ class StarredItem() : Item() { guid = item.guid readTime = item.readTime isRead = item.isRead - isReadChanged = item.isReadChanged isStarred = true // important here for the items query compatibility - isStarredChanged = item.isStarredChanged isReadItLater = item.isReadItLater remoteId = item.remoteId feedRemoteId = item.feedRemoteId diff --git a/db/src/main/java/com/readrops/db/pojo/ItemReadStarState.kt b/db/src/main/java/com/readrops/db/pojo/ItemReadStarState.kt new file mode 100644 index 00000000..4ada491f --- /dev/null +++ b/db/src/main/java/com/readrops/db/pojo/ItemReadStarState.kt @@ -0,0 +1,9 @@ +package com.readrops.db.pojo + +import androidx.room.ColumnInfo + +data class ItemReadStarState(val remoteId: String, + val read: Boolean, + val starred: Boolean, + @ColumnInfo(name = "read_change") val readChange: Boolean, + @ColumnInfo(name = "star_change") val starChange: Boolean) \ No newline at end of file From 66844cfaf82992c46633e8f455ab100e6517c3b9 Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Fri, 16 Apr 2021 22:45:40 +0200 Subject: [PATCH 141/187] Fix starred items query --- .../java/com/readrops/db/ItemsQueryBuilder.kt | 28 +++++++++++-------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/db/src/main/java/com/readrops/db/ItemsQueryBuilder.kt b/db/src/main/java/com/readrops/db/ItemsQueryBuilder.kt index 712fa4e1..2e4b5664 100644 --- a/db/src/main/java/com/readrops/db/ItemsQueryBuilder.kt +++ b/db/src/main/java/com/readrops/db/ItemsQueryBuilder.kt @@ -14,10 +14,6 @@ object ItemsQueryBuilder { private val ITEM_COLUMNS = arrayOf(".id", ".remoteId") - private const val SELECT_ALL_JOIN = "Item INNER JOIN Feed on Item.feed_id = Feed.id " + - "LEFT JOIN Folder on Feed.folder_id = Folder.id LEFT JOIN UnreadItemsIds On " + - "Item.remoteId = UnreadItemsIds.remote_id" - private const val ORDER_BY_ASC = ".id DESC" private const val ORDER_BY_DESC = "pub_date ASC" @@ -37,10 +33,13 @@ object ItemsQueryBuilder { if (filterType == FilterType.FEED_FILTER && filterFeedId == 0) throw IllegalArgumentException("FeedId must be greater than 0 if current filter is FEED_FILTER") - SupportSQLiteQueryBuilder.builder(if (starQuery) SELECT_ALL_JOIN.replace("Item", "StarredItem") else SELECT_ALL_JOIN).run { - columns(COLUMNS.plus(buildItemColumns(starQuery))) + val tableName = tableName(starQuery) + val selectAllJoin = buildSelectAllJoin(tableName) + + SupportSQLiteQueryBuilder.builder(selectAllJoin).run { + columns(COLUMNS.plus(buildItemColumns(tableName))) selection(buildWhereClause(this@with), null) - orderBy(if (sortType == ListSortType.NEWEST_TO_OLDEST) buildOrderByAsc(starQuery) else ORDER_BY_DESC) + orderBy(if (sortType == ListSortType.NEWEST_TO_OLDEST) buildOrderByAsc(tableName) else ORDER_BY_DESC) create() } @@ -62,18 +61,25 @@ object ItemsQueryBuilder { toString() } - private fun buildItemColumns(starQuery: Boolean): Array { + private fun tableName(starQuery: Boolean): String = if (starQuery) "StarredItem" else "Item" + + private fun buildItemColumns(tableName: String): Array { val columns = arrayListOf() for (column in ITEM_COLUMNS) { - columns += if (starQuery) "StarredItem$column" else "Item$column" + columns += tableName + column } return columns.toTypedArray() } - private fun buildOrderByAsc(starQuery: Boolean): String = - if (starQuery) "StarredItem$ORDER_BY_ASC" else "Item$ORDER_BY_ASC" + private fun buildOrderByAsc(tableName: String): String = tableName + ORDER_BY_ASC + + private fun buildSelectAllJoin(tableName: String): String = """ + $tableName INNER JOIN Feed on $tableName.feed_id = Feed.id + LEFT JOIN Folder on Feed.folder_id = Folder.id LEFT JOIN UnreadItemsIds On + $tableName.remoteId = UnreadItemsIds.remote_id + """.trimIndent() } From 1852a0bd9af9e07c57da29b4b92a5f868c0f0a58 Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Sun, 18 Apr 2021 19:44:14 +0200 Subject: [PATCH 142/187] Fix starred items selection queries --- .../java/com/readrops/app/item/ItemActivity.java | 4 +++- .../com/readrops/app/item/ItemViewModel.java | 8 ++++++-- .../readrops/app/itemslist/DrawerManager.java | 4 ++++ .../com/readrops/app/itemslist/MainActivity.java | 3 +++ .../readrops/app/itemslist/MainViewModel.java | 13 ++++++------- .../java/com/readrops/app/utils/ReadropsKeys.kt | 2 ++ .../java/com/readrops/db/dao/StarredItemDao.kt | 14 +++++++++++--- .../readrops/db/entities/account/Account.java | 4 ++++ .../db/entities/account/AccountConfig.java | 16 ++++++++++------ .../db/entities/account/AccountType.java | 2 +- 10 files changed, 50 insertions(+), 20 deletions(-) diff --git a/app/src/main/java/com/readrops/app/item/ItemActivity.java b/app/src/main/java/com/readrops/app/item/ItemActivity.java index 9e9ed6fb..be7e0a63 100644 --- a/app/src/main/java/com/readrops/app/item/ItemActivity.java +++ b/app/src/main/java/com/readrops/app/item/ItemActivity.java @@ -52,6 +52,7 @@ import io.reactivex.schedulers.Schedulers; import static com.readrops.app.utils.ReadropsKeys.ACTION_BAR_COLOR; import static com.readrops.app.utils.ReadropsKeys.IMAGE_URL; import static com.readrops.app.utils.ReadropsKeys.ITEM_ID; +import static com.readrops.app.utils.ReadropsKeys.STARRED_ITEM; import static com.readrops.app.utils.ReadropsKeys.WEB_URL; public class ItemActivity extends AppCompatActivity { @@ -81,6 +82,7 @@ public class ItemActivity extends AppCompatActivity { Intent intent = getIntent(); int itemId = intent.getIntExtra(ITEM_ID, 0); String imageUrl = intent.getStringExtra(IMAGE_URL); + boolean starredItem = intent.getBooleanExtra(STARRED_ITEM, false); setSupportActionBar(binding.collapsingLayoutToolbar); @@ -117,7 +119,7 @@ public class ItemActivity extends AppCompatActivity { })); viewModel = ViewModelCompat.getViewModel(this, ItemViewModel.class); - viewModel.getItemById(itemId).observe(this, itemWithFeed1 -> { + viewModel.getItemById(itemId, starredItem).observe(this, itemWithFeed1 -> { if (!uiBinded) { bindUI(itemWithFeed1); uiBinded = true; diff --git a/app/src/main/java/com/readrops/app/item/ItemViewModel.java b/app/src/main/java/com/readrops/app/item/ItemViewModel.java index 2c249c04..2e5f7c06 100644 --- a/app/src/main/java/com/readrops/app/item/ItemViewModel.java +++ b/app/src/main/java/com/readrops/app/item/ItemViewModel.java @@ -31,8 +31,12 @@ public class ItemViewModel extends ViewModel { this.database = database; } - public LiveData getItemById(int id) { - return database.itemDao().getItemById(id); + public LiveData getItemById(int id, boolean starredItem) { + if (starredItem) { + return database.starredItemDao().getStarredItemById(id); + } else { + return database.itemDao().getItemById(id); + } } public Completable setStarState(Item item) { diff --git a/app/src/main/java/com/readrops/app/itemslist/DrawerManager.java b/app/src/main/java/com/readrops/app/itemslist/DrawerManager.java index 53be84eb..4a9c30e9 100644 --- a/app/src/main/java/com/readrops/app/itemslist/DrawerManager.java +++ b/app/src/main/java/com/readrops/app/itemslist/DrawerManager.java @@ -315,4 +315,8 @@ public class DrawerManager { public void setDrawerSelection(long identifier) { drawer.setSelection(identifier); } + + public long getCurrentSelection() { + return drawer.getCurrentSelection(); + } } diff --git a/app/src/main/java/com/readrops/app/itemslist/MainActivity.java b/app/src/main/java/com/readrops/app/itemslist/MainActivity.java index d988bdae..fadacdf4 100644 --- a/app/src/main/java/com/readrops/app/itemslist/MainActivity.java +++ b/app/src/main/java/com/readrops/app/itemslist/MainActivity.java @@ -73,6 +73,7 @@ import static com.readrops.app.utils.ReadropsKeys.FROM_MAIN_ACTIVITY; import static com.readrops.app.utils.ReadropsKeys.IMAGE_URL; import static com.readrops.app.utils.ReadropsKeys.ITEM_ID; import static com.readrops.app.utils.ReadropsKeys.SETTINGS; +import static com.readrops.app.utils.ReadropsKeys.STARRED_ITEM; import static com.readrops.app.utils.ReadropsKeys.SYNCING; public class MainActivity extends AppCompatActivity implements SwipeRefreshLayout.OnRefreshListener, @@ -317,6 +318,8 @@ public class MainActivity extends AppCompatActivity implements SwipeRefreshLayou intent.putExtra(ITEM_ID, itemWithFeed.getItem().getId()); intent.putExtra(IMAGE_URL, itemWithFeed.getItem().getImageLink()); + intent.putExtra(STARRED_ITEM, drawerManager.getCurrentSelection() == DrawerManager.STARS_ID && + viewModel.getCurrentAccount().getConfig().useStarredItems()); startActivityForResult(intent, ITEM_REQUEST); itemWithFeed.getItem().setRead(true); diff --git a/app/src/main/java/com/readrops/app/itemslist/MainViewModel.java b/app/src/main/java/com/readrops/app/itemslist/MainViewModel.java index c6f9ab1a..2ad74215 100644 --- a/app/src/main/java/com/readrops/app/itemslist/MainViewModel.java +++ b/app/src/main/java/com/readrops/app/itemslist/MainViewModel.java @@ -4,9 +4,9 @@ import androidx.annotation.NonNull; import androidx.lifecycle.LiveData; import androidx.lifecycle.MediatorLiveData; import androidx.lifecycle.ViewModel; +import androidx.paging.DataSource; import androidx.paging.LivePagedListBuilder; import androidx.paging.PagedList; -import androidx.sqlite.db.SupportSQLiteQuery; import com.readrops.app.repositories.ARepository; import com.readrops.app.utils.SharedPreferencesManager; @@ -69,16 +69,15 @@ public class MainViewModel extends ViewModel { itemsWithFeed.removeSource(lastFetch); } - SupportSQLiteQuery query; + DataSource.Factory items; - if (queryFilters.getFilterType() == FilterType.STARS_FILTER && currentAccount.getAccountType().getAccountConfig().isUseStarredItems()) { - query = ItemsQueryBuilder.buildStarredItemsQuery(queryFilters); + if (queryFilters.getFilterType() == FilterType.STARS_FILTER && currentAccount.getAccountType().getAccountConfig().useStarredItems()) { + items = database.starredItemDao().selectAll(ItemsQueryBuilder.buildStarredItemsQuery(queryFilters)); } else { - query = ItemsQueryBuilder.buildItemsQuery(queryFilters); + items = database.itemDao().selectAll(ItemsQueryBuilder.buildItemsQuery(queryFilters)); } - lastFetch = new LivePagedListBuilder<>(new RoomFactoryWrapper<>(database.itemDao() - .selectAll(query)), + lastFetch = new LivePagedListBuilder<>(new RoomFactoryWrapper<>(items), new PagedList.Config.Builder() .setPageSize(100) .setPrefetchDistance(150) diff --git a/app/src/main/java/com/readrops/app/utils/ReadropsKeys.kt b/app/src/main/java/com/readrops/app/utils/ReadropsKeys.kt index 57247d0a..01833caf 100644 --- a/app/src/main/java/com/readrops/app/utils/ReadropsKeys.kt +++ b/app/src/main/java/com/readrops/app/utils/ReadropsKeys.kt @@ -20,4 +20,6 @@ object ReadropsKeys { const val ACTION_BAR_COLOR = "ACTION_BAR_COLOR_KEY" const val FEEDS = "FEEDS" + + const val STARRED_ITEM = "STARRED_ITEM" } \ No newline at end of file diff --git a/db/src/main/java/com/readrops/db/dao/StarredItemDao.kt b/db/src/main/java/com/readrops/db/dao/StarredItemDao.kt index bf78d748..98720321 100644 --- a/db/src/main/java/com/readrops/db/dao/StarredItemDao.kt +++ b/db/src/main/java/com/readrops/db/dao/StarredItemDao.kt @@ -1,14 +1,16 @@ package com.readrops.db.dao +import androidx.lifecycle.LiveData import androidx.paging.DataSource import androidx.room.Dao import androidx.room.Query import androidx.room.RawQuery +import androidx.room.RoomWarnings import androidx.sqlite.db.SupportSQLiteQuery import com.readrops.db.entities.Feed import com.readrops.db.entities.Folder -import com.readrops.db.entities.Item import com.readrops.db.entities.StarredItem +import com.readrops.db.entities.UnreadItemsIds import com.readrops.db.pojo.ItemWithFeed @Dao @@ -17,7 +19,13 @@ interface StarredItemDao : BaseDao { @Query("Delete From StarredItem Where feed_id In (Select feed_id From Feed Where account_id = :accountId)") fun deleteStarredItems(accountId: Int) - @RawQuery(observedEntities = [StarredItem::class, Folder::class, Feed::class]) - fun selectAll(query: SupportSQLiteQuery?): DataSource.Factory? + @RawQuery(observedEntities = [StarredItem::class, Folder::class, Feed::class, UnreadItemsIds::class]) + fun selectAll(query: SupportSQLiteQuery?): DataSource.Factory + + @SuppressWarnings(RoomWarnings.CURSOR_MISMATCH) + @Query("Select StarredItem.id, title, StarredItem.description, content, link, pub_date, image_link, author, read, text_color, " + + "background_color, read_time, starred, Feed.name, Feed.id as feedId, siteUrl, Folder.id as folder_id, " + + "Folder.name as folder_name from StarredItem Inner Join Feed On StarredItem.feed_id = Feed.id Left Join Folder on Folder.id = Feed.folder_id Where StarredItem.id = :id") + fun getStarredItemById(id: Int): LiveData } \ No newline at end of file diff --git a/db/src/main/java/com/readrops/db/entities/account/Account.java b/db/src/main/java/com/readrops/db/entities/account/Account.java index 950acd84..1e37ecc8 100644 --- a/db/src/main/java/com/readrops/db/entities/account/Account.java +++ b/db/src/main/java/com/readrops/db/entities/account/Account.java @@ -185,6 +185,10 @@ public class Account implements Parcelable { this.notificationsEnabled = notificationsEnabled; } + public AccountConfig getConfig() { + return accountType.getAccountConfig(); + } + @Override public int describeContents() { return 0; diff --git a/db/src/main/java/com/readrops/db/entities/account/AccountConfig.java b/db/src/main/java/com/readrops/db/entities/account/AccountConfig.java index 2c086408..e642949e 100644 --- a/db/src/main/java/com/readrops/db/entities/account/AccountConfig.java +++ b/db/src/main/java/com/readrops/db/entities/account/AccountConfig.java @@ -9,7 +9,7 @@ public class AccountConfig { .setUseStarredItems(false) .build(); - public static final AccountConfig NEXTNEWS = new AccountConfigBuilder() + public static final AccountConfig NEXTCLOUD_NEWS = new AccountConfigBuilder() .setFeedUrlEditable(false) .setFolderCreation(true) .setNoFolderCase(false) @@ -23,13 +23,17 @@ public class AccountConfig { .setUseStarredItems(true) .build(); - private boolean feedUrlEditable; + private final boolean feedUrlEditable; - private boolean folderCreation; + private final boolean folderCreation; - private boolean noFolderCase; + private final boolean noFolderCase; - private boolean useStarredItems; + /* + This parameter lets know if the account separates items and starred items + by using the StarredItem table + */ + private final boolean useStarredItems; public boolean isFeedUrlEditable() { return feedUrlEditable; @@ -43,7 +47,7 @@ public class AccountConfig { return noFolderCase; } - public boolean isUseStarredItems() { + public boolean useStarredItems() { return useStarredItems; } diff --git a/db/src/main/java/com/readrops/db/entities/account/AccountType.java b/db/src/main/java/com/readrops/db/entities/account/AccountType.java index c724893e..65ec00cd 100644 --- a/db/src/main/java/com/readrops/db/entities/account/AccountType.java +++ b/db/src/main/java/com/readrops/db/entities/account/AccountType.java @@ -11,7 +11,7 @@ import com.readrops.db.R; public enum AccountType implements Parcelable { LOCAL(R.mipmap.ic_launcher, R.string.local_account, AccountConfig.LOCAL), - NEXTCLOUD_NEWS(R.drawable.ic_nextcloud_news, R.string.nextcloud_news, AccountConfig.NEXTNEWS), + NEXTCLOUD_NEWS(R.drawable.ic_nextcloud_news, R.string.nextcloud_news, AccountConfig.NEXTCLOUD_NEWS), FEEDLY(R.drawable.ic_feedly, R.string.feedly, null), FRESHRSS(R.drawable.ic_freshrss, R.string.freshrss, AccountConfig.FRESHRSS); From 241144986343c4ce359989a01272d6a61163b548 Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Tue, 20 Apr 2021 23:19:12 +0200 Subject: [PATCH 143/187] Add a new table which merge FreshRSS items read/star state --- .../com/readrops/api/services/SyncResult.kt | 1 + .../app/repositories/FreshRSSRepository.java | 27 +++++++---- db/schemas/com.readrops.db.Database/3.json | 48 ++++++++++++++++++- db/src/main/java/com/readrops/db/Database.kt | 3 +- .../java/com/readrops/db/dao/ItemsIdsDao.kt | 7 +++ .../java/com/readrops/db/entities/ItemsIds.kt | 9 ++++ 6 files changed, 82 insertions(+), 13 deletions(-) diff --git a/api/src/main/java/com/readrops/api/services/SyncResult.kt b/api/src/main/java/com/readrops/api/services/SyncResult.kt index a409526a..6252affa 100644 --- a/api/src/main/java/com/readrops/api/services/SyncResult.kt +++ b/api/src/main/java/com/readrops/api/services/SyncResult.kt @@ -9,5 +9,6 @@ class SyncResult(var items: List = mutableListOf(), var feeds: List = listOf(), var folders: List = listOf(), var unreadIds: List? = null, + var starredIds: List? = null, var isError: Boolean = false ) diff --git a/app/src/main/java/com/readrops/app/repositories/FreshRSSRepository.java b/app/src/main/java/com/readrops/app/repositories/FreshRSSRepository.java index 45e69e8b..dc1843b7 100644 --- a/app/src/main/java/com/readrops/app/repositories/FreshRSSRepository.java +++ b/app/src/main/java/com/readrops/app/repositories/FreshRSSRepository.java @@ -17,8 +17,8 @@ import com.readrops.db.Database; import com.readrops.db.entities.Feed; import com.readrops.db.entities.Folder; import com.readrops.db.entities.Item; +import com.readrops.db.entities.ItemStateId; import com.readrops.db.entities.StarredItem; -import com.readrops.db.entities.UnreadItemsIds; import com.readrops.db.entities.account.Account; import org.joda.time.DateTime; @@ -93,11 +93,11 @@ public class FreshRSSRepository extends ARepository { TimingLogger logger = new TimingLogger(TAG, "FreshRSS sync timer"); return Single.create(emitter -> { - syncData.setReadItemsIds(database.itemDao().getReadChanges(account.getId())); - syncData.setUnreadItemsIds(database.itemDao().getUnreadChanges(account.getId())); + syncData.setReadItemsIds(/*database.itemDao().getReadChanges(account.getId()*/Collections.emptyList()); + syncData.setUnreadItemsIds(/*database.itemDao().getUnreadChanges(account.getId())*/Collections.emptyList()); - syncData.setStarredItemsIds(database.itemDao().getFreshRSSStarChanges(account.getId())); - syncData.setUnstarredItemsIds(database.itemDao().getFreshRSSUnstarChanges(account.getId())); + syncData.setStarredItemsIds(/*database.itemDao().getFreshRSSStarChanges(account.getId())*/Collections.emptyList()); + syncData.setUnstarredItemsIds(/*database.itemDao().getFreshRSSUnstarChanges(account.getId())*/Collections.emptyList()); emitter.onSuccess(syncData); }).flatMap(syncData1 -> dataSource.sync(syncType, syncData1, account.getWriteToken())) @@ -115,7 +115,7 @@ public class FreshRSSRepository extends ARepository { insertStarredItems(syncResult.getStarredItems()); logger.addSplit("starred items insertion"); - insertUnreadItemsIds(syncResult.getUnreadIds()); + insertItemsIds(syncResult.getUnreadIds(), syncResult.getStarredIds()); logger.addSplit("insert and update items ids"); account.setLastModified(newLastModified); @@ -266,12 +266,19 @@ public class FreshRSSRepository extends ARepository { } } - private void insertUnreadItemsIds(List ids) { - database.itemsIdsDao().deleteUnreadItemsIds(account.getId()); - database.itemsIdsDao().insertUnreadItemsIds(ids.stream().map(id -> + private void insertItemsIds(List unreadIds, List starredIds) { + /*database.itemsIdsDao().deleteUnreadItemsIds(account.getId()); + database.itemsIdsDao().insertUnreadItemsIds(unreadIds.stream().map(id -> new UnreadItemsIds(0, id, account.getId())).collect(Collectors.toList())); database.itemDao().updateUnreadState(account.getId()); - database.itemDao().updateReadState(account.getId()); + database.itemDao().updateReadState(account.getId());*/ + + database.itemsIdsDao().deleteItemsIds(account.getId()); + database.itemsIdsDao().insertItemsIds(unreadIds.stream().map(id -> + new ItemStateId(0, true, starredIds.stream() + .anyMatch(starredId -> starredId.equals(id)), id, account.getId())) + .collect(Collectors.toList())); + } } diff --git a/db/schemas/com.readrops.db.Database/3.json b/db/schemas/com.readrops.db.Database/3.json index aa6d7e4b..e1c6a87d 100644 --- a/db/schemas/com.readrops.db.Database/3.json +++ b/db/schemas/com.readrops.db.Database/3.json @@ -2,7 +2,7 @@ "formatVersion": 1, "database": { "version": 3, - "identityHash": "be089eb90d96a8582a76c667963886cc", + "identityHash": "ebbfc42b6562b2363ec0e3005d107d5e", "entities": [ { "tableName": "Feed", @@ -658,12 +658,56 @@ ] } ] + }, + { + "tableName": "ItemStateId", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `read` INTEGER NOT NULL, `starred` INTEGER NOT NULL, `remote_id` TEXT NOT NULL, `account_id` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "read", + "columnName": "read", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "starred", + "columnName": "starred", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "remoteId", + "columnName": "remote_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "accountId", + "columnName": "account_id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] } ], "views": [], "setupQueries": [ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", - "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'be089eb90d96a8582a76c667963886cc')" + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'ebbfc42b6562b2363ec0e3005d107d5e')" ] } } \ No newline at end of file diff --git a/db/src/main/java/com/readrops/db/Database.kt b/db/src/main/java/com/readrops/db/Database.kt index 4482c2f2..0c86a09c 100644 --- a/db/src/main/java/com/readrops/db/Database.kt +++ b/db/src/main/java/com/readrops/db/Database.kt @@ -8,7 +8,8 @@ import com.readrops.db.entities.* import com.readrops.db.entities.account.Account import dev.matrix.roomigrant.GenerateRoomMigrations -@Database(entities = [Feed::class, Item::class, Folder::class, Account::class, UnreadItemsIds::class, ReadStarStateChange::class, StarredItem::class], version = 3) +@Database(entities = [Feed::class, Item::class, Folder::class, Account::class, UnreadItemsIds::class, + ReadStarStateChange::class, StarredItem::class, ItemStateId::class], version = 3) @TypeConverters(Converters::class) @GenerateRoomMigrations abstract class Database : RoomDatabase() { diff --git a/db/src/main/java/com/readrops/db/dao/ItemsIdsDao.kt b/db/src/main/java/com/readrops/db/dao/ItemsIdsDao.kt index dff975fb..db3d274c 100644 --- a/db/src/main/java/com/readrops/db/dao/ItemsIdsDao.kt +++ b/db/src/main/java/com/readrops/db/dao/ItemsIdsDao.kt @@ -4,6 +4,7 @@ import androidx.room.Dao import androidx.room.Delete import androidx.room.Insert import androidx.room.Query +import com.readrops.db.entities.ItemStateId import com.readrops.db.entities.ReadStarStateChange import com.readrops.db.entities.UnreadItemsIds import com.readrops.db.pojo.ItemReadStarState @@ -74,4 +75,10 @@ interface ItemsIdsDao { @Query("Select Case When :itemId In (Select id From ReadStarStateChange Where star_change = 1) Then 1 Else 0 End") fun starStateChangeExists(itemId: Int): Boolean + + @Query("Delete From ItemStateId Where account_id = :accountId") + fun deleteItemsIds(accountId: Int) + + @Insert + fun insertItemsIds(itemsIds: List) } \ No newline at end of file diff --git a/db/src/main/java/com/readrops/db/entities/ItemsIds.kt b/db/src/main/java/com/readrops/db/entities/ItemsIds.kt index 4568ab0e..f2deb20a 100644 --- a/db/src/main/java/com/readrops/db/entities/ItemsIds.kt +++ b/db/src/main/java/com/readrops/db/entities/ItemsIds.kt @@ -20,4 +20,13 @@ data class ReadStarStateChange( @ColumnInfo(name = "read_change") val readChange: Boolean = false, @ColumnInfo(name = "star_change") val starChange: Boolean = false, @ColumnInfo(name = "account_id") val accountId: Int, +) + +@Entity +data class ItemStateId( + @PrimaryKey(autoGenerate = true) val id: Int = 0, + val read: Boolean = false, + val starred: Boolean = false, + @ColumnInfo(name = "remote_id") val remoteId: String, + @ColumnInfo(name = "account_id") val accountId: Int, ) \ No newline at end of file From f412ae2f1eb0fb54bfa48a0e17d6db18052d57b6 Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Tue, 20 Apr 2021 23:22:32 +0200 Subject: [PATCH 144/187] Get starred items separately only for initial synchronisation and fetch starred items ids everytime --- .../services/freshrss/FreshRSSDataSource.java | 24 ++++++++++++------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/api/src/main/java/com/readrops/api/services/freshrss/FreshRSSDataSource.java b/api/src/main/java/com/readrops/api/services/freshrss/FreshRSSDataSource.java index 267e6bcb..16f4c602 100644 --- a/api/src/main/java/com/readrops/api/services/freshrss/FreshRSSDataSource.java +++ b/api/src/main/java/com/readrops/api/services/freshrss/FreshRSSDataSource.java @@ -12,7 +12,6 @@ import com.readrops.db.entities.Item; import java.io.StringReader; import java.util.Arrays; -import java.util.Collections; import java.util.List; import java.util.Properties; @@ -24,7 +23,6 @@ import okhttp3.RequestBody; public class FreshRSSDataSource { private static final int MAX_ITEMS = 5000; - private static final int MAX_UNREAD_ITEMS_IDS = 5000; private static final int MAX_STARRED_ITEMS = 1000; public static final String GOOGLE_READ = "user/-/state/com.google/read"; @@ -33,7 +31,7 @@ public class FreshRSSDataSource { private static final String FEED_PREFIX = "feed/"; - private FreshRSSService api; + private final FreshRSSService api; public FreshRSSDataSource(FreshRSSService api) { this.api = api; @@ -106,21 +104,29 @@ public class FreshRSSDataSource { if (syncType == SyncType.INITIAL_SYNC) { return getItems(Arrays.asList(GOOGLE_READ, GOOGLE_STARRED), MAX_ITEMS, null); } else { - return getItems(Collections.singletonList(GOOGLE_STARRED), MAX_ITEMS, syncData.getLastModified()); + return getItems(null, MAX_ITEMS, syncData.getLastModified()); } }) .flatMap(freshRSSItems -> { syncResult.setItems(freshRSSItems); - return getItemsIds(GOOGLE_READ, GOOGLE_READING_LIST, MAX_UNREAD_ITEMS_IDS); + return getItemsIds(GOOGLE_READ, GOOGLE_READING_LIST, MAX_ITEMS); }).flatMap(unreadItemsIds -> { syncResult.setUnreadIds(unreadItemsIds); - return getStarredItems(MAX_STARRED_ITEMS); - }).flatMap(starredItems -> { - syncResult.setStarredItems(starredItems); + return getItemsIds(null, GOOGLE_STARRED, MAX_STARRED_ITEMS); + }).flatMap(starredItemsIds -> { + syncResult.setStarredIds(starredItemsIds); - return Single.just(syncResult); + if (syncType == SyncType.INITIAL_SYNC) { + return getStarredItems(MAX_STARRED_ITEMS).flatMap(starredItems -> { + syncResult.setStarredItems(starredItems); + + return Single.just(syncResult); + }); + } else { + return Single.just(syncResult); + } })); } From e55dcca556533392844126abbd4bf5d0246f1ec7 Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Wed, 21 Apr 2021 19:26:01 +0200 Subject: [PATCH 145/187] Update item list query to use ItemStateId table --- .../readrops/app/repositories/FreshRSSRepository.java | 2 +- db/src/main/java/com/readrops/db/ItemsQueryBuilder.kt | 11 ++++++----- db/src/main/java/com/readrops/db/dao/ItemDao.java | 4 ++-- 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/com/readrops/app/repositories/FreshRSSRepository.java b/app/src/main/java/com/readrops/app/repositories/FreshRSSRepository.java index dc1843b7..b4012177 100644 --- a/app/src/main/java/com/readrops/app/repositories/FreshRSSRepository.java +++ b/app/src/main/java/com/readrops/app/repositories/FreshRSSRepository.java @@ -276,7 +276,7 @@ public class FreshRSSRepository extends ARepository { database.itemsIdsDao().deleteItemsIds(account.getId()); database.itemsIdsDao().insertItemsIds(unreadIds.stream().map(id -> - new ItemStateId(0, true, starredIds.stream() + new ItemStateId(0, false, starredIds.stream() .anyMatch(starredId -> starredId.equals(id)), id, account.getId())) .collect(Collectors.toList())); diff --git a/db/src/main/java/com/readrops/db/ItemsQueryBuilder.kt b/db/src/main/java/com/readrops/db/ItemsQueryBuilder.kt index 2e4b5664..66029d61 100644 --- a/db/src/main/java/com/readrops/db/ItemsQueryBuilder.kt +++ b/db/src/main/java/com/readrops/db/ItemsQueryBuilder.kt @@ -10,7 +10,8 @@ object ItemsQueryBuilder { private val COLUMNS = arrayOf("title", "clean_description", "image_link", "pub_date", "read_it_later", "Feed.name", "text_color", "background_color", "icon_url", "read_time", "Feed.id as feedId", "Feed.account_id", "Folder.id as folder_id", "Folder.name as folder_name", - "case When UnreadItemsIds.remote_id is NULL Then 1 else 0 End read") + "case When ItemStateId.remote_id is NULL Or ItemStateId.read = 1 Then 1 else 0 End read", + "case When ItemStateId.remote_id is NULL or ItemStateId.starred = 1 Then 1 else 0 End starred") private val ITEM_COLUMNS = arrayOf(".id", ".remoteId") @@ -47,9 +48,9 @@ object ItemsQueryBuilder { private fun buildWhereClause(queryFilters: QueryFilters): String = StringBuilder(500).run { append("Feed.account_id = ${queryFilters.accountId} And " + - "UnreadItemsIds.account_id = ${queryFilters.accountId} Or UnreadItemsIds.account_id is NULL And ") + "ItemStateId.account_id = ${queryFilters.accountId} Or ItemStateId.account_id is NULL And ") - if (!queryFilters.showReadItems) append("read = 0 And ") + //if (!queryFilters.showReadItems) append("read = 0 And ") when (queryFilters.filterType) { FilterType.FEED_FILTER -> append("feed_id = ${queryFilters.filterFeedId} And read_it_later = 0") @@ -77,8 +78,8 @@ object ItemsQueryBuilder { private fun buildSelectAllJoin(tableName: String): String = """ $tableName INNER JOIN Feed on $tableName.feed_id = Feed.id - LEFT JOIN Folder on Feed.folder_id = Folder.id LEFT JOIN UnreadItemsIds On - $tableName.remoteId = UnreadItemsIds.remote_id + LEFT JOIN Folder on Feed.folder_id = Folder.id LEFT JOIN ItemStateId On + $tableName.remoteId = ItemStateId.remote_id """.trimIndent() } diff --git a/db/src/main/java/com/readrops/db/dao/ItemDao.java b/db/src/main/java/com/readrops/db/dao/ItemDao.java index 71eca34e..acb83f7e 100644 --- a/db/src/main/java/com/readrops/db/dao/ItemDao.java +++ b/db/src/main/java/com/readrops/db/dao/ItemDao.java @@ -12,7 +12,7 @@ import androidx.sqlite.db.SupportSQLiteQuery; import com.readrops.db.entities.Feed; import com.readrops.db.entities.Folder; import com.readrops.db.entities.Item; -import com.readrops.db.entities.UnreadItemsIds; +import com.readrops.db.entities.ItemStateId; import com.readrops.db.pojo.ItemWithFeed; import com.readrops.db.pojo.StarItem; @@ -23,7 +23,7 @@ import io.reactivex.Completable; @Dao public interface ItemDao extends BaseDao { - @RawQuery(observedEntities = {Item.class, Folder.class, Feed.class, UnreadItemsIds.class}) + @RawQuery(observedEntities = {Item.class, Folder.class, Feed.class, ItemStateId.class}) DataSource.Factory selectAll(SupportSQLiteQuery query); @Query("Select * From Item Where id = :itemId") From b7b4ae9dc427bc497e214b09b0b25abb66d59c99 Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Thu, 22 Apr 2021 17:31:43 +0200 Subject: [PATCH 146/187] Disable query observing when synchronising to avoid view flickering --- app/src/main/java/com/readrops/app/itemslist/MainActivity.java | 2 ++ app/src/main/java/com/readrops/app/itemslist/MainViewModel.java | 1 + 2 files changed, 3 insertions(+) diff --git a/app/src/main/java/com/readrops/app/itemslist/MainActivity.java b/app/src/main/java/com/readrops/app/itemslist/MainActivity.java index fadacdf4..3e8992fc 100644 --- a/app/src/main/java/com/readrops/app/itemslist/MainActivity.java +++ b/app/src/main/java/com/readrops/app/itemslist/MainActivity.java @@ -631,6 +631,8 @@ public class MainActivity extends AppCompatActivity implements SwipeRefreshLayou @Override public void onComplete() { + viewModel.invalidate(); + if (viewModel.isAccountLocal() && feedNb > 0) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) binding.syncProgressBar.setProgress(100, true); diff --git a/app/src/main/java/com/readrops/app/itemslist/MainViewModel.java b/app/src/main/java/com/readrops/app/itemslist/MainViewModel.java index 2ad74215..9e8c0bc8 100644 --- a/app/src/main/java/com/readrops/app/itemslist/MainViewModel.java +++ b/app/src/main/java/com/readrops/app/itemslist/MainViewModel.java @@ -125,6 +125,7 @@ public class MainViewModel extends ViewModel { } public Observable sync(List feeds) { + itemsWithFeed.removeSource(lastFetch); return repository.sync(feeds); } From 3be044758449cab65b34ac057ba746a2f651c259 Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Thu, 22 Apr 2021 18:49:45 +0200 Subject: [PATCH 147/187] Update read state in ItemState table --- .../readrops/app/itemslist/MainActivity.java | 2 +- .../readrops/app/itemslist/MainViewModel.java | 3 +- .../app/repositories/ARepository.java | 3 +- .../app/repositories/FreshRSSRepository.java | 2 +- .../java/com/readrops/db/dao/ItemsIdsDao.kt | 53 +++++++++---------- 5 files changed, 31 insertions(+), 32 deletions(-) diff --git a/app/src/main/java/com/readrops/app/itemslist/MainActivity.java b/app/src/main/java/com/readrops/app/itemslist/MainActivity.java index 3e8992fc..daa37d5b 100644 --- a/app/src/main/java/com/readrops/app/itemslist/MainActivity.java +++ b/app/src/main/java/com/readrops/app/itemslist/MainActivity.java @@ -504,7 +504,7 @@ public class MainActivity extends AppCompatActivity implements SwipeRefreshLayou allItemsSelected = false; } else { - viewModel.setItemsReadState(adapter.getSelectedItems()) + viewModel.setItemsReadState(adapter.getSelectedItems(), read) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .doOnError(throwable -> Utils.showSnackbar(binding.mainRoot, throwable.getMessage())) diff --git a/app/src/main/java/com/readrops/app/itemslist/MainViewModel.java b/app/src/main/java/com/readrops/app/itemslist/MainViewModel.java index 9e8c0bc8..17571ee4 100644 --- a/app/src/main/java/com/readrops/app/itemslist/MainViewModel.java +++ b/app/src/main/java/com/readrops/app/itemslist/MainViewModel.java @@ -232,10 +232,11 @@ public class MainViewModel extends ViewModel { return repository.setItemReadState(item); } - public Completable setItemsReadState(List items) { + public Completable setItemsReadState(List items, boolean read) { List completableList = new ArrayList<>(); for (ItemWithFeed itemWithFeed : items) { + itemWithFeed.getItem().setRead(read); completableList.add(setItemReadState(itemWithFeed)); } diff --git a/app/src/main/java/com/readrops/app/repositories/ARepository.java b/app/src/main/java/com/readrops/app/repositories/ARepository.java index 6fbafa66..c5618d74 100644 --- a/app/src/main/java/com/readrops/app/repositories/ARepository.java +++ b/app/src/main/java/com/readrops/app/repositories/ARepository.java @@ -17,6 +17,7 @@ import com.readrops.db.Database; import com.readrops.db.entities.Feed; import com.readrops.db.entities.Folder; import com.readrops.db.entities.Item; +import com.readrops.db.entities.ItemStateId; import com.readrops.db.entities.ReadStarStateChange; import com.readrops.db.entities.account.Account; @@ -115,7 +116,7 @@ public abstract class ARepository { } public Completable setItemReadState(Item item) { - return database.itemDao().setReadState(item.getId(), item.isRead()) + return database.itemsIdsDao().upsertItemReadState(new ItemStateId(0, item.isRead(), item.isStarred(), item.getRemoteId(), account.getId())) .andThen(database.itemsIdsDao().upsertReadStarStateChange(new ReadStarStateChange(item.getId(), true, false, account.getId()))); } diff --git a/app/src/main/java/com/readrops/app/repositories/FreshRSSRepository.java b/app/src/main/java/com/readrops/app/repositories/FreshRSSRepository.java index b4012177..87de086a 100644 --- a/app/src/main/java/com/readrops/app/repositories/FreshRSSRepository.java +++ b/app/src/main/java/com/readrops/app/repositories/FreshRSSRepository.java @@ -275,7 +275,7 @@ public class FreshRSSRepository extends ARepository { database.itemDao().updateReadState(account.getId());*/ database.itemsIdsDao().deleteItemsIds(account.getId()); - database.itemsIdsDao().insertItemsIds(unreadIds.stream().map(id -> + database.itemsIdsDao().insertItemStateId(unreadIds.stream().map(id -> new ItemStateId(0, false, starredIds.stream() .anyMatch(starredId -> starredId.equals(id)), id, account.getId())) .collect(Collectors.toList())); diff --git a/db/src/main/java/com/readrops/db/dao/ItemsIdsDao.kt b/db/src/main/java/com/readrops/db/dao/ItemsIdsDao.kt index db3d274c..3bae0549 100644 --- a/db/src/main/java/com/readrops/db/dao/ItemsIdsDao.kt +++ b/db/src/main/java/com/readrops/db/dao/ItemsIdsDao.kt @@ -13,46 +13,21 @@ import io.reactivex.Completable @Dao interface ItemsIdsDao { - @Insert - fun insertUnreadItemsIds(unreadItemsIds: List) - - @Insert - fun insertUnreadItemId(unreadItemId: UnreadItemsIds) - - @Query("Delete From UnreadItemsIds Where remote_id = :remoteId And account_id = :accountId") - fun deleteUnreadItemId(remoteId: String, accountId: Int) - - fun upsertUnreadItemId(unreadItemId: UnreadItemsIds) = Completable.create { - if (unreadItemIdExists(unreadItemId.remoteId, unreadItemId.accountId)) { - deleteUnreadItemId(unreadItemId.remoteId, unreadItemId.accountId) - } else { - insertUnreadItemId(unreadItemId) - } - - it.onComplete() - } - - @Query("Select case When Exists (Select remote_id, account_id From UnreadItemsIds Where remote_id = :remoteId And account_id = :accountId) Then 1 else 0 End") - fun unreadItemIdExists(remoteId: String, accountId: Int): Boolean - @Insert fun insertReadStarStateChange(readStarStateChange: ReadStarStateChange) @Delete fun deleteReadStarStateChange(readStarStateChange: ReadStarStateChange) - @Query("Delete From UnreadItemsIds Where account_id = :accountId") - fun deleteUnreadItemsIds(accountId: Int) - @Query("Delete From ReadStarStateChange Where account_id = :accountId") fun deleteReadStarStateChanges(accountId: Int) @Query("Delete From ReadStarStateChange Where account_id = :accountId") fun deleteStateChanges(accountId: Int) - @Query("Select case When UnreadItemsIds.remote_id is NULL Then 1 else 0 End read, Item.remoteId, ReadStarStateChange.read_change, Item.starred, ReadStarStateChange.star_change " + + @Query("Select case When ItemStateId.remote_id is NULL Or ItemStateId.read = 1 Then 1 else 0 End read, Item.remoteId, ReadStarStateChange.read_change, Item.starred, ReadStarStateChange.star_change " + "From ReadStarStateChange Inner Join Item On ReadStarStateChange.id = Item.id " + - "Left Join UnreadItemsIds On UnreadItemsIds.remote_id = Item.remoteId Where ReadStarStateChange.account_id = :accountId") + "Left Join ItemStateId On ItemStateId.remote_id = Item.remoteId Where ReadStarStateChange.account_id = :accountId") fun getItemStateChanges(accountId: Int): List @Query("Select StarredItem.remoteId, Case When StarredItem.read = 1 then 0 else 1 end read, StarredItem.starred, ReadStarStateChange.read_change, " + @@ -79,6 +54,28 @@ interface ItemsIdsDao { @Query("Delete From ItemStateId Where account_id = :accountId") fun deleteItemsIds(accountId: Int) + @Query("Delete From ItemStateId Where remote_id = :remoteId And account_id = :accountId") + fun deleteItemStateId(remoteId: String, accountId: Int) + @Insert - fun insertItemsIds(itemsIds: List) + fun insertItemStateId(itemsIds: List) + + @Insert + fun insertItemStateId(itemStateId: ItemStateId) + + @Query("Update ItemStateId set read = :read Where remote_id = :remoteId And account_id = :accountId") + fun updateItemReadState(read: Boolean, remoteId: String, accountId: Int) + + @Query("Select case When Exists (Select remote_id, account_id From ItemStateId Where remote_id = :remoteId And account_id = :accountId) Then 1 else 0 End") + fun itemStateExists(remoteId: String, accountId: Int): Boolean + + fun upsertItemReadState(itemStateId: ItemStateId) = Completable.create { + if (itemStateExists(itemStateId.remoteId, itemStateId.accountId)) { + updateItemReadState(itemStateId.read, itemStateId.remoteId, itemStateId.accountId) + } else { + insertItemStateId(itemStateId) + } + + it.onComplete() + } } \ No newline at end of file From 90468faefcda56a61ffd125cba5a3eae96a46bee Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Thu, 22 Apr 2021 22:30:24 +0200 Subject: [PATCH 148/187] Send read/star state update with new tables --- .../app/repositories/FreshRSSRepository.java | 67 +++++++------------ .../java/com/readrops/db/dao/ItemsIdsDao.kt | 11 ++- 2 files changed, 27 insertions(+), 51 deletions(-) diff --git a/app/src/main/java/com/readrops/app/repositories/FreshRSSRepository.java b/app/src/main/java/com/readrops/app/repositories/FreshRSSRepository.java index 87de086a..ad3168c7 100644 --- a/app/src/main/java/com/readrops/app/repositories/FreshRSSRepository.java +++ b/app/src/main/java/com/readrops/app/repositories/FreshRSSRepository.java @@ -18,8 +18,8 @@ import com.readrops.db.entities.Feed; import com.readrops.db.entities.Folder; import com.readrops.db.entities.Item; import com.readrops.db.entities.ItemStateId; -import com.readrops.db.entities.StarredItem; import com.readrops.db.entities.account.Account; +import com.readrops.db.pojo.ItemReadStarState; import org.joda.time.DateTime; @@ -93,11 +93,27 @@ public class FreshRSSRepository extends ARepository { TimingLogger logger = new TimingLogger(TAG, "FreshRSS sync timer"); return Single.create(emitter -> { - syncData.setReadItemsIds(/*database.itemDao().getReadChanges(account.getId()*/Collections.emptyList()); - syncData.setUnreadItemsIds(/*database.itemDao().getUnreadChanges(account.getId())*/Collections.emptyList()); + List itemStateChanges = database.itemsIdsDao().getItemStateChanges(account.getId()); - syncData.setStarredItemsIds(/*database.itemDao().getFreshRSSStarChanges(account.getId())*/Collections.emptyList()); - syncData.setUnstarredItemsIds(/*database.itemDao().getFreshRSSUnstarChanges(account.getId())*/Collections.emptyList()); + syncData.setReadItemsIds(itemStateChanges.stream() + .filter(it -> it.getReadChange() && it.getRead()) + .map(ItemReadStarState::getRemoteId) + .collect(Collectors.toList())); + + syncData.setUnreadItemsIds(itemStateChanges.stream() + .filter(it -> it.getReadChange() && !it.getRead()) + .map(ItemReadStarState::getRemoteId) + .collect(Collectors.toList())); + + syncData.setStarredItemsIds(itemStateChanges.stream() + .filter(it -> it.getStarChange() && it.getStarred()) + .map(ItemReadStarState::getRemoteId) + .collect(Collectors.toList())); + + syncData.setUnstarredItemsIds(itemStateChanges.stream() + .filter(it -> it.getStarChange() && !it.getStarred()) + .map(ItemReadStarState::getRemoteId) + .collect(Collectors.toList())); emitter.onSuccess(syncData); }).flatMap(syncData1 -> dataSource.sync(syncType, syncData1, account.getWriteToken())) @@ -112,15 +128,14 @@ public class FreshRSSRepository extends ARepository { insertItems(syncResult.getItems()); logger.addSplit("items insertion"); - insertStarredItems(syncResult.getStarredItems()); - logger.addSplit("starred items insertion"); - insertItemsIds(syncResult.getUnreadIds(), syncResult.getStarredIds()); logger.addSplit("insert and update items ids"); account.setLastModified(newLastModified); database.accountDao().updateLastModified(account.getId(), newLastModified); + database.itemsIdsDao().resetStateChanges(account.getId()); + logger.dumpToLog(); this.syncResult = syncResult; @@ -237,43 +252,7 @@ public class FreshRSSRepository extends ARepository { } } - private void insertStarredItems(List items) { - List starredItems = items.stream().map(StarredItem::new).collect(Collectors.toList()); - - List itemsToInsert = new ArrayList<>(); - Map itemsFeedsIds = new HashMap<>(); - - for (StarredItem item : starredItems) { - int feedId; - - if (itemsFeedsIds.containsKey(item.getFeedRemoteId())) { - feedId = itemsFeedsIds.get(item.getFeedRemoteId()); - } else { - feedId = database.feedDao().getFeedIdByRemoteId(item.getFeedRemoteId(), account.getId()); - itemsFeedsIds.put(item.getFeedRemoteId(), feedId); - } - - item.setFeedId(feedId); - item.setReadTime(Utils.readTimeFromString(item.getContent())); - itemsToInsert.add(item); - } - - if (!itemsToInsert.isEmpty()) { - Collections.sort(itemsToInsert, Item::compareTo); - - database.starredItemDao().deleteStarredItems(account.getId()); - database.starredItemDao().insert(itemsToInsert); - } - } - private void insertItemsIds(List unreadIds, List starredIds) { - /*database.itemsIdsDao().deleteUnreadItemsIds(account.getId()); - database.itemsIdsDao().insertUnreadItemsIds(unreadIds.stream().map(id -> - new UnreadItemsIds(0, id, account.getId())).collect(Collectors.toList())); - - database.itemDao().updateUnreadState(account.getId()); - database.itemDao().updateReadState(account.getId());*/ - database.itemsIdsDao().deleteItemsIds(account.getId()); database.itemsIdsDao().insertItemStateId(unreadIds.stream().map(id -> new ItemStateId(0, false, starredIds.stream() diff --git a/db/src/main/java/com/readrops/db/dao/ItemsIdsDao.kt b/db/src/main/java/com/readrops/db/dao/ItemsIdsDao.kt index 3bae0549..eaa82f3b 100644 --- a/db/src/main/java/com/readrops/db/dao/ItemsIdsDao.kt +++ b/db/src/main/java/com/readrops/db/dao/ItemsIdsDao.kt @@ -6,7 +6,6 @@ import androidx.room.Insert import androidx.room.Query import com.readrops.db.entities.ItemStateId import com.readrops.db.entities.ReadStarStateChange -import com.readrops.db.entities.UnreadItemsIds import com.readrops.db.pojo.ItemReadStarState import io.reactivex.Completable @@ -23,17 +22,15 @@ interface ItemsIdsDao { fun deleteReadStarStateChanges(accountId: Int) @Query("Delete From ReadStarStateChange Where account_id = :accountId") - fun deleteStateChanges(accountId: Int) + fun resetStateChanges(accountId: Int) - @Query("Select case When ItemStateId.remote_id is NULL Or ItemStateId.read = 1 Then 1 else 0 End read, Item.remoteId, ReadStarStateChange.read_change, Item.starred, ReadStarStateChange.star_change " + + @Query("Select case When ItemStateId.remote_id is NULL Or ItemStateId.read = 1 Then 1 else 0 End read, " + + "case When ItemStateId.remote_id is NULL Or ItemStateId.starred = 1 Then 1 else 0 End starred," + + "ReadStarStateChange.read_change, ReadStarStateChange.star_change, Item.remoteId " + "From ReadStarStateChange Inner Join Item On ReadStarStateChange.id = Item.id " + "Left Join ItemStateId On ItemStateId.remote_id = Item.remoteId Where ReadStarStateChange.account_id = :accountId") fun getItemStateChanges(accountId: Int): List - @Query("Select StarredItem.remoteId, Case When StarredItem.read = 1 then 0 else 1 end read, StarredItem.starred, ReadStarStateChange.read_change, " + - "ReadStarStateChange.star_change From StarredItem Inner Join ReadStarStateChange On StarredItem.id = ReadStarStateChange.id Where account_id = :accountId") - fun getStarredItemStateChanges(accountId: Int): List - fun upsertReadStarStateChange(readStarStateChange: ReadStarStateChange) = Completable.create { if (readStarStateChange.readChange && readStateChangeExists(readStarStateChange.id) || readStarStateChange.starChange && starStateChangeExists(readStarStateChange.id)) { From 750bf10902b2a4b690d93fa7c968a2d5569e8179 Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Mon, 26 Apr 2021 13:56:10 +0200 Subject: [PATCH 149/187] Delete UnreadItemsIds and StarredItem tables --- .../com/readrops/app/item/ItemActivity.java | 2 +- .../com/readrops/app/item/ItemViewModel.java | 8 +- .../readrops/app/itemslist/MainViewModel.java | 6 +- db/schemas/com.readrops.db.Database/3.json | 204 +----------------- db/src/main/java/com/readrops/db/Database.kt | 6 +- .../com/readrops/db/dao/StarredItemDao.kt | 31 --- .../java/com/readrops/db/entities/ItemsIds.kt | 10 - .../com/readrops/db/entities/StarredItem.kt | 33 --- 8 files changed, 12 insertions(+), 288 deletions(-) delete mode 100644 db/src/main/java/com/readrops/db/dao/StarredItemDao.kt delete mode 100644 db/src/main/java/com/readrops/db/entities/StarredItem.kt diff --git a/app/src/main/java/com/readrops/app/item/ItemActivity.java b/app/src/main/java/com/readrops/app/item/ItemActivity.java index be7e0a63..45b2b59e 100644 --- a/app/src/main/java/com/readrops/app/item/ItemActivity.java +++ b/app/src/main/java/com/readrops/app/item/ItemActivity.java @@ -119,7 +119,7 @@ public class ItemActivity extends AppCompatActivity { })); viewModel = ViewModelCompat.getViewModel(this, ItemViewModel.class); - viewModel.getItemById(itemId, starredItem).observe(this, itemWithFeed1 -> { + viewModel.getItemById(itemId).observe(this, itemWithFeed1 -> { if (!uiBinded) { bindUI(itemWithFeed1); uiBinded = true; diff --git a/app/src/main/java/com/readrops/app/item/ItemViewModel.java b/app/src/main/java/com/readrops/app/item/ItemViewModel.java index 2e5f7c06..2c249c04 100644 --- a/app/src/main/java/com/readrops/app/item/ItemViewModel.java +++ b/app/src/main/java/com/readrops/app/item/ItemViewModel.java @@ -31,12 +31,8 @@ public class ItemViewModel extends ViewModel { this.database = database; } - public LiveData getItemById(int id, boolean starredItem) { - if (starredItem) { - return database.starredItemDao().getStarredItemById(id); - } else { - return database.itemDao().getItemById(id); - } + public LiveData getItemById(int id) { + return database.itemDao().getItemById(id); } public Completable setStarState(Item item) { diff --git a/app/src/main/java/com/readrops/app/itemslist/MainViewModel.java b/app/src/main/java/com/readrops/app/itemslist/MainViewModel.java index 17571ee4..087debe2 100644 --- a/app/src/main/java/com/readrops/app/itemslist/MainViewModel.java +++ b/app/src/main/java/com/readrops/app/itemslist/MainViewModel.java @@ -71,11 +71,15 @@ public class MainViewModel extends ViewModel { DataSource.Factory items; + +/* if (queryFilters.getFilterType() == FilterType.STARS_FILTER && currentAccount.getAccountType().getAccountConfig().useStarredItems()) { items = database.starredItemDao().selectAll(ItemsQueryBuilder.buildStarredItemsQuery(queryFilters)); } else { - items = database.itemDao().selectAll(ItemsQueryBuilder.buildItemsQuery(queryFilters)); + } +*/ + items = database.itemDao().selectAll(ItemsQueryBuilder.buildItemsQuery(queryFilters)); lastFetch = new LivePagedListBuilder<>(new RoomFactoryWrapper<>(items), new PagedList.Config.Builder() diff --git a/db/schemas/com.readrops.db.Database/3.json b/db/schemas/com.readrops.db.Database/3.json index e1c6a87d..7acfa765 100644 --- a/db/schemas/com.readrops.db.Database/3.json +++ b/db/schemas/com.readrops.db.Database/3.json @@ -2,7 +2,7 @@ "formatVersion": 1, "database": { "version": 3, - "identityHash": "ebbfc42b6562b2363ec0e3005d107d5e", + "identityHash": "276d6f5e56615e86b3634302b7565e95", "entities": [ { "tableName": "Feed", @@ -421,67 +421,6 @@ "indices": [], "foreignKeys": [] }, - { - "tableName": "UnreadItemsIds", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `remote_id` TEXT NOT NULL, `account_id` INTEGER NOT NULL, FOREIGN KEY(`account_id`) REFERENCES `Account`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", - "fields": [ - { - "fieldPath": "id", - "columnName": "id", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "remoteId", - "columnName": "remote_id", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "accountId", - "columnName": "account_id", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "columnNames": [ - "id" - ], - "autoGenerate": true - }, - "indices": [ - { - "name": "index_UnreadItemsIds_remote_id", - "unique": false, - "columnNames": [ - "remote_id" - ], - "createSql": "CREATE INDEX IF NOT EXISTS `index_UnreadItemsIds_remote_id` ON `${TABLE_NAME}` (`remote_id`)" - }, - { - "name": "index_UnreadItemsIds_account_id", - "unique": false, - "columnNames": [ - "account_id" - ], - "createSql": "CREATE INDEX IF NOT EXISTS `index_UnreadItemsIds_account_id` ON `${TABLE_NAME}` (`account_id`)" - } - ], - "foreignKeys": [ - { - "table": "Account", - "onDelete": "CASCADE", - "onUpdate": "NO ACTION", - "columns": [ - "account_id" - ], - "referencedColumns": [ - "id" - ] - } - ] - }, { "tableName": "ReadStarStateChange", "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `read_change` INTEGER NOT NULL, `star_change` INTEGER NOT NULL, `account_id` INTEGER NOT NULL, PRIMARY KEY(`id`))", @@ -520,145 +459,6 @@ "indices": [], "foreignKeys": [] }, - { - "tableName": "StarredItem", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `title` TEXT, `description` TEXT, `clean_description` TEXT, `link` TEXT, `image_link` TEXT, `author` TEXT, `pub_date` INTEGER, `content` TEXT, `feed_id` INTEGER NOT NULL, `guid` TEXT, `read_time` REAL NOT NULL, `read` INTEGER NOT NULL, `starred` INTEGER NOT NULL, `read_it_later` INTEGER NOT NULL, `remoteId` TEXT, FOREIGN KEY(`feed_id`) REFERENCES `Feed`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", - "fields": [ - { - "fieldPath": "id", - "columnName": "id", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "title", - "columnName": "title", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "description", - "columnName": "description", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "cleanDescription", - "columnName": "clean_description", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "link", - "columnName": "link", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "imageLink", - "columnName": "image_link", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "author", - "columnName": "author", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "pubDate", - "columnName": "pub_date", - "affinity": "INTEGER", - "notNull": false - }, - { - "fieldPath": "content", - "columnName": "content", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "feedId", - "columnName": "feed_id", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "guid", - "columnName": "guid", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "readTime", - "columnName": "read_time", - "affinity": "REAL", - "notNull": true - }, - { - "fieldPath": "read", - "columnName": "read", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "starred", - "columnName": "starred", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "readItLater", - "columnName": "read_it_later", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "remoteId", - "columnName": "remoteId", - "affinity": "TEXT", - "notNull": false - } - ], - "primaryKey": { - "columnNames": [ - "id" - ], - "autoGenerate": true - }, - "indices": [ - { - "name": "index_StarredItem_feed_id", - "unique": false, - "columnNames": [ - "feed_id" - ], - "createSql": "CREATE INDEX IF NOT EXISTS `index_StarredItem_feed_id` ON `${TABLE_NAME}` (`feed_id`)" - }, - { - "name": "index_StarredItem_guid", - "unique": false, - "columnNames": [ - "guid" - ], - "createSql": "CREATE INDEX IF NOT EXISTS `index_StarredItem_guid` ON `${TABLE_NAME}` (`guid`)" - } - ], - "foreignKeys": [ - { - "table": "Feed", - "onDelete": "CASCADE", - "onUpdate": "NO ACTION", - "columns": [ - "feed_id" - ], - "referencedColumns": [ - "id" - ] - } - ] - }, { "tableName": "ItemStateId", "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `read` INTEGER NOT NULL, `starred` INTEGER NOT NULL, `remote_id` TEXT NOT NULL, `account_id` INTEGER NOT NULL)", @@ -707,7 +507,7 @@ "views": [], "setupQueries": [ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", - "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'ebbfc42b6562b2363ec0e3005d107d5e')" + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '276d6f5e56615e86b3634302b7565e95')" ] } } \ No newline at end of file diff --git a/db/src/main/java/com/readrops/db/Database.kt b/db/src/main/java/com/readrops/db/Database.kt index 0c86a09c..fa645b35 100644 --- a/db/src/main/java/com/readrops/db/Database.kt +++ b/db/src/main/java/com/readrops/db/Database.kt @@ -8,8 +8,8 @@ import com.readrops.db.entities.* import com.readrops.db.entities.account.Account import dev.matrix.roomigrant.GenerateRoomMigrations -@Database(entities = [Feed::class, Item::class, Folder::class, Account::class, UnreadItemsIds::class, - ReadStarStateChange::class, StarredItem::class, ItemStateId::class], version = 3) +@Database(entities = [Feed::class, Item::class, Folder::class, Account::class, + ReadStarStateChange::class, ItemStateId::class], version = 3) @TypeConverters(Converters::class) @GenerateRoomMigrations abstract class Database : RoomDatabase() { @@ -22,6 +22,4 @@ abstract class Database : RoomDatabase() { abstract fun accountDao(): AccountDao abstract fun itemsIdsDao(): ItemsIdsDao - - abstract fun starredItemDao(): StarredItemDao } \ No newline at end of file diff --git a/db/src/main/java/com/readrops/db/dao/StarredItemDao.kt b/db/src/main/java/com/readrops/db/dao/StarredItemDao.kt deleted file mode 100644 index 98720321..00000000 --- a/db/src/main/java/com/readrops/db/dao/StarredItemDao.kt +++ /dev/null @@ -1,31 +0,0 @@ -package com.readrops.db.dao - -import androidx.lifecycle.LiveData -import androidx.paging.DataSource -import androidx.room.Dao -import androidx.room.Query -import androidx.room.RawQuery -import androidx.room.RoomWarnings -import androidx.sqlite.db.SupportSQLiteQuery -import com.readrops.db.entities.Feed -import com.readrops.db.entities.Folder -import com.readrops.db.entities.StarredItem -import com.readrops.db.entities.UnreadItemsIds -import com.readrops.db.pojo.ItemWithFeed - -@Dao -interface StarredItemDao : BaseDao { - - @Query("Delete From StarredItem Where feed_id In (Select feed_id From Feed Where account_id = :accountId)") - fun deleteStarredItems(accountId: Int) - - @RawQuery(observedEntities = [StarredItem::class, Folder::class, Feed::class, UnreadItemsIds::class]) - fun selectAll(query: SupportSQLiteQuery?): DataSource.Factory - - @SuppressWarnings(RoomWarnings.CURSOR_MISMATCH) - @Query("Select StarredItem.id, title, StarredItem.description, content, link, pub_date, image_link, author, read, text_color, " + - "background_color, read_time, starred, Feed.name, Feed.id as feedId, siteUrl, Folder.id as folder_id, " + - "Folder.name as folder_name from StarredItem Inner Join Feed On StarredItem.feed_id = Feed.id Left Join Folder on Folder.id = Feed.folder_id Where StarredItem.id = :id") - fun getStarredItemById(id: Int): LiveData - -} \ No newline at end of file diff --git a/db/src/main/java/com/readrops/db/entities/ItemsIds.kt b/db/src/main/java/com/readrops/db/entities/ItemsIds.kt index f2deb20a..4ba00a4c 100644 --- a/db/src/main/java/com/readrops/db/entities/ItemsIds.kt +++ b/db/src/main/java/com/readrops/db/entities/ItemsIds.kt @@ -2,17 +2,7 @@ package com.readrops.db.entities import androidx.room.ColumnInfo import androidx.room.Entity -import androidx.room.ForeignKey import androidx.room.PrimaryKey -import com.readrops.db.entities.account.Account - -@Entity(foreignKeys = [ForeignKey(entity = Account::class, parentColumns = ["id"], - childColumns = ["account_id"], onDelete = ForeignKey.CASCADE)]) -data class UnreadItemsIds( - @PrimaryKey(autoGenerate = true) val id: Int = 0, - @ColumnInfo(name = "remote_id", index = true) val remoteId: String, - @ColumnInfo(name = "account_id", index = true) val accountId: Int, -) @Entity data class ReadStarStateChange( diff --git a/db/src/main/java/com/readrops/db/entities/StarredItem.kt b/db/src/main/java/com/readrops/db/entities/StarredItem.kt deleted file mode 100644 index a77dc2ba..00000000 --- a/db/src/main/java/com/readrops/db/entities/StarredItem.kt +++ /dev/null @@ -1,33 +0,0 @@ -package com.readrops.db.entities - -import androidx.room.Entity -import androidx.room.ForeignKey -import androidx.room.Ignore - -@Entity(foreignKeys = [ForeignKey(entity = Feed::class, parentColumns = ["id"], childColumns = ["feed_id"], - onDelete = ForeignKey.CASCADE)], inheritSuperIndices = true) -class StarredItem() : Item() { - - // TODO really hacky, should be replaced by something better - @Ignore - constructor(item: Item) : this() { - id = item.id - title = item.title - description = item.description - cleanDescription = item.cleanDescription - link = item.link - imageLink = item.imageLink - author = item.author - pubDate = item.pubDate - content = item.content - feedId = item.feedId - guid = item.guid - readTime = item.readTime - isRead = item.isRead - isStarred = true // important here for the items query compatibility - isReadItLater = item.isReadItLater - remoteId = item.remoteId - feedRemoteId = item.feedRemoteId - } - -} \ No newline at end of file From 4540870257712dff3b1e2b3ec0bc3dea5e6fab3e Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Mon, 26 Apr 2021 19:53:54 +0200 Subject: [PATCH 150/187] Rename item state tables --- .../app/repositories/ARepository.java | 10 +-- .../app/repositories/FreshRSSRepository.java | 14 ++-- db/schemas/com.readrops.db.Database/3.json | 8 +- db/src/main/java/com/readrops/db/Database.kt | 6 +- .../java/com/readrops/db/dao/ItemDao.java | 4 +- .../com/readrops/db/dao/ItemStateChangeDao.kt | 46 +++++++++++ .../java/com/readrops/db/dao/ItemStateDao.kt | 39 ++++++++++ .../java/com/readrops/db/dao/ItemsIdsDao.kt | 78 ------------------- .../db/entities/{ItemsIds.kt => ItemState.kt} | 4 +- 9 files changed, 110 insertions(+), 99 deletions(-) create mode 100644 db/src/main/java/com/readrops/db/dao/ItemStateChangeDao.kt create mode 100644 db/src/main/java/com/readrops/db/dao/ItemStateDao.kt delete mode 100644 db/src/main/java/com/readrops/db/dao/ItemsIdsDao.kt rename db/src/main/java/com/readrops/db/entities/{ItemsIds.kt => ItemState.kt} (91%) diff --git a/app/src/main/java/com/readrops/app/repositories/ARepository.java b/app/src/main/java/com/readrops/app/repositories/ARepository.java index c5618d74..3ff351cb 100644 --- a/app/src/main/java/com/readrops/app/repositories/ARepository.java +++ b/app/src/main/java/com/readrops/app/repositories/ARepository.java @@ -17,8 +17,8 @@ import com.readrops.db.Database; import com.readrops.db.entities.Feed; import com.readrops.db.entities.Folder; import com.readrops.db.entities.Item; -import com.readrops.db.entities.ItemStateId; -import com.readrops.db.entities.ReadStarStateChange; +import com.readrops.db.entities.ItemState; +import com.readrops.db.entities.ItemStateChange; import com.readrops.db.entities.account.Account; import org.koin.java.KoinJavaComponent; @@ -116,8 +116,8 @@ public abstract class ARepository { } public Completable setItemReadState(Item item) { - return database.itemsIdsDao().upsertItemReadState(new ItemStateId(0, item.isRead(), item.isStarred(), item.getRemoteId(), account.getId())) - .andThen(database.itemsIdsDao().upsertReadStarStateChange(new ReadStarStateChange(item.getId(), + return database.itemStateDao().upsertItemReadState(new ItemState(0, item.isRead(), item.isStarred(), item.getRemoteId(), account.getId())) + .andThen(database.itemStateChangesDao().upsertItemStateChange(new ItemStateChange(item.getId(), true, false, account.getId()))); } @@ -131,7 +131,7 @@ public abstract class ARepository { public Completable setItemStarState(Item item) { return database.itemDao().setStarState(item.getId(), item.isStarred()) - .andThen(database.itemsIdsDao().upsertReadStarStateChange(new ReadStarStateChange(item.getId(), + .andThen(database.itemStateChangesDao().upsertItemStateChange(new ItemStateChange(item.getId(), false, true, account.getId()))); } diff --git a/app/src/main/java/com/readrops/app/repositories/FreshRSSRepository.java b/app/src/main/java/com/readrops/app/repositories/FreshRSSRepository.java index ad3168c7..a0398ebf 100644 --- a/app/src/main/java/com/readrops/app/repositories/FreshRSSRepository.java +++ b/app/src/main/java/com/readrops/app/repositories/FreshRSSRepository.java @@ -17,7 +17,7 @@ import com.readrops.db.Database; import com.readrops.db.entities.Feed; import com.readrops.db.entities.Folder; import com.readrops.db.entities.Item; -import com.readrops.db.entities.ItemStateId; +import com.readrops.db.entities.ItemState; import com.readrops.db.entities.account.Account; import com.readrops.db.pojo.ItemReadStarState; @@ -93,7 +93,9 @@ public class FreshRSSRepository extends ARepository { TimingLogger logger = new TimingLogger(TAG, "FreshRSS sync timer"); return Single.create(emitter -> { - List itemStateChanges = database.itemsIdsDao().getItemStateChanges(account.getId()); + List itemStateChanges = database + .itemStateChangesDao() + .getItemStateChanges(account.getId()); syncData.setReadItemsIds(itemStateChanges.stream() .filter(it -> it.getReadChange() && it.getRead()) @@ -134,7 +136,7 @@ public class FreshRSSRepository extends ARepository { account.setLastModified(newLastModified); database.accountDao().updateLastModified(account.getId(), newLastModified); - database.itemsIdsDao().resetStateChanges(account.getId()); + database.itemStateChangesDao().resetStateChanges(account.getId()); logger.dumpToLog(); @@ -253,9 +255,9 @@ public class FreshRSSRepository extends ARepository { } private void insertItemsIds(List unreadIds, List starredIds) { - database.itemsIdsDao().deleteItemsIds(account.getId()); - database.itemsIdsDao().insertItemStateId(unreadIds.stream().map(id -> - new ItemStateId(0, false, starredIds.stream() + database.itemStateDao().deleteItemsStates(account.getId()); + database.itemStateDao().insertItemStates(unreadIds.stream().map(id -> + new ItemState(0, false, starredIds.stream() .anyMatch(starredId -> starredId.equals(id)), id, account.getId())) .collect(Collectors.toList())); diff --git a/db/schemas/com.readrops.db.Database/3.json b/db/schemas/com.readrops.db.Database/3.json index 7acfa765..cfd61c7c 100644 --- a/db/schemas/com.readrops.db.Database/3.json +++ b/db/schemas/com.readrops.db.Database/3.json @@ -2,7 +2,7 @@ "formatVersion": 1, "database": { "version": 3, - "identityHash": "276d6f5e56615e86b3634302b7565e95", + "identityHash": "a1f1be38bb62d3d52c1563c7a2472a4c", "entities": [ { "tableName": "Feed", @@ -422,7 +422,7 @@ "foreignKeys": [] }, { - "tableName": "ReadStarStateChange", + "tableName": "ItemStateChange", "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `read_change` INTEGER NOT NULL, `star_change` INTEGER NOT NULL, `account_id` INTEGER NOT NULL, PRIMARY KEY(`id`))", "fields": [ { @@ -460,7 +460,7 @@ "foreignKeys": [] }, { - "tableName": "ItemStateId", + "tableName": "ItemState", "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `read` INTEGER NOT NULL, `starred` INTEGER NOT NULL, `remote_id` TEXT NOT NULL, `account_id` INTEGER NOT NULL)", "fields": [ { @@ -507,7 +507,7 @@ "views": [], "setupQueries": [ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", - "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '276d6f5e56615e86b3634302b7565e95')" + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'a1f1be38bb62d3d52c1563c7a2472a4c')" ] } } \ No newline at end of file diff --git a/db/src/main/java/com/readrops/db/Database.kt b/db/src/main/java/com/readrops/db/Database.kt index fa645b35..16ab3856 100644 --- a/db/src/main/java/com/readrops/db/Database.kt +++ b/db/src/main/java/com/readrops/db/Database.kt @@ -9,7 +9,7 @@ import com.readrops.db.entities.account.Account import dev.matrix.roomigrant.GenerateRoomMigrations @Database(entities = [Feed::class, Item::class, Folder::class, Account::class, - ReadStarStateChange::class, ItemStateId::class], version = 3) + ItemStateChange::class, ItemState::class], version = 3) @TypeConverters(Converters::class) @GenerateRoomMigrations abstract class Database : RoomDatabase() { @@ -21,5 +21,7 @@ abstract class Database : RoomDatabase() { abstract fun accountDao(): AccountDao - abstract fun itemsIdsDao(): ItemsIdsDao + abstract fun itemStateDao(): ItemStateDao + + abstract fun itemStateChangesDao(): ItemStateChangeDao } \ No newline at end of file diff --git a/db/src/main/java/com/readrops/db/dao/ItemDao.java b/db/src/main/java/com/readrops/db/dao/ItemDao.java index acb83f7e..3d1cf353 100644 --- a/db/src/main/java/com/readrops/db/dao/ItemDao.java +++ b/db/src/main/java/com/readrops/db/dao/ItemDao.java @@ -12,7 +12,7 @@ import androidx.sqlite.db.SupportSQLiteQuery; import com.readrops.db.entities.Feed; import com.readrops.db.entities.Folder; import com.readrops.db.entities.Item; -import com.readrops.db.entities.ItemStateId; +import com.readrops.db.entities.ItemState; import com.readrops.db.pojo.ItemWithFeed; import com.readrops.db.pojo.StarItem; @@ -23,7 +23,7 @@ import io.reactivex.Completable; @Dao public interface ItemDao extends BaseDao { - @RawQuery(observedEntities = {Item.class, Folder.class, Feed.class, ItemStateId.class}) + @RawQuery(observedEntities = {Item.class, Folder.class, Feed.class, ItemState.class}) DataSource.Factory selectAll(SupportSQLiteQuery query); @Query("Select * From Item Where id = :itemId") diff --git a/db/src/main/java/com/readrops/db/dao/ItemStateChangeDao.kt b/db/src/main/java/com/readrops/db/dao/ItemStateChangeDao.kt new file mode 100644 index 00000000..4a25de7a --- /dev/null +++ b/db/src/main/java/com/readrops/db/dao/ItemStateChangeDao.kt @@ -0,0 +1,46 @@ +package com.readrops.db.dao + +import androidx.room.Dao +import androidx.room.Delete +import androidx.room.Insert +import androidx.room.Query +import com.readrops.db.entities.ItemStateChange +import com.readrops.db.pojo.ItemReadStarState +import io.reactivex.Completable + +@Dao +interface ItemStateChangeDao : BaseDao { + + @Insert + fun insertItemStateChange(itemStateChange: ItemStateChange) + + @Delete + fun deleteItemStateChange(itemStateChange: ItemStateChange) + + @Query("Delete From ItemStateChange Where account_id = :accountId") + fun resetStateChanges(accountId: Int) + + @Query("Select case When ItemState.remote_id is NULL Or ItemState.read = 1 Then 1 else 0 End read, " + + "case When ItemState.remote_id is NULL Or ItemState.starred = 1 Then 1 else 0 End starred," + + "ItemStateChange.read_change, ItemStateChange.star_change, Item.remoteId " + + "From ItemStateChange Inner Join Item On ItemStateChange.id = Item.id " + + "Left Join ItemState On ItemState.remote_id = Item.remoteId Where ItemStateChange.account_id = :accountId") + fun getItemStateChanges(accountId: Int): List + + fun upsertItemStateChange(itemStateChange: ItemStateChange) = Completable.create { + if (itemStateChange.readChange && readStateChangeExists(itemStateChange.id) || + itemStateChange.starChange && starStateChangeExists(itemStateChange.id)) { + deleteItemStateChange(itemStateChange) + } else { + insertItemStateChange(itemStateChange) + } + + it.onComplete() + } + + @Query("Select Case When :itemId In (Select id From ItemStateChange Where read_change = 1) Then 1 Else 0 End") + fun readStateChangeExists(itemId: Int): Boolean + + @Query("Select Case When :itemId In (Select id From ItemStateChange Where star_change = 1) Then 1 Else 0 End") + fun starStateChangeExists(itemId: Int): Boolean +} \ No newline at end of file diff --git a/db/src/main/java/com/readrops/db/dao/ItemStateDao.kt b/db/src/main/java/com/readrops/db/dao/ItemStateDao.kt new file mode 100644 index 00000000..7efa52ef --- /dev/null +++ b/db/src/main/java/com/readrops/db/dao/ItemStateDao.kt @@ -0,0 +1,39 @@ +package com.readrops.db.dao + +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.Query +import com.readrops.db.entities.ItemState +import io.reactivex.Completable + +@Dao +interface ItemStateDao : BaseDao { + + @Query("Delete From ItemState Where account_id = :accountId") + fun deleteItemsStates(accountId: Int) + + @Query("Delete From ItemState Where remote_id = :remoteId And account_id = :accountId") + fun deleteItemState(remoteId: String, accountId: Int) + + @Insert + fun insertItemStates(items: List) + + @Insert + fun insertItemState(itemState: ItemState) + + @Query("Update ItemState set read = :read Where remote_id = :remoteId And account_id = :accountId") + fun updateItemReadState(read: Boolean, remoteId: String, accountId: Int) + + @Query("Select case When Exists (Select remote_id, account_id From ItemState Where remote_id = :remoteId And account_id = :accountId) Then 1 else 0 End") + fun itemStateExists(remoteId: String, accountId: Int): Boolean + + fun upsertItemReadState(itemState: ItemState) = Completable.create { + if (itemStateExists(itemState.remoteId, itemState.accountId)) { + updateItemReadState(itemState.read, itemState.remoteId, itemState.accountId) + } else { + insertItemState(itemState) + } + + it.onComplete() + } +} \ No newline at end of file diff --git a/db/src/main/java/com/readrops/db/dao/ItemsIdsDao.kt b/db/src/main/java/com/readrops/db/dao/ItemsIdsDao.kt deleted file mode 100644 index eaa82f3b..00000000 --- a/db/src/main/java/com/readrops/db/dao/ItemsIdsDao.kt +++ /dev/null @@ -1,78 +0,0 @@ -package com.readrops.db.dao - -import androidx.room.Dao -import androidx.room.Delete -import androidx.room.Insert -import androidx.room.Query -import com.readrops.db.entities.ItemStateId -import com.readrops.db.entities.ReadStarStateChange -import com.readrops.db.pojo.ItemReadStarState -import io.reactivex.Completable - -@Dao -interface ItemsIdsDao { - - @Insert - fun insertReadStarStateChange(readStarStateChange: ReadStarStateChange) - - @Delete - fun deleteReadStarStateChange(readStarStateChange: ReadStarStateChange) - - @Query("Delete From ReadStarStateChange Where account_id = :accountId") - fun deleteReadStarStateChanges(accountId: Int) - - @Query("Delete From ReadStarStateChange Where account_id = :accountId") - fun resetStateChanges(accountId: Int) - - @Query("Select case When ItemStateId.remote_id is NULL Or ItemStateId.read = 1 Then 1 else 0 End read, " + - "case When ItemStateId.remote_id is NULL Or ItemStateId.starred = 1 Then 1 else 0 End starred," + - "ReadStarStateChange.read_change, ReadStarStateChange.star_change, Item.remoteId " + - "From ReadStarStateChange Inner Join Item On ReadStarStateChange.id = Item.id " + - "Left Join ItemStateId On ItemStateId.remote_id = Item.remoteId Where ReadStarStateChange.account_id = :accountId") - fun getItemStateChanges(accountId: Int): List - - fun upsertReadStarStateChange(readStarStateChange: ReadStarStateChange) = Completable.create { - if (readStarStateChange.readChange && readStateChangeExists(readStarStateChange.id) || - readStarStateChange.starChange && starStateChangeExists(readStarStateChange.id)) { - deleteReadStarStateChange(readStarStateChange) - } else { - insertReadStarStateChange(readStarStateChange) - } - - it.onComplete() - } - - @Query("Select Case When :itemId In (Select id From ReadStarStateChange Where read_change = 1) Then 1 Else 0 End") - fun readStateChangeExists(itemId: Int): Boolean - - @Query("Select Case When :itemId In (Select id From ReadStarStateChange Where star_change = 1) Then 1 Else 0 End") - fun starStateChangeExists(itemId: Int): Boolean - - @Query("Delete From ItemStateId Where account_id = :accountId") - fun deleteItemsIds(accountId: Int) - - @Query("Delete From ItemStateId Where remote_id = :remoteId And account_id = :accountId") - fun deleteItemStateId(remoteId: String, accountId: Int) - - @Insert - fun insertItemStateId(itemsIds: List) - - @Insert - fun insertItemStateId(itemStateId: ItemStateId) - - @Query("Update ItemStateId set read = :read Where remote_id = :remoteId And account_id = :accountId") - fun updateItemReadState(read: Boolean, remoteId: String, accountId: Int) - - @Query("Select case When Exists (Select remote_id, account_id From ItemStateId Where remote_id = :remoteId And account_id = :accountId) Then 1 else 0 End") - fun itemStateExists(remoteId: String, accountId: Int): Boolean - - fun upsertItemReadState(itemStateId: ItemStateId) = Completable.create { - if (itemStateExists(itemStateId.remoteId, itemStateId.accountId)) { - updateItemReadState(itemStateId.read, itemStateId.remoteId, itemStateId.accountId) - } else { - insertItemStateId(itemStateId) - } - - it.onComplete() - } -} \ No newline at end of file diff --git a/db/src/main/java/com/readrops/db/entities/ItemsIds.kt b/db/src/main/java/com/readrops/db/entities/ItemState.kt similarity index 91% rename from db/src/main/java/com/readrops/db/entities/ItemsIds.kt rename to db/src/main/java/com/readrops/db/entities/ItemState.kt index 4ba00a4c..96f719a9 100644 --- a/db/src/main/java/com/readrops/db/entities/ItemsIds.kt +++ b/db/src/main/java/com/readrops/db/entities/ItemState.kt @@ -5,7 +5,7 @@ import androidx.room.Entity import androidx.room.PrimaryKey @Entity -data class ReadStarStateChange( +data class ItemStateChange( @PrimaryKey val id: Int = 0, @ColumnInfo(name = "read_change") val readChange: Boolean = false, @ColumnInfo(name = "star_change") val starChange: Boolean = false, @@ -13,7 +13,7 @@ data class ReadStarStateChange( ) @Entity -data class ItemStateId( +data class ItemState( @PrimaryKey(autoGenerate = true) val id: Int = 0, val read: Boolean = false, val starred: Boolean = false, From 80f4590014b3885b4cfdd5fc28603b0f77e21d43 Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Thu, 6 May 2021 21:38:48 +0200 Subject: [PATCH 151/187] Add foreign keys to ItemState and ItemStateChange tables --- db/schemas/com.readrops.db.Database/3.json | 36 +++++++++++++++---- .../com/readrops/db/entities/ItemState.kt | 8 +++-- 2 files changed, 36 insertions(+), 8 deletions(-) diff --git a/db/schemas/com.readrops.db.Database/3.json b/db/schemas/com.readrops.db.Database/3.json index cfd61c7c..464e5f8b 100644 --- a/db/schemas/com.readrops.db.Database/3.json +++ b/db/schemas/com.readrops.db.Database/3.json @@ -2,7 +2,7 @@ "formatVersion": 1, "database": { "version": 3, - "identityHash": "a1f1be38bb62d3d52c1563c7a2472a4c", + "identityHash": "3c36644243041ad4676b32310d961ca9", "entities": [ { "tableName": "Feed", @@ -423,7 +423,7 @@ }, { "tableName": "ItemStateChange", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `read_change` INTEGER NOT NULL, `star_change` INTEGER NOT NULL, `account_id` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `read_change` INTEGER NOT NULL, `star_change` INTEGER NOT NULL, `account_id` INTEGER NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`account_id`) REFERENCES `Account`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", "fields": [ { "fieldPath": "id", @@ -457,11 +457,23 @@ "autoGenerate": false }, "indices": [], - "foreignKeys": [] + "foreignKeys": [ + { + "table": "Account", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "account_id" + ], + "referencedColumns": [ + "id" + ] + } + ] }, { "tableName": "ItemState", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `read` INTEGER NOT NULL, `starred` INTEGER NOT NULL, `remote_id` TEXT NOT NULL, `account_id` INTEGER NOT NULL)", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `read` INTEGER NOT NULL, `starred` INTEGER NOT NULL, `remote_id` TEXT NOT NULL, `account_id` INTEGER NOT NULL, FOREIGN KEY(`account_id`) REFERENCES `Account`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", "fields": [ { "fieldPath": "id", @@ -501,13 +513,25 @@ "autoGenerate": true }, "indices": [], - "foreignKeys": [] + "foreignKeys": [ + { + "table": "Account", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "account_id" + ], + "referencedColumns": [ + "id" + ] + } + ] } ], "views": [], "setupQueries": [ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", - "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'a1f1be38bb62d3d52c1563c7a2472a4c')" + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '3c36644243041ad4676b32310d961ca9')" ] } } \ No newline at end of file diff --git a/db/src/main/java/com/readrops/db/entities/ItemState.kt b/db/src/main/java/com/readrops/db/entities/ItemState.kt index 96f719a9..aebca1de 100644 --- a/db/src/main/java/com/readrops/db/entities/ItemState.kt +++ b/db/src/main/java/com/readrops/db/entities/ItemState.kt @@ -2,9 +2,12 @@ package com.readrops.db.entities import androidx.room.ColumnInfo import androidx.room.Entity +import androidx.room.ForeignKey import androidx.room.PrimaryKey +import com.readrops.db.entities.account.Account -@Entity +@Entity(foreignKeys = [ForeignKey(entity = Account::class, parentColumns = ["id"], + childColumns = ["account_id"], onDelete = ForeignKey.CASCADE)]) data class ItemStateChange( @PrimaryKey val id: Int = 0, @ColumnInfo(name = "read_change") val readChange: Boolean = false, @@ -12,7 +15,8 @@ data class ItemStateChange( @ColumnInfo(name = "account_id") val accountId: Int, ) -@Entity +@Entity(foreignKeys = [ForeignKey(entity = Account::class, parentColumns = ["id"], + childColumns = ["account_id"], onDelete = ForeignKey.CASCADE)]) data class ItemState( @PrimaryKey(autoGenerate = true) val id: Int = 0, val read: Boolean = false, From ede2b77ef8990c4cf6e2ac9d34d8589c36df7aea Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Thu, 6 May 2021 21:42:14 +0200 Subject: [PATCH 152/187] Fix item list query table renaming --- db/src/main/java/com/readrops/db/ItemsQueryBuilder.kt | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/db/src/main/java/com/readrops/db/ItemsQueryBuilder.kt b/db/src/main/java/com/readrops/db/ItemsQueryBuilder.kt index 66029d61..0820ca64 100644 --- a/db/src/main/java/com/readrops/db/ItemsQueryBuilder.kt +++ b/db/src/main/java/com/readrops/db/ItemsQueryBuilder.kt @@ -10,8 +10,8 @@ object ItemsQueryBuilder { private val COLUMNS = arrayOf("title", "clean_description", "image_link", "pub_date", "read_it_later", "Feed.name", "text_color", "background_color", "icon_url", "read_time", "Feed.id as feedId", "Feed.account_id", "Folder.id as folder_id", "Folder.name as folder_name", - "case When ItemStateId.remote_id is NULL Or ItemStateId.read = 1 Then 1 else 0 End read", - "case When ItemStateId.remote_id is NULL or ItemStateId.starred = 1 Then 1 else 0 End starred") + "case When ItemState.remote_id is NULL Or ItemState.read = 1 Then 1 else 0 End read", + "case When ItemState.remote_id is NULL or ItemState.starred = 1 Then 1 else 0 End starred") private val ITEM_COLUMNS = arrayOf(".id", ".remoteId") @@ -48,7 +48,7 @@ object ItemsQueryBuilder { private fun buildWhereClause(queryFilters: QueryFilters): String = StringBuilder(500).run { append("Feed.account_id = ${queryFilters.accountId} And " + - "ItemStateId.account_id = ${queryFilters.accountId} Or ItemStateId.account_id is NULL And ") + "ItemState.account_id = ${queryFilters.accountId} Or ItemState.account_id is NULL And ") //if (!queryFilters.showReadItems) append("read = 0 And ") @@ -78,8 +78,8 @@ object ItemsQueryBuilder { private fun buildSelectAllJoin(tableName: String): String = """ $tableName INNER JOIN Feed on $tableName.feed_id = Feed.id - LEFT JOIN Folder on Feed.folder_id = Folder.id LEFT JOIN ItemStateId On - $tableName.remoteId = ItemStateId.remote_id + LEFT JOIN Folder on Feed.folder_id = Folder.id LEFT JOIN ItemState On + $tableName.remoteId = ItemState.remote_id """.trimIndent() } From d15315b53f6465af44f0ce522482455de95b5e99 Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Thu, 6 May 2021 22:10:27 +0200 Subject: [PATCH 153/187] Synchronize local read/star changes --- .../com/readrops/app/item/ItemActivity.java | 6 +- .../com/readrops/app/item/ItemViewModel.java | 10 ++- .../readrops/app/itemslist/MainActivity.java | 6 +- .../app/repositories/ARepository.java | 14 ++-- .../java/com/readrops/db/dao/ItemDao.java | 11 +-- .../com/readrops/db/dao/ItemStateChangeDao.kt | 76 ++++++++++++++++--- .../java/com/readrops/db/dao/ItemStateDao.kt | 13 ++++ 7 files changed, 102 insertions(+), 34 deletions(-) diff --git a/app/src/main/java/com/readrops/app/item/ItemActivity.java b/app/src/main/java/com/readrops/app/item/ItemActivity.java index 45b2b59e..f45f5e1d 100644 --- a/app/src/main/java/com/readrops/app/item/ItemActivity.java +++ b/app/src/main/java/com/readrops/app/item/ItemActivity.java @@ -38,6 +38,7 @@ import com.readrops.app.utils.PermissionManager; import com.readrops.app.utils.SharedPreferencesManager; import com.readrops.app.utils.Utils; import com.readrops.db.entities.Item; +import com.readrops.db.entities.account.Account; import com.readrops.db.pojo.ItemWithFeed; import org.koin.androidx.viewmodel.compat.ViewModelCompat; @@ -49,10 +50,10 @@ import java.util.regex.Pattern; import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.schedulers.Schedulers; +import static com.readrops.app.utils.ReadropsKeys.ACCOUNT; import static com.readrops.app.utils.ReadropsKeys.ACTION_BAR_COLOR; import static com.readrops.app.utils.ReadropsKeys.IMAGE_URL; import static com.readrops.app.utils.ReadropsKeys.ITEM_ID; -import static com.readrops.app.utils.ReadropsKeys.STARRED_ITEM; import static com.readrops.app.utils.ReadropsKeys.WEB_URL; public class ItemActivity extends AppCompatActivity { @@ -82,7 +83,7 @@ public class ItemActivity extends AppCompatActivity { Intent intent = getIntent(); int itemId = intent.getIntExtra(ITEM_ID, 0); String imageUrl = intent.getStringExtra(IMAGE_URL); - boolean starredItem = intent.getBooleanExtra(STARRED_ITEM, false); + Account account = intent.getParcelableExtra(ACCOUNT); setSupportActionBar(binding.collapsingLayoutToolbar); @@ -119,6 +120,7 @@ public class ItemActivity extends AppCompatActivity { })); viewModel = ViewModelCompat.getViewModel(this, ItemViewModel.class); + viewModel.setAccount(account); viewModel.getItemById(itemId).observe(this, itemWithFeed1 -> { if (!uiBinded) { bindUI(itemWithFeed1); diff --git a/app/src/main/java/com/readrops/app/item/ItemViewModel.java b/app/src/main/java/com/readrops/app/item/ItemViewModel.java index 2c249c04..d71bcca6 100644 --- a/app/src/main/java/com/readrops/app/item/ItemViewModel.java +++ b/app/src/main/java/com/readrops/app/item/ItemViewModel.java @@ -12,8 +12,10 @@ import androidx.lifecycle.ViewModel; import com.readrops.app.repositories.ARepository; import com.readrops.db.Database; import com.readrops.db.entities.Item; +import com.readrops.db.entities.account.Account; import com.readrops.db.pojo.ItemWithFeed; +import org.koin.core.parameter.DefinitionParametersKt; import org.koin.java.KoinJavaComponent; import java.io.File; @@ -26,17 +28,23 @@ import io.reactivex.Completable; public class ItemViewModel extends ViewModel { private final Database database; + private Account account; public ItemViewModel(@NonNull Database database) { this.database = database; } + public void setAccount(Account account) { + this.account = account; + } + public LiveData getItemById(int id) { return database.itemDao().getItemById(id); } public Completable setStarState(Item item) { - return KoinJavaComponent.get(ARepository.class).setItemStarState(item); + return KoinJavaComponent.get(ARepository.class, null, () -> DefinitionParametersKt.parametersOf(account)) + .setItemStarState(item); } public Uri saveImageInCache(Bitmap bitmap, Context context) throws IOException { diff --git a/app/src/main/java/com/readrops/app/itemslist/MainActivity.java b/app/src/main/java/com/readrops/app/itemslist/MainActivity.java index daa37d5b..fe084296 100644 --- a/app/src/main/java/com/readrops/app/itemslist/MainActivity.java +++ b/app/src/main/java/com/readrops/app/itemslist/MainActivity.java @@ -73,7 +73,6 @@ import static com.readrops.app.utils.ReadropsKeys.FROM_MAIN_ACTIVITY; import static com.readrops.app.utils.ReadropsKeys.IMAGE_URL; import static com.readrops.app.utils.ReadropsKeys.ITEM_ID; import static com.readrops.app.utils.ReadropsKeys.SETTINGS; -import static com.readrops.app.utils.ReadropsKeys.STARRED_ITEM; import static com.readrops.app.utils.ReadropsKeys.SYNCING; public class MainActivity extends AppCompatActivity implements SwipeRefreshLayout.OnRefreshListener, @@ -229,6 +228,7 @@ public class MainActivity extends AppCompatActivity implements SwipeRefreshLayou if (intent.hasExtra(ITEM_ID) && intent.hasExtra(IMAGE_URL)) { Intent itemIntent = new Intent(this, ItemActivity.class); itemIntent.putExtras(intent); + itemIntent.putExtra(ACCOUNT, viewModel.getCurrentAccount()); startActivity(itemIntent); @@ -318,8 +318,8 @@ public class MainActivity extends AppCompatActivity implements SwipeRefreshLayou intent.putExtra(ITEM_ID, itemWithFeed.getItem().getId()); intent.putExtra(IMAGE_URL, itemWithFeed.getItem().getImageLink()); - intent.putExtra(STARRED_ITEM, drawerManager.getCurrentSelection() == DrawerManager.STARS_ID && - viewModel.getCurrentAccount().getConfig().useStarredItems()); + intent.putExtra(ACCOUNT, viewModel.getCurrentAccount()); + startActivityForResult(intent, ITEM_REQUEST); itemWithFeed.getItem().setRead(true); diff --git a/app/src/main/java/com/readrops/app/repositories/ARepository.java b/app/src/main/java/com/readrops/app/repositories/ARepository.java index 3ff351cb..068b71f1 100644 --- a/app/src/main/java/com/readrops/app/repositories/ARepository.java +++ b/app/src/main/java/com/readrops/app/repositories/ARepository.java @@ -18,7 +18,6 @@ import com.readrops.db.entities.Feed; import com.readrops.db.entities.Folder; import com.readrops.db.entities.Item; import com.readrops.db.entities.ItemState; -import com.readrops.db.entities.ItemStateChange; import com.readrops.db.entities.account.Account; import org.koin.java.KoinJavaComponent; @@ -116,9 +115,9 @@ public abstract class ARepository { } public Completable setItemReadState(Item item) { - return database.itemStateDao().upsertItemReadState(new ItemState(0, item.isRead(), item.isStarred(), item.getRemoteId(), account.getId())) - .andThen(database.itemStateChangesDao().upsertItemStateChange(new ItemStateChange(item.getId(), - true, false, account.getId()))); + return database.itemStateChangesDao().upsertItemReadStateChange(item, account.getId()) + .andThen(database.itemStateDao().upsertItemReadState(new ItemState(0, item.isRead(), + item.isStarred(), item.getRemoteId(), account.getId()))); } public Completable setAllItemsReadState(boolean read) { @@ -130,9 +129,10 @@ public abstract class ARepository { } public Completable setItemStarState(Item item) { - return database.itemDao().setStarState(item.getId(), item.isStarred()) - .andThen(database.itemStateChangesDao().upsertItemStateChange(new ItemStateChange(item.getId(), - false, true, account.getId()))); + return database.itemStateChangesDao().upsertItemStarStateChange(item, account.getId()) + .andThen(database.itemStateDao().upsertItemStarState(new ItemState(0, item.isRead(), + item.isStarred(), item.getRemoteId(), account.getId()))); + } public Single getFeedCount(int accountId) { diff --git a/db/src/main/java/com/readrops/db/dao/ItemDao.java b/db/src/main/java/com/readrops/db/dao/ItemDao.java index 3d1cf353..6a3c7021 100644 --- a/db/src/main/java/com/readrops/db/dao/ItemDao.java +++ b/db/src/main/java/com/readrops/db/dao/ItemDao.java @@ -38,12 +38,6 @@ public interface ItemDao extends BaseDao { @Query("Select * From Item Where remoteId = :remoteId And feed_id = :feedId") Item selectByRemoteId(String remoteId, int feedId); - /** - * Set an item read or unread - * - * @param itemId id of the item to update - * @param read 1 for read, 0 for unread - */ @Query("Update Item Set read = :read Where id = :itemId") Completable setReadState(int itemId, boolean read); @@ -60,7 +54,7 @@ public interface ItemDao extends BaseDao { int getUnreadCount(int feedId); @SuppressWarnings(RoomWarnings.CURSOR_MISMATCH) - @Query("Select Item.id, title, Item.description, content, link, pub_date, image_link, author, read, text_color, " + + @Query("Select Item.id, Item.remoteId, title, Item.description, content, link, pub_date, image_link, author, read, text_color, " + "background_color, read_time, starred, Feed.name, Feed.id as feedId, siteUrl, Folder.id as folder_id, " + "Folder.name as folder_name from Item Inner Join Feed On Item.feed_id = Feed.id Left Join Folder on Folder.id = Feed.folder_id Where Item.id = :id") LiveData getItemById(int id); @@ -79,7 +73,4 @@ public interface ItemDao extends BaseDao { @Query("Update Item set read = :read, starred = :starred Where remoteId = :remoteId") void setReadAndStarState(String remoteId, boolean read, boolean starred); - - @Query("Update Item set starred = :starred Where id = :itemId") - Completable setStarState(int itemId, boolean starred); } diff --git a/db/src/main/java/com/readrops/db/dao/ItemStateChangeDao.kt b/db/src/main/java/com/readrops/db/dao/ItemStateChangeDao.kt index 4a25de7a..8eb6cc3c 100644 --- a/db/src/main/java/com/readrops/db/dao/ItemStateChangeDao.kt +++ b/db/src/main/java/com/readrops/db/dao/ItemStateChangeDao.kt @@ -4,6 +4,7 @@ import androidx.room.Dao import androidx.room.Delete import androidx.room.Insert import androidx.room.Query +import com.readrops.db.entities.Item import com.readrops.db.entities.ItemStateChange import com.readrops.db.pojo.ItemReadStarState import io.reactivex.Completable @@ -27,20 +28,73 @@ interface ItemStateChangeDao : BaseDao { "Left Join ItemState On ItemState.remote_id = Item.remoteId Where ItemStateChange.account_id = :accountId") fun getItemStateChanges(accountId: Int): List - fun upsertItemStateChange(itemStateChange: ItemStateChange) = Completable.create { - if (itemStateChange.readChange && readStateChangeExists(itemStateChange.id) || - itemStateChange.starChange && starStateChangeExists(itemStateChange.id)) { - deleteItemStateChange(itemStateChange) - } else { - insertItemStateChange(itemStateChange) - } - - it.onComplete() - } - @Query("Select Case When :itemId In (Select id From ItemStateChange Where read_change = 1) Then 1 Else 0 End") fun readStateChangeExists(itemId: Int): Boolean @Query("Select Case When :itemId In (Select id From ItemStateChange Where star_change = 1) Then 1 Else 0 End") fun starStateChangeExists(itemId: Int): Boolean + + fun upsertItemReadStateChange(item: Item, accountId: Int) = Completable.create { + if (itemStateChangeExists(item.id, accountId)) { + val oldItemReadState = getItemReadState(item.remoteId, accountId) + val readChange = item.isRead != oldItemReadState + + if (readChange) { + val oldItemStateChange = selectItemStateChange(item.id) + val newReadChange = !oldItemStateChange.readChange + + if (!newReadChange && !oldItemStateChange.starChange) { + deleteItemStateChange(oldItemStateChange) + } else { + updateItemReadStateChange(newReadChange, oldItemStateChange.id) + } + } + } else { + insertItemStateChange(ItemStateChange(id = item.id, readChange = true, accountId = accountId)) + } + + it.onComplete() + } + + fun upsertItemStarStateChange(item: Item, accountId: Int) = Completable.create { + if (itemStateChangeExists(item.id, accountId)) { + val oldItemStarState = getItemStarState(item.remoteId, accountId) + val starChange = item.isStarred != oldItemStarState + + if (starChange) { + val oldItemStateChange = selectItemStateChange(item.id) + val newStarChange = !oldItemStateChange.starChange + + if (!newStarChange && !oldItemStateChange.readChange) { + deleteItemStateChange(oldItemStateChange) + } else { + updateItemStarStateChange(newStarChange, oldItemStateChange.id) + } + } + } else { + insertItemStateChange(ItemStateChange(id = item.id, starChange = true, accountId = accountId)) + } + + + it.onComplete() + } + + @Query("Select * From ItemStateChange Where id = :id") + fun selectItemStateChange(id: Int): ItemStateChange + + @Query("Select case When Exists (Select id, account_id From ItemStateChange Where id = :id And account_id = :accountId) Then 1 else 0 End") + fun itemStateChangeExists(id: Int, accountId: Int): Boolean + + @Query("Select read From ItemState Where remote_id = :remoteId And account_id = :accountId") + fun getItemReadState(remoteId: String, accountId: Int): Boolean + + @Query("Select starred From ItemState Where remote_id = :remoteId And account_id = :accountId") + fun getItemStarState(remoteId: String, accountId: Int): Boolean + + @Query("Update ItemStateChange set read_change = :readChange Where id = :id") + fun updateItemReadStateChange(readChange: Boolean, id: Int) + + @Query("Update ItemStateChange set star_change = :starChange Where id = :id") + fun updateItemStarStateChange(starChange: Boolean, id: Int) + } \ No newline at end of file diff --git a/db/src/main/java/com/readrops/db/dao/ItemStateDao.kt b/db/src/main/java/com/readrops/db/dao/ItemStateDao.kt index 7efa52ef..770da7c8 100644 --- a/db/src/main/java/com/readrops/db/dao/ItemStateDao.kt +++ b/db/src/main/java/com/readrops/db/dao/ItemStateDao.kt @@ -24,6 +24,9 @@ interface ItemStateDao : BaseDao { @Query("Update ItemState set read = :read Where remote_id = :remoteId And account_id = :accountId") fun updateItemReadState(read: Boolean, remoteId: String, accountId: Int) + @Query("Update ItemState set starred = :star Where remote_id = :remoteId And account_id = :accountId") + fun updateItemStarState(star: Boolean, remoteId: String, accountId: Int) + @Query("Select case When Exists (Select remote_id, account_id From ItemState Where remote_id = :remoteId And account_id = :accountId) Then 1 else 0 End") fun itemStateExists(remoteId: String, accountId: Int): Boolean @@ -36,4 +39,14 @@ interface ItemStateDao : BaseDao { it.onComplete() } + + fun upsertItemStarState(itemState: ItemState) = Completable.create { + if (itemStateExists(itemState.remoteId, itemState.accountId)) { + updateItemStarState(itemState.starred, itemState.remoteId, itemState.accountId) + } else { + insertItemState(itemState) + } + + it.onComplete() + } } \ No newline at end of file From b9d6e6d135955d3aa5b1b63773906c3e1667df18 Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Thu, 13 May 2021 21:19:04 +0200 Subject: [PATCH 154/187] Rename config variable for separate state --- .../db/entities/account/AccountConfig.java | 23 +++++++++---------- 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/db/src/main/java/com/readrops/db/entities/account/AccountConfig.java b/db/src/main/java/com/readrops/db/entities/account/AccountConfig.java index e642949e..7474c10c 100644 --- a/db/src/main/java/com/readrops/db/entities/account/AccountConfig.java +++ b/db/src/main/java/com/readrops/db/entities/account/AccountConfig.java @@ -6,21 +6,21 @@ public class AccountConfig { .setFeedUrlEditable(true) .setFolderCreation(true) .setNoFolderCase(false) - .setUseStarredItems(false) + .setUseSeparateState(false) .build(); public static final AccountConfig NEXTCLOUD_NEWS = new AccountConfigBuilder() .setFeedUrlEditable(false) .setFolderCreation(true) .setNoFolderCase(false) - .setUseStarredItems(false) + .setUseSeparateState(false) .build(); public static final AccountConfig FRESHRSS = new AccountConfigBuilder() .setFeedUrlEditable(false) .setFolderCreation(false) .setNoFolderCase(true) - .setUseStarredItems(true) + .setUseSeparateState(true) .build(); private final boolean feedUrlEditable; @@ -30,10 +30,9 @@ public class AccountConfig { private final boolean noFolderCase; /* - This parameter lets know if the account separates items and starred items - by using the StarredItem table + Let knows if it uses ItemState table to synchronize state */ - private final boolean useStarredItems; + private final boolean useSeparateState; public boolean isFeedUrlEditable() { return feedUrlEditable; @@ -47,22 +46,22 @@ public class AccountConfig { return noFolderCase; } - public boolean useStarredItems() { - return useStarredItems; + public boolean useSeparateState() { + return useSeparateState; } public AccountConfig(AccountConfigBuilder builder) { this.feedUrlEditable = builder.feedUrlEditable; this.folderCreation = builder.folderCreation; this.noFolderCase = builder.noFolderCase; - this.useStarredItems = builder.useStarredItems; + this.useSeparateState = builder.useSeparateState; } public static class AccountConfigBuilder { private boolean feedUrlEditable; private boolean folderCreation; private boolean noFolderCase; - private boolean useStarredItems; + private boolean useSeparateState; public AccountConfigBuilder setFeedUrlEditable(boolean feedUrlEditable) { this.feedUrlEditable = feedUrlEditable; @@ -79,8 +78,8 @@ public class AccountConfig { return this; } - public AccountConfigBuilder setUseStarredItems(boolean useStarredItems) { - this.useStarredItems = useStarredItems; + public AccountConfigBuilder setUseSeparateState(boolean useSeparateState) { + this.useSeparateState = useSeparateState; return this; } From d9b31d4cfaf39812fecec82ec465ef092d82fad6 Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Thu, 13 May 2021 21:21:14 +0200 Subject: [PATCH 155/187] Build manually the item selection query --- .../com/readrops/app/item/ItemViewModel.java | 4 ++- .../java/com/readrops/db/dao/ItemDao.java | 10 ++---- .../db/queries/ItemSelectionQueryBuilder.kt | 34 +++++++++++++++++++ 3 files changed, 40 insertions(+), 8 deletions(-) create mode 100644 db/src/main/java/com/readrops/db/queries/ItemSelectionQueryBuilder.kt diff --git a/app/src/main/java/com/readrops/app/item/ItemViewModel.java b/app/src/main/java/com/readrops/app/item/ItemViewModel.java index d71bcca6..3976312b 100644 --- a/app/src/main/java/com/readrops/app/item/ItemViewModel.java +++ b/app/src/main/java/com/readrops/app/item/ItemViewModel.java @@ -14,6 +14,7 @@ import com.readrops.db.Database; import com.readrops.db.entities.Item; import com.readrops.db.entities.account.Account; import com.readrops.db.pojo.ItemWithFeed; +import com.readrops.db.queries.ItemSelectionQueryBuilder; import org.koin.core.parameter.DefinitionParametersKt; import org.koin.java.KoinJavaComponent; @@ -39,7 +40,8 @@ public class ItemViewModel extends ViewModel { } public LiveData getItemById(int id) { - return database.itemDao().getItemById(id); + return database.itemDao().getItemById(ItemSelectionQueryBuilder.buildQuery(id, + account.getConfig().useSeparateState())); } public Completable setStarState(Item item) { diff --git a/db/src/main/java/com/readrops/db/dao/ItemDao.java b/db/src/main/java/com/readrops/db/dao/ItemDao.java index 6a3c7021..7d225199 100644 --- a/db/src/main/java/com/readrops/db/dao/ItemDao.java +++ b/db/src/main/java/com/readrops/db/dao/ItemDao.java @@ -6,7 +6,6 @@ import androidx.paging.DataSource; import androidx.room.Dao; import androidx.room.Query; import androidx.room.RawQuery; -import androidx.room.RoomWarnings; import androidx.sqlite.db.SupportSQLiteQuery; import com.readrops.db.entities.Feed; @@ -52,12 +51,9 @@ public interface ItemDao extends BaseDao { @Query("Select count(*) From Item Where feed_id = :feedId And read = 0") int getUnreadCount(int feedId); - - @SuppressWarnings(RoomWarnings.CURSOR_MISMATCH) - @Query("Select Item.id, Item.remoteId, title, Item.description, content, link, pub_date, image_link, author, read, text_color, " + - "background_color, read_time, starred, Feed.name, Feed.id as feedId, siteUrl, Folder.id as folder_id, " + - "Folder.name as folder_name from Item Inner Join Feed On Item.feed_id = Feed.id Left Join Folder on Folder.id = Feed.folder_id Where Item.id = :id") - LiveData getItemById(int id); + + @RawQuery(observedEntities = {Item.class, ItemState.class}) + LiveData getItemById(SupportSQLiteQuery query); @Query("Select Item.remoteId From Item Inner Join Feed On Item.feed_id = Feed.id Where read = 1 And account_id = :accountId") List getReadChanges(int accountId); diff --git a/db/src/main/java/com/readrops/db/queries/ItemSelectionQueryBuilder.kt b/db/src/main/java/com/readrops/db/queries/ItemSelectionQueryBuilder.kt new file mode 100644 index 00000000..1da869d1 --- /dev/null +++ b/db/src/main/java/com/readrops/db/queries/ItemSelectionQueryBuilder.kt @@ -0,0 +1,34 @@ +package com.readrops.db.queries + +import androidx.sqlite.db.SupportSQLiteQuery +import androidx.sqlite.db.SupportSQLiteQueryBuilder + +object ItemSelectionQueryBuilder { + + private val COLUMNS = arrayOf("Item.id", "Item.remoteId", "title", "Item.description", "content", + "link", "pub_date", "image_link", "author", "Item.read", "text_color", + "background_color", "read_time", "Feed.name", "Feed.id as feedId", "siteUrl", + "Folder.id as folder_id", "Folder.name as folder_name") + + private val SEPARATE_STATE_COLUMNS = arrayOf("case When ItemState.starred = 1 Then 1 else 0 End starred") + + private const val JOIN = "Item Inner Join Feed On Item.feed_id = Feed.id Left Join Folder on Folder.id = Feed.folder_id" + + private const val SEPARATE_STATE_JOIN = " Left Join ItemState On ItemState.remote_id = Item.remoteId" + + /** + * @param separateState Indicates if item state must be retrieved from ItemState table + */ + @JvmStatic + fun buildQuery(itemId: Int, separateState: Boolean): SupportSQLiteQuery { + val tables = if (separateState) JOIN + SEPARATE_STATE_JOIN else JOIN + val columns = if (separateState) COLUMNS.plus(SEPARATE_STATE_COLUMNS) else COLUMNS.plus("starred") + + return SupportSQLiteQueryBuilder.builder(tables).run { + columns(columns) + selection("Item.id = $itemId", null) + + create() + } + } +} \ No newline at end of file From 15596e5fff457bb5fbb95ce1c8fe4e15f1843a71 Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Thu, 1 Jul 2021 21:40:16 +0200 Subject: [PATCH 156/187] Remove usage of StarredItem table in the main query --- .../readrops/app/itemslist/MainViewModel.java | 15 +-- .../java/com/readrops/db/ItemsQueryBuilder.kt | 93 ------------------- .../readrops/db/queries/ItemsQueryBuilder.kt | 87 +++++++++++++++++ .../com/readrops/db/ItemsQueryBuilderTest.kt | 2 + 4 files changed, 92 insertions(+), 105 deletions(-) delete mode 100644 db/src/main/java/com/readrops/db/ItemsQueryBuilder.kt create mode 100644 db/src/main/java/com/readrops/db/queries/ItemsQueryBuilder.kt diff --git a/app/src/main/java/com/readrops/app/itemslist/MainViewModel.java b/app/src/main/java/com/readrops/app/itemslist/MainViewModel.java index 087debe2..b8ce1837 100644 --- a/app/src/main/java/com/readrops/app/itemslist/MainViewModel.java +++ b/app/src/main/java/com/readrops/app/itemslist/MainViewModel.java @@ -11,8 +11,8 @@ import androidx.paging.PagedList; import com.readrops.app.repositories.ARepository; import com.readrops.app.utils.SharedPreferencesManager; import com.readrops.db.Database; -import com.readrops.db.ItemsQueryBuilder; -import com.readrops.db.QueryFilters; +import com.readrops.db.queries.ItemsQueryBuilder; +import com.readrops.db.queries.QueryFilters; import com.readrops.db.RoomFactoryWrapper; import com.readrops.db.entities.Feed; import com.readrops.db.entities.Folder; @@ -70,16 +70,7 @@ public class MainViewModel extends ViewModel { } DataSource.Factory items; - - -/* - if (queryFilters.getFilterType() == FilterType.STARS_FILTER && currentAccount.getAccountType().getAccountConfig().useStarredItems()) { - items = database.starredItemDao().selectAll(ItemsQueryBuilder.buildStarredItemsQuery(queryFilters)); - } else { - - } -*/ - items = database.itemDao().selectAll(ItemsQueryBuilder.buildItemsQuery(queryFilters)); + items = database.itemDao().selectAll(ItemsQueryBuilder.buildItemsQuery(queryFilters, currentAccount.getConfig().useSeparateState())); lastFetch = new LivePagedListBuilder<>(new RoomFactoryWrapper<>(items), new PagedList.Config.Builder() diff --git a/db/src/main/java/com/readrops/db/ItemsQueryBuilder.kt b/db/src/main/java/com/readrops/db/ItemsQueryBuilder.kt deleted file mode 100644 index 0820ca64..00000000 --- a/db/src/main/java/com/readrops/db/ItemsQueryBuilder.kt +++ /dev/null @@ -1,93 +0,0 @@ -package com.readrops.db - -import androidx.sqlite.db.SupportSQLiteQuery -import androidx.sqlite.db.SupportSQLiteQueryBuilder -import com.readrops.db.filters.FilterType -import com.readrops.db.filters.ListSortType - -object ItemsQueryBuilder { - - private val COLUMNS = arrayOf("title", "clean_description", "image_link", "pub_date", - "read_it_later", "Feed.name", "text_color", "background_color", "icon_url", "read_time", - "Feed.id as feedId", "Feed.account_id", "Folder.id as folder_id", "Folder.name as folder_name", - "case When ItemState.remote_id is NULL Or ItemState.read = 1 Then 1 else 0 End read", - "case When ItemState.remote_id is NULL or ItemState.starred = 1 Then 1 else 0 End starred") - - private val ITEM_COLUMNS = arrayOf(".id", ".remoteId") - - private const val ORDER_BY_ASC = ".id DESC" - - private const val ORDER_BY_DESC = "pub_date ASC" - - @JvmStatic - fun buildItemsQuery(queryFilters: QueryFilters): SupportSQLiteQuery = - buildQuery(queryFilters, false) - - @JvmStatic - fun buildStarredItemsQuery(queryFilters: QueryFilters): SupportSQLiteQuery = - buildQuery(queryFilters, true) - - private fun buildQuery(queryFilters: QueryFilters, starQuery: Boolean): SupportSQLiteQuery = with(queryFilters) { - if (accountId == 0) - throw IllegalArgumentException("AccountId must be greater than 0") - - if (filterType == FilterType.FEED_FILTER && filterFeedId == 0) - throw IllegalArgumentException("FeedId must be greater than 0 if current filter is FEED_FILTER") - - val tableName = tableName(starQuery) - val selectAllJoin = buildSelectAllJoin(tableName) - - SupportSQLiteQueryBuilder.builder(selectAllJoin).run { - columns(COLUMNS.plus(buildItemColumns(tableName))) - selection(buildWhereClause(this@with), null) - orderBy(if (sortType == ListSortType.NEWEST_TO_OLDEST) buildOrderByAsc(tableName) else ORDER_BY_DESC) - - create() - } - } - - private fun buildWhereClause(queryFilters: QueryFilters): String = StringBuilder(500).run { - append("Feed.account_id = ${queryFilters.accountId} And " + - "ItemState.account_id = ${queryFilters.accountId} Or ItemState.account_id is NULL And ") - - //if (!queryFilters.showReadItems) append("read = 0 And ") - - when (queryFilters.filterType) { - FilterType.FEED_FILTER -> append("feed_id = ${queryFilters.filterFeedId} And read_it_later = 0") - FilterType.READ_IT_LATER_FILTER -> append("read_it_later = 1") - FilterType.STARS_FILTER -> append("starred = 1 And read_it_later = 0") - else -> append("read_it_later = 0") - } - - toString() - } - - private fun tableName(starQuery: Boolean): String = if (starQuery) "StarredItem" else "Item" - - private fun buildItemColumns(tableName: String): Array { - val columns = arrayListOf() - - for (column in ITEM_COLUMNS) { - columns += tableName + column - } - - return columns.toTypedArray() - } - - private fun buildOrderByAsc(tableName: String): String = tableName + ORDER_BY_ASC - - private fun buildSelectAllJoin(tableName: String): String = """ - $tableName INNER JOIN Feed on $tableName.feed_id = Feed.id - LEFT JOIN Folder on Feed.folder_id = Folder.id LEFT JOIN ItemState On - $tableName.remoteId = ItemState.remote_id - """.trimIndent() - -} - -class QueryFilters( - var showReadItems: Boolean = true, - var filterFeedId: Int = 0, - var accountId: Int = 0, - var filterType: FilterType = FilterType.NO_FILTER, - var sortType: ListSortType = ListSortType.NEWEST_TO_OLDEST, -) \ No newline at end of file diff --git a/db/src/main/java/com/readrops/db/queries/ItemsQueryBuilder.kt b/db/src/main/java/com/readrops/db/queries/ItemsQueryBuilder.kt new file mode 100644 index 00000000..b1e81815 --- /dev/null +++ b/db/src/main/java/com/readrops/db/queries/ItemsQueryBuilder.kt @@ -0,0 +1,87 @@ +package com.readrops.db.queries + +import androidx.sqlite.db.SupportSQLiteQuery +import androidx.sqlite.db.SupportSQLiteQueryBuilder +import com.readrops.db.filters.FilterType +import com.readrops.db.filters.ListSortType + +object ItemsQueryBuilder { + + private val COLUMNS = arrayOf("Item.id", "Item.remoteId", "title", "clean_description", "image_link", "pub_date", + "read_it_later", "Feed.name", "text_color", "background_color", "icon_url", "read_time", + "Feed.id as feedId", "Feed.account_id", "Folder.id as folder_id", "Folder.name as folder_name") + + private val SEPARATE_STATE_COLUMNS = arrayOf("case When ItemState.remote_id is NULL Or ItemState.read = 1 Then 1 else 0 End read", + "case When ItemState.remote_id is NULL or ItemState.starred = 1 Then 1 else 0 End starred") + + private val OTHER_COLUMNS = arrayOf("read", "starred") + + private val SELECT_ALL_JOIN = """Item INNER JOIN Feed on Item.feed_id = Feed.id + LEFT JOIN Folder on Feed.folder_id = Folder.id """.trimIndent() + + private const val SEPARATE_STATE_JOIN = "LEFT JOIN ItemState On Item.remoteId = ItemState.remote_id" + + private const val ORDER_BY_ASC = "Item.id DESC" + + private const val ORDER_BY_DESC = "pub_date ASC" + + @JvmStatic + fun buildItemsQuery(queryFilters: QueryFilters, separateState: Boolean): SupportSQLiteQuery = + buildQuery(queryFilters, separateState) + + @JvmStatic + fun buildItemsQuery(queryFilters: QueryFilters): SupportSQLiteQuery = + buildQuery(queryFilters, false) + + private fun buildQuery(queryFilters: QueryFilters, separateState: Boolean): SupportSQLiteQuery = with(queryFilters) { + if (accountId == 0) + throw IllegalArgumentException("AccountId must be greater than 0") + + if (filterType == FilterType.FEED_FILTER && filterFeedId == 0) + throw IllegalArgumentException("FeedId must be greater than 0 if current filter is FEED_FILTER") + + val columns = if (separateState) COLUMNS.plus(SEPARATE_STATE_COLUMNS) else COLUMNS.plus(OTHER_COLUMNS) + val selectAllJoin = if (separateState) SELECT_ALL_JOIN + SEPARATE_STATE_JOIN else SELECT_ALL_JOIN + + SupportSQLiteQueryBuilder.builder(selectAllJoin).run { + columns(columns) + selection(buildWhereClause(this@with, separateState), null) + orderBy(if (sortType == ListSortType.NEWEST_TO_OLDEST) ORDER_BY_ASC else ORDER_BY_DESC) + + create() + } + } + + private fun buildWhereClause(queryFilters: QueryFilters, separateState: Boolean): String = StringBuilder(500).run { + if (separateState) + append("ItemState.account_id = ${queryFilters.accountId} And ") + else + append("Feed.account_id = ${queryFilters.accountId} And ") + + if (!queryFilters.showReadItems) append("ItemState.read = 0 And ") + + when (queryFilters.filterType) { + FilterType.FEED_FILTER -> append("feed_id = ${queryFilters.filterFeedId} And read_it_later = 0") + FilterType.READ_IT_LATER_FILTER -> append("read_it_later = 1") + FilterType.STARS_FILTER -> { + if (separateState) { + append("ItemState.remote_id is NULL or ItemState.starred = 1 And read_it_later = 0") + } else { + append("starred = 1 And read_it_later = 0") + } + } + else -> append("read_it_later = 0") + } + + toString() + } + +} + +class QueryFilters( + var showReadItems: Boolean = true, + var filterFeedId: Int = 0, + var accountId: Int = 0, + var filterType: FilterType = FilterType.NO_FILTER, + var sortType: ListSortType = ListSortType.NEWEST_TO_OLDEST, +) \ No newline at end of file diff --git a/db/src/test/java/com/readrops/db/ItemsQueryBuilderTest.kt b/db/src/test/java/com/readrops/db/ItemsQueryBuilderTest.kt index fd16b23b..59fa0e9f 100644 --- a/db/src/test/java/com/readrops/db/ItemsQueryBuilderTest.kt +++ b/db/src/test/java/com/readrops/db/ItemsQueryBuilderTest.kt @@ -2,6 +2,8 @@ package com.readrops.db import com.readrops.db.filters.FilterType import com.readrops.db.filters.ListSortType +import com.readrops.db.queries.ItemsQueryBuilder +import com.readrops.db.queries.QueryFilters import junit.framework.TestCase.assertFalse import junit.framework.TestCase.assertTrue import org.junit.Test From d600633acafb45e6dcd90478ea7945ff8f0dc0b5 Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Thu, 1 Jul 2021 22:17:59 +0200 Subject: [PATCH 157/187] Fix showing/hide read items crash --- .../com/readrops/db/queries/ItemsQueryBuilder.kt | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/db/src/main/java/com/readrops/db/queries/ItemsQueryBuilder.kt b/db/src/main/java/com/readrops/db/queries/ItemsQueryBuilder.kt index b1e81815..92a41a75 100644 --- a/db/src/main/java/com/readrops/db/queries/ItemsQueryBuilder.kt +++ b/db/src/main/java/com/readrops/db/queries/ItemsQueryBuilder.kt @@ -53,12 +53,14 @@ object ItemsQueryBuilder { } private fun buildWhereClause(queryFilters: QueryFilters, separateState: Boolean): String = StringBuilder(500).run { - if (separateState) - append("ItemState.account_id = ${queryFilters.accountId} And ") - else - append("Feed.account_id = ${queryFilters.accountId} And ") + append("Feed.account_id = ${queryFilters.accountId} And ") - if (!queryFilters.showReadItems) append("ItemState.read = 0 And ") + if (!queryFilters.showReadItems) { + if (separateState) + append("ItemState.read = 0 And ") + else + append("Item.read = 0 And ") + } when (queryFilters.filterType) { FilterType.FEED_FILTER -> append("feed_id = ${queryFilters.filterFeedId} And read_it_later = 0") From 1011eee9df3e9e6217941a1c9928fc3f3b8ca3f2 Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Thu, 1 Jul 2021 22:53:25 +0200 Subject: [PATCH 158/187] Handle read/star state for local account --- .../app/repositories/ARepository.java | 25 ++++++++++++++----- .../java/com/readrops/db/dao/ItemDao.java | 3 +++ 2 files changed, 22 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/com/readrops/app/repositories/ARepository.java b/app/src/main/java/com/readrops/app/repositories/ARepository.java index 068b71f1..a952574a 100644 --- a/app/src/main/java/com/readrops/app/repositories/ARepository.java +++ b/app/src/main/java/com/readrops/app/repositories/ARepository.java @@ -115,9 +115,16 @@ public abstract class ARepository { } public Completable setItemReadState(Item item) { - return database.itemStateChangesDao().upsertItemReadStateChange(item, account.getId()) - .andThen(database.itemStateDao().upsertItemReadState(new ItemState(0, item.isRead(), - item.isStarred(), item.getRemoteId(), account.getId()))); + if (account.getConfig().useSeparateState()) { + return database.itemStateChangesDao().upsertItemReadStateChange(item, account.getId()) + .andThen(database.itemStateDao().upsertItemReadState(new ItemState(0, item.isRead(), + item.isStarred(), item.getRemoteId(), account.getId()))); + } else if (account.isLocal()) { + return database.itemDao().setReadState(item.getId(), item.isRead()); + } else { // TODO nextcloud case, use only ItemStateChange table + return Completable.complete(); + } + } public Completable setAllItemsReadState(boolean read) { @@ -129,9 +136,15 @@ public abstract class ARepository { } public Completable setItemStarState(Item item) { - return database.itemStateChangesDao().upsertItemStarStateChange(item, account.getId()) - .andThen(database.itemStateDao().upsertItemStarState(new ItemState(0, item.isRead(), - item.isStarred(), item.getRemoteId(), account.getId()))); + if (account.getConfig().useSeparateState()) { + return database.itemStateChangesDao().upsertItemStarStateChange(item, account.getId()) + .andThen(database.itemStateDao().upsertItemStarState(new ItemState(0, item.isRead(), + item.isStarred(), item.getRemoteId(), account.getId()))); + } else if (account.isLocal()) { + return database.itemDao().setStarState(item.getId(), item.isRead()); + } else { // TODO nextcloud case, use only ItemStateChange table + return Completable.complete(); + } } diff --git a/db/src/main/java/com/readrops/db/dao/ItemDao.java b/db/src/main/java/com/readrops/db/dao/ItemDao.java index 7d225199..3e46a613 100644 --- a/db/src/main/java/com/readrops/db/dao/ItemDao.java +++ b/db/src/main/java/com/readrops/db/dao/ItemDao.java @@ -40,6 +40,9 @@ public interface ItemDao extends BaseDao { @Query("Update Item Set read = :read Where id = :itemId") Completable setReadState(int itemId, boolean read); + @Query("Update Item set starred = :starred Where id = :itemId") + Completable setStarState(int itemId, boolean starred); + @Query("Update Item set read = :readState Where feed_id In (Select id From Feed Where account_id = :accountId)") Completable setAllItemsReadState(int readState, int accountId); From af5138c1cb9507106be87a3286fd1c3ff96e8198 Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Fri, 2 Jul 2021 14:30:01 +0200 Subject: [PATCH 159/187] =?UTF-8?q?Insert=20starred=20items=20at=20initial?= =?UTF-8?q?=20synchronisation=20and=20don=C2=B4t=20forget=20starred=20item?= =?UTF-8?q?s=20which=20are=20read?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../app/repositories/FreshRSSRepository.java | 21 +++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/com/readrops/app/repositories/FreshRSSRepository.java b/app/src/main/java/com/readrops/app/repositories/FreshRSSRepository.java index a0398ebf..5bd7d593 100644 --- a/app/src/main/java/com/readrops/app/repositories/FreshRSSRepository.java +++ b/app/src/main/java/com/readrops/app/repositories/FreshRSSRepository.java @@ -130,6 +130,9 @@ public class FreshRSSRepository extends ARepository { insertItems(syncResult.getItems()); logger.addSplit("items insertion"); + insertItems(syncResult.getStarredItems()); + logger.addSplit("starred items insertion"); + insertItemsIds(syncResult.getUnreadIds(), syncResult.getStarredIds()); logger.addSplit("insert and update items ids"); @@ -256,10 +259,20 @@ public class FreshRSSRepository extends ARepository { private void insertItemsIds(List unreadIds, List starredIds) { database.itemStateDao().deleteItemsStates(account.getId()); - database.itemStateDao().insertItemStates(unreadIds.stream().map(id -> - new ItemState(0, false, starredIds.stream() - .anyMatch(starredId -> starredId.equals(id)), id, account.getId())) - .collect(Collectors.toList())); + database.itemStateDao().insertItemStates(unreadIds.stream().map(id -> { + boolean starred = starredIds.stream().filter(starredId -> starredId.equals(id)).count() == 1; + starredIds.remove(id); + + return new ItemState(0, false, starred, id, account.getId()); + } + ).collect(Collectors.toList())); + + // insert starred items ids which are read + if (!starredIds.isEmpty()) { + database.itemStateDao().insertItemStates(starredIds.stream().map(id -> + new ItemState(0, true, true, id, account.getId())) + .collect(Collectors.toList())); + } } } From ea14ae3013ee8aac351a76dcc06d49c49f78963f Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Fri, 2 Jul 2021 14:48:36 +0200 Subject: [PATCH 160/187] Fix star filter query part --- db/src/main/java/com/readrops/db/queries/ItemsQueryBuilder.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/db/src/main/java/com/readrops/db/queries/ItemsQueryBuilder.kt b/db/src/main/java/com/readrops/db/queries/ItemsQueryBuilder.kt index 92a41a75..55c33ea3 100644 --- a/db/src/main/java/com/readrops/db/queries/ItemsQueryBuilder.kt +++ b/db/src/main/java/com/readrops/db/queries/ItemsQueryBuilder.kt @@ -67,7 +67,7 @@ object ItemsQueryBuilder { FilterType.READ_IT_LATER_FILTER -> append("read_it_later = 1") FilterType.STARS_FILTER -> { if (separateState) { - append("ItemState.remote_id is NULL or ItemState.starred = 1 And read_it_later = 0") + append("ItemState.starred = 1 And read_it_later = 0") } else { append("starred = 1 And read_it_later = 0") } From 2c5672b3d578f26fd99b36833b39a81e382c7423 Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Fri, 2 Jul 2021 17:50:40 +0200 Subject: [PATCH 161/187] Fix starred items duplication --- .../app/repositories/FreshRSSRepository.java | 21 ++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/com/readrops/app/repositories/FreshRSSRepository.java b/app/src/main/java/com/readrops/app/repositories/FreshRSSRepository.java index 5bd7d593..5161c6ea 100644 --- a/app/src/main/java/com/readrops/app/repositories/FreshRSSRepository.java +++ b/app/src/main/java/com/readrops/app/repositories/FreshRSSRepository.java @@ -127,10 +127,10 @@ public class FreshRSSRepository extends ARepository { insertFeeds(syncResult.getFeeds()); logger.addSplit("feeds insertion"); - insertItems(syncResult.getItems()); + insertItems(syncResult.getItems(), false); logger.addSplit("items insertion"); - insertItems(syncResult.getStarredItems()); + insertItems(syncResult.getStarredItems(), true); logger.addSplit("starred items insertion"); insertItemsIds(syncResult.getUnreadIds(), syncResult.getStarredIds()); @@ -233,7 +233,7 @@ public class FreshRSSRepository extends ARepository { database.folderDao().foldersUpsert(freshRSSFolders, account); } - private void insertItems(List items) { + private void insertItems(List items, boolean starredItems) { List itemsToInsert = new ArrayList<>(); Map itemsFeedsIds = new HashMap<>(); @@ -248,7 +248,16 @@ public class FreshRSSRepository extends ARepository { item.setFeedId(feedId); item.setReadTime(Utils.readTimeFromString(item.getContent())); - itemsToInsert.add(item); + + // workaround to avoid inserting starred items coming from the main item call + // as the API exclusion filter doesn't seem to work + if (!starredItems) { + if (!item.isStarred()) { + itemsToInsert.add(item); + } + } else { + itemsToInsert.add(item); + } } if (!itemsToInsert.isEmpty()) { @@ -262,7 +271,9 @@ public class FreshRSSRepository extends ARepository { database.itemStateDao().insertItemStates(unreadIds.stream().map(id -> { boolean starred = starredIds.stream().filter(starredId -> starredId.equals(id)).count() == 1; - starredIds.remove(id); + if (starred) { + starredIds.remove(id); + } return new ItemState(0, false, starred, id, account.getId()); } From cbff42b4f25dac02d840f729d79fdcec8cae625a Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Fri, 2 Jul 2021 18:56:11 +0200 Subject: [PATCH 162/187] Remove jcenter dependency --- api/build.gradle | 11 +++++++---- api/src/main/java/com/readrops/api/ApiModule.kt | 2 +- .../com/readrops/api/localfeed/LocalRSSDataSource.kt | 4 ++-- app/build.gradle | 8 ++++---- app/src/main/java/com/readrops/app/AppModule.kt | 4 ++-- .../NotificationPermissionListAdapter.kt | 4 ++-- .../app/notifications/sync/SyncResultAnalyser.kt | 4 ++-- .../app/notifications/sync/SyncResultDebugData.kt | 4 ++-- .../com/readrops/app/notifications/sync/SyncWorker.kt | 4 ++-- .../com/readrops/app/utils/ReadropsGlideModule.kt | 4 ++-- .../app/utils/feedscolors/FeedsColorsIntentService.kt | 4 ++-- build.gradle | 3 +-- db/build.gradle | 11 +++++++---- 13 files changed, 36 insertions(+), 31 deletions(-) diff --git a/api/build.gradle b/api/build.gradle index 3f609172..5e19b01c 100644 --- a/api/build.gradle +++ b/api/build.gradle @@ -36,6 +36,9 @@ android { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } + kotlinOptions { + jvmTarget = '1.8' + } } dependencies { @@ -48,8 +51,8 @@ dependencies { androidTestImplementation 'androidx.test:rules:1.3.0' androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0' testImplementation 'com.squareup.okhttp3:mockwebserver:4.9.0' - androidTestImplementation "org.koin:koin-test:2.1.6" - testImplementation "org.koin:koin-test:2.1.6" + androidTestImplementation "io.insert-koin:koin-test:2.1.6" + testImplementation "io.insert-koin:koin-test:2.1.6" implementation 'com.gitlab.mvysny.konsume-xml:konsume-xml:0.12' @@ -76,6 +79,6 @@ dependencies { api 'io.reactivex.rxjava2:rxandroid:2.1.1' api 'org.jsoup:jsoup:1.13.1' - debugApi 'com.icapps.niddler:niddler:1.2.0' - releaseApi 'com.icapps.niddler:niddler-noop:1.2.0' + debugApi 'com.chimerapps.niddler:niddler:1.5.5' + releaseApi 'com.chimerapps.niddler:niddler-noop:1.5.5' } diff --git a/api/src/main/java/com/readrops/api/ApiModule.kt b/api/src/main/java/com/readrops/api/ApiModule.kt index da607bad..c1a1ace9 100644 --- a/api/src/main/java/com/readrops/api/ApiModule.kt +++ b/api/src/main/java/com/readrops/api/ApiModule.kt @@ -1,6 +1,6 @@ package com.readrops.api -import com.icapps.niddler.interceptor.okhttp.NiddlerOkHttpInterceptor +import com.chimerapps.niddler.interceptor.okhttp.NiddlerOkHttpInterceptor import com.readrops.api.localfeed.LocalRSSDataSource import com.readrops.api.services.freshrss.FreshRSSDataSource import com.readrops.api.services.freshrss.FreshRSSService diff --git a/api/src/main/java/com/readrops/api/localfeed/LocalRSSDataSource.kt b/api/src/main/java/com/readrops/api/localfeed/LocalRSSDataSource.kt index 6916809e..d0900cec 100644 --- a/api/src/main/java/com/readrops/api/localfeed/LocalRSSDataSource.kt +++ b/api/src/main/java/com/readrops/api/localfeed/LocalRSSDataSource.kt @@ -17,8 +17,8 @@ import okhttp3.OkHttpClient import okhttp3.Request import okhttp3.Response import okio.Buffer -import org.koin.core.KoinComponent -import org.koin.core.get +import org.koin.core.component.KoinComponent +import org.koin.core.component.get import java.io.ByteArrayInputStream import java.io.IOException import java.io.InputStream diff --git a/app/build.gradle b/app/build.gradle index 9969a26c..3538d38d 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -76,7 +76,7 @@ dependencies { implementation "androidx.work:work-runtime-ktx:2.4.0" implementation "androidx.fragment:fragment-ktx:1.2.5" implementation "androidx.browser:browser:1.2.0" - testImplementation "org.koin:koin-test:2.1.6" + testImplementation "io.insert-koin:koin-test:2.1.6" implementation 'com.github.bumptech.glide:glide:4.11.0' kapt 'com.github.bumptech.glide:compiler:4.11.0' @@ -95,7 +95,7 @@ dependencies { implementation 'com.mikepenz:materialdrawer:6.1.2' implementation "com.mikepenz:aboutlibraries:6.2.3" - debugImplementation 'com.facebook.flipper:flipper:0.30.1' - debugImplementation 'com.facebook.soloader:soloader:0.9.0' - debugImplementation 'com.facebook.flipper:flipper-network-plugin:0.30.1' + debugImplementation 'com.facebook.flipper:flipper:0.96.1' + debugImplementation 'com.facebook.soloader:soloader:0.10.1' + debugImplementation 'com.facebook.flipper:flipper-network-plugin:0.96.1' } diff --git a/app/src/main/java/com/readrops/app/AppModule.kt b/app/src/main/java/com/readrops/app/AppModule.kt index bd947d82..0de02df7 100644 --- a/app/src/main/java/com/readrops/app/AppModule.kt +++ b/app/src/main/java/com/readrops/app/AppModule.kt @@ -1,8 +1,8 @@ package com.readrops.app import androidx.preference.PreferenceManager -import com.icapps.niddler.core.AndroidNiddler -import com.icapps.niddler.core.Niddler +import com.chimerapps.niddler.core.AndroidNiddler +import com.chimerapps.niddler.core.Niddler import com.readrops.app.account.AccountViewModel import com.readrops.app.addfeed.AddFeedsViewModel import com.readrops.app.feedsfolders.ManageFeedsFoldersViewModel diff --git a/app/src/main/java/com/readrops/app/notifications/NotificationPermissionListAdapter.kt b/app/src/main/java/com/readrops/app/notifications/NotificationPermissionListAdapter.kt index dade0c3a..01cd67f0 100644 --- a/app/src/main/java/com/readrops/app/notifications/NotificationPermissionListAdapter.kt +++ b/app/src/main/java/com/readrops/app/notifications/NotificationPermissionListAdapter.kt @@ -10,8 +10,8 @@ import com.readrops.app.R import com.readrops.app.databinding.NotificationPermissionLayoutBinding import com.readrops.app.utils.GlideRequests import com.readrops.db.entities.Feed -import org.koin.core.KoinComponent -import org.koin.core.get +import org.koin.core.component.KoinComponent +import org.koin.core.component.get class NotificationPermissionListAdapter(var enableAll: Boolean, val listener: (feed: Feed) -> Unit) : ListAdapter(DIFF_CALLBACK), KoinComponent { diff --git a/app/src/main/java/com/readrops/app/notifications/sync/SyncResultAnalyser.kt b/app/src/main/java/com/readrops/app/notifications/sync/SyncResultAnalyser.kt index 9b90df50..b957e24b 100644 --- a/app/src/main/java/com/readrops/app/notifications/sync/SyncResultAnalyser.kt +++ b/app/src/main/java/com/readrops/app/notifications/sync/SyncResultAnalyser.kt @@ -11,8 +11,8 @@ import com.readrops.db.entities.account.Account import com.readrops.api.services.SyncResult import com.readrops.app.utils.GlideRequests import com.readrops.app.utils.Utils -import org.koin.core.KoinComponent -import org.koin.core.get +import org.koin.core.component.KoinComponent +import org.koin.core.component.get /** * Simple class to get synchro notification content (title, content and largeIcon) according to some rules diff --git a/app/src/main/java/com/readrops/app/notifications/sync/SyncResultDebugData.kt b/app/src/main/java/com/readrops/app/notifications/sync/SyncResultDebugData.kt index 9931db0b..847a2730 100644 --- a/app/src/main/java/com/readrops/app/notifications/sync/SyncResultDebugData.kt +++ b/app/src/main/java/com/readrops/app/notifications/sync/SyncResultDebugData.kt @@ -6,8 +6,8 @@ import com.readrops.db.entities.Item import com.readrops.db.entities.account.Account import com.readrops.db.entities.account.AccountType import org.jetbrains.annotations.TestOnly -import org.koin.core.KoinComponent -import org.koin.core.get +import org.koin.core.component.KoinComponent +import org.koin.core.component.get class SyncResultDebugData { diff --git a/app/src/main/java/com/readrops/app/notifications/sync/SyncWorker.kt b/app/src/main/java/com/readrops/app/notifications/sync/SyncWorker.kt index 9ac3da1c..b919fc60 100644 --- a/app/src/main/java/com/readrops/app/notifications/sync/SyncWorker.kt +++ b/app/src/main/java/com/readrops/app/notifications/sync/SyncWorker.kt @@ -21,8 +21,8 @@ import com.readrops.db.entities.Item import com.readrops.db.entities.account.Account import io.reactivex.disposables.Disposable import io.reactivex.schedulers.Schedulers -import org.koin.core.KoinComponent -import org.koin.core.get +import org.koin.core.component.KoinComponent +import org.koin.core.component.get import org.koin.core.parameter.parametersOf class SyncWorker(context: Context, parameters: WorkerParameters) : Worker(context, parameters), KoinComponent { diff --git a/app/src/main/java/com/readrops/app/utils/ReadropsGlideModule.kt b/app/src/main/java/com/readrops/app/utils/ReadropsGlideModule.kt index 621e241e..aebc4491 100644 --- a/app/src/main/java/com/readrops/app/utils/ReadropsGlideModule.kt +++ b/app/src/main/java/com/readrops/app/utils/ReadropsGlideModule.kt @@ -8,8 +8,8 @@ import com.bumptech.glide.integration.okhttp3.OkHttpUrlLoader import com.bumptech.glide.load.model.GlideUrl import com.bumptech.glide.module.AppGlideModule import okhttp3.OkHttpClient -import org.koin.core.KoinComponent -import org.koin.core.get +import org.koin.core.component.KoinComponent +import org.koin.core.component.get import java.io.InputStream @GlideModule diff --git a/app/src/main/java/com/readrops/app/utils/feedscolors/FeedsColorsIntentService.kt b/app/src/main/java/com/readrops/app/utils/feedscolors/FeedsColorsIntentService.kt index 6fbf9912..464b0603 100644 --- a/app/src/main/java/com/readrops/app/utils/feedscolors/FeedsColorsIntentService.kt +++ b/app/src/main/java/com/readrops/app/utils/feedscolors/FeedsColorsIntentService.kt @@ -9,8 +9,8 @@ import com.readrops.app.ReadropsApp import com.readrops.app.utils.ReadropsKeys.FEEDS import com.readrops.db.Database import com.readrops.db.entities.Feed -import org.koin.core.KoinComponent -import org.koin.core.get +import org.koin.core.component.KoinComponent +import org.koin.core.component.get class FeedsColorsIntentService : IntentService("FeedsColorsIntentService"), KoinComponent { diff --git a/build.gradle b/build.gradle index ed832462..e69a5989 100644 --- a/build.gradle +++ b/build.gradle @@ -5,7 +5,7 @@ buildscript { repositories { google() - jcenter() + mavenCentral() } dependencies { classpath 'com.android.tools.build:gradle:4.1.3' @@ -16,7 +16,6 @@ buildscript { allprojects { repositories { google() - jcenter() mavenCentral() maven { url 'https://jitpack.io' } } diff --git a/db/build.gradle b/db/build.gradle index 854a67c2..0e8230dd 100644 --- a/db/build.gradle +++ b/db/build.gradle @@ -47,6 +47,9 @@ android { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } + kotlinOptions { + jvmTarget = '1.8' + } } dependencies { @@ -75,8 +78,8 @@ dependencies { api 'joda-time:joda-time:2.10.5' - def koin_version = "2.1.6" - api "org.koin:koin-android:$koin_version" - api "org.koin:koin-androidx-scope:$koin_version" - api "org.koin:koin-androidx-viewmodel:$koin_version" + def koin_version = "2.2.3" + api "io.insert-koin:koin-android:$koin_version" + api "io.insert-koin:koin-androidx-scope:$koin_version" + api "io.insert-koin:koin-androidx-viewmodel:$koin_version" } From bdc277f216003be3e337a1e68e7da1c938cb53b8 Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Fri, 2 Jul 2021 19:13:49 +0200 Subject: [PATCH 163/187] Fix failing tests --- api/build.gradle | 4 ++-- app/build.gradle | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/api/build.gradle b/api/build.gradle index 5e19b01c..991e4995 100644 --- a/api/build.gradle +++ b/api/build.gradle @@ -51,8 +51,8 @@ dependencies { androidTestImplementation 'androidx.test:rules:1.3.0' androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0' testImplementation 'com.squareup.okhttp3:mockwebserver:4.9.0' - androidTestImplementation "io.insert-koin:koin-test:2.1.6" - testImplementation "io.insert-koin:koin-test:2.1.6" + androidTestImplementation "io.insert-koin:koin-test:2.2.3" + testImplementation "io.insert-koin:koin-test:2.2.3" implementation 'com.gitlab.mvysny.konsume-xml:konsume-xml:0.12' diff --git a/app/build.gradle b/app/build.gradle index 3538d38d..f9e97567 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -76,7 +76,7 @@ dependencies { implementation "androidx.work:work-runtime-ktx:2.4.0" implementation "androidx.fragment:fragment-ktx:1.2.5" implementation "androidx.browser:browser:1.2.0" - testImplementation "io.insert-koin:koin-test:2.1.6" + testImplementation "io.insert-koin:koin-test:2.2.3" implementation 'com.github.bumptech.glide:glide:4.11.0' kapt 'com.github.bumptech.glide:compiler:4.11.0' From 4251d1dc9f2ed18b0b5f5a9af75dd598e4126e75 Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Sat, 3 Jul 2021 19:29:42 +0200 Subject: [PATCH 164/187] Update android gradle plugin --- build.gradle | 2 +- gradle/wrapper/gradle-wrapper.properties | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index e69a5989..4b6f21c2 100644 --- a/build.gradle +++ b/build.gradle @@ -8,7 +8,7 @@ buildscript { mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:4.1.3' + classpath 'com.android.tools.build:gradle:4.2.2' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" } } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 9f2e253f..0d90a675 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-6.5-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-6.7.1-bin.zip From 20ab97393407a2b7abea68e6cb2117fbc6b5d767 Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Sat, 3 Jul 2021 19:34:37 +0200 Subject: [PATCH 165/187] Upgrade sdk version to 30 --- app/src/main/java/com/readrops/app/item/WebViewActivity.kt | 6 +++--- build.gradle | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/com/readrops/app/item/WebViewActivity.kt b/app/src/main/java/com/readrops/app/item/WebViewActivity.kt index a64a2464..8e5f0eab 100644 --- a/app/src/main/java/com/readrops/app/item/WebViewActivity.kt +++ b/app/src/main/java/com/readrops/app/item/WebViewActivity.kt @@ -97,8 +97,8 @@ class WebViewActivity : AppCompatActivity() { super.onBackPressed() } - override fun onOptionsItemSelected(item: MenuItem?): Boolean { - when (item?.itemId) { + override fun onOptionsItemSelected(item: MenuItem): Boolean { + when (item.itemId) { android.R.id.home -> { if (binding.webView.canGoBack()) binding.webView.goBack() @@ -114,7 +114,7 @@ class WebViewActivity : AppCompatActivity() { } } - return super.onOptionsItemSelected(item!!) + return super.onOptionsItemSelected(item) } private fun shareLink() { diff --git a/build.gradle b/build.gradle index 4b6f21c2..70d196b9 100644 --- a/build.gradle +++ b/build.gradle @@ -27,10 +27,10 @@ allprojects { } ext { - compileSdkVersion = 29 + compileSdkVersion = 30 minSdkVersion = 21 - targetSdkVersion = 29 - buildToolsVersion = "29.0.3" + targetSdkVersion = 30 + buildToolsVersion = "30.0.3" } task clean(type: Delete) { From 42cb3adee944b7752dd89da1b10cb30ceec27ab5 Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Sat, 3 Jul 2021 19:44:42 +0200 Subject: [PATCH 166/187] Update dependencies --- api/build.gradle | 16 ++++++++-------- app/build.gradle | 34 +++++++++++++++++----------------- db/build.gradle | 27 ++++++++++++++------------- 3 files changed, 39 insertions(+), 38 deletions(-) diff --git a/api/build.gradle b/api/build.gradle index 991e4995..02985dcc 100644 --- a/api/build.gradle +++ b/api/build.gradle @@ -46,17 +46,17 @@ dependencies { implementation project(':db') testImplementation 'junit:junit:4.13' - androidTestImplementation 'androidx.test.ext:junit:1.1.2' - androidTestImplementation 'androidx.test:runner:1.3.0' - androidTestImplementation 'androidx.test:rules:1.3.0' - androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0' + androidTestImplementation 'androidx.test.ext:junit:1.1.3' + androidTestImplementation 'androidx.test:runner:1.4.0' + androidTestImplementation 'androidx.test:rules:1.4.0' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' testImplementation 'com.squareup.okhttp3:mockwebserver:4.9.0' androidTestImplementation "io.insert-koin:koin-test:2.2.3" testImplementation "io.insert-koin:koin-test:2.2.3" - implementation 'com.gitlab.mvysny.konsume-xml:konsume-xml:0.12' + implementation 'com.gitlab.mvysny.konsume-xml:konsume-xml:1.0' - implementation 'com.squareup.okhttp3:okhttp:4.9.0' + implementation 'com.squareup.okhttp3:okhttp:4.9.1' implementation('com.squareup.retrofit2:retrofit:2.9.0') { exclude group: 'okhttp3', module: 'okhttp3' @@ -73,8 +73,8 @@ dependencies { implementation 'com.squareup.retrofit2:adapter-rxjava2:2.9.0' - implementation 'com.squareup.moshi:moshi:1.11.0' - kapt 'com.squareup.moshi:moshi-kotlin-codegen:1.11.0' + implementation 'com.squareup.moshi:moshi:1.12.0' + kapt 'com.squareup.moshi:moshi-kotlin-codegen:1.12.0' api 'io.reactivex.rxjava2:rxandroid:2.1.1' api 'org.jsoup:jsoup:1.13.1' diff --git a/app/build.gradle b/app/build.gradle index f9e97567..861eb3d0 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -58,35 +58,35 @@ dependencies { implementation project(':api') implementation project(':db') - coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.0.10' + coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5' testImplementation 'junit:junit:4.13' - androidTestImplementation 'androidx.test.ext:junit:1.1.2' - androidTestImplementation 'androidx.test:runner:1.3.0' - androidTestImplementation 'androidx.test:rules:1.3.0' - androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0' + androidTestImplementation 'androidx.test.ext:junit:1.1.3' + androidTestImplementation 'androidx.test:runner:1.4.0' + androidTestImplementation 'androidx.test:rules:1.4.0' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' - implementation 'com.google.android.material:material:1.2.1' + implementation 'com.google.android.material:material:1.4.0' implementation 'androidx.cardview:cardview:1.0.0' - implementation 'androidx.palette:palette:1.0.0' - implementation 'androidx.recyclerview:recyclerview:1.1.0' - implementation 'androidx.constraintlayout:constraintlayout:2.0.1' + implementation 'androidx.palette:palette-ktx:1.0.0' + implementation 'androidx.recyclerview:recyclerview:1.2.1' + implementation 'androidx.constraintlayout:constraintlayout:2.0.4' implementation 'androidx.legacy:legacy-support-v4:1.0.0' implementation 'androidx.preference:preference:1.1.1' - implementation "androidx.work:work-runtime-ktx:2.4.0" - implementation "androidx.fragment:fragment-ktx:1.2.5" - implementation "androidx.browser:browser:1.2.0" + implementation "androidx.work:work-runtime-ktx:2.5.0" + implementation "androidx.fragment:fragment-ktx:1.3.5" + implementation "androidx.browser:browser:1.3.0" testImplementation "io.insert-koin:koin-test:2.2.3" - implementation 'com.github.bumptech.glide:glide:4.11.0' - kapt 'com.github.bumptech.glide:compiler:4.11.0' - implementation 'com.github.bumptech.glide:okhttp3-integration:4.11.0' - implementation('com.github.bumptech.glide:recyclerview-integration:4.11.0') { + implementation 'com.github.bumptech.glide:glide:4.12.0' + kapt 'com.github.bumptech.glide:compiler:4.12.0' + implementation 'com.github.bumptech.glide:okhttp3-integration:4.12.0' + implementation('com.github.bumptech.glide:recyclerview-integration:4.12.0') { transitive = false } implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0' - kapt 'androidx.lifecycle:lifecycle-common-java8:2.2.0' + kapt 'androidx.lifecycle:lifecycle-common-java8:2.3.1' implementation 'com.afollestad.material-dialogs:core:0.9.6.0' diff --git a/db/build.gradle b/db/build.gradle index 0e8230dd..f6d28aac 100644 --- a/db/build.gradle +++ b/db/build.gradle @@ -55,28 +55,29 @@ android { dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) - api "androidx.core:core-ktx:1.3.2" - api 'androidx.appcompat:appcompat:1.2.0' + api "androidx.core:core-ktx:1.6.0" + api 'androidx.appcompat:appcompat:1.3.0' api "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" testImplementation 'junit:junit:4.13' - androidTestImplementation 'androidx.test.ext:junit:1.1.2' - androidTestImplementation 'androidx.test:runner:1.3.0' - androidTestImplementation 'androidx.test:rules:1.3.0' - androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0' + androidTestImplementation 'androidx.test.ext:junit:1.1.3' + androidTestImplementation 'androidx.test:runner:1.4.0' + androidTestImplementation 'androidx.test:rules:1.4.0' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' - api 'androidx.room:room-runtime:2.2.5' - kapt 'androidx.room:room-compiler:2.2.5' - implementation 'androidx.room:room-rxjava2:2.2.5' - androidTestImplementation "androidx.room:room-testing:2.2.5" + def room_version = "2.3.0" + api "androidx.room:room-runtime:$room_version" + kapt "androidx.room:room-compiler:$room_version" + implementation "androidx.room:room-rxjava2:$room_version" + androidTestImplementation "androidx.room:room-testing:$room_version" - implementation 'com.github.MatrixDev.Roomigrant:RoomigrantLib:0.2.0' - kapt 'com.github.MatrixDev.Roomigrant:RoomigrantCompiler:0.2.0' + implementation 'com.github.MatrixDev.Roomigrant:RoomigrantLib:0.3.4' + kapt 'com.github.MatrixDev.Roomigrant:RoomigrantCompiler:0.3.4' api 'androidx.paging:paging-runtime:2.1.2' api 'androidx.paging:paging-common:2.1.2' - api 'joda-time:joda-time:2.10.5' + api 'joda-time:joda-time:2.10.10' def koin_version = "2.2.3" api "io.insert-koin:koin-android:$koin_version" From 2c7e5de2946e50ef9929566d815abaa59833a969 Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Sun, 4 Jul 2021 13:24:36 +0200 Subject: [PATCH 167/187] Sort items descendantly by publication date --- db/src/main/java/com/readrops/db/queries/ItemsQueryBuilder.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/db/src/main/java/com/readrops/db/queries/ItemsQueryBuilder.kt b/db/src/main/java/com/readrops/db/queries/ItemsQueryBuilder.kt index 55c33ea3..f400fa0a 100644 --- a/db/src/main/java/com/readrops/db/queries/ItemsQueryBuilder.kt +++ b/db/src/main/java/com/readrops/db/queries/ItemsQueryBuilder.kt @@ -21,7 +21,7 @@ object ItemsQueryBuilder { private const val SEPARATE_STATE_JOIN = "LEFT JOIN ItemState On Item.remoteId = ItemState.remote_id" - private const val ORDER_BY_ASC = "Item.id DESC" + private const val ORDER_BY_ASC = "pub_date DESC" private const val ORDER_BY_DESC = "pub_date ASC" From f546fdd31ae2efe6204fed9fc39dc3df544d0324 Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Sun, 4 Jul 2021 13:36:41 +0200 Subject: [PATCH 168/187] Fix failing query test --- db/src/test/java/com/readrops/db/ItemsQueryBuilderTest.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/db/src/test/java/com/readrops/db/ItemsQueryBuilderTest.kt b/db/src/test/java/com/readrops/db/ItemsQueryBuilderTest.kt index 59fa0e9f..ae2baa01 100644 --- a/db/src/test/java/com/readrops/db/ItemsQueryBuilderTest.kt +++ b/db/src/test/java/com/readrops/db/ItemsQueryBuilderTest.kt @@ -18,7 +18,7 @@ class ItemsQueryBuilderTest { assertTrue(query.contains("Feed.account_id = 1")) assertTrue(query.contains("read_it_later = 0")) - assertTrue(query.contains("Item.id DESC")) + assertTrue(query.contains("pub_date DESC")) assertFalse(query.contains("read = 0 And")) } From dfe225ed8d7fb8e91a754ae3ae64a4023716fd10 Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Sun, 4 Jul 2021 13:58:16 +0200 Subject: [PATCH 169/187] Fix some new warnings --- .../readrops/app/itemslist/MainActivity.java | 105 +++++++++--------- 1 file changed, 52 insertions(+), 53 deletions(-) diff --git a/app/src/main/java/com/readrops/app/itemslist/MainActivity.java b/app/src/main/java/com/readrops/app/itemslist/MainActivity.java index fe084296..db1ab988 100644 --- a/app/src/main/java/com/readrops/app/itemslist/MainActivity.java +++ b/app/src/main/java/com/readrops/app/itemslist/MainActivity.java @@ -250,6 +250,7 @@ public class MainActivity extends AppCompatActivity implements SwipeRefreshLayou int id = (int) drawerItem.getIdentifier(); switch (id) { + default: case DrawerManager.ARTICLES_ITEM_ID: viewModel.setFilterType(FilterType.NO_FILTER); scrollToTop = true; @@ -358,7 +359,7 @@ public class MainActivity extends AppCompatActivity implements SwipeRefreshLayou RecyclerViewPreloader preloader = new RecyclerViewPreloader(Glide.with(this), adapter, preloadSizeProvider, 10); binding.itemsRecyclerView.addOnScrollListener(preloader); - binding.itemsRecyclerView.setRecyclerListener(viewHolder -> { + binding.itemsRecyclerView.addRecyclerListener(viewHolder -> { MainItemListAdapter.ItemViewHolder vh = (MainItemListAdapter.ItemViewHolder) viewHolder; KoinJavaComponent.get(GlideRequests.class).clear(vh.getItemImage()); }); @@ -418,7 +419,7 @@ public class MainActivity extends AppCompatActivity implements SwipeRefreshLayou @Override public void onSwipe(@NotNull RecyclerView.ViewHolder viewHolder, int direction) { if (direction == ItemTouchHelper.LEFT) { // set item read state - ItemWithFeed itemWithFeed = adapter.getItemWithFeed(viewHolder.getAdapterPosition()); + ItemWithFeed itemWithFeed = adapter.getItemWithFeed(viewHolder.getBindingAdapterPosition()); itemWithFeed.getItem().setRead(!itemWithFeed.getItem().isRead()); viewModel.setItemReadState(itemWithFeed) @@ -427,16 +428,16 @@ public class MainActivity extends AppCompatActivity implements SwipeRefreshLayou .doOnError(throwable -> Utils.showSnackbar(binding.mainRoot, throwable.getMessage())) .subscribe(); - adapter.notifyItemChanged(viewHolder.getAdapterPosition()); + adapter.notifyItemChanged(viewHolder.getBindingAdapterPosition()); } else { // add item to read it later section - viewModel.setItemReadItLater((int) adapter.getItemId(viewHolder.getAdapterPosition())) + viewModel.setItemReadItLater((int) adapter.getItemId(viewHolder.getBindingAdapterPosition())) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .doOnError(throwable -> Utils.showSnackbar(binding.mainRoot, throwable.getMessage())) .subscribe(); if (viewModel.getFilterType() == FilterType.READ_IT_LATER_FILTER) - adapter.notifyItemChanged(viewHolder.getAdapterPosition()); + adapter.notifyItemChanged(viewHolder.getBindingAdapterPosition()); } } @@ -461,23 +462,21 @@ public class MainActivity extends AppCompatActivity implements SwipeRefreshLayou @Override public boolean onActionItemClicked(ActionMode actionMode, MenuItem menuItem) { - switch (menuItem.getItemId()) { - case R.id.item_mark_read: - setReadState(true); - break; - case R.id.item_mark_unread: - setReadState(false); - break; - case R.id.item_select_all: - if (allItemsSelected) { - adapter.unselectAll(); - allItemsSelected = false; - actionMode.finish(); - } else { - adapter.selectAll(); - allItemsSelected = true; - } - break; + int itemId = menuItem.getItemId(); + + if (itemId == R.id.item_mark_read) { + setReadState(true); + } else if (itemId == R.id.item_mark_unread) { + setReadState(false); + } else if (itemId == R.id.item_select_all) { + if (allItemsSelected) { + adapter.unselectAll(); + allItemsSelected = false; + actionMode.finish(); + } else { + adapter.selectAll(); + allItemsSelected = true; + } } return true; @@ -528,13 +527,13 @@ public class MainActivity extends AppCompatActivity implements SwipeRefreshLayou .observeOn(AndroidSchedulers.mainThread()) .subscribe(new DisposableSingleObserver() { @Override - public void onSuccess(Integer integer) { + public void onSuccess(@NonNull Integer integer) { feedNb = integer; sync(null); } @Override - public void onError(Throwable e) { + public void onError(@NonNull Throwable e) { Utils.showSnackbar(binding.mainRoot, e.getMessage()); } }); @@ -596,7 +595,7 @@ public class MainActivity extends AppCompatActivity implements SwipeRefreshLayou .observeOn(AndroidSchedulers.mainThread()) .subscribe(new Observer() { @Override - public void onSubscribe(Disposable d) { + public void onSubscribe(@NonNull Disposable d) { syncDisposable = d; if (viewModel.isAccountLocal() && feedNb > 0) { @@ -606,7 +605,7 @@ public class MainActivity extends AppCompatActivity implements SwipeRefreshLayou } @Override - public void onNext(Feed feed) { + public void onNext(@NonNull Feed feed) { if (viewModel.isAccountLocal() && feedNb > 0) { binding.syncProgressTextView.setText(getString(R.string.updating_feed, feed.getName())); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { @@ -619,7 +618,7 @@ public class MainActivity extends AppCompatActivity implements SwipeRefreshLayou } @Override - public void onError(Throwable e) { + public void onError(@NonNull Throwable e) { e.printStackTrace(); binding.swipeRefreshLayout.setRefreshing(false); binding.syncProgressLayout.setVisibility(View.GONE); @@ -667,31 +666,31 @@ public class MainActivity extends AppCompatActivity implements SwipeRefreshLayou @Override public boolean onOptionsItemSelected(MenuItem item) { - switch (item.getItemId()) { - case R.id.item_filter_read_items: - if (item.isChecked()) { - item.setChecked(false); - viewModel.setShowReadItems(false); - SharedPreferencesManager.writeValue( - SharedPreferencesManager.SharedPrefKey.SHOW_READ_ARTICLES, false); - } else { - item.setChecked(true); - viewModel.setShowReadItems(true); - SharedPreferencesManager.writeValue( - SharedPreferencesManager.SharedPrefKey.SHOW_READ_ARTICLES, true); - } + int itemId = item.getItemId(); - viewModel.invalidate(); - return true; - case R.id.item_sort: - displayFilterDialog(); - return true; - case R.id.start_sync: - if (!viewModel.isAccountLocal()) { - binding.swipeRefreshLayout.setRefreshing(true); - } - onRefresh(); - break; + if (itemId == R.id.item_filter_read_items) { + if (item.isChecked()) { + item.setChecked(false); + viewModel.setShowReadItems(false); + SharedPreferencesManager.writeValue( + SharedPreferencesManager.SharedPrefKey.SHOW_READ_ARTICLES, false); + } else { + item.setChecked(true); + viewModel.setShowReadItems(true); + SharedPreferencesManager.writeValue( + SharedPreferencesManager.SharedPrefKey.SHOW_READ_ARTICLES, true); + } + + viewModel.invalidate(); + return true; + } else if (itemId == R.id.item_sort) { + displayFilterDialog(); + return true; + } else if (itemId == R.id.start_sync) { + if (!viewModel.isAccountLocal()) { + binding.swipeRefreshLayout.setRefreshing(true); + } + onRefresh(); } return super.onOptionsItemSelected(item); @@ -730,7 +729,7 @@ public class MainActivity extends AppCompatActivity implements SwipeRefreshLayou private void startAboutActivity() { Libs.ActivityStyle activityStyle; - if (Boolean.valueOf(SharedPreferencesManager.readString(SharedPreferencesManager.SharedPrefKey.DARK_THEME))) + if (Boolean.parseBoolean(SharedPreferencesManager.readString(SharedPreferencesManager.SharedPrefKey.DARK_THEME))) activityStyle = Libs.ActivityStyle.DARK; else activityStyle = Libs.ActivityStyle.LIGHT_DARK_TOOLBAR; @@ -757,7 +756,7 @@ public class MainActivity extends AppCompatActivity implements SwipeRefreshLayou } @Override - protected void onSaveInstanceState(Bundle outState) { + protected void onSaveInstanceState(@NonNull Bundle outState) { if (binding.swipeRefreshLayout.isRefreshing()) outState.putBoolean(SYNCING, true); From c9caa0be89641b264161aaebb160fe99d3c2954f Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Sun, 4 Jul 2021 19:12:29 +0200 Subject: [PATCH 170/187] Integrate ItemStateChange table in Nextcloud News synchronisation --- .../app/repositories/ARepository.java | 10 ++-- .../app/repositories/NextNewsRepository.java | 52 ++++++++++++++----- .../java/com/readrops/db/dao/ItemDao.java | 14 ++--- 3 files changed, 49 insertions(+), 27 deletions(-) diff --git a/app/src/main/java/com/readrops/app/repositories/ARepository.java b/app/src/main/java/com/readrops/app/repositories/ARepository.java index a952574a..ea9da56e 100644 --- a/app/src/main/java/com/readrops/app/repositories/ARepository.java +++ b/app/src/main/java/com/readrops/app/repositories/ARepository.java @@ -121,8 +121,9 @@ public abstract class ARepository { item.isStarred(), item.getRemoteId(), account.getId()))); } else if (account.isLocal()) { return database.itemDao().setReadState(item.getId(), item.isRead()); - } else { // TODO nextcloud case, use only ItemStateChange table - return Completable.complete(); + } else { // nextcloud case + return database.itemStateChangesDao().upsertItemReadStateChange(item, account.getId()) + .andThen(database.itemDao().setReadState(item.getId(), item.isRead())); } } @@ -142,8 +143,9 @@ public abstract class ARepository { item.isStarred(), item.getRemoteId(), account.getId()))); } else if (account.isLocal()) { return database.itemDao().setStarState(item.getId(), item.isRead()); - } else { // TODO nextcloud case, use only ItemStateChange table - return Completable.complete(); + } else { // nextcloud case + return database.itemStateChangesDao().upsertItemReadStateChange(item, account.getId()) + .andThen(database.itemDao().setStarState(item.getId(), item.isRead())); } } diff --git a/app/src/main/java/com/readrops/app/repositories/NextNewsRepository.java b/app/src/main/java/com/readrops/app/repositories/NextNewsRepository.java index 7da5c170..5a491cea 100644 --- a/app/src/main/java/com/readrops/app/repositories/NextNewsRepository.java +++ b/app/src/main/java/com/readrops/app/repositories/NextNewsRepository.java @@ -23,6 +23,7 @@ import com.readrops.db.entities.Feed; import com.readrops.db.entities.Folder; import com.readrops.db.entities.Item; import com.readrops.db.entities.account.Account; +import com.readrops.db.pojo.ItemReadStarState; import org.joda.time.LocalDateTime; import org.koin.java.KoinJavaComponent; @@ -31,6 +32,7 @@ import java.io.IOException; import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.stream.Collectors; import io.reactivex.Completable; import io.reactivex.Observable; @@ -81,26 +83,55 @@ public class NextNewsRepository extends ARepository { @Override public Observable sync(List feeds) { + setCredentials(account); return Observable.create(emitter -> { try { long lastModified = LocalDateTime.now().toDateTime().getMillis(); SyncType syncType; - if (account.getLastModified() != 0) + if (account.getLastModified() != 0) { syncType = SyncType.CLASSIC_SYNC; - else + } else { syncType = SyncType.INITIAL_SYNC; + } NextNewsSyncData syncData = new NextNewsSyncData(); if (syncType == SyncType.CLASSIC_SYNC) { syncData.setLastModified(account.getLastModified() / 1000L); - syncData.setReadItems(database.itemDao().getReadChanges(account.getId())); - syncData.setUnreadItems(database.itemDao().getUnreadChanges(account.getId())); + List itemStateChanges = database + .itemStateChangesDao() + .getItemStateChanges(account.getId()); + + syncData.setReadItems(itemStateChanges.stream() + .filter(it -> it.getReadChange() && it.getRead()) + .map(ItemReadStarState::getRemoteId) + .collect(Collectors.toList())); + + syncData.setUnreadItems(itemStateChanges.stream() + .filter(it -> it.getReadChange() && !it.getRead()) + .map(ItemReadStarState::getRemoteId) + .collect(Collectors.toList())); + + List starredItemsIds = itemStateChanges.stream() + .filter(it -> it.getStarChange() && it.getStarred()) + .map(ItemReadStarState::getRemoteId) + .collect(Collectors.toList()); + + if (!starredItemsIds.isEmpty()) { + syncData.setStarredItems(database.itemDao().getStarChanges(starredItemsIds, account.getId())); + } + + List unstarredItemsIds = itemStateChanges.stream() + .filter(it -> it.getStarChange() && !it.getStarred()) + .map(ItemReadStarState::getRemoteId) + .collect(Collectors.toList()); + + if (!unstarredItemsIds.isEmpty()) { + syncData.setUnstarredItems(database.itemDao().getUnstarChanges(unstarredItemsIds, account.getId())); + } - syncData.setStarredItems(database.itemDao().getStarChanges(account.getId())); - syncData.setUnstarredItems(database.itemDao().getUnstarChanges(account.getId())); } TimingLogger timings = new TimingLogger(TAG, "nextcloud news " + syncType.name().toLowerCase()); @@ -123,13 +154,7 @@ public class NextNewsRepository extends ARepository { account.setLastModified(lastModified); database.accountDao().updateLastModified(account.getId(), lastModified); - if (!syncData.getReadItems().isEmpty() || !syncData.getUnreadItems().isEmpty()) { - - } - - if (!syncData.getStarredItems().isEmpty() || !syncData.getUnstarredItems().isEmpty()) { - - } + database.itemStateChangesDao().resetStateChanges(account.getId()); emitter.onComplete(); } else { @@ -145,6 +170,7 @@ public class NextNewsRepository extends ARepository { @Override public Single> addFeeds(List results) { + setCredentials(account); return Single.create(emitter -> { List feedInsertionResults = new ArrayList<>(); diff --git a/db/src/main/java/com/readrops/db/dao/ItemDao.java b/db/src/main/java/com/readrops/db/dao/ItemDao.java index 3e46a613..8f6c63be 100644 --- a/db/src/main/java/com/readrops/db/dao/ItemDao.java +++ b/db/src/main/java/com/readrops/db/dao/ItemDao.java @@ -58,17 +58,11 @@ public interface ItemDao extends BaseDao { @RawQuery(observedEntities = {Item.class, ItemState.class}) LiveData getItemById(SupportSQLiteQuery query); - @Query("Select Item.remoteId From Item Inner Join Feed On Item.feed_id = Feed.id Where read = 1 And account_id = :accountId") - List getReadChanges(int accountId); + @Query("Select Item.guid, Feed.remoteId as feedRemoteId From Item Inner Join Feed On Item.feed_id = Feed.id Where Item.remoteId In (:remoteIds) And account_id = :accountId") + List getStarChanges(List remoteIds, int accountId); - @Query("Select Item.remoteId From Item Inner Join Feed On Item.feed_id = Feed.id Where read = 0 And account_id = :accountId") - List getUnreadChanges(int accountId); - - @Query("Select Item.guid, Feed.remoteId as feedRemoteId From Item Inner Join Feed On Item.feed_id = Feed.id Where starred = 1 And account_id = :accountId") - List getStarChanges(int accountId); - - @Query("Select Item.guid, Feed.remoteId as feedRemoteId From Item Inner Join Feed On Item.feed_id = Feed.id Where starred = 0 And account_id = :accountId") - List getUnstarChanges(int accountId); + @Query("Select Item.guid, Feed.remoteId as feedRemoteId From Item Inner Join Feed On Item.feed_id = Feed.id Where Item.remoteId In (:remoteIds) And account_id = :accountId") + List getUnstarChanges(List remoteIds, int accountId); @Query("Update Item set read = :read, starred = :starred Where remoteId = :remoteId") void setReadAndStarState(String remoteId, boolean read, boolean starred); From 8c9b2b2ab108a33ad082e46f561d74240631d1d9 Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Mon, 5 Jul 2021 21:00:40 +0200 Subject: [PATCH 171/187] Nextcloud News : get read/unread starred items at initial synchronisation --- .../nextcloudnews/NextNewsDataSource.java | 33 +++++++++++++++++-- .../nextcloudnews/NextNewsService.java | 2 +- .../app/repositories/NextNewsRepository.java | 5 ++- 3 files changed, 35 insertions(+), 5 deletions(-) diff --git a/api/src/main/java/com/readrops/api/services/nextcloudnews/NextNewsDataSource.java b/api/src/main/java/com/readrops/api/services/nextcloudnews/NextNewsDataSource.java index 24875679..fd8c57ee 100644 --- a/api/src/main/java/com/readrops/api/services/nextcloudnews/NextNewsDataSource.java +++ b/api/src/main/java/com/readrops/api/services/nextcloudnews/NextNewsDataSource.java @@ -30,7 +30,8 @@ public class NextNewsDataSource { private static final String TAG = NextNewsDataSource.class.getSimpleName(); - protected static final int MAX_ITEMS = 5000; + private static final int MAX_ITEMS = 5000; + private static final int MAX_STARRED_ITEMS = 1000; private NextNewsService api; @@ -86,7 +87,8 @@ public class NextNewsDataSource { private void initialSync(SyncResult syncResult) throws IOException { getFeedsAndFolders(syncResult); - Response> itemsResponse = api.getItems(3, false, MAX_ITEMS).execute(); + // unread items + Response> itemsResponse = api.getItems(ItemQueryType.ALL.value, false, MAX_ITEMS).execute(); List itemList = itemsResponse.body(); if (!itemsResponse.isSuccessful()) @@ -94,13 +96,23 @@ public class NextNewsDataSource { if (itemList != null) syncResult.setItems(itemList); + + // starred items + Response> starredItemsResponse = api.getItems(ItemQueryType.STARRED.value, true, MAX_STARRED_ITEMS).execute(); + List starredItems = starredItemsResponse.body(); + + if (!itemsResponse.isSuccessful()) + syncResult.setError(true); + + if (itemList != null) + syncResult.setStarredItems(starredItems); } private void classicSync(SyncResult syncResult, NextNewsSyncData data) throws IOException { putModifiedItems(data, syncResult); getFeedsAndFolders(syncResult); - Response> itemsResponse = api.getNewItems(data.getLastModified(), 3).execute(); + Response> itemsResponse = api.getNewItems(data.getLastModified(), ItemQueryType.ALL.value).execute(); List itemList = itemsResponse.body(); if (!itemsResponse.isSuccessful()) @@ -265,4 +277,19 @@ public class NextNewsDataSource { STAR, UNSTAR } + + public enum ItemQueryType { + ALL(3), + STARRED(2); + + private int value; + + ItemQueryType(int value) { + this.value = value; + } + + public int getValue() { + return value; + } + } } diff --git a/api/src/main/java/com/readrops/api/services/nextcloudnews/NextNewsService.java b/api/src/main/java/com/readrops/api/services/nextcloudnews/NextNewsService.java index cf1ce098..1520ffa7 100644 --- a/api/src/main/java/com/readrops/api/services/nextcloudnews/NextNewsService.java +++ b/api/src/main/java/com/readrops/api/services/nextcloudnews/NextNewsService.java @@ -20,7 +20,7 @@ import retrofit2.http.Query; public interface NextNewsService { - String END_POINT = "/index.php/apps/news/api/v1-2/"; + String END_POINT = "/index.php/apps/news/api/v1-2"; @GET("/ocs/v1.php/cloud/users/{userId}") @Headers("OCS-APIRequest: true") diff --git a/app/src/main/java/com/readrops/app/repositories/NextNewsRepository.java b/app/src/main/java/com/readrops/app/repositories/NextNewsRepository.java index 5a491cea..6fe9dc1e 100644 --- a/app/src/main/java/com/readrops/app/repositories/NextNewsRepository.java +++ b/app/src/main/java/com/readrops/app/repositories/NextNewsRepository.java @@ -147,8 +147,11 @@ public class NextNewsRepository extends ARepository { insertFeeds(result.getFeeds(), false); timings.addSplit("insert feeds"); - insertItems(result.getItems(), syncType == SyncType.INITIAL_SYNC); + boolean initialSync = syncType == SyncType.INITIAL_SYNC; + insertItems(result.getItems(), initialSync); timings.addSplit("insert items"); + + insertItems(result.getStarredItems(), initialSync); timings.dumpToLog(); account.setLastModified(lastModified); From 3d0ff2619fdbe8ba18734e9e2171d0f5a73cd1c5 Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Tue, 6 Jul 2021 22:04:53 +0200 Subject: [PATCH 172/187] Adapt new state changes management for Nextcloud News --- .../com/readrops/app/item/ItemViewModel.java | 5 ++-- .../app/repositories/ARepository.java | 10 +++---- .../app/repositories/NextNewsRepository.java | 4 +-- .../java/com/readrops/db/dao/ItemDao.java | 3 -- .../com/readrops/db/dao/ItemStateChangeDao.kt | 29 +++++++++++++++---- 5 files changed, 34 insertions(+), 17 deletions(-) diff --git a/app/src/main/java/com/readrops/app/item/ItemViewModel.java b/app/src/main/java/com/readrops/app/item/ItemViewModel.java index 3976312b..9fddfe78 100644 --- a/app/src/main/java/com/readrops/app/item/ItemViewModel.java +++ b/app/src/main/java/com/readrops/app/item/ItemViewModel.java @@ -45,8 +45,9 @@ public class ItemViewModel extends ViewModel { } public Completable setStarState(Item item) { - return KoinJavaComponent.get(ARepository.class, null, () -> DefinitionParametersKt.parametersOf(account)) - .setItemStarState(item); + ARepository repository = KoinJavaComponent.get(ARepository.class, null, () -> DefinitionParametersKt.parametersOf(account)); + + return repository.setItemStarState(item); } public Uri saveImageInCache(Bitmap bitmap, Context context) throws IOException { diff --git a/app/src/main/java/com/readrops/app/repositories/ARepository.java b/app/src/main/java/com/readrops/app/repositories/ARepository.java index ea9da56e..b1d5f72e 100644 --- a/app/src/main/java/com/readrops/app/repositories/ARepository.java +++ b/app/src/main/java/com/readrops/app/repositories/ARepository.java @@ -116,13 +116,13 @@ public abstract class ARepository { public Completable setItemReadState(Item item) { if (account.getConfig().useSeparateState()) { - return database.itemStateChangesDao().upsertItemReadStateChange(item, account.getId()) + return database.itemStateChangesDao().upsertItemReadStateChange(item, account.getId(), true) .andThen(database.itemStateDao().upsertItemReadState(new ItemState(0, item.isRead(), item.isStarred(), item.getRemoteId(), account.getId()))); } else if (account.isLocal()) { return database.itemDao().setReadState(item.getId(), item.isRead()); } else { // nextcloud case - return database.itemStateChangesDao().upsertItemReadStateChange(item, account.getId()) + return database.itemStateChangesDao().upsertItemReadStateChange(item, account.getId(), false) .andThen(database.itemDao().setReadState(item.getId(), item.isRead())); } @@ -138,14 +138,14 @@ public abstract class ARepository { public Completable setItemStarState(Item item) { if (account.getConfig().useSeparateState()) { - return database.itemStateChangesDao().upsertItemStarStateChange(item, account.getId()) + return database.itemStateChangesDao().upsertItemStarStateChange(item, account.getId(), true) .andThen(database.itemStateDao().upsertItemStarState(new ItemState(0, item.isRead(), item.isStarred(), item.getRemoteId(), account.getId()))); } else if (account.isLocal()) { return database.itemDao().setStarState(item.getId(), item.isRead()); } else { // nextcloud case - return database.itemStateChangesDao().upsertItemReadStateChange(item, account.getId()) - .andThen(database.itemDao().setStarState(item.getId(), item.isRead())); + return database.itemStateChangesDao().upsertItemStarStateChange(item, account.getId(), false) + .andThen(database.itemDao().setStarState(item.getId(), item.isStarred())); } } diff --git a/app/src/main/java/com/readrops/app/repositories/NextNewsRepository.java b/app/src/main/java/com/readrops/app/repositories/NextNewsRepository.java index 6fe9dc1e..6187478d 100644 --- a/app/src/main/java/com/readrops/app/repositories/NextNewsRepository.java +++ b/app/src/main/java/com/readrops/app/repositories/NextNewsRepository.java @@ -102,7 +102,7 @@ public class NextNewsRepository extends ARepository { List itemStateChanges = database .itemStateChangesDao() - .getItemStateChanges(account.getId()); + .getNextcloudNewsStateChanges(account.getId()); syncData.setReadItems(itemStateChanges.stream() .filter(it -> it.getReadChange() && it.getRead()) @@ -129,7 +129,7 @@ public class NextNewsRepository extends ARepository { .collect(Collectors.toList()); if (!unstarredItemsIds.isEmpty()) { - syncData.setUnstarredItems(database.itemDao().getUnstarChanges(unstarredItemsIds, account.getId())); + syncData.setUnstarredItems(database.itemDao().getStarChanges(unstarredItemsIds, account.getId())); } } diff --git a/db/src/main/java/com/readrops/db/dao/ItemDao.java b/db/src/main/java/com/readrops/db/dao/ItemDao.java index 8f6c63be..3c1b5b6c 100644 --- a/db/src/main/java/com/readrops/db/dao/ItemDao.java +++ b/db/src/main/java/com/readrops/db/dao/ItemDao.java @@ -61,9 +61,6 @@ public interface ItemDao extends BaseDao { @Query("Select Item.guid, Feed.remoteId as feedRemoteId From Item Inner Join Feed On Item.feed_id = Feed.id Where Item.remoteId In (:remoteIds) And account_id = :accountId") List getStarChanges(List remoteIds, int accountId); - @Query("Select Item.guid, Feed.remoteId as feedRemoteId From Item Inner Join Feed On Item.feed_id = Feed.id Where Item.remoteId In (:remoteIds) And account_id = :accountId") - List getUnstarChanges(List remoteIds, int accountId); - @Query("Update Item set read = :read, starred = :starred Where remoteId = :remoteId") void setReadAndStarState(String remoteId, boolean read, boolean starred); } diff --git a/db/src/main/java/com/readrops/db/dao/ItemStateChangeDao.kt b/db/src/main/java/com/readrops/db/dao/ItemStateChangeDao.kt index 8eb6cc3c..0ae93b72 100644 --- a/db/src/main/java/com/readrops/db/dao/ItemStateChangeDao.kt +++ b/db/src/main/java/com/readrops/db/dao/ItemStateChangeDao.kt @@ -28,15 +28,25 @@ interface ItemStateChangeDao : BaseDao { "Left Join ItemState On ItemState.remote_id = Item.remoteId Where ItemStateChange.account_id = :accountId") fun getItemStateChanges(accountId: Int): List + @Query("Select Item.read, Item.starred," + + "ItemStateChange.read_change, ItemStateChange.star_change, Item.remoteId " + + "From ItemStateChange Inner Join Item On ItemStateChange.id = Item.id " + + "Where ItemStateChange.account_id = :accountId") + fun getNextcloudNewsStateChanges(accountId: Int): List + @Query("Select Case When :itemId In (Select id From ItemStateChange Where read_change = 1) Then 1 Else 0 End") fun readStateChangeExists(itemId: Int): Boolean @Query("Select Case When :itemId In (Select id From ItemStateChange Where star_change = 1) Then 1 Else 0 End") fun starStateChangeExists(itemId: Int): Boolean - fun upsertItemReadStateChange(item: Item, accountId: Int) = Completable.create { + fun upsertItemReadStateChange(item: Item, accountId: Int, useSeparateState: Boolean) = Completable.create { if (itemStateChangeExists(item.id, accountId)) { - val oldItemReadState = getItemReadState(item.remoteId, accountId) + val oldItemReadState = if (useSeparateState) + getItemReadState(item.remoteId, accountId) + else + getStandardItemReadState(item.remoteId, accountId) + val readChange = item.isRead != oldItemReadState if (readChange) { @@ -56,9 +66,13 @@ interface ItemStateChangeDao : BaseDao { it.onComplete() } - fun upsertItemStarStateChange(item: Item, accountId: Int) = Completable.create { + fun upsertItemStarStateChange(item: Item, accountId: Int, useSeparateState: Boolean) = Completable.create { if (itemStateChangeExists(item.id, accountId)) { - val oldItemStarState = getItemStarState(item.remoteId, accountId) + val oldItemStarState = if (useSeparateState) + getItemStarState(item.remoteId, accountId) + else + getStandardItemStarState(item.remoteId, accountId) + val starChange = item.isStarred != oldItemStarState if (starChange) { @@ -75,7 +89,6 @@ interface ItemStateChangeDao : BaseDao { insertItemStateChange(ItemStateChange(id = item.id, starChange = true, accountId = accountId)) } - it.onComplete() } @@ -88,9 +101,15 @@ interface ItemStateChangeDao : BaseDao { @Query("Select read From ItemState Where remote_id = :remoteId And account_id = :accountId") fun getItemReadState(remoteId: String, accountId: Int): Boolean + @Query("Select read From Item Inner Join Feed On Item.feed_id = Feed.id Where Item.remoteId = :remoteId And account_id = :accountId") + fun getStandardItemReadState(remoteId: String, accountId: Int): Boolean + @Query("Select starred From ItemState Where remote_id = :remoteId And account_id = :accountId") fun getItemStarState(remoteId: String, accountId: Int): Boolean + @Query("Select starred From Item Inner Join Feed On Item.feed_id = Feed.id Where Item.remoteId = :remoteId And account_id = :accountId") + fun getStandardItemStarState(remoteId: String, accountId: Int): Boolean + @Query("Update ItemStateChange set read_change = :readChange Where id = :id") fun updateItemReadStateChange(readChange: Boolean, id: Int) From 629ca050a392cfa9d4c5fa355f999b28519b6962 Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Thu, 8 Jul 2021 19:05:25 +0200 Subject: [PATCH 173/187] Remove url building in AuthInterceptor --- .../main/java/com/readrops/api/ApiModule.kt | 35 +++++++++---------- .../nextcloudnews/NextNewsService.java | 2 +- .../com/readrops/api/utils/AuthInterceptor.kt | 17 +++------ .../main/java/com/readrops/app/AppModule.kt | 8 +++-- .../app/account/AccountViewModel.java | 7 +--- .../app/account/AddAccountActivity.java | 4 +-- .../app/repositories/NextNewsRepository.java | 30 +++++++++++----- 7 files changed, 52 insertions(+), 51 deletions(-) diff --git a/api/src/main/java/com/readrops/api/ApiModule.kt b/api/src/main/java/com/readrops/api/ApiModule.kt index c1a1ace9..fe8e9960 100644 --- a/api/src/main/java/com/readrops/api/ApiModule.kt +++ b/api/src/main/java/com/readrops/api/ApiModule.kt @@ -2,6 +2,7 @@ package com.readrops.api import com.chimerapps.niddler.interceptor.okhttp.NiddlerOkHttpInterceptor import com.readrops.api.localfeed.LocalRSSDataSource +import com.readrops.api.services.Credentials import com.readrops.api.services.freshrss.FreshRSSDataSource import com.readrops.api.services.freshrss.FreshRSSService import com.readrops.api.services.freshrss.adapters.FreshRSSFeedsAdapter @@ -25,8 +26,6 @@ import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory import retrofit2.converter.moshi.MoshiConverterFactory import java.util.concurrent.TimeUnit -const val BASE_URL = "https://baseurl.com" - val apiModule = module { single(createdAtStart = true) { @@ -44,19 +43,19 @@ val apiModule = module { //region freshrss - single { FreshRSSDataSource(get()) } + factory { params -> FreshRSSDataSource(get(parameters = { params })) } - single { - get(named("freshrssRetrofit")) + factory { params -> + get(named("freshrssRetrofit"), parameters = { params }) .create(FreshRSSService::class.java) } - single(named("freshrssRetrofit")) { - Retrofit.Builder() // url will be set dynamically in AuthInterceptor - .baseUrl(BASE_URL) + factory(named("freshrssRetrofit")) { (credentials: Credentials) -> + Retrofit.Builder() + .baseUrl(credentials.url) .addCallAdapterFactory(RxJava2CallAdapterFactory.create()) - .client(get()) - .addConverterFactory(MoshiConverterFactory.create(get(named("freshrssMoshi")))) + .client(get()) + .addConverterFactory(MoshiConverterFactory.create(get(named("freshrssMoshi")))) .build() } @@ -73,19 +72,19 @@ val apiModule = module { //region nextcloud news - single { NextNewsDataSource(get()) } + factory { params -> NextNewsDataSource(get(parameters = { params })) } - single { - get(named("nextcloudNewsRetrofit")) + factory { params -> + get(named("nextcloudNewsRetrofit"), parameters = { params }) .create(NextNewsService::class.java) } - single(named("nextcloudNewsRetrofit")) { - Retrofit.Builder() // url will be set dynamically in AuthInterceptor - .baseUrl(BASE_URL) + factory(named("nextcloudNewsRetrofit")) { (credentials: Credentials) -> + Retrofit.Builder() + .baseUrl(credentials.url) .addCallAdapterFactory(RxJava2CallAdapterFactory.create()) - .client(get()) - .addConverterFactory(MoshiConverterFactory.create(get(named("nextcloudNewsMoshi")))) + .client(get()) + .addConverterFactory(MoshiConverterFactory.create(get(named("nextcloudNewsMoshi")))) .build() } diff --git a/api/src/main/java/com/readrops/api/services/nextcloudnews/NextNewsService.java b/api/src/main/java/com/readrops/api/services/nextcloudnews/NextNewsService.java index 1520ffa7..cf1ce098 100644 --- a/api/src/main/java/com/readrops/api/services/nextcloudnews/NextNewsService.java +++ b/api/src/main/java/com/readrops/api/services/nextcloudnews/NextNewsService.java @@ -20,7 +20,7 @@ import retrofit2.http.Query; public interface NextNewsService { - String END_POINT = "/index.php/apps/news/api/v1-2"; + String END_POINT = "/index.php/apps/news/api/v1-2/"; @GET("/ocs/v1.php/cloud/users/{userId}") @Headers("OCS-APIRequest: true") diff --git a/api/src/main/java/com/readrops/api/utils/AuthInterceptor.kt b/api/src/main/java/com/readrops/api/utils/AuthInterceptor.kt index 9c0e6797..06f9e3df 100644 --- a/api/src/main/java/com/readrops/api/utils/AuthInterceptor.kt +++ b/api/src/main/java/com/readrops/api/utils/AuthInterceptor.kt @@ -9,22 +9,13 @@ class AuthInterceptor(var credentials: Credentials? = null) : Interceptor { override fun intercept(chain: Interceptor.Chain): Response { val requestBuilder = chain.request().newBuilder() - val urlBuilder = chain.request().url.newBuilder() - if (credentials != null) { - if (credentials!!.url != null) { - val uri = URI.create(credentials!!.url) - urlBuilder - .scheme(uri.scheme!!) - .host(uri.host!!) - .encodedPath(uri.path + chain.request().url.encodedPath) - } - - if (credentials!!.authorization != null) { - requestBuilder.addHeader("Authorization", credentials!!.authorization) + credentials?.let { + if (it.authorization != null) { + requestBuilder.addHeader("Authorization", it.authorization) } } - return chain.proceed(requestBuilder.url(urlBuilder.build()).build()) + return chain.proceed(requestBuilder.build()) } } \ No newline at end of file diff --git a/app/src/main/java/com/readrops/app/AppModule.kt b/app/src/main/java/com/readrops/app/AppModule.kt index 0de02df7..8e9000b2 100644 --- a/app/src/main/java/com/readrops/app/AppModule.kt +++ b/app/src/main/java/com/readrops/app/AppModule.kt @@ -3,6 +3,7 @@ package com.readrops.app import androidx.preference.PreferenceManager import com.chimerapps.niddler.core.AndroidNiddler import com.chimerapps.niddler.core.Niddler +import com.readrops.api.services.Credentials import com.readrops.app.account.AccountViewModel import com.readrops.app.addfeed.AddFeedsViewModel import com.readrops.app.feedsfolders.ManageFeedsFoldersViewModel @@ -18,6 +19,7 @@ import com.readrops.db.entities.account.AccountType import org.koin.android.ext.koin.androidApplication import org.koin.android.ext.koin.androidContext import org.koin.androidx.viewmodel.dsl.viewModel +import org.koin.core.parameter.parametersOf import org.koin.dsl.module val appModule = module { @@ -25,8 +27,10 @@ val appModule = module { factory { (account: Account) -> when (account.accountType) { AccountType.LOCAL -> LocalFeedRepository(get(), get(), androidContext(), account) - AccountType.NEXTCLOUD_NEWS -> NextNewsRepository(get(), get(), androidContext(), account) - AccountType.FRESHRSS -> FreshRSSRepository(get(), get(), androidContext(), account) + AccountType.NEXTCLOUD_NEWS -> NextNewsRepository(get(parameters = { parametersOf(Credentials.toCredentials(account)) }), + get(), androidContext(), account) + AccountType.FRESHRSS -> FreshRSSRepository(get(parameters = { parametersOf(Credentials.toCredentials(account)) }), + get(), androidContext(), account) else -> throw IllegalArgumentException("Account type not supported") } } diff --git a/app/src/main/java/com/readrops/app/account/AccountViewModel.java b/app/src/main/java/com/readrops/app/account/AccountViewModel.java index c846e37d..7b4d5de3 100644 --- a/app/src/main/java/com/readrops/app/account/AccountViewModel.java +++ b/app/src/main/java/com/readrops/app/account/AccountViewModel.java @@ -12,7 +12,6 @@ import com.readrops.db.Database; import com.readrops.db.entities.Feed; import com.readrops.db.entities.Folder; import com.readrops.db.entities.account.Account; -import com.readrops.db.entities.account.AccountType; import org.koin.core.parameter.DefinitionParametersKt; import org.koin.java.KoinJavaComponent; @@ -32,17 +31,13 @@ public class AccountViewModel extends ViewModel { this.database = database; } - public void setAccountType(AccountType accountType) { - repository = KoinJavaComponent.get(ARepository.class, null, - () -> DefinitionParametersKt.parametersOf(new Account(null, null, accountType))); - } - public void setAccount(Account account) { repository = KoinJavaComponent.get(ARepository.class, null, () -> DefinitionParametersKt.parametersOf(account)); } public Completable login(Account account, boolean insert) { + setAccount(account); return repository.login(account, insert); } diff --git a/app/src/main/java/com/readrops/app/account/AddAccountActivity.java b/app/src/main/java/com/readrops/app/account/AddAccountActivity.java index d6cb63ea..23f11283 100644 --- a/app/src/main/java/com/readrops/app/account/AddAccountActivity.java +++ b/app/src/main/java/com/readrops/app/account/AddAccountActivity.java @@ -61,15 +61,13 @@ public class AddAccountActivity extends AppCompatActivity { getSupportActionBar().setDisplayHomeAsUpEnabled(true); if (accountToEdit != null) { - viewModel.setAccountType(accountToEdit.getAccountType()); editAccount = true; fillFields(); } else { - viewModel.setAccountType(accountType); - binding.providerImage.setImageResource(accountType.getIconRes()); binding.providerName.setText(accountType.getName()); binding.addAccountName.setText(accountType.getName()); + if (accountType == AccountType.FRESHRSS) { binding.addAccountPasswordLayout.setHelperText(getString(R.string.password_helper)); } diff --git a/app/src/main/java/com/readrops/app/repositories/NextNewsRepository.java b/app/src/main/java/com/readrops/app/repositories/NextNewsRepository.java index 6187478d..ea6f6e57 100644 --- a/app/src/main/java/com/readrops/app/repositories/NextNewsRepository.java +++ b/app/src/main/java/com/readrops/app/repositories/NextNewsRepository.java @@ -11,9 +11,8 @@ import androidx.annotation.Nullable; import com.readrops.api.services.SyncResult; import com.readrops.api.services.SyncType; import com.readrops.api.services.nextcloudnews.NextNewsDataSource; -import com.readrops.api.services.nextcloudnews.NextNewsService; import com.readrops.api.services.nextcloudnews.NextNewsSyncData; -import com.readrops.api.utils.AuthInterceptor; +import com.readrops.api.services.nextcloudnews.adapters.NextNewsUserAdapter; import com.readrops.api.utils.exceptions.UnknownFormatException; import com.readrops.app.addfeed.FeedInsertionResult; import com.readrops.app.addfeed.ParsingResult; @@ -37,6 +36,9 @@ import java.util.stream.Collectors; import io.reactivex.Completable; import io.reactivex.Observable; import io.reactivex.Single; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.Response; public class NextNewsRepository extends ARepository { @@ -54,16 +56,23 @@ public class NextNewsRepository extends ARepository { public Completable login(Account account, boolean insert) { setCredentials(account); return Single.create(emitter -> { - // workaround to have the Nextcloud API call working, as I don't know how to do otherwise - AuthInterceptor authInterceptor = KoinJavaComponent.get(AuthInterceptor.class); - authInterceptor.getCredentials().setUrl(authInterceptor.getCredentials().getUrl().replace(NextNewsService.END_POINT, "")); + OkHttpClient httpClient = KoinJavaComponent.get(OkHttpClient.class); - String displayName = dataSource.login(account.getLogin()); + Request request = new Request.Builder() + .url(account.getUrl() + "/ocs/v1.php/cloud/users/" + account.getLogin()) + .addHeader("OCS-APIRequest", "true") + .build(); + + Response response = httpClient.newCall(request).execute(); + + if (response.isSuccessful()) { + String displayName = new NextNewsUserAdapter().fromXml(response.body().byteStream()); + response.body().close(); - if (displayName != null) { emitter.onSuccess(displayName); } else { - emitter.onError(new Exception("Login failed. Please check your credentials and your Nextcloud News setup.")); + // TODO better error handling + emitter.onError(new Exception("Login exception : " + response.code() + " error")); } }).flatMapCompletable(displayName -> { account.setDisplayedName(displayName); @@ -211,6 +220,7 @@ public class NextNewsRepository extends ARepository { @Override public Completable updateFeed(Feed feed) { + setCredentials(account); return Completable.create(emitter -> { Folder folder = feed.getFolderId() == null ? null : database.folderDao().select(feed.getFolderId()); @@ -232,6 +242,7 @@ public class NextNewsRepository extends ARepository { @Override public Completable deleteFeed(Feed feed) { + setCredentials(account); return Completable.create(emitter -> { try { if (dataSource.deleteFeed(Integer.parseInt(feed.getRemoteId()))) { @@ -248,6 +259,7 @@ public class NextNewsRepository extends ARepository { @Override public Single addFolder(Folder folder) { + setCredentials(account); return Single.create(emitter -> { try { List folders = dataSource.createFolder(folder); @@ -267,6 +279,7 @@ public class NextNewsRepository extends ARepository { @Override public Completable updateFolder(Folder folder) { + setCredentials(account); return Completable.create(emitter -> { try { if (dataSource.renameFolder(folder)) { @@ -284,6 +297,7 @@ public class NextNewsRepository extends ARepository { @Override public Completable deleteFolder(Folder folder) { + setCredentials(account); return Completable.create(emitter -> { try { if (dataSource.deleteFolder(folder)) { From bd333afbf22e977d1640a662eee981e20778fd9f Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Sun, 11 Jul 2021 16:25:11 +0200 Subject: [PATCH 174/187] Fix failing test --- api/src/test/java/com/readrops/api/utils/AuthInterceptorTest.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/api/src/test/java/com/readrops/api/utils/AuthInterceptorTest.kt b/api/src/test/java/com/readrops/api/utils/AuthInterceptorTest.kt index ea425a05..7bd94878 100644 --- a/api/src/test/java/com/readrops/api/utils/AuthInterceptorTest.kt +++ b/api/src/test/java/com/readrops/api/utils/AuthInterceptorTest.kt @@ -36,7 +36,6 @@ class AuthInterceptorTest { okHttpClient.newCall(Request.Builder().url(mockServer.url("/url")).build()).execute() val request = mockServer.takeRequest() - assertEquals(request.requestUrl.toString(), "http://localhost:8080/rss/url") assertEquals(request.headers["Authorization"], "GoogleLogin auth=token") } From 38bd3e3ae5fe7ccaa2f070cf50f7813fe48faefe Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Sun, 11 Jul 2021 18:55:38 +0200 Subject: [PATCH 175/187] Add a new folder filter --- .../main/java/com/readrops/app/itemslist/MainViewModel.java | 4 ++++ db/src/main/java/com/readrops/db/filters/FilterType.java | 1 + db/src/main/java/com/readrops/db/queries/ItemsQueryBuilder.kt | 2 ++ 3 files changed, 7 insertions(+) diff --git a/app/src/main/java/com/readrops/app/itemslist/MainViewModel.java b/app/src/main/java/com/readrops/app/itemslist/MainViewModel.java index b8ce1837..47dab168 100644 --- a/app/src/main/java/com/readrops/app/itemslist/MainViewModel.java +++ b/app/src/main/java/com/readrops/app/itemslist/MainViewModel.java @@ -115,6 +115,10 @@ public class MainViewModel extends ViewModel { queryFilters.setFilterFeedId(filterFeedId); } + public void setFilerFolderId(int folderId) { + queryFilters.setFilterFolderId(folderId); + } + public MediatorLiveData> getItemsWithFeed() { return itemsWithFeed; } diff --git a/db/src/main/java/com/readrops/db/filters/FilterType.java b/db/src/main/java/com/readrops/db/filters/FilterType.java index a3388429..e971c0af 100644 --- a/db/src/main/java/com/readrops/db/filters/FilterType.java +++ b/db/src/main/java/com/readrops/db/filters/FilterType.java @@ -2,6 +2,7 @@ package com.readrops.db.filters; public enum FilterType { FEED_FILTER, + FOLDER_FILER, READ_IT_LATER_FILTER, STARS_FILTER, NO_FILTER diff --git a/db/src/main/java/com/readrops/db/queries/ItemsQueryBuilder.kt b/db/src/main/java/com/readrops/db/queries/ItemsQueryBuilder.kt index f400fa0a..c5f5bc13 100644 --- a/db/src/main/java/com/readrops/db/queries/ItemsQueryBuilder.kt +++ b/db/src/main/java/com/readrops/db/queries/ItemsQueryBuilder.kt @@ -64,6 +64,7 @@ object ItemsQueryBuilder { when (queryFilters.filterType) { FilterType.FEED_FILTER -> append("feed_id = ${queryFilters.filterFeedId} And read_it_later = 0") + FilterType.FOLDER_FILER -> append("folder_id = ${queryFilters.filterFolderId} And read_it_later = 0") FilterType.READ_IT_LATER_FILTER -> append("read_it_later = 1") FilterType.STARS_FILTER -> { if (separateState) { @@ -83,6 +84,7 @@ object ItemsQueryBuilder { class QueryFilters( var showReadItems: Boolean = true, var filterFeedId: Int = 0, + var filterFolderId: Int = 0, var accountId: Int = 0, var filterType: FilterType = FilterType.NO_FILTER, var sortType: ListSortType = ListSortType.NEWEST_TO_OLDEST, From 725c20a78e380ae5cf6d988a25ada238c51cbd08 Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Sun, 11 Jul 2021 19:26:11 +0200 Subject: [PATCH 176/187] New folder view when clicking on an expandable drawer item --- app/build.gradle | 4 +- .../readrops/app/itemslist/DrawerManager.java | 81 +++++++- .../readrops/app/itemslist/MainActivity.java | 7 + .../CustomExpandableBadgeDrawerItem.java | 174 ++++++++++++++++++ .../layout/custom_expandable_drawer_item.xml | 109 +++++++++++ 5 files changed, 370 insertions(+), 5 deletions(-) create mode 100644 app/src/main/java/com/readrops/app/utils/customviews/CustomExpandableBadgeDrawerItem.java create mode 100644 app/src/main/res/layout/custom_expandable_drawer_item.xml diff --git a/app/build.gradle b/app/build.gradle index 861eb3d0..3b00d304 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -90,10 +90,12 @@ dependencies { implementation 'com.afollestad.material-dialogs:core:0.9.6.0' - implementation 'com.mikepenz:fastadapter:3.3.1' + implementation 'com.mikepenz:fastadapter:3.2.9' implementation 'com.mikepenz:fastadapter-commons:3.3.0' implementation 'com.mikepenz:materialdrawer:6.1.2' implementation "com.mikepenz:aboutlibraries:6.2.3" + implementation "com.mikepenz:iconics-views:3.2.5" + implementation "com.mikepenz:iconics-core:3.2.5" debugImplementation 'com.facebook.flipper:flipper:0.96.1' debugImplementation 'com.facebook.soloader:soloader:0.10.1' diff --git a/app/src/main/java/com/readrops/app/itemslist/DrawerManager.java b/app/src/main/java/com/readrops/app/itemslist/DrawerManager.java index 4a9c30e9..a6805c8a 100644 --- a/app/src/main/java/com/readrops/app/itemslist/DrawerManager.java +++ b/app/src/main/java/com/readrops/app/itemslist/DrawerManager.java @@ -2,23 +2,28 @@ package com.readrops.app.itemslist; import android.app.Activity; import android.graphics.drawable.Drawable; +import android.view.View; import android.widget.ImageView; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.widget.Toolbar; +import androidx.recyclerview.widget.RecyclerView; import com.bumptech.glide.Glide; import com.bumptech.glide.load.engine.DiskCacheStrategy; import com.bumptech.glide.request.target.CustomTarget; import com.bumptech.glide.request.transition.Transition; +import com.mikepenz.fastadapter.FastAdapter; +import com.mikepenz.fastadapter.expandable.ExpandableExtension; +import com.mikepenz.fastadapter.listeners.ClickEventHook; +import com.mikepenz.fastadapter.select.SelectExtension; import com.mikepenz.materialdrawer.AccountHeader; import com.mikepenz.materialdrawer.AccountHeaderBuilder; import com.mikepenz.materialdrawer.Drawer; import com.mikepenz.materialdrawer.DrawerBuilder; import com.mikepenz.materialdrawer.holder.ImageHolder; import com.mikepenz.materialdrawer.model.DividerDrawerItem; -import com.mikepenz.materialdrawer.model.ExpandableBadgeDrawerItem; import com.mikepenz.materialdrawer.model.PrimaryDrawerItem; import com.mikepenz.materialdrawer.model.ProfileDrawerItem; import com.mikepenz.materialdrawer.model.ProfileSettingDrawerItem; @@ -26,11 +31,14 @@ import com.mikepenz.materialdrawer.model.SecondaryDrawerItem; import com.mikepenz.materialdrawer.model.interfaces.IDrawerItem; import com.mikepenz.materialdrawer.model.interfaces.IProfile; import com.readrops.app.R; +import com.readrops.app.utils.customviews.CustomExpandableBadgeDrawerItem; import com.readrops.db.entities.Feed; import com.readrops.db.entities.Folder; import com.readrops.db.entities.account.Account; import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -50,6 +58,7 @@ public class DrawerManager { private Activity activity; private Toolbar toolbar; private Drawer drawer; + private FastAdapter adapter; private AccountHeader header; private Drawer.OnDrawerItemClickListener listener; @@ -76,11 +85,76 @@ public class DrawerManager { .withOnDrawerItemClickListener(listener) .build(); + adapter = drawer.getAdapter(); + buildFastAdapter(); + addDefaultPlaces(); return drawer; } + public void buildFastAdapter() { + // Folder click + adapter.withEventHook(new ClickEventHook() { + @Override + public void onClick(@NonNull View v, int position, @NonNull FastAdapter fastAdapter, @NonNull IDrawerItem item) { + SelectExtension selectExtension = adapter.getExtension(SelectExtension.class); + + selectExtension.deselect(selectExtension.getSelections()); + + if (!item.isSelected()) { + selectExtension.select(position); + } + + listener.onItemClick(v, position, item); + } + + @Override + public List onBindMany(@NonNull RecyclerView.ViewHolder viewHolder) { + if (viewHolder instanceof CustomExpandableBadgeDrawerItem.ViewHolder) { + CustomExpandableBadgeDrawerItem.ViewHolder expandableViewHolder = (CustomExpandableBadgeDrawerItem.ViewHolder) viewHolder; + + return Arrays.asList(new View[]{ + expandableViewHolder.itemView.findViewById(R.id.expandable_item_container), + expandableViewHolder.itemView.findViewById(R.id.material_drawer_icon), + expandableViewHolder.itemView.findViewById(R.id.material_drawer_name), + expandableViewHolder.itemView.findViewById(R.id.material_drawer_description) + }.clone()); + + } else { + return Collections.emptyList(); + } + } + }); + + // Expandable click + adapter.withEventHook(new ClickEventHook() { + @Override + public void onClick(@NonNull View v, int position, @NonNull FastAdapter fastAdapter, @NonNull IDrawerItem item) { + ExpandableExtension expandableExtension = adapter.getExtension(ExpandableExtension.class); + + expandableExtension.toggleExpandable(position); + } + + @Override + public List onBindMany(@NonNull RecyclerView.ViewHolder viewHolder) { + if (viewHolder instanceof CustomExpandableBadgeDrawerItem.ViewHolder) { + CustomExpandableBadgeDrawerItem.ViewHolder expandableViewHolder = (CustomExpandableBadgeDrawerItem.ViewHolder) viewHolder; + + return Arrays.asList(new View[]{ + expandableViewHolder.badge, + expandableViewHolder.badgeContainer, + expandableViewHolder.arrow, + expandableViewHolder.itemView.findViewById(R.id.material_drawer_arrow_container) + }.clone()); + + } else { + return Collections.emptyList(); + } + } + }); + } + public void updateDrawer(Map> folderListMap) { drawer.removeAllItems(); drawer.removeAllStickyFooterItems(); @@ -92,9 +166,8 @@ public class DrawerManager { for (Map.Entry> entry : folderListMap.entrySet()) { Folder folder = entry.getKey(); if (folder != null) { - // no identifier for badge items, but if needed, be aware of not getting conflicts - // with secondary item identifiers (folder and feed ids can be the same) - ExpandableBadgeDrawerItem badgeDrawerItem = new ExpandableBadgeDrawerItem() + CustomExpandableBadgeDrawerItem badgeDrawerItem = new CustomExpandableBadgeDrawerItem() + .withIdentifier(folder.getId() * 1000L) // to avoid any id conflict with other items .withName(folder.getName()) .withIcon(R.drawable.ic_folder_grey); diff --git a/app/src/main/java/com/readrops/app/itemslist/MainActivity.java b/app/src/main/java/com/readrops/app/itemslist/MainActivity.java index db1ab988..775e591c 100644 --- a/app/src/main/java/com/readrops/app/itemslist/MainActivity.java +++ b/app/src/main/java/com/readrops/app/itemslist/MainActivity.java @@ -42,6 +42,7 @@ import com.readrops.app.settings.SettingsActivity; import com.readrops.app.utils.GlideRequests; import com.readrops.app.utils.SharedPreferencesManager; import com.readrops.app.utils.Utils; +import com.readrops.app.utils.customviews.CustomExpandableBadgeDrawerItem; import com.readrops.app.utils.customviews.ReadropsItemTouchCallback; import com.readrops.db.entities.Feed; import com.readrops.db.entities.Folder; @@ -280,6 +281,12 @@ public class MainActivity extends AppCompatActivity implements SwipeRefreshLayou viewModel.setFilterFeedId((int) drawerItem.getIdentifier()); viewModel.setFilterType(FilterType.FEED_FILTER); viewModel.invalidate(); + } else if (drawerItem instanceof CustomExpandableBadgeDrawerItem) { + drawer.closeDrawer(); + + viewModel.setFilerFolderId((int) (drawerItem.getIdentifier() / 1000)); + viewModel.setFilterType(FilterType.FOLDER_FILER); + viewModel.invalidate(); } } diff --git a/app/src/main/java/com/readrops/app/utils/customviews/CustomExpandableBadgeDrawerItem.java b/app/src/main/java/com/readrops/app/utils/customviews/CustomExpandableBadgeDrawerItem.java new file mode 100644 index 00000000..31277365 --- /dev/null +++ b/app/src/main/java/com/readrops/app/utils/customviews/CustomExpandableBadgeDrawerItem.java @@ -0,0 +1,174 @@ +package com.readrops.app.utils.customviews; + +import android.content.Context; +import android.graphics.Color; +import android.view.View; +import android.widget.ImageView; +import android.widget.TextView; + +import androidx.annotation.LayoutRes; +import androidx.annotation.StringRes; + +import com.mikepenz.fastadapter.IClickable; +import com.mikepenz.fastadapter.IItem; +import com.mikepenz.fastadapter.listeners.OnClickListener; +import com.mikepenz.iconics.IconicsDrawable; +import com.mikepenz.materialdrawer.Drawer; +import com.mikepenz.materialdrawer.holder.BadgeStyle; +import com.mikepenz.materialdrawer.holder.ColorHolder; +import com.mikepenz.materialdrawer.holder.StringHolder; +import com.mikepenz.materialdrawer.icons.MaterialDrawerFont; +import com.mikepenz.materialdrawer.model.BaseDescribeableDrawerItem; +import com.mikepenz.materialdrawer.model.BaseViewHolder; +import com.mikepenz.materialdrawer.model.interfaces.ColorfulBadgeable; +import com.readrops.app.R; + +import java.util.List; + +/** + * This a simple modification of original ExpandableBadgeDrawerItem from MaterialDrawer lib to get two click events from an expandable drawer item + */ +public class CustomExpandableBadgeDrawerItem extends BaseDescribeableDrawerItem + implements ColorfulBadgeable, IClickable { + + protected ColorHolder arrowColor; + + protected int arrowRotationAngleStart = 0; + + protected int arrowRotationAngleEnd = 180; + + protected StringHolder mBadge; + protected BadgeStyle mBadgeStyle = new BadgeStyle(); + + @Override + public int getType() { + return R.id.material_drawer_item_expandable_badge; + } + + @Override + @LayoutRes + public int getLayoutRes() { + return R.layout.custom_expandable_drawer_item; + } + + @Override + public void bindView(CustomExpandableBadgeDrawerItem.ViewHolder viewHolder, List payloads) { + super.bindView(viewHolder, payloads); + + Context ctx = viewHolder.itemView.getContext(); + //bind the basic view parts + bindViewHelper(viewHolder); + + //set the text for the badge or hide + boolean badgeVisible = StringHolder.applyToOrHide(mBadge, viewHolder.badge); + //style the badge if it is visible + if (true) { + mBadgeStyle.style(viewHolder.badge, getTextColorStateList(getColor(ctx), getSelectedTextColor(ctx))); + viewHolder.badgeContainer.setVisibility(View.VISIBLE); + } else { + viewHolder.badgeContainer.setVisibility(View.GONE); + } + + //define the typeface for our textViews + if (getTypeface() != null) { + viewHolder.badge.setTypeface(getTypeface()); + } + + //make sure all animations are stopped + if (viewHolder.arrow.getDrawable() instanceof IconicsDrawable) { + ((IconicsDrawable) viewHolder.arrow.getDrawable()).color(this.arrowColor != null ? this.arrowColor.color(ctx) : getIconColor(ctx)); + } + viewHolder.arrow.clearAnimation(); + if (!isExpanded()) { + viewHolder.arrow.setRotation(this.arrowRotationAngleStart); + } else { + viewHolder.arrow.setRotation(this.arrowRotationAngleEnd); + } + + //call the onPostBindView method to trigger post bind view actions (like the listener to modify the item if required) + onPostBindView(this, viewHolder.itemView); + } + + @Override + public CustomExpandableBadgeDrawerItem withOnDrawerItemClickListener(Drawer.OnDrawerItemClickListener onDrawerItemClickListener) { + mOnDrawerItemClickListener = null; + return this; + } + + @Override + public Drawer.OnDrawerItemClickListener getOnDrawerItemClickListener() { + return null; + } + + @Override + public CustomExpandableBadgeDrawerItem withBadge(StringHolder badge) { + this.mBadge = badge; + return this; + } + + @Override + public CustomExpandableBadgeDrawerItem withBadge(String badge) { + this.mBadge = new StringHolder(badge); + return this; + } + + @Override + public CustomExpandableBadgeDrawerItem withBadge(@StringRes int badgeRes) { + this.mBadge = new StringHolder(badgeRes); + return this; + } + + @Override + public CustomExpandableBadgeDrawerItem withBadgeStyle(BadgeStyle badgeStyle) { + this.mBadgeStyle = badgeStyle; + return this; + } + + public StringHolder getBadge() { + return mBadge; + } + + public BadgeStyle getBadgeStyle() { + return mBadgeStyle; + } + + @Override + public ViewHolder getViewHolder(View v) { + return new ViewHolder(v); + } + + @Override + public IItem withOnItemPreClickListener(OnClickListener onItemPreClickListener) { + return null; + } + + @Override + public OnClickListener getOnPreItemClickListener() { + return null; + } + + @Override + public IItem withOnItemClickListener(OnClickListener onItemClickListener) { + return null; + } + + @Override + public OnClickListener getOnItemClickListener() { + return null; + } + + public static class ViewHolder extends BaseViewHolder { + public ImageView arrow; + public View badgeContainer; + public TextView badge; + + public ViewHolder(View view) { + super(view); + badgeContainer = view.findViewById(R.id.material_drawer_badge_container); + badge = view.findViewById(R.id.material_drawer_badge); + arrow = view.findViewById(R.id.material_drawer_arrow); + arrow.setImageDrawable(new IconicsDrawable(view.getContext(), MaterialDrawerFont.Icon.mdf_expand_more).sizeDp(16).paddingDp(2).color(Color.BLACK)); + } + } +} diff --git a/app/src/main/res/layout/custom_expandable_drawer_item.xml b/app/src/main/res/layout/custom_expandable_drawer_item.xml new file mode 100644 index 00000000..1139334e --- /dev/null +++ b/app/src/main/res/layout/custom_expandable_drawer_item.xml @@ -0,0 +1,109 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file From 440e6ea810cb8cfd2477dbaf3aac388f288cb18c Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Sun, 11 Jul 2021 19:39:10 +0200 Subject: [PATCH 177/187] Set toolbar title with categories names --- .../main/java/com/readrops/app/itemslist/MainActivity.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/src/main/java/com/readrops/app/itemslist/MainActivity.java b/app/src/main/java/com/readrops/app/itemslist/MainActivity.java index 775e591c..b27717fa 100644 --- a/app/src/main/java/com/readrops/app/itemslist/MainActivity.java +++ b/app/src/main/java/com/readrops/app/itemslist/MainActivity.java @@ -256,14 +256,17 @@ public class MainActivity extends AppCompatActivity implements SwipeRefreshLayou viewModel.setFilterType(FilterType.NO_FILTER); scrollToTop = true; viewModel.invalidate(); + setTitle(R.string.articles); break; case DrawerManager.READ_LATER_ID: viewModel.setFilterType(FilterType.READ_IT_LATER_FILTER); viewModel.invalidate(); + setTitle(R.string.read_later); break; case DrawerManager.STARS_ID: viewModel.setFilterType(FilterType.STARS_FILTER); viewModel.invalidate(); + setTitle(R.string.favorites); break; case DrawerManager.ABOUT_ID: startAboutActivity(); @@ -281,12 +284,14 @@ public class MainActivity extends AppCompatActivity implements SwipeRefreshLayou viewModel.setFilterFeedId((int) drawerItem.getIdentifier()); viewModel.setFilterType(FilterType.FEED_FILTER); viewModel.invalidate(); + setTitle(((SecondaryDrawerItem) drawerItem).getName().getText()); } else if (drawerItem instanceof CustomExpandableBadgeDrawerItem) { drawer.closeDrawer(); viewModel.setFilerFolderId((int) (drawerItem.getIdentifier() / 1000)); viewModel.setFilterType(FilterType.FOLDER_FILER); viewModel.invalidate(); + setTitle(((CustomExpandableBadgeDrawerItem) drawerItem).getName().getText()); } } From d51c7ed90aba4aa72c7077a05afa1cba82d3f5dc Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Fri, 16 Jul 2021 17:34:03 +0200 Subject: [PATCH 178/187] Fix OPML export not working with API 29+ --- api/build.gradle | 6 +----- .../app/settings/AccountSettingsFragment.java | 20 ++++++++++++------- .../java/com/readrops/app/utils/FileUtils.kt | 14 ++++++------- 3 files changed, 21 insertions(+), 19 deletions(-) diff --git a/api/build.gradle b/api/build.gradle index 02985dcc..72abc1e1 100644 --- a/api/build.gradle +++ b/api/build.gradle @@ -65,11 +65,7 @@ dependencies { exclude group: 'moshi', module: 'moshi' // moshi converter uses moshi 1.8.0 which breaks codegen 1.9.2 } - implementation ('com.squareup.retrofit2:converter-simplexml:2.9.0') { - exclude module: 'stax' - exclude module: 'stax-api' - exclude module: 'xpp3' - } + implementation ('com.squareup.retrofit2:converter-simplexml:2.9.0') implementation 'com.squareup.retrofit2:adapter-rxjava2:2.9.0' diff --git a/app/src/main/java/com/readrops/app/settings/AccountSettingsFragment.java b/app/src/main/java/com/readrops/app/settings/AccountSettingsFragment.java index 767c5241..ecf63de3 100644 --- a/app/src/main/java/com/readrops/app/settings/AccountSettingsFragment.java +++ b/app/src/main/java/com/readrops/app/settings/AccountSettingsFragment.java @@ -24,6 +24,7 @@ import com.readrops.api.opml.OPMLHelper; import com.readrops.api.opml.OPMLParser; import com.readrops.app.R; import com.readrops.app.ReadropsApp; +import com.readrops.app.account.AccountViewModel; import com.readrops.app.account.AddAccountActivity; import com.readrops.app.feedsfolders.ManageFeedsFoldersActivity; import com.readrops.app.notifications.NotificationPermissionActivity; @@ -31,12 +32,16 @@ import com.readrops.app.utils.FileUtils; import com.readrops.app.utils.PermissionManager; import com.readrops.app.utils.SharedPreferencesManager; import com.readrops.app.utils.Utils; -import com.readrops.app.account.AccountViewModel; +import com.readrops.db.entities.Feed; +import com.readrops.db.entities.Folder; import com.readrops.db.entities.account.Account; import com.readrops.db.entities.account.AccountType; import org.koin.androidx.viewmodel.compat.ViewModelCompat; +import java.util.List; +import java.util.Map; + import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.observers.DisposableCompletableObserver; import io.reactivex.schedulers.Schedulers; @@ -239,13 +244,14 @@ public class AccountSettingsFragment extends PreferenceFragmentCompat { String fileName = "subscriptions.opml"; try { - String path = FileUtils.writeDownloadFile(getContext(), fileName, "text/xml", outputStream -> { - viewModel.getFoldersWithFeeds() - .flatMapCompletable(folderListMap -> OPMLParser.write(folderListMap, outputStream)) + String path = FileUtils.writeDownloadFile(getContext(), fileName, "text/x-opml", outputStream -> { + Map> folderListMap = viewModel.getFoldersWithFeeds() .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .doOnError(e -> Utils.showSnackbar(getView(), e.getMessage())) - .subscribe(); + .blockingGet(); + + + OPMLParser.write(folderListMap, outputStream) + .blockingAwait(); return Unit.INSTANCE; }); diff --git a/app/src/main/java/com/readrops/app/utils/FileUtils.kt b/app/src/main/java/com/readrops/app/utils/FileUtils.kt index 9ae24dff..e0aba2ff 100644 --- a/app/src/main/java/com/readrops/app/utils/FileUtils.kt +++ b/app/src/main/java/com/readrops/app/utils/FileUtils.kt @@ -34,20 +34,20 @@ object FileUtils { val contentUri = resolver.insert(downloadsUri, fileDetails) - resolver.openFileDescriptor(contentUri!!, "w", null).use { pfd -> - val outputStream = FileOutputStream(pfd?.fileDescriptor!!) - + resolver.openOutputStream(contentUri!!)!!.use { stream -> try { - listener(outputStream) + listener(stream) } catch (e: Exception) { throw e } finally { - outputStream.flush() - outputStream.close() + stream.flush() + stream.close() } + + fileDetails.put(MediaStore.Downloads.IS_PENDING, 0) + resolver.update(contentUri, fileDetails, null, null) } - fileDetails.clear() fileDetails.put(MediaStore.Downloads.IS_PENDING, 0) resolver.update(contentUri, fileDetails, null, null) From ef195b4231ff2ff1f391ff4baacd27315ca519c8 Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Fri, 16 Jul 2021 17:51:23 +0200 Subject: [PATCH 179/187] Delete feeds and folders by ids and by account id (closes #100) --- db/src/main/java/com/readrops/db/dao/FeedDao.java | 12 +++++------- db/src/main/java/com/readrops/db/dao/FolderDao.java | 11 ++++++----- 2 files changed, 11 insertions(+), 12 deletions(-) diff --git a/db/src/main/java/com/readrops/db/dao/FeedDao.java b/db/src/main/java/com/readrops/db/dao/FeedDao.java index f1a70691..af171860 100644 --- a/db/src/main/java/com/readrops/db/dao/FeedDao.java +++ b/db/src/main/java/com/readrops/db/dao/FeedDao.java @@ -29,9 +29,6 @@ public abstract class FeedDao implements BaseDao { @Query("Select * from Feed Where id = :feedId") public abstract Feed getFeedById(int feedId); - @Query("Select id From Feed Where url = :url and account_id = :accountId") - public abstract int getFeedIdByUrl(String url, int accountId); - @Query("Select case When :feedUrl In (Select url from Feed Where account_id = :accountId) Then 1 else 0 end") public abstract boolean feedExists(String feedUrl, int accountId); @@ -81,8 +78,8 @@ public abstract class FeedDao implements BaseDao { @Query("Select remoteId From Feed Where account_id = :accountId") public abstract List getFeedRemoteIdsOfAccount(int accountId); - @Query("Delete from Feed Where remoteId in (:ids)") - abstract void deleteByIds(List ids); + @Query("Delete from Feed Where remoteId in (:ids) And account_id = :accountId") + abstract void deleteByIds(List ids, int accountId); @Query("Select id From Folder Where remoteId = :remoteId And account_id = :accountId") abstract int getRemoteFolderLocalId(String remoteId, int accountId); @@ -126,8 +123,9 @@ public abstract class FeedDao implements BaseDao { } } - if (!accountFeedIds.isEmpty()) - deleteByIds(accountFeedIds); + if (!accountFeedIds.isEmpty()) { + deleteByIds(accountFeedIds, account.getId()); + } return insert(feedsToInsert); } diff --git a/db/src/main/java/com/readrops/db/dao/FolderDao.java b/db/src/main/java/com/readrops/db/dao/FolderDao.java index b527c4df..75f1b507 100644 --- a/db/src/main/java/com/readrops/db/dao/FolderDao.java +++ b/db/src/main/java/com/readrops/db/dao/FolderDao.java @@ -36,8 +36,8 @@ public abstract class FolderDao implements BaseDao { @Query("Select remoteId From Folder Where account_id = :accountId") public abstract List getFolderRemoteIdsOfAccount(int accountId); - @Query("Delete From Folder Where remoteId in (:ids)") - abstract void deleteByIds(List ids); + @Query("Delete From Folder Where remoteId in (:ids) And account_id = :accountId") + abstract void deleteByIds(List ids, int accountId); @Query("Select * From Folder Where name = :name And account_id = :accountId") public abstract Folder getFolderByName(String name, int accountId); @@ -47,7 +47,7 @@ public abstract class FolderDao implements BaseDao { * * @param folders folders to insert or update * @param account owner of the feeds - * @return the list of the inserted feeds ids + * @return the list of the inserted folders ids */ @Transaction public List foldersUpsert(List folders, Account account) { @@ -64,8 +64,9 @@ public abstract class FolderDao implements BaseDao { } } - if (!accountFolderIds.isEmpty()) - deleteByIds(accountFolderIds); + if (!accountFolderIds.isEmpty()) { + deleteByIds(accountFolderIds, account.getId()); + } return insert(foldersToInsert); } From 4c618b404407ab3df44d4fb1dd5aa08722d38880 Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Sat, 17 Jul 2021 14:59:50 +0200 Subject: [PATCH 180/187] Let items be removed from read it later category --- .../readrops/app/itemslist/MainActivity.java | 19 ++++++++++--------- .../readrops/app/itemslist/MainViewModel.java | 4 ++-- .../app/notifications/sync/SyncWorker.kt | 5 ++++- .../java/com/readrops/db/dao/ItemDao.java | 4 ++-- 4 files changed, 18 insertions(+), 14 deletions(-) diff --git a/app/src/main/java/com/readrops/app/itemslist/MainActivity.java b/app/src/main/java/com/readrops/app/itemslist/MainActivity.java index b27717fa..627474f6 100644 --- a/app/src/main/java/com/readrops/app/itemslist/MainActivity.java +++ b/app/src/main/java/com/readrops/app/itemslist/MainActivity.java @@ -430,27 +430,28 @@ public class MainActivity extends AppCompatActivity implements SwipeRefreshLayou @Override public void onSwipe(@NotNull RecyclerView.ViewHolder viewHolder, int direction) { + Item item = adapter.getItemWithFeed(viewHolder.getBindingAdapterPosition()).getItem(); + if (direction == ItemTouchHelper.LEFT) { // set item read state - ItemWithFeed itemWithFeed = adapter.getItemWithFeed(viewHolder.getBindingAdapterPosition()); + item.setRead(!item.isRead()); - itemWithFeed.getItem().setRead(!itemWithFeed.getItem().isRead()); - viewModel.setItemReadState(itemWithFeed) + viewModel.setItemReadState(item) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .doOnError(throwable -> Utils.showSnackbar(binding.mainRoot, throwable.getMessage())) .subscribe(); - adapter.notifyItemChanged(viewHolder.getBindingAdapterPosition()); - } else { // add item to read it later section - viewModel.setItemReadItLater((int) adapter.getItemId(viewHolder.getBindingAdapterPosition())) + } else { // set item read it later state + item.setReadItLater(!item.isReadItLater()); + + viewModel.setItemReadItLater(item.isReadItLater(), item.getId()) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .doOnError(throwable -> Utils.showSnackbar(binding.mainRoot, throwable.getMessage())) .subscribe(); - - if (viewModel.getFilterType() == FilterType.READ_IT_LATER_FILTER) - adapter.notifyItemChanged(viewHolder.getBindingAdapterPosition()); } + + adapter.notifyItemChanged(viewHolder.getBindingAdapterPosition()); } @Override diff --git a/app/src/main/java/com/readrops/app/itemslist/MainViewModel.java b/app/src/main/java/com/readrops/app/itemslist/MainViewModel.java index 47dab168..f9c46dd1 100644 --- a/app/src/main/java/com/readrops/app/itemslist/MainViewModel.java +++ b/app/src/main/java/com/readrops/app/itemslist/MainViewModel.java @@ -249,8 +249,8 @@ public class MainViewModel extends ViewModel { return repository.setAllItemsReadState(read); } - public Completable setItemReadItLater(int itemId) { - return database.itemDao().setReadItLater(itemId); + public Completable setItemReadItLater(boolean readLater, int itemId) { + return database.itemDao().setReadItLater(readLater, itemId); } //endregion diff --git a/app/src/main/java/com/readrops/app/notifications/sync/SyncWorker.kt b/app/src/main/java/com/readrops/app/notifications/sync/SyncWorker.kt index b919fc60..0ccd90c9 100644 --- a/app/src/main/java/com/readrops/app/notifications/sync/SyncWorker.kt +++ b/app/src/main/java/com/readrops/app/notifications/sync/SyncWorker.kt @@ -166,7 +166,10 @@ class SyncWorker(context: Context, parameters: WorkerParameters) : Worker(contex val itemId = intent?.getIntExtra(ReadropsKeys.ITEM_ID, 0)!! with(get()) { - itemDao().setReadItLater(itemId) + val item = itemDao().select(itemId) + item.isReadItLater = !item.isReadItLater + + itemDao().setReadItLater(item.isReadItLater, itemId) .subscribeOn(Schedulers.io()) .subscribe() } diff --git a/db/src/main/java/com/readrops/db/dao/ItemDao.java b/db/src/main/java/com/readrops/db/dao/ItemDao.java index 3c1b5b6c..30f368c0 100644 --- a/db/src/main/java/com/readrops/db/dao/ItemDao.java +++ b/db/src/main/java/com/readrops/db/dao/ItemDao.java @@ -49,8 +49,8 @@ public interface ItemDao extends BaseDao { @Query("Update Item set read = :readState Where feed_id = :feedId") Completable setAllFeedItemsReadState(int feedId, int readState); - @Query("Update Item set read_it_later = 1 Where id = :itemId") - Completable setReadItLater(int itemId); + @Query("Update Item set read_it_later = :readLater Where id = :itemId") + Completable setReadItLater(boolean readLater, int itemId); @Query("Select count(*) From Item Where feed_id = :feedId And read = 0") int getUnreadCount(int feedId); From 056d8db78f6e22b7f359bb9f9db2132a52d79c3f Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Sat, 17 Jul 2021 15:00:06 +0200 Subject: [PATCH 181/187] Change read it later icon --- app/src/main/res/drawable/ic_read_later.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/res/drawable/ic_read_later.xml b/app/src/main/res/drawable/ic_read_later.xml index 3d68d402..c1c86f62 100644 --- a/app/src/main/res/drawable/ic_read_later.xml +++ b/app/src/main/res/drawable/ic_read_later.xml @@ -1,5 +1,5 @@ - + From 8f017753ccd89e84d4906e9c3cccbd2ebd54d40f Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Sat, 17 Jul 2021 15:02:44 +0200 Subject: [PATCH 182/187] Enable set all items and all feed items read state features for local accounts only --- .../com/readrops/app/repositories/ARepository.java | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/readrops/app/repositories/ARepository.java b/app/src/main/java/com/readrops/app/repositories/ARepository.java index b1d5f72e..45839309 100644 --- a/app/src/main/java/com/readrops/app/repositories/ARepository.java +++ b/app/src/main/java/com/readrops/app/repositories/ARepository.java @@ -129,11 +129,19 @@ public abstract class ARepository { } public Completable setAllItemsReadState(boolean read) { - return database.itemDao().setAllItemsReadState(read ? 1 : 0, account.getId()); + if (account.isLocal()) { // TODO see if it's possible to implement for others accounts + return database.itemDao().setAllItemsReadState(read ? 1 : 0, account.getId()); + } else { + return Completable.complete(); + } } public Completable setAllFeedItemsReadState(int feedId, boolean read) { - return database.itemDao().setAllFeedItemsReadState(feedId, read ? 1 : 0); + if (account.isLocal()) { + return database.itemDao().setAllFeedItemsReadState(feedId, read ? 1 : 0); + } else { + return Completable.complete(); + } } public Completable setItemStarState(Item item) { @@ -147,7 +155,6 @@ public abstract class ARepository { return database.itemStateChangesDao().upsertItemStarStateChange(item, account.getId(), false) .andThen(database.itemDao().setStarState(item.getId(), item.isStarred())); } - } public Single getFeedCount(int accountId) { From b608fcb1bc5d675a65d1a5ba03889cf19c3c81b3 Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Sun, 18 Jul 2021 12:56:09 +0200 Subject: [PATCH 183/187] Update api-level for instrumented tests --- .github/workflows/android.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml index 72b9c21e..3174aec5 100644 --- a/.github/workflows/android.yml +++ b/.github/workflows/android.yml @@ -24,6 +24,6 @@ jobs: - name: Android Emulator Runner uses: ReactiveCircus/android-emulator-runner@v2 with: - api-level: 29 + api-level: 30 script: ./gradlew connectedCheck From a4625c816793cf262df896e6990446578ee1bdd2 Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Sun, 18 Jul 2021 13:46:38 +0200 Subject: [PATCH 184/187] Set instrumented tests api-level to 28 --- .github/workflows/android.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml index 3174aec5..c6b1cf4b 100644 --- a/.github/workflows/android.yml +++ b/.github/workflows/android.yml @@ -24,6 +24,6 @@ jobs: - name: Android Emulator Runner uses: ReactiveCircus/android-emulator-runner@v2 with: - api-level: 30 + api-level: 28 script: ./gradlew connectedCheck From b8521a2360b39ac93470e6d879bb8ffe6ffc443b Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Sun, 18 Jul 2021 14:10:58 +0200 Subject: [PATCH 185/187] Update android emulator runner --- .github/workflows/android.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml index c6b1cf4b..08c8baa0 100644 --- a/.github/workflows/android.yml +++ b/.github/workflows/android.yml @@ -22,7 +22,7 @@ jobs: - name: Build with Gradle run: ./gradlew clean build - name: Android Emulator Runner - uses: ReactiveCircus/android-emulator-runner@v2 + uses: ReactiveCircus/android-emulator-runner@v2.19.0 with: api-level: 28 script: ./gradlew connectedCheck From c9155a53c41de709f7752a7808bf150f2bf0fb89 Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Sun, 18 Jul 2021 14:34:21 +0200 Subject: [PATCH 186/187] Add emulator-build attribute to android-emulator-runner action step --- .github/workflows/android.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml index 08c8baa0..64845c3d 100644 --- a/.github/workflows/android.yml +++ b/.github/workflows/android.yml @@ -25,5 +25,6 @@ jobs: uses: ReactiveCircus/android-emulator-runner@v2.19.0 with: api-level: 28 + emulator-build: 7425822 script: ./gradlew connectedCheck From 58e134ba11a59670946accb1b55ffc8ed5544870 Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Sun, 18 Jul 2021 14:54:43 +0200 Subject: [PATCH 187/187] Remove emulator-build attribute from android-emulator-runner action step --- .github/workflows/android.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml index 64845c3d..8b80ad42 100644 --- a/.github/workflows/android.yml +++ b/.github/workflows/android.yml @@ -24,7 +24,6 @@ jobs: - name: Android Emulator Runner uses: ReactiveCircus/android-emulator-runner@v2.19.0 with: - api-level: 28 - emulator-build: 7425822 + api-level: 29 script: ./gradlew connectedCheck