diff --git a/.gitignore b/.gitignore index 418215154..9b63e14e7 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,7 @@ /.idea/assetWizardSettings.xml /.idea/kotlinScripting.xml /.idea/deploymentTargetDropDown.xml +/.idea/androidTestResultsUserPreferences.xml .DS_Store /build /captures diff --git a/app/build.gradle b/app/build.gradle index 05755e6f7..0eab1d24d 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -116,11 +116,16 @@ dependencies { testImplementation 'junit:junit:4.13.2' testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.2' - testImplementation 'io.insert-koin:koin-test-junit4:3.2.0' androidTestImplementation 'androidx.test:runner:1.4.0' androidTestImplementation 'androidx.test:rules:1.4.0' androidTestImplementation 'androidx.test:core-ktx:1.4.0' androidTestImplementation 'androidx.test.ext:junit-ktx:1.1.3' + + androidTestImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.2' + androidTestImplementation 'io.insert-koin:koin-test:3.2.0' + androidTestImplementation 'io.insert-koin:koin-test-junit4:3.2.0' + androidTestImplementation 'androidx.room:room-testing:2.4.2' + androidTestImplementation 'com.squareup.moshi:moshi-kotlin:1.13.0' } \ No newline at end of file diff --git a/app/src/androidTest/assets/manga/bad_ids.json b/app/src/androidTest/assets/manga/bad_ids.json new file mode 100644 index 000000000..d0f9001a0 --- /dev/null +++ b/app/src/androidTest/assets/manga/bad_ids.json @@ -0,0 +1,163 @@ +{ + "id": -2096681732556647985, + "title": "Странствия Эманон", + "url": "/stranstviia_emanon", + "publicUrl": "https://readmanga.io/stranstviia_emanon", + "rating": 0.9400894, + "isNsfw": true, + "coverUrl": "https://staticrm.rmr.rocks/uploads/pics/01/12/559_p.jpg", + "tags": [ + { + "title": "Сверхъестественное", + "key": "supernatural", + "source": "READMANGA_RU" + }, + { + "title": "Сэйнэн", + "key": "seinen", + "source": "READMANGA_RU" + }, + { + "title": "Повседневность", + "key": "slice_of_life", + "source": "READMANGA_RU" + }, + { + "title": "Приключения", + "key": "adventure", + "source": "READMANGA_RU" + } + ], + "state": "FINISHED", + "largeCoverUrl": "https://staticrm.rmr.rocks/uploads/pics/01/12/559_o.jpg", + "description": "Продолжение истории о загадочной девушке по имени Эманон, которая помнит всё, что происходило на Земле за последние три миллиарда лет. \n
Начало истории читайте в \"Воспоминаниях Эманон\". \n
", + "chapters": [ + { + "id": 1552943969433540704, + "name": "1 - 1", + "number": 1, + "url": "/stranstviia_emanon/vol1/1", + "scanlator": "Sad-Robot", + "uploadDate": 1342731600000, + "source": "READMANGA_RU" + }, + { + "id": 1552943969433540705, + "name": "1 - 2", + "number": 2, + "url": "/stranstviia_emanon/vol1/2", + "scanlator": "Sad-Robot", + "uploadDate": 1342731600000, + "source": "READMANGA_RU" + }, + { + "id": 1552943969433540706, + "name": "1 - 3", + "number": 3, + "url": "/stranstviia_emanon/vol1/3", + "scanlator": "Sad-Robot", + "uploadDate": 1342731600000, + "source": "READMANGA_RU" + }, + { + "id": 1552943969433540707, + "name": "1 - 4", + "number": 4, + "url": "/stranstviia_emanon/vol1/4", + "scanlator": "Sad-Robot", + "uploadDate": 1342731600000, + "source": "READMANGA_RU" + }, + { + "id": 1552943969433540708, + "name": "1 - 5", + "number": 5, + "url": "/stranstviia_emanon/vol1/5", + "scanlator": "Sad-Robot", + "uploadDate": 1342731600000, + "source": "READMANGA_RU" + }, + { + "id": 1552943969433541665, + "name": "2 - 1", + "number": 6, + "url": "/stranstviia_emanon/vol2/1", + "scanlator": "Sup!", + "uploadDate": 1415570400000, + "source": "READMANGA_RU" + }, + { + "id": 1552943969433541666, + "name": "2 - 2", + "number": 7, + "url": "/stranstviia_emanon/vol2/2", + "scanlator": "Sup!", + "uploadDate": 1419976800000, + "source": "READMANGA_RU" + }, + { + "id": 1552943969433541667, + "name": "2 - 3", + "number": 8, + "url": "/stranstviia_emanon/vol2/3", + "scanlator": "Sup!", + "uploadDate": 1427922000000, + "source": "READMANGA_RU" + }, + { + "id": 1552943969433541668, + "name": "2 - 4", + "number": 9, + "url": "/stranstviia_emanon/vol2/4", + "scanlator": "Sup!", + "uploadDate": 1436907600000, + "source": "READMANGA_RU" + }, + { + "id": 1552943969433541669, + "name": "2 - 5", + "number": 10, + "url": "/stranstviia_emanon/vol2/5", + "scanlator": "Sup!", + "uploadDate": 1446674400000, + "source": "READMANGA_RU" + }, + { + "id": 1552943969433541670, + "name": "2 - 6", + "number": 11, + "url": "/stranstviia_emanon/vol2/6", + "scanlator": "Sup!", + "uploadDate": 1451512800000, + "source": "READMANGA_RU" + }, + { + "id": 1552943969433542626, + "name": "3 - 1", + "number": 12, + "url": "/stranstviia_emanon/vol3/1", + "scanlator": "Sup!", + "uploadDate": 1461618000000, + "source": "READMANGA_RU" + }, + { + "id": 1552943969433542627, + "name": "3 - 2", + "number": 13, + "url": "/stranstviia_emanon/vol3/2", + "scanlator": "Sup!", + "uploadDate": 1461618000000, + "source": "READMANGA_RU" + }, + { + "id": 1552943969433542628, + "name": "3 - 3", + "number": 14, + "url": "/stranstviia_emanon/vol3/3", + "scanlator": "", + "uploadDate": 1465851600000, + "source": "READMANGA_RU" + } + ], + "source": "READMANGA_RU" +} \ No newline at end of file diff --git a/app/src/androidTest/assets/manga/empty.json b/app/src/androidTest/assets/manga/empty.json new file mode 100644 index 000000000..369f0e237 --- /dev/null +++ b/app/src/androidTest/assets/manga/empty.json @@ -0,0 +1,36 @@ +{ + "id": -2096681732556647985, + "title": "Странствия Эманон", + "url": "/stranstviia_emanon", + "publicUrl": "https://readmanga.io/stranstviia_emanon", + "rating": 0.9400894, + "isNsfw": true, + "coverUrl": "https://staticrm.rmr.rocks/uploads/pics/01/12/559_p.jpg", + "tags": [ + { + "title": "Сверхъестественное", + "key": "supernatural", + "source": "READMANGA_RU" + }, + { + "title": "Сэйнэн", + "key": "seinen", + "source": "READMANGA_RU" + }, + { + "title": "Повседневность", + "key": "slice_of_life", + "source": "READMANGA_RU" + }, + { + "title": "Приключения", + "key": "adventure", + "source": "READMANGA_RU" + } + ], + "state": "FINISHED", + "largeCoverUrl": "https://staticrm.rmr.rocks/uploads/pics/01/12/559_o.jpg", + "description": "Продолжение истории о загадочной девушке по имени Эманон, которая помнит всё, что происходило на Земле за последние три миллиарда лет. \n
Начало истории читайте в \"Воспоминаниях Эманон\". \n
", + "chapters": [], + "source": "READMANGA_RU" +} \ No newline at end of file diff --git a/app/src/androidTest/assets/manga/first_chapters.json b/app/src/androidTest/assets/manga/first_chapters.json new file mode 100644 index 000000000..697dec9c8 --- /dev/null +++ b/app/src/androidTest/assets/manga/first_chapters.json @@ -0,0 +1,136 @@ +{ + "id": -2096681732556647985, + "title": "Странствия Эманон", + "url": "/stranstviia_emanon", + "publicUrl": "https://readmanga.io/stranstviia_emanon", + "rating": 0.9400894, + "isNsfw": true, + "coverUrl": "https://staticrm.rmr.rocks/uploads/pics/01/12/559_p.jpg", + "tags": [ + { + "title": "Сверхъестественное", + "key": "supernatural", + "source": "READMANGA_RU" + }, + { + "title": "Сэйнэн", + "key": "seinen", + "source": "READMANGA_RU" + }, + { + "title": "Повседневность", + "key": "slice_of_life", + "source": "READMANGA_RU" + }, + { + "title": "Приключения", + "key": "adventure", + "source": "READMANGA_RU" + } + ], + "state": "FINISHED", + "largeCoverUrl": "https://staticrm.rmr.rocks/uploads/pics/01/12/559_o.jpg", + "description": "Продолжение истории о загадочной девушке по имени Эманон, которая помнит всё, что происходило на Земле за последние три миллиарда лет. \n
Начало истории читайте в \"Воспоминаниях Эманон\". \n
", + "chapters": [ + { + "id": 3552943969433540704, + "name": "1 - 1", + "number": 1, + "url": "/stranstviia_emanon/vol1/1", + "scanlator": "Sad-Robot", + "uploadDate": 1342731600000, + "source": "READMANGA_RU" + }, + { + "id": 3552943969433540705, + "name": "1 - 2", + "number": 2, + "url": "/stranstviia_emanon/vol1/2", + "scanlator": "Sad-Robot", + "uploadDate": 1342731600000, + "source": "READMANGA_RU" + }, + { + "id": 3552943969433540706, + "name": "1 - 3", + "number": 3, + "url": "/stranstviia_emanon/vol1/3", + "scanlator": "Sad-Robot", + "uploadDate": 1342731600000, + "source": "READMANGA_RU" + }, + { + "id": 3552943969433540707, + "name": "1 - 4", + "number": 4, + "url": "/stranstviia_emanon/vol1/4", + "scanlator": "Sad-Robot", + "uploadDate": 1342731600000, + "source": "READMANGA_RU" + }, + { + "id": 3552943969433540708, + "name": "1 - 5", + "number": 5, + "url": "/stranstviia_emanon/vol1/5", + "scanlator": "Sad-Robot", + "uploadDate": 1342731600000, + "source": "READMANGA_RU" + }, + { + "id": 3552943969433541665, + "name": "2 - 1", + "number": 6, + "url": "/stranstviia_emanon/vol2/1", + "scanlator": "Sup!", + "uploadDate": 1415570400000, + "source": "READMANGA_RU" + }, + { + "id": 3552943969433541666, + "name": "2 - 2", + "number": 7, + "url": "/stranstviia_emanon/vol2/2", + "scanlator": "Sup!", + "uploadDate": 1419976800000, + "source": "READMANGA_RU" + }, + { + "id": 3552943969433541667, + "name": "2 - 3", + "number": 8, + "url": "/stranstviia_emanon/vol2/3", + "scanlator": "Sup!", + "uploadDate": 1427922000000, + "source": "READMANGA_RU" + }, + { + "id": 3552943969433541668, + "name": "2 - 4", + "number": 9, + "url": "/stranstviia_emanon/vol2/4", + "scanlator": "Sup!", + "uploadDate": 1436907600000, + "source": "READMANGA_RU" + }, + { + "id": 3552943969433541669, + "name": "2 - 5", + "number": 10, + "url": "/stranstviia_emanon/vol2/5", + "scanlator": "Sup!", + "uploadDate": 1446674400000, + "source": "READMANGA_RU" + }, + { + "id": 3552943969433541670, + "name": "2 - 6", + "number": 11, + "url": "/stranstviia_emanon/vol2/6", + "scanlator": "Sup!", + "uploadDate": 1451512800000, + "source": "READMANGA_RU" + } + ], + "source": "READMANGA_RU" +} \ No newline at end of file diff --git a/app/src/androidTest/assets/manga/full.json b/app/src/androidTest/assets/manga/full.json new file mode 100644 index 000000000..9667baa9c --- /dev/null +++ b/app/src/androidTest/assets/manga/full.json @@ -0,0 +1,163 @@ +{ + "id": -2096681732556647985, + "title": "Странствия Эманон", + "url": "/stranstviia_emanon", + "publicUrl": "https://readmanga.io/stranstviia_emanon", + "rating": 0.9400894, + "isNsfw": true, + "coverUrl": "https://staticrm.rmr.rocks/uploads/pics/01/12/559_p.jpg", + "tags": [ + { + "title": "Сверхъестественное", + "key": "supernatural", + "source": "READMANGA_RU" + }, + { + "title": "Сэйнэн", + "key": "seinen", + "source": "READMANGA_RU" + }, + { + "title": "Повседневность", + "key": "slice_of_life", + "source": "READMANGA_RU" + }, + { + "title": "Приключения", + "key": "adventure", + "source": "READMANGA_RU" + } + ], + "state": "FINISHED", + "largeCoverUrl": "https://staticrm.rmr.rocks/uploads/pics/01/12/559_o.jpg", + "description": "Продолжение истории о загадочной девушке по имени Эманон, которая помнит всё, что происходило на Земле за последние три миллиарда лет. \n
Начало истории читайте в \"Воспоминаниях Эманон\". \n
", + "chapters": [ + { + "id": 3552943969433540704, + "name": "1 - 1", + "number": 1, + "url": "/stranstviia_emanon/vol1/1", + "scanlator": "Sad-Robot", + "uploadDate": 1342731600000, + "source": "READMANGA_RU" + }, + { + "id": 3552943969433540705, + "name": "1 - 2", + "number": 2, + "url": "/stranstviia_emanon/vol1/2", + "scanlator": "Sad-Robot", + "uploadDate": 1342731600000, + "source": "READMANGA_RU" + }, + { + "id": 3552943969433540706, + "name": "1 - 3", + "number": 3, + "url": "/stranstviia_emanon/vol1/3", + "scanlator": "Sad-Robot", + "uploadDate": 1342731600000, + "source": "READMANGA_RU" + }, + { + "id": 3552943969433540707, + "name": "1 - 4", + "number": 4, + "url": "/stranstviia_emanon/vol1/4", + "scanlator": "Sad-Robot", + "uploadDate": 1342731600000, + "source": "READMANGA_RU" + }, + { + "id": 3552943969433540708, + "name": "1 - 5", + "number": 5, + "url": "/stranstviia_emanon/vol1/5", + "scanlator": "Sad-Robot", + "uploadDate": 1342731600000, + "source": "READMANGA_RU" + }, + { + "id": 3552943969433541665, + "name": "2 - 1", + "number": 6, + "url": "/stranstviia_emanon/vol2/1", + "scanlator": "Sup!", + "uploadDate": 1415570400000, + "source": "READMANGA_RU" + }, + { + "id": 3552943969433541666, + "name": "2 - 2", + "number": 7, + "url": "/stranstviia_emanon/vol2/2", + "scanlator": "Sup!", + "uploadDate": 1419976800000, + "source": "READMANGA_RU" + }, + { + "id": 3552943969433541667, + "name": "2 - 3", + "number": 8, + "url": "/stranstviia_emanon/vol2/3", + "scanlator": "Sup!", + "uploadDate": 1427922000000, + "source": "READMANGA_RU" + }, + { + "id": 3552943969433541668, + "name": "2 - 4", + "number": 9, + "url": "/stranstviia_emanon/vol2/4", + "scanlator": "Sup!", + "uploadDate": 1436907600000, + "source": "READMANGA_RU" + }, + { + "id": 3552943969433541669, + "name": "2 - 5", + "number": 10, + "url": "/stranstviia_emanon/vol2/5", + "scanlator": "Sup!", + "uploadDate": 1446674400000, + "source": "READMANGA_RU" + }, + { + "id": 3552943969433541670, + "name": "2 - 6", + "number": 11, + "url": "/stranstviia_emanon/vol2/6", + "scanlator": "Sup!", + "uploadDate": 1451512800000, + "source": "READMANGA_RU" + }, + { + "id": 3552943969433542626, + "name": "3 - 1", + "number": 12, + "url": "/stranstviia_emanon/vol3/1", + "scanlator": "Sup!", + "uploadDate": 1461618000000, + "source": "READMANGA_RU" + }, + { + "id": 3552943969433542627, + "name": "3 - 2", + "number": 13, + "url": "/stranstviia_emanon/vol3/2", + "scanlator": "Sup!", + "uploadDate": 1461618000000, + "source": "READMANGA_RU" + }, + { + "id": 3552943969433542628, + "name": "3 - 3", + "number": 14, + "url": "/stranstviia_emanon/vol3/3", + "scanlator": "", + "uploadDate": 1465851600000, + "source": "READMANGA_RU" + } + ], + "source": "READMANGA_RU" +} \ No newline at end of file diff --git a/app/src/androidTest/assets/manga/without_middle_chapter.json b/app/src/androidTest/assets/manga/without_middle_chapter.json new file mode 100644 index 000000000..97d797b53 --- /dev/null +++ b/app/src/androidTest/assets/manga/without_middle_chapter.json @@ -0,0 +1,154 @@ +{ + "id": -2096681732556647985, + "title": "Странствия Эманон", + "url": "/stranstviia_emanon", + "publicUrl": "https://readmanga.io/stranstviia_emanon", + "rating": 0.9400894, + "isNsfw": true, + "coverUrl": "https://staticrm.rmr.rocks/uploads/pics/01/12/559_p.jpg", + "tags": [ + { + "title": "Сверхъестественное", + "key": "supernatural", + "source": "READMANGA_RU" + }, + { + "title": "Сэйнэн", + "key": "seinen", + "source": "READMANGA_RU" + }, + { + "title": "Повседневность", + "key": "slice_of_life", + "source": "READMANGA_RU" + }, + { + "title": "Приключения", + "key": "adventure", + "source": "READMANGA_RU" + } + ], + "state": "FINISHED", + "largeCoverUrl": "https://staticrm.rmr.rocks/uploads/pics/01/12/559_o.jpg", + "description": "Продолжение истории о загадочной девушке по имени Эманон, которая помнит всё, что происходило на Земле за последние три миллиарда лет. \n
Начало истории читайте в \"Воспоминаниях Эманон\". \n
", + "chapters": [ + { + "id": 3552943969433540704, + "name": "1 - 1", + "number": 1, + "url": "/stranstviia_emanon/vol1/1", + "scanlator": "Sad-Robot", + "uploadDate": 1342731600000, + "source": "READMANGA_RU" + }, + { + "id": 3552943969433540705, + "name": "1 - 2", + "number": 2, + "url": "/stranstviia_emanon/vol1/2", + "scanlator": "Sad-Robot", + "uploadDate": 1342731600000, + "source": "READMANGA_RU" + }, + { + "id": 3552943969433540706, + "name": "1 - 3", + "number": 3, + "url": "/stranstviia_emanon/vol1/3", + "scanlator": "Sad-Robot", + "uploadDate": 1342731600000, + "source": "READMANGA_RU" + }, + { + "id": 3552943969433540707, + "name": "1 - 4", + "number": 4, + "url": "/stranstviia_emanon/vol1/4", + "scanlator": "Sad-Robot", + "uploadDate": 1342731600000, + "source": "READMANGA_RU" + }, + { + "id": 3552943969433540708, + "name": "1 - 5", + "number": 5, + "url": "/stranstviia_emanon/vol1/5", + "scanlator": "Sad-Robot", + "uploadDate": 1342731600000, + "source": "READMANGA_RU" + }, + { + "id": 3552943969433541666, + "name": "2 - 2", + "number": 7, + "url": "/stranstviia_emanon/vol2/2", + "scanlator": "Sup!", + "uploadDate": 1419976800000, + "source": "READMANGA_RU" + }, + { + "id": 3552943969433541667, + "name": "2 - 3", + "number": 8, + "url": "/stranstviia_emanon/vol2/3", + "scanlator": "Sup!", + "uploadDate": 1427922000000, + "source": "READMANGA_RU" + }, + { + "id": 3552943969433541668, + "name": "2 - 4", + "number": 9, + "url": "/stranstviia_emanon/vol2/4", + "scanlator": "Sup!", + "uploadDate": 1436907600000, + "source": "READMANGA_RU" + }, + { + "id": 3552943969433541669, + "name": "2 - 5", + "number": 10, + "url": "/stranstviia_emanon/vol2/5", + "scanlator": "Sup!", + "uploadDate": 1446674400000, + "source": "READMANGA_RU" + }, + { + "id": 3552943969433541670, + "name": "2 - 6", + "number": 11, + "url": "/stranstviia_emanon/vol2/6", + "scanlator": "Sup!", + "uploadDate": 1451512800000, + "source": "READMANGA_RU" + }, + { + "id": 3552943969433542626, + "name": "3 - 1", + "number": 12, + "url": "/stranstviia_emanon/vol3/1", + "scanlator": "Sup!", + "uploadDate": 1461618000000, + "source": "READMANGA_RU" + }, + { + "id": 3552943969433542627, + "name": "3 - 2", + "number": 13, + "url": "/stranstviia_emanon/vol3/2", + "scanlator": "Sup!", + "uploadDate": 1461618000000, + "source": "READMANGA_RU" + }, + { + "id": 3552943969433542628, + "name": "3 - 3", + "number": 14, + "url": "/stranstviia_emanon/vol3/3", + "scanlator": "", + "uploadDate": 1465851600000, + "source": "READMANGA_RU" + } + ], + "source": "READMANGA_RU" +} \ No newline at end of file diff --git a/app/src/androidTest/java/org/koitharu/kotatsu/core/db/MangaDatabaseTest.kt b/app/src/androidTest/java/org/koitharu/kotatsu/core/db/MangaDatabaseTest.kt index f0f37c2a1..54141f3e6 100644 --- a/app/src/androidTest/java/org/koitharu/kotatsu/core/db/MangaDatabaseTest.kt +++ b/app/src/androidTest/java/org/koitharu/kotatsu/core/db/MangaDatabaseTest.kt @@ -1,14 +1,13 @@ package org.koitharu.kotatsu.core.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 java.io.IOException import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import org.koitharu.kotatsu.core.db.migrations.* -import java.io.IOException @RunWith(AndroidJUnit4::class) class MangaDatabaseTest { @@ -16,8 +15,7 @@ class MangaDatabaseTest { @get:Rule val helper: MigrationTestHelper = MigrationTestHelper( InstrumentationRegistry.getInstrumentation(), - MangaDatabase::class.java.canonicalName, - FrameworkSQLiteOpenHelperFactory() + MangaDatabase::class.java, ) @Test @@ -37,7 +35,6 @@ class MangaDatabaseTest { } } - private companion object { const val TEST_DB = "test-db" @@ -50,6 +47,9 @@ class MangaDatabaseTest { Migration5To6(), Migration6To7(), Migration7To8(), + Migration8To9(), + Migration9To10(), + Migration10To11(), ) } } \ No newline at end of file diff --git a/app/src/androidTest/java/org/koitharu/kotatsu/tracker/domain/TrackerTest.kt b/app/src/androidTest/java/org/koitharu/kotatsu/tracker/domain/TrackerTest.kt new file mode 100644 index 000000000..b97aa575c --- /dev/null +++ b/app/src/androidTest/java/org/koitharu/kotatsu/tracker/domain/TrackerTest.kt @@ -0,0 +1,160 @@ +package org.koitharu.kotatsu.tracker.domain + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry +import com.squareup.moshi.Moshi +import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertTrue +import kotlinx.coroutines.test.runTest +import okio.buffer +import okio.source +import org.junit.Test +import org.junit.runner.RunWith +import org.koin.test.KoinTest +import org.koin.test.inject +import org.koitharu.kotatsu.base.domain.MangaDataRepository +import org.koitharu.kotatsu.history.domain.HistoryRepository +import org.koitharu.kotatsu.parsers.model.Manga + +@RunWith(AndroidJUnit4::class) +class TrackerTest : KoinTest { + + private val moshi = Moshi.Builder().add(KotlinJsonAdapterFactory()).build() + private val mangaAdapter = moshi.adapter(Manga::class.java) + private val historyRegistry by inject() + private val repository by inject() + private val dataRepository by inject() + private val tracker by inject() + + @Test + fun noUpdates() = runTest { + val manga = loadManga("full.json") + tracker.deleteTrack(manga.id) + + tracker.checkUpdates(manga, commit = true).apply { + assertFalse(isValid) + assert(newChapters.isEmpty()) + } + assertEquals(0, repository.getNewChaptersCount(manga.id)) + tracker.checkUpdates(manga, commit = true).apply { + assertTrue(isValid) + assert(newChapters.isEmpty()) + } + assertEquals(0, repository.getNewChaptersCount(manga.id)) + } + + @Test + fun hasUpdates() = runTest { + val mangaFirst = loadManga("first_chapters.json") + val mangaFull = loadManga("full.json") + tracker.deleteTrack(mangaFirst.id) + + tracker.checkUpdates(mangaFirst, commit = true).apply { + assertFalse(isValid) + assert(newChapters.isEmpty()) + } + assertEquals(0, repository.getNewChaptersCount(mangaFirst.id)) + tracker.checkUpdates(mangaFull, commit = true).apply { + assertTrue(isValid) + assertEquals(3, newChapters.size) + } + assertEquals(3, repository.getNewChaptersCount(mangaFirst.id)) + tracker.checkUpdates(mangaFull, commit = true).apply { + assertTrue(isValid) + assert(newChapters.isEmpty()) + } + assertEquals(3, repository.getNewChaptersCount(mangaFirst.id)) + } + + @Test + fun badIds() = runTest { + val mangaFirst = loadManga("first_chapters.json") + val mangaBad = loadManga("bad_ids.json") + tracker.deleteTrack(mangaFirst.id) + + tracker.checkUpdates(mangaFirst, commit = true).apply { + assertFalse(isValid) + assert(newChapters.isEmpty()) + } + assertEquals(0, repository.getNewChaptersCount(mangaFirst.id)) + tracker.checkUpdates(mangaBad, commit = true).apply { + assertFalse(isValid) + assert(newChapters.isEmpty()) + } + assertEquals(0, repository.getNewChaptersCount(mangaFirst.id)) + tracker.checkUpdates(mangaFirst, commit = true).apply { + assertFalse(isValid) + assert(newChapters.isEmpty()) + } + assertEquals(0, repository.getNewChaptersCount(mangaFirst.id)) + } + + @Test + fun badIds2() = runTest { + val mangaFirst = loadManga("first_chapters.json") + val mangaBad = loadManga("bad_ids.json") + val mangaFull = loadManga("full.json") + tracker.deleteTrack(mangaFirst.id) + + tracker.checkUpdates(mangaFirst, commit = true).apply { + assertFalse(isValid) + assert(newChapters.isEmpty()) + } + assertEquals(0, repository.getNewChaptersCount(mangaFirst.id)) + tracker.checkUpdates(mangaFull, commit = true).apply { + assertTrue(isValid) + assertEquals(3, newChapters.size) + } + assertEquals(3, repository.getNewChaptersCount(mangaFull.id)) + tracker.checkUpdates(mangaBad, commit = true).apply { + assertFalse(isValid) + assert(newChapters.isEmpty()) + } + assertEquals(0, repository.getNewChaptersCount(mangaFirst.id)) + } + + @Test + fun fullReset() = runTest { + val mangaFull = loadManga("full.json") + val mangaFirst = loadManga("first_chapters.json") + val mangaEmpty = loadManga("empty.json") + tracker.deleteTrack(mangaFull.id) + + assertEquals(0, repository.getNewChaptersCount(mangaFull.id)) + tracker.checkUpdates(mangaFull, commit = true).apply { + assertFalse(isValid) + assert(newChapters.isEmpty()) + } + assertEquals(0, repository.getNewChaptersCount(mangaFull.id)) + tracker.checkUpdates(mangaEmpty, commit = true).apply { + assert(newChapters.isEmpty()) + } + assertEquals(0, repository.getNewChaptersCount(mangaFull.id)) + tracker.checkUpdates(mangaFirst, commit = true).apply { + assertFalse(isValid) + assert(newChapters.isEmpty()) + } + assertEquals(0, repository.getNewChaptersCount(mangaFull.id)) + tracker.checkUpdates(mangaFull, commit = true).apply { + assertTrue(isValid) + assertEquals(3, newChapters.size) + } + assertEquals(3, repository.getNewChaptersCount(mangaFull.id)) + tracker.checkUpdates(mangaEmpty, commit = true).apply { + assertFalse(isValid) + assert(newChapters.isEmpty()) + } + assertEquals(0, repository.getNewChaptersCount(mangaFull.id)) + } + + private suspend fun loadManga(name: String): Manga { + val assets = InstrumentationRegistry.getInstrumentation().context.assets + val manga = assets.open("manga/$name").use { + mangaAdapter.fromJson(it.source().buffer()) + } ?: throw RuntimeException("Cannot read manga from json \"$name\"") + dataRepository.storeManga(manga) + return manga + } +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/core/db/MangaDatabase.kt b/app/src/main/java/org/koitharu/kotatsu/core/db/MangaDatabase.kt index 0714c0fcc..2c74455f8 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/db/MangaDatabase.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/db/MangaDatabase.kt @@ -17,6 +17,9 @@ import org.koitharu.kotatsu.history.data.HistoryDao import org.koitharu.kotatsu.history.data.HistoryEntity import org.koitharu.kotatsu.suggestions.data.SuggestionDao import org.koitharu.kotatsu.suggestions.data.SuggestionEntity +import org.koitharu.kotatsu.tracker.data.TrackEntity +import org.koitharu.kotatsu.tracker.data.TrackLogEntity +import org.koitharu.kotatsu.tracker.data.TracksDao @Database( entities = [ diff --git a/app/src/main/java/org/koitharu/kotatsu/core/db/dao/TrackLogsDao.kt b/app/src/main/java/org/koitharu/kotatsu/core/db/dao/TrackLogsDao.kt index 496a5539b..ade35613b 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/db/dao/TrackLogsDao.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/db/dao/TrackLogsDao.kt @@ -1,8 +1,8 @@ package org.koitharu.kotatsu.core.db.dao import androidx.room.* -import org.koitharu.kotatsu.core.db.entity.TrackLogEntity -import org.koitharu.kotatsu.core.db.entity.TrackLogWithManga +import org.koitharu.kotatsu.tracker.data.TrackLogEntity +import org.koitharu.kotatsu.tracker.data.TrackLogWithManga @Dao interface TrackLogsDao { diff --git a/app/src/main/java/org/koitharu/kotatsu/core/db/entity/EntityMapping.kt b/app/src/main/java/org/koitharu/kotatsu/core/db/entity/EntityMapping.kt index 5bdd0ca4a..af938a813 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/db/entity/EntityMapping.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/db/entity/EntityMapping.kt @@ -1,7 +1,5 @@ package org.koitharu.kotatsu.core.db.entity -import java.util.* -import org.koitharu.kotatsu.core.model.TrackingLogItem import org.koitharu.kotatsu.parsers.model.* import org.koitharu.kotatsu.parsers.util.mapToSet import org.koitharu.kotatsu.parsers.util.toTitleCase @@ -35,13 +33,6 @@ fun MangaEntity.toManga(tags: Set) = Manga( fun MangaWithTags.toManga() = manga.toManga(tags.toMangaTags()) -fun TrackLogWithManga.toTrackingLogItem() = TrackingLogItem( - id = trackLog.id, - chapters = trackLog.chapters.split('\n').filterNot { x -> x.isEmpty() }, - manga = manga.toManga(tags.toMangaTags()), - createdAt = Date(trackLog.createdAt) -) - // Model to entity fun Manga.toEntity() = MangaEntity( diff --git a/app/src/main/java/org/koitharu/kotatsu/history/domain/HistoryRepository.kt b/app/src/main/java/org/koitharu/kotatsu/history/domain/HistoryRepository.kt index a4b2ab772..4519b60e4 100644 --- a/app/src/main/java/org/koitharu/kotatsu/history/domain/HistoryRepository.kt +++ b/app/src/main/java/org/koitharu/kotatsu/history/domain/HistoryRepository.kt @@ -77,7 +77,7 @@ class HistoryRepository( scroll = scroll.toFloat(), // we migrate to int, but decide to not update database ) ) - trackingRepository.upsert(manga) + trackingRepository.syncWithHistory(manga, chapterId) } } diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/HistorySettingsFragment.kt b/app/src/main/java/org/koitharu/kotatsu/settings/HistorySettingsFragment.kt index 1ff363c35..dfa8a7bd0 100644 --- a/app/src/main/java/org/koitharu/kotatsu/settings/HistorySettingsFragment.kt +++ b/app/src/main/java/org/koitharu/kotatsu/settings/HistorySettingsFragment.kt @@ -43,7 +43,7 @@ class HistorySettingsFragment : BasePreferenceFragment(R.string.history_and_cach } findPreference(AppSettings.KEY_UPDATES_FEED_CLEAR)?.let { pref -> viewLifecycleScope.launchWhenResumed { - val items = trackerRepo.count() + val items = trackerRepo.getLogsCount() pref.summary = pref.context.resources.getQuantityString(R.plurals.items, items, items) } @@ -142,4 +142,4 @@ class HistorySettingsFragment : BasePreferenceFragment(R.string.history_and_cach } }.show() } -} +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/tracker/TrackerModule.kt b/app/src/main/java/org/koitharu/kotatsu/tracker/TrackerModule.kt index d1b44e64d..e15791927 100644 --- a/app/src/main/java/org/koitharu/kotatsu/tracker/TrackerModule.kt +++ b/app/src/main/java/org/koitharu/kotatsu/tracker/TrackerModule.kt @@ -14,7 +14,7 @@ val trackerModule factory { TrackingRepository(get()) } factory { TrackerNotificationChannels(androidContext(), get()) } - factory { Tracker(get()) } + factory { Tracker(get(), get(), get()) } viewModel { FeedViewModel(get()) } } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/tracker/data/EntityMapping.kt b/app/src/main/java/org/koitharu/kotatsu/tracker/data/EntityMapping.kt new file mode 100644 index 000000000..452f60f8c --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/tracker/data/EntityMapping.kt @@ -0,0 +1,13 @@ +package org.koitharu.kotatsu.tracker.data + +import java.util.* +import org.koitharu.kotatsu.core.db.entity.toManga +import org.koitharu.kotatsu.core.db.entity.toMangaTags +import org.koitharu.kotatsu.tracker.domain.model.TrackingLogItem + +fun TrackLogWithManga.toTrackingLogItem() = TrackingLogItem( + id = trackLog.id, + chapters = trackLog.chapters.split('\n').filterNot { x -> x.isEmpty() }, + manga = manga.toManga(tags.toMangaTags()), + createdAt = Date(trackLog.createdAt) +) \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/core/db/entity/TrackEntity.kt b/app/src/main/java/org/koitharu/kotatsu/tracker/data/TrackEntity.kt similarity index 74% rename from app/src/main/java/org/koitharu/kotatsu/core/db/entity/TrackEntity.kt rename to app/src/main/java/org/koitharu/kotatsu/tracker/data/TrackEntity.kt index 91d65d82b..52f7b8d18 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/db/entity/TrackEntity.kt +++ b/app/src/main/java/org/koitharu/kotatsu/tracker/data/TrackEntity.kt @@ -1,9 +1,10 @@ -package org.koitharu.kotatsu.core.db.entity +package org.koitharu.kotatsu.tracker.data import androidx.room.ColumnInfo import androidx.room.Entity import androidx.room.ForeignKey import androidx.room.PrimaryKey +import org.koitharu.kotatsu.core.db.entity.MangaEntity @Entity( tableName = "tracks", @@ -19,9 +20,11 @@ import androidx.room.PrimaryKey class TrackEntity( @PrimaryKey(autoGenerate = false) @ColumnInfo(name = "manga_id") val mangaId: Long, + @get:Deprecated(message = "Should not be used", level = DeprecationLevel.ERROR) @ColumnInfo(name = "chapters_total") val totalChapters: Int, @ColumnInfo(name = "last_chapter_id") val lastChapterId: Long, @ColumnInfo(name = "chapters_new") val newChapters: Int, @ColumnInfo(name = "last_check") val lastCheck: Long, + @get:Deprecated(message = "Should not be used", level = DeprecationLevel.ERROR) @ColumnInfo(name = "last_notified_id") val lastNotifiedChapterId: Long ) \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/core/db/entity/TrackLogEntity.kt b/app/src/main/java/org/koitharu/kotatsu/tracker/data/TrackLogEntity.kt similarity index 86% rename from app/src/main/java/org/koitharu/kotatsu/core/db/entity/TrackLogEntity.kt rename to app/src/main/java/org/koitharu/kotatsu/tracker/data/TrackLogEntity.kt index 8bb8e61b4..1fedc4663 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/db/entity/TrackLogEntity.kt +++ b/app/src/main/java/org/koitharu/kotatsu/tracker/data/TrackLogEntity.kt @@ -1,9 +1,10 @@ -package org.koitharu.kotatsu.core.db.entity +package org.koitharu.kotatsu.tracker.data import androidx.room.ColumnInfo import androidx.room.Entity import androidx.room.ForeignKey import androidx.room.PrimaryKey +import org.koitharu.kotatsu.core.db.entity.MangaEntity @Entity( tableName = "track_logs", diff --git a/app/src/main/java/org/koitharu/kotatsu/core/db/entity/TrackLogWithManga.kt b/app/src/main/java/org/koitharu/kotatsu/tracker/data/TrackLogWithManga.kt similarity index 65% rename from app/src/main/java/org/koitharu/kotatsu/core/db/entity/TrackLogWithManga.kt rename to app/src/main/java/org/koitharu/kotatsu/tracker/data/TrackLogWithManga.kt index 7a6e145a4..e83675b41 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/db/entity/TrackLogWithManga.kt +++ b/app/src/main/java/org/koitharu/kotatsu/tracker/data/TrackLogWithManga.kt @@ -1,8 +1,11 @@ -package org.koitharu.kotatsu.core.db.entity +package org.koitharu.kotatsu.tracker.data import androidx.room.Embedded import androidx.room.Junction import androidx.room.Relation +import org.koitharu.kotatsu.core.db.entity.MangaEntity +import org.koitharu.kotatsu.core.db.entity.MangaTagsEntity +import org.koitharu.kotatsu.core.db.entity.TagEntity class TrackLogWithManga( @Embedded val trackLog: TrackLogEntity, diff --git a/app/src/main/java/org/koitharu/kotatsu/core/db/dao/TracksDao.kt b/app/src/main/java/org/koitharu/kotatsu/tracker/data/TracksDao.kt similarity index 91% rename from app/src/main/java/org/koitharu/kotatsu/core/db/dao/TracksDao.kt rename to app/src/main/java/org/koitharu/kotatsu/tracker/data/TracksDao.kt index c0fb163a7..8a566ce9d 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/db/dao/TracksDao.kt +++ b/app/src/main/java/org/koitharu/kotatsu/tracker/data/TracksDao.kt @@ -1,7 +1,6 @@ -package org.koitharu.kotatsu.core.db.dao +package org.koitharu.kotatsu.tracker.data import androidx.room.* -import org.koitharu.kotatsu.core.db.entity.TrackEntity @Dao abstract class TracksDao { diff --git a/app/src/main/java/org/koitharu/kotatsu/tracker/domain/Tracker.kt b/app/src/main/java/org/koitharu/kotatsu/tracker/domain/Tracker.kt index 425cabcdf..e93902485 100644 --- a/app/src/main/java/org/koitharu/kotatsu/tracker/domain/Tracker.kt +++ b/app/src/main/java/org/koitharu/kotatsu/tracker/domain/Tracker.kt @@ -1,132 +1,115 @@ package org.koitharu.kotatsu.tracker.domain -import org.koitharu.kotatsu.core.model.MangaTracking +import androidx.annotation.VisibleForTesting import org.koitharu.kotatsu.core.parser.MangaRepository +import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.parsers.model.Manga -import org.koitharu.kotatsu.parsers.model.MangaChapter +import org.koitharu.kotatsu.tracker.domain.model.MangaTracking import org.koitharu.kotatsu.tracker.domain.model.MangaUpdates +import org.koitharu.kotatsu.tracker.work.TrackerNotificationChannels +import org.koitharu.kotatsu.tracker.work.TrackingItem class Tracker( + private val settings: AppSettings, private val repository: TrackingRepository, + private val channels: TrackerNotificationChannels, ) { - suspend fun fetchUpdates(track: MangaTracking, commit: Boolean): MangaUpdates { - val repo = MangaRepository(track.manga.source) - val details = repo.getDetails(track.manga) - val chapters = details.chapters.orEmpty() - if (track.isEmpty()) { - // first check or manga was empty on last check - if (commit) { - repository.storeTrackResult( - mangaId = track.manga.id, - knownChaptersCount = chapters.size, - lastChapterId = chapters.lastOrNull()?.id ?: 0L, - previousTrackChapterId = 0L, - newChapters = emptyList(), - saveTrackLog = false, - ) - } - return MangaUpdates( - manga = details, - newChapters = emptyList(), - ) - } - val newChapters = details.getNewChapters(track.lastChapterId) - if (newChapters.isEmpty()) { - if (commit) { - repository.storeTrackResult( - mangaId = track.manga.id, - knownChaptersCount = chapters.size, - lastChapterId = chapters.lastOrNull()?.id ?: 0L, - previousTrackChapterId = 0L, - newChapters = emptyList(), - saveTrackLog = false, - ) - } - return MangaUpdates( - manga = details, - newChapters = emptyList(), - ) + suspend fun getAllTracks(): List { + val sources = settings.trackSources + if (sources.isEmpty()) { + return emptyList() } - return when { - - // the same chapters count - chapters.size == track.knownChaptersCount -> { - if (chapters.lastOrNull()?.id == track.lastChapterId) { - // manga was not updated. skip - MangaUpdates( - manga = details, - newChapters = emptyList(), - ) + val knownIds = HashSet() + val result = ArrayList() + // Favourites + if (AppSettings.TRACK_FAVOURITES in sources) { + val favourites = repository.getAllFavouritesManga() + channels.updateChannels(favourites.keys) + for ((category, mangaList) in favourites) { + if (!category.isTrackingEnabled || mangaList.isEmpty()) { + continue + } + val categoryTracks = repository.getTracks(mangaList) + val channelId = if (channels.isFavouriteNotificationsEnabled(category)) { + channels.getFavouritesChannelId(category.id) } else { - // number of chapters still the same, bu last chapter changed. - // maybe some chapters are removed. we need to find last known chapter - val knownChapter = chapters.indexOfLast { it.id == track.lastChapterId } - if (knownChapter == -1) { - // confuse. reset anything - if (commit) { - repository.storeTrackResult( - mangaId = track.manga.id, - knownChaptersCount = chapters.size, - lastChapterId = chapters.lastOrNull()?.id ?: 0L, - previousTrackChapterId = 0L, - newChapters = emptyList(), - saveTrackLog = false, - ) - } - MangaUpdates( - manga = details, - newChapters = emptyList(), - ) - } else { - val newChapters = chapters.takeLast(chapters.size - knownChapter + 1) - if (commit) { - repository.storeTrackResult( - mangaId = track.manga.id, - knownChaptersCount = knownChapter + 1, - lastChapterId = track.lastChapterId, - previousTrackChapterId = track.lastNotifiedChapterId, - newChapters = newChapters, - saveTrackLog = true, - ) - } - MangaUpdates( - manga = details, - newChapters = details.getNewChapters(track.lastNotifiedChapterId), - ) + null + } + for (track in categoryTracks) { + if (knownIds.add(track.manga)) { + result.add(TrackingItem(track, channelId)) } } } - else -> { - val newChapters = chapters.takeLast(chapters.size - track.knownChaptersCount) - if (commit) { - repository.storeTrackResult( - mangaId = track.manga.id, - knownChaptersCount = track.knownChaptersCount, - lastChapterId = track.lastChapterId, - previousTrackChapterId = track.lastNotifiedChapterId, - newChapters = newChapters, - saveTrackLog = true, - ) + } + // History + if (AppSettings.TRACK_HISTORY in sources) { + val history = repository.getAllHistoryManga() + val historyTracks = repository.getTracks(history) + val channelId = if (channels.isHistoryNotificationsEnabled()) { + channels.getHistoryChannelId() + } else { + null + } + for (track in historyTracks) { + if (knownIds.add(track.manga)) { + result.add(TrackingItem(track, channelId)) } - MangaUpdates( - manga = details, - newChapters = details.getNewChapters(track.lastNotifiedChapterId), - ) } } + result.trimToSize() + return result } - private fun Manga.getNewChapters(lastChapterId: Long): List { - val chapters = chapters ?: return emptyList() - if (lastChapterId == 0L) { - return emptyList() + suspend fun gc() { + repository.gc() + } + + suspend fun fetchUpdates(track: MangaTracking, commit: Boolean): MangaUpdates { + val manga = MangaRepository(track.manga.source).getDetails(track.manga) + val updates = compare(track, manga) + if (commit) { + repository.saveUpdates(updates) + } + return updates + } + + @VisibleForTesting + suspend fun checkUpdates(manga: Manga, commit: Boolean): MangaUpdates { + val track = repository.getTrack(manga) + val updates = compare(track, manga) + if (commit) { + repository.saveUpdates(updates) + } + return updates + } + + @VisibleForTesting + suspend fun deleteTrack(mangaId: Long) { + repository.deleteTrack(mangaId) + } + + /** + * The main functionality of tracker: check new chapters in [manga] comparing to the [track] + */ + private fun compare(track: MangaTracking, manga: Manga): MangaUpdates { + if (track.isEmpty()) { + // first check or manga was empty on last check + return MangaUpdates(manga, emptyList(), isValid = false) } - val raw = chapters.takeLastWhile { x -> x.id != lastChapterId } - return if (raw.isEmpty() || raw.size == chapters.size) { - emptyList() - } else { - raw + val chapters = requireNotNull(manga.chapters) + val newChapters = chapters.takeLastWhile { x -> x.id != track.lastChapterId } + return when { + newChapters.isEmpty() -> { + return MangaUpdates(manga, emptyList(), isValid = chapters.lastOrNull()?.id == track.lastChapterId) + } + newChapters.size == chapters.size -> { + return MangaUpdates(manga, emptyList(), isValid = false) + } + else -> { + return MangaUpdates(manga, newChapters, isValid = true) + } } } } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/tracker/domain/TrackingRepository.kt b/app/src/main/java/org/koitharu/kotatsu/tracker/domain/TrackingRepository.kt index b9f7e46a0..694f66197 100644 --- a/app/src/main/java/org/koitharu/kotatsu/tracker/domain/TrackingRepository.kt +++ b/app/src/main/java/org/koitharu/kotatsu/tracker/domain/TrackingRepository.kt @@ -1,17 +1,24 @@ package org.koitharu.kotatsu.tracker.domain +import androidx.annotation.VisibleForTesting import androidx.room.withTransaction import java.util.* import org.koitharu.kotatsu.core.db.MangaDatabase -import org.koitharu.kotatsu.core.db.entity.* +import org.koitharu.kotatsu.core.db.entity.MangaEntity +import org.koitharu.kotatsu.core.db.entity.toManga import org.koitharu.kotatsu.core.model.FavouriteCategory -import org.koitharu.kotatsu.core.model.MangaTracking -import org.koitharu.kotatsu.core.model.TrackingLogItem import org.koitharu.kotatsu.favourites.data.toFavouriteCategory import org.koitharu.kotatsu.parsers.model.Manga -import org.koitharu.kotatsu.parsers.model.MangaChapter import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.parsers.util.mapToSet +import org.koitharu.kotatsu.tracker.data.TrackEntity +import org.koitharu.kotatsu.tracker.data.TrackLogEntity +import org.koitharu.kotatsu.tracker.data.toTrackingLogItem +import org.koitharu.kotatsu.tracker.domain.model.MangaTracking +import org.koitharu.kotatsu.tracker.domain.model.MangaUpdates +import org.koitharu.kotatsu.tracker.domain.model.TrackingLogItem + +private const val NO_ID = 0L class TrackingRepository( private val db: MangaDatabase, @@ -21,42 +28,38 @@ class TrackingRepository( return db.tracksDao.findNewChapters(mangaId) ?: 0 } - suspend fun getHistoryManga(): List { - return db.historyDao.findAllManga().toMangaList() - } - - suspend fun getFavouritesManga(): Map> { - val categories = db.favouriteCategoriesDao.findAll() - return categories.associateTo(LinkedHashMap(categories.size)) { categoryEntity -> - categoryEntity.toFavouriteCategory() to db.favouritesDao.findAllManga(categoryEntity.categoryId) - .toMangaList() + suspend fun getTracks(mangaList: Collection): List { + val ids = mangaList.mapToSet { it.id } + val tracks = db.tracksDao.findAll(ids).groupBy { it.mangaId } + val idSet = HashSet() + val result = ArrayList(mangaList.size) + for (item in mangaList) { + if (item.source == MangaSource.LOCAL || !idSet.add(item.id)) { + continue + } + val track = tracks[item.id]?.lastOrNull() + result += MangaTracking( + manga = item, + lastChapterId = track?.lastChapterId ?: NO_ID, + lastCheck = track?.lastCheck?.takeUnless { it == 0L }?.let(::Date) + ) } + return result } - suspend fun getCategoriesCount(): IntArray { - val categories = db.favouriteCategoriesDao.findAll() - return intArrayOf( - categories.count { it.track }, - categories.size, + @VisibleForTesting + suspend fun getTrack(manga: Manga): MangaTracking { + val track = db.tracksDao.find(manga.id) + return MangaTracking( + manga = manga, + lastChapterId = track?.lastChapterId ?: NO_ID, + lastCheck = track?.lastCheck?.takeUnless { it == 0L }?.let(::Date) ) } - suspend fun getTracks(mangaList: Collection): List { - val ids = mangaList.mapToSet { it.id } - val tracks = db.tracksDao.findAll(ids).groupBy { it.mangaId } - return mangaList // TODO optimize - .filterNot { it.source == MangaSource.LOCAL } - .distinctBy { it.id } - .map { manga -> - val track = tracks[manga.id]?.singleOrNull() - MangaTracking( - manga = manga, - knownChaptersCount = track?.totalChapters ?: -1, - lastChapterId = track?.lastChapterId ?: 0L, - lastNotifiedChapterId = track?.lastNotifiedChapterId ?: 0L, - lastCheck = track?.lastCheck?.takeUnless { it == 0L }?.let(::Date) - ) - } + @VisibleForTesting + suspend fun deleteTrack(mangaId: Long) { + db.tracksDao.delete(mangaId) } suspend fun getTrackingLog(offset: Int, limit: Int): List { @@ -65,7 +68,7 @@ class TrackingRepository( } } - suspend fun count() = db.trackLogsDao.count() + suspend fun getLogsCount() = db.trackLogsDao.count() suspend fun clearLogs() = db.trackLogsDao.clear() @@ -76,50 +79,85 @@ class TrackingRepository( } } - suspend fun storeTrackResult( - mangaId: Long, - knownChaptersCount: Int, // how many chapters user already seen - lastChapterId: Long, // in upstream manga - newChapters: List, - previousTrackChapterId: Long, // from previous check - saveTrackLog: Boolean, - ) { + suspend fun saveUpdates(updates: MangaUpdates) { db.withTransaction { - val entity = TrackEntity( - mangaId = mangaId, - newChapters = newChapters.size, - lastCheck = System.currentTimeMillis(), - lastChapterId = lastChapterId, - totalChapters = knownChaptersCount, - lastNotifiedChapterId = newChapters.lastOrNull()?.id ?: previousTrackChapterId - ) - db.tracksDao.upsert(entity) - if (saveTrackLog && previousTrackChapterId != 0L) { - val foundChapters = newChapters.takeLastWhile { x -> x.id != previousTrackChapterId } - if (foundChapters.isNotEmpty()) { - val logEntity = TrackLogEntity( - mangaId = mangaId, - chapters = foundChapters.joinToString("\n") { x -> x.name }, - createdAt = System.currentTimeMillis() - ) - db.trackLogsDao.insert(logEntity) - } + val track = getOrCreateTrack(updates.manga.id).mergeWith(updates) + db.tracksDao.upsert(track) + if (updates.isValid && updates.newChapters.isNotEmpty()) { + val logEntity = TrackLogEntity( + mangaId = updates.manga.id, + chapters = updates.newChapters.joinToString("\n") { x -> x.name }, + createdAt = System.currentTimeMillis(), + ) + db.trackLogsDao.insert(logEntity) } } } - suspend fun upsert(manga: Manga) { + suspend fun syncWithHistory(manga: Manga, chapterId: Long) { val chapters = manga.chapters ?: return + val chapterIndex = chapters.indexOfFirst { x -> x.id == chapterId } + val track = getOrCreateTrack(manga.id) + val lastNewChapterIndex = chapters.size - track.newChapters + val lastChapterId = chapters.lastOrNull()?.id ?: NO_ID val entity = TrackEntity( mangaId = manga.id, totalChapters = chapters.size, - lastChapterId = chapters.lastOrNull()?.id ?: 0L, - newChapters = 0, + lastChapterId = lastChapterId, + newChapters = when { + track.newChapters == 0 -> 0 + chapterIndex < 0 -> track.newChapters + chapterIndex > lastNewChapterIndex -> chapters.lastIndex - chapterIndex + else -> track.newChapters + }, lastCheck = System.currentTimeMillis(), - lastNotifiedChapterId = 0L + lastNotifiedChapterId = lastChapterId, ) db.tracksDao.upsert(entity) } + suspend fun getCategoriesCount(): IntArray { + val categories = db.favouriteCategoriesDao.findAll() + return intArrayOf( + categories.count { it.track }, + categories.size, + ) + } + + suspend fun getAllFavouritesManga(): Map> { + val categories = db.favouriteCategoriesDao.findAll() + return categories.associateTo(LinkedHashMap(categories.size)) { categoryEntity -> + categoryEntity.toFavouriteCategory() to + db.favouritesDao.findAllManga(categoryEntity.categoryId).toMangaList() + } + } + + suspend fun getAllHistoryManga(): List { + return db.historyDao.findAllManga().toMangaList() + } + + private suspend fun getOrCreateTrack(mangaId: Long): TrackEntity { + return db.tracksDao.find(mangaId) ?: TrackEntity( + mangaId = mangaId, + totalChapters = 0, + lastChapterId = 0L, + newChapters = 0, + lastCheck = 0L, + lastNotifiedChapterId = 0L, + ) + } + + private fun TrackEntity.mergeWith(updates: MangaUpdates): TrackEntity { + val chapters = updates.manga.chapters.orEmpty() + return TrackEntity( + mangaId = mangaId, + totalChapters = chapters.size, + lastChapterId = chapters.lastOrNull()?.id ?: NO_ID, + newChapters = if (updates.isValid) newChapters + updates.newChapters.size else 0, + lastCheck = System.currentTimeMillis(), + lastNotifiedChapterId = NO_ID, + ) + } + private fun Collection.toMangaList() = map { it.toManga(emptySet()) } } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/core/model/MangaTracking.kt b/app/src/main/java/org/koitharu/kotatsu/tracker/domain/model/MangaTracking.kt similarity index 63% rename from app/src/main/java/org/koitharu/kotatsu/core/model/MangaTracking.kt rename to app/src/main/java/org/koitharu/kotatsu/tracker/domain/model/MangaTracking.kt index 50814dff0..74c964ec8 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/model/MangaTracking.kt +++ b/app/src/main/java/org/koitharu/kotatsu/tracker/domain/model/MangaTracking.kt @@ -1,18 +1,16 @@ -package org.koitharu.kotatsu.core.model +package org.koitharu.kotatsu.tracker.domain.model import java.util.* import org.koitharu.kotatsu.parsers.model.Manga class MangaTracking( val manga: Manga, - val knownChaptersCount: Int, val lastChapterId: Long, - val lastNotifiedChapterId: Long, val lastCheck: Date?, ) { fun isEmpty(): Boolean { - return knownChaptersCount <= 0 || lastChapterId == 0L + return lastChapterId == 0L } override fun equals(other: Any?): Boolean { @@ -22,9 +20,7 @@ class MangaTracking( other as MangaTracking if (manga != other.manga) return false - if (knownChaptersCount != other.knownChaptersCount) return false if (lastChapterId != other.lastChapterId) return false - if (lastNotifiedChapterId != other.lastNotifiedChapterId) return false if (lastCheck != other.lastCheck) return false return true @@ -32,9 +28,7 @@ class MangaTracking( override fun hashCode(): Int { var result = manga.hashCode() - result = 31 * result + knownChaptersCount result = 31 * result + lastChapterId.hashCode() - result = 31 * result + lastNotifiedChapterId.hashCode() result = 31 * result + (lastCheck?.hashCode() ?: 0) return result } diff --git a/app/src/main/java/org/koitharu/kotatsu/tracker/domain/model/MangaUpdates.kt b/app/src/main/java/org/koitharu/kotatsu/tracker/domain/model/MangaUpdates.kt index 436849e02..3c17281d0 100644 --- a/app/src/main/java/org/koitharu/kotatsu/tracker/domain/model/MangaUpdates.kt +++ b/app/src/main/java/org/koitharu/kotatsu/tracker/domain/model/MangaUpdates.kt @@ -6,4 +6,5 @@ import org.koitharu.kotatsu.parsers.model.MangaChapter class MangaUpdates( val manga: Manga, val newChapters: List, + val isValid: Boolean, ) \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/core/model/TrackingLogItem.kt b/app/src/main/java/org/koitharu/kotatsu/tracker/domain/model/TrackingLogItem.kt similarity index 63% rename from app/src/main/java/org/koitharu/kotatsu/core/model/TrackingLogItem.kt rename to app/src/main/java/org/koitharu/kotatsu/tracker/domain/model/TrackingLogItem.kt index 23a922cb6..c5021eaf3 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/model/TrackingLogItem.kt +++ b/app/src/main/java/org/koitharu/kotatsu/tracker/domain/model/TrackingLogItem.kt @@ -1,9 +1,7 @@ -package org.koitharu.kotatsu.core.model +package org.koitharu.kotatsu.tracker.domain.model -import android.os.Parcelable -import kotlinx.parcelize.Parcelize -import org.koitharu.kotatsu.parsers.model.Manga import java.util.* +import org.koitharu.kotatsu.parsers.model.Manga data class TrackingLogItem( val id: Long, diff --git a/app/src/main/java/org/koitharu/kotatsu/tracker/ui/FeedViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/tracker/ui/FeedViewModel.kt index 0170ee01d..3cb415143 100644 --- a/app/src/main/java/org/koitharu/kotatsu/tracker/ui/FeedViewModel.kt +++ b/app/src/main/java/org/koitharu/kotatsu/tracker/ui/FeedViewModel.kt @@ -1,6 +1,8 @@ package org.koitharu.kotatsu.tracker.ui import androidx.lifecycle.viewModelScope +import java.util.* +import java.util.concurrent.TimeUnit import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.cancelAndJoin @@ -9,16 +11,14 @@ import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.filterNotNull import org.koitharu.kotatsu.R import org.koitharu.kotatsu.base.ui.BaseViewModel -import org.koitharu.kotatsu.core.model.TrackingLogItem import org.koitharu.kotatsu.core.ui.DateTimeAgo import org.koitharu.kotatsu.list.ui.model.* import org.koitharu.kotatsu.tracker.domain.TrackingRepository +import org.koitharu.kotatsu.tracker.domain.model.TrackingLogItem import org.koitharu.kotatsu.tracker.ui.model.toFeedItem import org.koitharu.kotatsu.utils.SingleLiveEvent import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct import org.koitharu.kotatsu.utils.ext.daysDiff -import java.util.* -import java.util.concurrent.TimeUnit class FeedViewModel( private val repository: TrackingRepository diff --git a/app/src/main/java/org/koitharu/kotatsu/tracker/ui/model/ListModelConversionExt.kt b/app/src/main/java/org/koitharu/kotatsu/tracker/ui/model/ListModelConversionExt.kt index fc4bc6080..b12c4ecff 100644 --- a/app/src/main/java/org/koitharu/kotatsu/tracker/ui/model/ListModelConversionExt.kt +++ b/app/src/main/java/org/koitharu/kotatsu/tracker/ui/model/ListModelConversionExt.kt @@ -1,6 +1,6 @@ package org.koitharu.kotatsu.tracker.ui.model -import org.koitharu.kotatsu.core.model.TrackingLogItem +import org.koitharu.kotatsu.tracker.domain.model.TrackingLogItem fun TrackingLogItem.toFeedItem() = FeedItem( id = id, diff --git a/app/src/main/java/org/koitharu/kotatsu/tracker/work/TrackWorker.kt b/app/src/main/java/org/koitharu/kotatsu/tracker/work/TrackWorker.kt index 9a659566e..6cd53b67c 100644 --- a/app/src/main/java/org/koitharu/kotatsu/tracker/work/TrackWorker.kt +++ b/app/src/main/java/org/koitharu/kotatsu/tracker/work/TrackWorker.kt @@ -25,7 +25,6 @@ import org.koitharu.kotatsu.details.ui.DetailsActivity import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.MangaChapter import org.koitharu.kotatsu.tracker.domain.Tracker -import org.koitharu.kotatsu.tracker.domain.TrackingRepository import org.koitharu.kotatsu.utils.PendingIntentCompat import org.koitharu.kotatsu.utils.ext.referer import org.koitharu.kotatsu.utils.ext.toBitmapOrNull @@ -41,10 +40,7 @@ class TrackWorker(context: Context, workerParams: WorkerParameters) : } private val coil by inject() - - private val repository by inject() private val settings by inject() - private val channels by inject() private val tracker by inject() override suspend fun doWork(): Result { @@ -54,7 +50,7 @@ class TrackWorker(context: Context, workerParams: WorkerParameters) : if (TAG in tags) { // not expedited trySetForeground() } - val tracks = getAllTracks() + val tracks = tracker.getAllTracks() var success = 0 val workData = Data.Builder().putInt(DATA_TOTAL, tracks.size) @@ -75,7 +71,7 @@ class TrackWorker(context: Context, workerParams: WorkerParameters) : ) } } - repository.gc() + tracker.gc() return if (success == 0) { Result.retry() } else { @@ -83,53 +79,6 @@ class TrackWorker(context: Context, workerParams: WorkerParameters) : } } - private suspend fun getAllTracks(): List { - val sources = settings.trackSources - if (sources.isEmpty()) { - return emptyList() - } - val knownIds = HashSet() - val result = ArrayList() - // Favourites - if (AppSettings.TRACK_FAVOURITES in sources) { - val favourites = repository.getFavouritesManga() - channels.updateChannels(favourites.keys) - for ((category, mangaList) in favourites) { - if (!category.isTrackingEnabled || mangaList.isEmpty()) { - continue - } - val categoryTracks = repository.getTracks(mangaList) - val channelId = if (channels.isFavouriteNotificationsEnabled(category)) { - channels.getFavouritesChannelId(category.id) - } else { - null - } - for (track in categoryTracks) { - if (knownIds.add(track.manga)) { - result.add(TrackingItem(track, channelId)) - } - } - } - } - // History - if (AppSettings.TRACK_HISTORY in sources) { - val history = repository.getHistoryManga() - val historyTracks = repository.getTracks(history) - val channelId = if (channels.isHistoryNotificationsEnabled()) { - channels.getHistoryChannelId() - } else { - null - } - for (track in historyTracks) { - if (knownIds.add(track.manga)) { - result.add(TrackingItem(track, channelId)) - } - } - } - result.trimToSize() - return result - } - private suspend fun showNotification(manga: Manga, channelId: String?, newChapters: List) { if (newChapters.isEmpty() || channelId == null) { return diff --git a/app/src/main/java/org/koitharu/kotatsu/tracker/work/TrackingItem.kt b/app/src/main/java/org/koitharu/kotatsu/tracker/work/TrackingItem.kt index 933918009..1b945618a 100644 --- a/app/src/main/java/org/koitharu/kotatsu/tracker/work/TrackingItem.kt +++ b/app/src/main/java/org/koitharu/kotatsu/tracker/work/TrackingItem.kt @@ -1,6 +1,6 @@ package org.koitharu.kotatsu.tracker.work -import org.koitharu.kotatsu.core.model.MangaTracking +import org.koitharu.kotatsu.tracker.domain.model.MangaTracking class TrackingItem( val tracking: MangaTracking, diff --git a/app/src/test/java/org/koitharu/kotatsu/utils/CoroutineTestRule.kt b/app/src/test/java/org/koitharu/kotatsu/utils/CoroutineTestRule.kt deleted file mode 100644 index 1f9354a72..000000000 --- a/app/src/test/java/org/koitharu/kotatsu/utils/CoroutineTestRule.kt +++ /dev/null @@ -1,32 +0,0 @@ -package org.koitharu.kotatsu.utils - -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.runBlocking -import kotlinx.coroutines.test.StandardTestDispatcher -import kotlinx.coroutines.test.TestDispatcher -import kotlinx.coroutines.test.resetMain -import kotlinx.coroutines.test.setMain -import org.junit.rules.TestWatcher -import org.junit.runner.Description - -class CoroutineTestRule( - private val testDispatcher: TestDispatcher = StandardTestDispatcher(), -) : TestWatcher() { - - override fun starting(description: Description) { - super.starting(description) - Dispatchers.setMain(testDispatcher) - } - - override fun finished(description: Description) { - super.finished(description) - Dispatchers.resetMain() - } - - fun runBlockingTest(block: suspend CoroutineScope.() -> Unit) { - runBlocking(testDispatcher) { - block() - } - } -} \ No newline at end of file