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