diff --git a/app/dependencies.gradle.kts b/app/dependencies.gradle.kts index 1f3eedb5..cacb3f06 100644 --- a/app/dependencies.gradle.kts +++ b/app/dependencies.gradle.kts @@ -98,6 +98,5 @@ dependencies { androidTestImplementation("androidx.test.espresso:espresso-contrib:3.3.0") androidTestImplementation("androidx.test:runner:1.3.0") androidTestImplementation("androidx.test:rules:1.3.0") - androidTestImplementation("io.mockk:mockk-android:1.11.0") testImplementation("junit:junit:4.13.2") } diff --git a/app/src/androidTest/java/de/xikolo/testing/instrumented/unit/FileDownloadHandlerTest.kt b/app/src/androidTest/java/de/xikolo/testing/instrumented/unit/FileDownloadHandlerTest.kt deleted file mode 100644 index a5bbe285..00000000 --- a/app/src/androidTest/java/de/xikolo/testing/instrumented/unit/FileDownloadHandlerTest.kt +++ /dev/null @@ -1,39 +0,0 @@ -package de.xikolo.testing.instrumented.unit - -import de.xikolo.download.DownloadCategory -import de.xikolo.download.filedownload.FileDownloadHandler -import de.xikolo.download.filedownload.FileDownloadIdentifier -import de.xikolo.download.filedownload.FileDownloadRequest -import de.xikolo.utils.extensions.preferredStorage -import io.mockk.every -import io.mockk.spyk - -class FileDownloadHandlerTest : DownloadHandlerTest() { - - override var downloadHandler = spyk(FileDownloadHandler, recordPrivateCalls = true) { - every { this@spyk getProperty "context" } answers { context } - } - - override var successfulTestRequest = FileDownloadRequest( - "https://file-examples-com.github.io/uploads/2017/04/file_example_MP4_1280_10MG.mp4", - createTempFile(directory = context.preferredStorage.file), - "File 1", - true, - DownloadCategory.Other - ) - override var successfulTestRequest2 = FileDownloadRequest( - "https://file-examples-com.github.io/uploads/2017/10/file-example_PDF_1MB.pdf", - createTempFile(directory = context.preferredStorage.file), - "File 2", - true, - DownloadCategory.Other - ) - override var failingTestRequest = FileDownloadRequest( - "https://www.example.com/notfoundfilehwqnqkdrzn42r.mp4", - createTempFile(directory = context.preferredStorage.file), - "Failing File", - true, - DownloadCategory.Other - ) -} diff --git a/app/src/androidTest/java/de/xikolo/testing/instrumented/unit/HlsVideoDownloadItemTest.kt b/app/src/androidTest/java/de/xikolo/testing/instrumented/unit/HlsVideoDownloadItemTest.kt deleted file mode 100644 index 7e338cae..00000000 --- a/app/src/androidTest/java/de/xikolo/testing/instrumented/unit/HlsVideoDownloadItemTest.kt +++ /dev/null @@ -1,33 +0,0 @@ -package de.xikolo.testing.instrumented.unit - -import com.google.android.exoplayer2.source.MediaSource -import de.xikolo.controllers.helper.VideoSettingsHelper -import de.xikolo.download.DownloadCategory -import de.xikolo.download.hlsvideodownload.HlsVideoDownloadIdentifier -import de.xikolo.download.hlsvideodownload.HlsVideoDownloadItem -import de.xikolo.utils.extensions.preferredStorage -import io.mockk.every - -class HlsVideoDownloadItemTest : DownloadItemTest() { - - private val storage = context.preferredStorage - - override val testDownloadItem = HlsVideoDownloadItem( - "https://open.hpi.de/playlists/93a84211-e40a-416a-b224-4d3ecdbb12f9.m3u8?embed_subtitles_for_video=d7e056da-756f-4437-b64a-16970a33d5ef", - DownloadCategory.Other, - VideoSettingsHelper.VideoQuality.HIGH.qualityFraction, - storage - ) - override val testDownloadItemNotDownloadable = HlsVideoDownloadItem( - null, - DownloadCategory.Other, - 0.0f, - storage - ) - - init { - every { testDownloadItem.downloader } returns HSLS().downloadHandler - every { testDownloadItemNotDownloadable.downloader } returns HSLS().downloadHandler - } -} diff --git a/app/src/androidTest/java/de/xikolo/testing/instrumented/unit/BaseDownloadTest.kt b/app/src/androidTest/java/de/xikolo/testing/instrumented/unit/download/BaseDownloadTest.kt similarity index 69% rename from app/src/androidTest/java/de/xikolo/testing/instrumented/unit/BaseDownloadTest.kt rename to app/src/androidTest/java/de/xikolo/testing/instrumented/unit/download/BaseDownloadTest.kt index 13d5ffa4..f821af4d 100644 --- a/app/src/androidTest/java/de/xikolo/testing/instrumented/unit/BaseDownloadTest.kt +++ b/app/src/androidTest/java/de/xikolo/testing/instrumented/unit/download/BaseDownloadTest.kt @@ -1,4 +1,4 @@ -package de.xikolo.testing.instrumented.unit +package de.xikolo.testing.instrumented.unit.download import android.Manifest import androidx.test.rule.ActivityTestRule @@ -7,6 +7,8 @@ import de.xikolo.controllers.downloads.DownloadsActivity import de.xikolo.testing.instrumented.mocking.base.BaseTest import org.junit.Rule +// Parallel test execution needs to be disabled for all downloading tests, because it can occur +// that @Before deleteAllDownloads is called while another test is still being executed. abstract class BaseDownloadTest : BaseTest() { @Rule diff --git a/app/src/androidTest/java/de/xikolo/testing/instrumented/unit/DownloadHandlerTest.kt b/app/src/androidTest/java/de/xikolo/testing/instrumented/unit/download/DownloadHandlerTest.kt similarity index 86% rename from app/src/androidTest/java/de/xikolo/testing/instrumented/unit/DownloadHandlerTest.kt rename to app/src/androidTest/java/de/xikolo/testing/instrumented/unit/download/DownloadHandlerTest.kt index e2488ea1..4a28f8d5 100644 --- a/app/src/androidTest/java/de/xikolo/testing/instrumented/unit/DownloadHandlerTest.kt +++ b/app/src/androidTest/java/de/xikolo/testing/instrumented/unit/download/DownloadHandlerTest.kt @@ -1,11 +1,10 @@ -package de.xikolo.testing.instrumented.unit +package de.xikolo.testing.instrumented.unit.download -import de.xikolo.download.DownloadCategory import de.xikolo.download.DownloadHandler import de.xikolo.download.DownloadIdentifier import de.xikolo.download.DownloadRequest import de.xikolo.download.DownloadStatus -import de.xikolo.utils.extensions.preferredStorage +import de.xikolo.models.Storage import org.junit.Assert.assertEquals import org.junit.Assert.assertNotNull import org.junit.Assert.assertTrue @@ -16,11 +15,12 @@ import org.junit.Test abstract class DownloadHandlerTest, I : DownloadIdentifier, R : DownloadRequest> : BaseDownloadTest() { - abstract var downloadHandler: T + abstract val downloadHandler: T - abstract var successfulTestRequest: R - abstract var successfulTestRequest2: R - abstract var failingTestRequest: R + abstract val successfulTestRequest: R + abstract val successfulTestRequest2: R + abstract val failingTestRequest: R + abstract val storage: Storage @Before fun deleteAllDownloads() { @@ -56,7 +56,7 @@ abstract class DownloadHandlerTest, downloadHandler.listen(identifier) { listenerCalled = true } - assertTrue(listenerCalled) + waitWhile({ !listenerCalled }, 3000) // reset `listenerCalled` to false listenerCalled = false @@ -95,7 +95,7 @@ abstract class DownloadHandlerTest, status?.state?.equals(DownloadStatus.State.DELETED) != false || (status?.state?.equals(DownloadStatus.State.PENDING) != true && status?.state?.equals(DownloadStatus.State.RUNNING) != true) - }, 30000) + }) // test status after start assertNotNull(status!!.totalBytes) @@ -111,7 +111,7 @@ abstract class DownloadHandlerTest, isDownloadingAnythingCallbackCalled = it } // assert that isDownloadingAnything returns true - waitWhile({ !isDownloadingAnythingCallbackCalled }, 1000) + waitWhile({ !isDownloadingAnythingCallbackCalled }, 3000) // wait for download to finish waitWhile({ status?.state?.equals(DownloadStatus.State.DOWNLOADED) != true }) @@ -151,7 +151,7 @@ abstract class DownloadHandlerTest, // assert that the delete callback has been called waitWhile({ !deleteCallbackCalled }, 3000) - waitWhile({ status!!.state != DownloadStatus.State.DELETED }, 3000) + waitWhile({ status?.state?.equals(DownloadStatus.State.DELETED) != true }, 3000) } @Test @@ -170,28 +170,32 @@ abstract class DownloadHandlerTest, @Test fun testGettingDownloads() { - var result: Map>? = null - downloadHandler.getDownloads(context.preferredStorage) { - result = it + var count: Int? = null + downloadHandler.getDownloads(storage) { + count = it.size } // wait for and check result - waitWhile({ result?.size?.equals(0) != true }, 1000) + waitWhile({ count == null }, 1000) - var status: DownloadStatus? = null + var downloaded = false downloadHandler.listen(downloadHandler.identify(successfulTestRequest)) { - status = it + if(it.state == DownloadStatus.State.DOWNLOADED){ + downloaded = true + } } // start download downloadHandler.download(successfulTestRequest) // wait for download to finish - waitWhile({ status?.state?.equals(DownloadStatus.State.DOWNLOADED) != true }) + waitWhile({ !downloaded }) - result = null - downloadHandler.getDownloads(context.preferredStorage) { - result = it + var nextCount: Int? = null + downloadHandler.getDownloads(storage) { + nextCount = it.size } // wait for result - waitWhile({ result?.size?.equals(1) != true }, 1000) + waitWhile({ nextCount == null }, 1000) + + assertEquals(count!! + 1, nextCount); } @Test @@ -205,10 +209,10 @@ abstract class DownloadHandlerTest, result2 = it } // wait for `result` and `result2` to become true - waitWhile({ !result || !result2 }, 30000) + waitWhile({ !result || !result2 }) } - protected fun waitWhile(condition: () -> Boolean, timeout: Long = 300000) { + protected fun waitWhile(condition: () -> Boolean, timeout: Long = 60000) { var waited = 0 while (condition()) { Thread.sleep(100) diff --git a/app/src/androidTest/java/de/xikolo/testing/instrumented/unit/DownloadItemTest.kt b/app/src/androidTest/java/de/xikolo/testing/instrumented/unit/download/DownloadItemTest.kt similarity index 54% rename from app/src/androidTest/java/de/xikolo/testing/instrumented/unit/DownloadItemTest.kt rename to app/src/androidTest/java/de/xikolo/testing/instrumented/unit/download/DownloadItemTest.kt index 82de269d..274f3943 100644 --- a/app/src/androidTest/java/de/xikolo/testing/instrumented/unit/DownloadItemTest.kt +++ b/app/src/androidTest/java/de/xikolo/testing/instrumented/unit/download/DownloadItemTest.kt @@ -1,4 +1,4 @@ -package de.xikolo.testing.instrumented.unit +package de.xikolo.testing.instrumented.unit.download import de.xikolo.download.DownloadIdentifier import de.xikolo.download.DownloadItem @@ -8,6 +8,7 @@ import org.junit.Assert.assertFalse import org.junit.Assert.assertNotNull import org.junit.Assert.assertNull import org.junit.Assert.assertTrue +import org.junit.Assert.fail import org.junit.Before import org.junit.Test @@ -17,34 +18,34 @@ abstract class DownloadItemTest, abstract val testDownloadItem: T abstract val testDownloadItemNotDownloadable: T - protected fun onUi(action: () -> Unit) { - activityTestRule.activity.runOnUiThread(action) - } - - protected fun deleteItem(item: DownloadItem) { - var deleted = false - var deleteCallbackCalled = false - onUi { - item.status.observe(activityTestRule.activity) { - if (it.state == DownloadStatus.State.DELETED) { - deleted = true + @Before + fun deleteAllItems() { + fun deleteItem(item: DownloadItem) { + var deleted = false + onUiThread { + item.status.observe(activityTestRule.activity) { + if (it.state == DownloadStatus.State.DELETED) { + deleted = true + } } + item.delete(activityTestRule.activity) } - item.delete(activityTestRule.activity) { - deleteCallbackCalled = true - } + + waitWhile({ !deleted }, 10000) } - waitWhile({ !deleted || !deleteCallbackCalled }, 3000) - } - @Before - fun deleteAllItems() { - //deleteItem(testDownloadItem) + deleteItem(testDownloadItem) } @Test fun testIdentifier() { testDownloadItem.identifier + try { + testDownloadItemNotDownloadable.identifier + fail("Statement should fail") + } catch (e: Exception) { + // expected behavior + } } @Test @@ -79,34 +80,54 @@ abstract class DownloadItemTest, @Test fun testStatus() { - testDownloadItem.status - testDownloadItemNotDownloadable.status + var called = false + onUiThread { + testDownloadItem.status + called = true + } + + waitWhile({ !called }, 1000) + + try { + testDownloadItemNotDownloadable.status + fail("Statement should fail") + } catch (e: Exception) { + // expected behavior + } } @Test fun testStatusBefore() { - onUi { + var called = false + onUiThread { testDownloadItem.status.observe(activityTestRule.activity) { assertNotNull(it) assertNull(testDownloadItem.download) + called = true } } + + waitWhile({ !called }, 1000) } @Test fun testDownloadAndDelete() { var downloaded = false - testDownloadItem.status.observe(activityTestRule.activity) { - if (it.state == DownloadStatus.State.DOWNLOADED) { - downloaded = true + onUiThread { + testDownloadItem.status.observe(activityTestRule.activity) { + if (it.state == DownloadStatus.State.DOWNLOADED) { + downloaded = true + } } } assertNull(testDownloadItem.download) var startResult = false - testDownloadItem.start(activityTestRule.activity) { - startResult = it + onUiThread { + testDownloadItem.start(activityTestRule.activity) { + startResult = it + } } waitWhile({ !startResult }, 3000) @@ -114,15 +135,19 @@ abstract class DownloadItemTest, assertNotNull(testDownloadItem.download) var deleted = false - testDownloadItem.status.observe(activityTestRule.activity) { - if (it.state == DownloadStatus.State.DELETED) { - deleted = true + onUiThread { + testDownloadItem.status.observe(activityTestRule.activity) { + if (it.state == DownloadStatus.State.DELETED) { + deleted = true + } } } var deleteResult = false - testDownloadItem.delete(activityTestRule.activity) { - deleteResult = it + onUiThread { + testDownloadItem.delete(activityTestRule.activity) { + deleteResult = it + } } waitWhile({ !deleteResult }, 3000) @@ -130,7 +155,19 @@ abstract class DownloadItemTest, assertNull(testDownloadItem.download) } - protected fun waitWhile(condition: () -> Boolean, timeout: Long = 300000) { + @Test + fun testDownloadStartForNotDownloadable() { + var startResult = true + onUiThread { + testDownloadItemNotDownloadable.start(activityTestRule.activity) { + startResult = it + } + } + + waitWhile({ startResult }, 3000) + } + + protected fun waitWhile(condition: () -> Boolean, timeout: Long = 60000) { var waited = 0 while (condition()) { Thread.sleep(100) @@ -140,4 +177,8 @@ abstract class DownloadItemTest, } } } + + protected fun onUiThread(block: Runnable) { + activityTestRule.activity.runOnUiThread(block) + } } diff --git a/app/src/androidTest/java/de/xikolo/testing/instrumented/unit/download/filedownload/AbstractFileDownloadHandlerTest.kt b/app/src/androidTest/java/de/xikolo/testing/instrumented/unit/download/filedownload/AbstractFileDownloadHandlerTest.kt new file mode 100644 index 00000000..8fa5dc57 --- /dev/null +++ b/app/src/androidTest/java/de/xikolo/testing/instrumented/unit/download/filedownload/AbstractFileDownloadHandlerTest.kt @@ -0,0 +1,56 @@ +package de.xikolo.testing.instrumented.unit.download.filedownload + +import de.xikolo.download.DownloadCategory +import de.xikolo.download.DownloadStatus +import de.xikolo.download.filedownload.FileDownloadHandler +import de.xikolo.download.filedownload.FileDownloadIdentifier +import de.xikolo.download.filedownload.FileDownloadRequest +import de.xikolo.testing.instrumented.unit.download.DownloadHandlerTest +import org.junit.Assert.assertTrue +import org.junit.Test +import java.io.File + +abstract class AbstractFileDownloadHandlerTest : DownloadHandlerTest() { + + override val downloadHandler = FileDownloadHandler + + override val successfulTestRequest: FileDownloadRequest + get() = FileDownloadRequest( + "https://file-examples-com.github.io/uploads/2017/04/file_example_MP4_1280_10MG.mp4", + File(storage.file, "file1"), + "File 1", + true, + DownloadCategory.Other + ) + override val successfulTestRequest2 + get() = FileDownloadRequest( + "https://file-examples-com.github.io/uploads/2017/10/file-example_PDF_1MB.pdf", + File(storage.file, "file2"), + "File 2", + true, + DownloadCategory.Other + ) + override val failingTestRequest + get() = FileDownloadRequest( + "https://www.example.com/notfoundfilehwqnqkdrzn42r.mp4", + File(storage.file, "failingfile"), + "Failing File", + true, + DownloadCategory.Other + ) + + @Test + fun testSizeAfterDownload() { + var status: DownloadStatus? = null + downloadHandler.listen(downloadHandler.identify(successfulTestRequest)) { + status = it + } + // start download + downloadHandler.download(successfulTestRequest) + // wait for download to finish + waitWhile({ status?.state?.equals(DownloadStatus.State.DOWNLOADED) != true }) + + assertTrue(successfulTestRequest.localFile.length() > 0) + } +} diff --git a/app/src/androidTest/java/de/xikolo/testing/instrumented/unit/FileDownloadItemTest.kt b/app/src/androidTest/java/de/xikolo/testing/instrumented/unit/download/filedownload/AbstractFileDownloadItemTest.kt similarity index 71% rename from app/src/androidTest/java/de/xikolo/testing/instrumented/unit/FileDownloadItemTest.kt rename to app/src/androidTest/java/de/xikolo/testing/instrumented/unit/download/filedownload/AbstractFileDownloadItemTest.kt index 06f1f04f..58f429ae 100644 --- a/app/src/androidTest/java/de/xikolo/testing/instrumented/unit/FileDownloadItemTest.kt +++ b/app/src/androidTest/java/de/xikolo/testing/instrumented/unit/download/filedownload/AbstractFileDownloadItemTest.kt @@ -1,49 +1,38 @@ -package de.xikolo.testing.instrumented.unit +package de.xikolo.testing.instrumented.unit.download.filedownload -import androidx.test.annotation.UiThreadTest import de.xikolo.download.DownloadCategory import de.xikolo.download.DownloadStatus -import de.xikolo.download.filedownload.FileDownloadHandler import de.xikolo.download.filedownload.FileDownloadIdentifier import de.xikolo.download.filedownload.FileDownloadItem -import de.xikolo.utils.extensions.preferredStorage -import io.mockk.every -import io.mockk.spyk +import de.xikolo.models.Storage +import de.xikolo.testing.instrumented.unit.download.DownloadItemTest import org.junit.Assert.assertFalse import org.junit.Assert.assertTrue import org.junit.Test import java.io.File -class FileDownloadItemTest : DownloadItemTest() { - private val storage = context.preferredStorage + abstract val storage: Storage - override val testDownloadItem = spyk( - FileDownloadItem( + override val testDownloadItem + get() = FileDownloadItem( "https://file-examples-com.github.io/uploads/2017/04/file_example_MP4_1280_10MG.mp4", DownloadCategory.Other, "video.mp4", storage ) - ) - override val testDownloadItemNotDownloadable = spyk( - FileDownloadItem( + + override val testDownloadItemNotDownloadable + get() = FileDownloadItem( null, DownloadCategory.Other, - "null.null" + "null.null", + storage ) - ) - - init { - every { testDownloadItem.downloader } returns spyk(FileDownloadHandler, recordPrivateCalls = true) { - every { this@spyk getProperty "context" } answers { context } - } - every { testDownloadItemNotDownloadable.downloader } returns FileDownloadHandlerTest().downloadHandler - } @Test - @UiThreadTest fun testFileName() { testDownloadItem.fileName testDownloadItemNotDownloadable.fileName @@ -53,13 +42,12 @@ class FileDownloadItemTest : DownloadItemTest() { - override var downloadHandler = spyk(HlsVideoDownloadHandler, recordPrivateCalls = true) { - every { this@spyk getProperty "context" } answers { context } - } + override val downloadHandler = HlsVideoDownloadHandler - override var successfulTestRequest = HlsVideoDownloadRequest( - "https://open.hpi.de/playlists/93a84211-e40a-416a-b224-4d3ecdbb12f9.m3u8?embed_subtitles_for_video=d7e056da-756f-4437-b64a-16970a33d5ef", - VideoSettingsHelper.VideoQuality.LOW.qualityFraction, - context.preferredStorage, - "Video 1", - true, - DownloadCategory.Other - ) - override var successfulTestRequest2 = HlsVideoDownloadRequest( + override val successfulTestRequest + get() = HlsVideoDownloadRequest( + "https://open.hpi.de/playlists/93a84211-e40a-416a-b224-4d3ecdbb12f9.m3u8?embed_subtitles_for_video=d7e056da-756f-4437-b64a-16970a33d5ef", + VideoSettingsHelper.VideoQuality.LOW.qualityFraction, + context.preferredStorage, + "Video 1", + true, + DownloadCategory.Other + ) + override val successfulTestRequest2 = HlsVideoDownloadRequest( "https://open.hpi.de/playlists/04012fde-be48-47b6-a742-0edc69a9c2a9.m3u8?embed_subtitles_for_video=d7e056da-756f-4437-b64a-16970a33d5ef", VideoSettingsHelper.VideoQuality.BEST.qualityFraction, context.preferredStorage, @@ -35,7 +33,7 @@ class HSLS : DownloadHandlerTest() { + + abstract val storage: Storage + + override val testDownloadItem + get() = HlsVideoDownloadItem( + "https://open.hpi.de/playlists/93a84211-e40a-416a-b224-4d3ecdbb12f9.m3u8?embed_subtitles_for_video=d7e056da-756f-4437-b64a-16970a33d5ef", + DownloadCategory.Other, + VideoSettingsHelper.VideoQuality.HIGH.qualityFraction, + storage + ) + + override val testDownloadItemNotDownloadable + get() = HlsVideoDownloadItem( + null, + DownloadCategory.Other, + 0.0f, + storage + ) +} diff --git a/app/src/androidTest/java/de/xikolo/testing/instrumented/unit/download/hlsvideodownload/ExternalStorageHlsVideoDownloadHandlerTest.kt b/app/src/androidTest/java/de/xikolo/testing/instrumented/unit/download/hlsvideodownload/ExternalStorageHlsVideoDownloadHandlerTest.kt new file mode 100644 index 00000000..d7555b1a --- /dev/null +++ b/app/src/androidTest/java/de/xikolo/testing/instrumented/unit/download/hlsvideodownload/ExternalStorageHlsVideoDownloadHandlerTest.kt @@ -0,0 +1,8 @@ +package de.xikolo.testing.instrumented.unit.download.hlsvideodownload + +import de.xikolo.utils.extensions.sdcardStorage + +class ExternalStorageHlsVideoDownloadHandlerTest : AbstractHlsVideoDownloadHandlerTest() { + + override val storage = context.sdcardStorage!! +} diff --git a/app/src/androidTest/java/de/xikolo/testing/instrumented/unit/download/hlsvideodownload/ExternalStorageHlsVideoDownloadItemTest.kt b/app/src/androidTest/java/de/xikolo/testing/instrumented/unit/download/hlsvideodownload/ExternalStorageHlsVideoDownloadItemTest.kt new file mode 100644 index 00000000..0446f81d --- /dev/null +++ b/app/src/androidTest/java/de/xikolo/testing/instrumented/unit/download/hlsvideodownload/ExternalStorageHlsVideoDownloadItemTest.kt @@ -0,0 +1,8 @@ +package de.xikolo.testing.instrumented.unit.download.hlsvideodownload + +import de.xikolo.utils.extensions.sdcardStorage + +class ExternalStorageHlsVideoDownloadItemTest : AbstractHlsVideoDownloadItemTest() { + + override val storage = context.sdcardStorage!! +} diff --git a/app/src/androidTest/java/de/xikolo/testing/instrumented/unit/download/hlsvideodownload/InternalStorageHlsVideoDownloadHandlerTest.kt b/app/src/androidTest/java/de/xikolo/testing/instrumented/unit/download/hlsvideodownload/InternalStorageHlsVideoDownloadHandlerTest.kt new file mode 100644 index 00000000..20499cd2 --- /dev/null +++ b/app/src/androidTest/java/de/xikolo/testing/instrumented/unit/download/hlsvideodownload/InternalStorageHlsVideoDownloadHandlerTest.kt @@ -0,0 +1,8 @@ +package de.xikolo.testing.instrumented.unit.download.hlsvideodownload + +import de.xikolo.utils.extensions.internalStorage + +class InternalStorageHlsVideoDownloadHandlerTest : AbstractHlsVideoDownloadHandlerTest() { + + override val storage = context.internalStorage +} diff --git a/app/src/androidTest/java/de/xikolo/testing/instrumented/unit/download/hlsvideodownload/InternalStorageHlsVideoDownloadItemTest.kt b/app/src/androidTest/java/de/xikolo/testing/instrumented/unit/download/hlsvideodownload/InternalStorageHlsVideoDownloadItemTest.kt new file mode 100644 index 00000000..daacebc3 --- /dev/null +++ b/app/src/androidTest/java/de/xikolo/testing/instrumented/unit/download/hlsvideodownload/InternalStorageHlsVideoDownloadItemTest.kt @@ -0,0 +1,8 @@ +package de.xikolo.testing.instrumented.unit.download.hlsvideodownload + +import de.xikolo.utils.extensions.internalStorage + +class InternalStorageHlsVideoDownloadItemTest : AbstractHlsVideoDownloadItemTest() { + + override val storage = context.internalStorage +} diff --git a/app/src/main/java/de/xikolo/download/DownloadHandler.kt b/app/src/main/java/de/xikolo/download/DownloadHandler.kt index 035b027c..c8f22e69 100644 --- a/app/src/main/java/de/xikolo/download/DownloadHandler.kt +++ b/app/src/main/java/de/xikolo/download/DownloadHandler.kt @@ -56,7 +56,7 @@ interface DownloadHandler { /** * Registers a listener for a download that notifies when the download status changes. * Overrides the previous listener. - * The listener is always invoked immediately after registering it. + * The listener is set immediately, but the callback might get called with a delay. * * @param identifier The identifier of the download. * @param listener An asynchronous callback that is invoked regularly with the most recent diff --git a/app/src/main/java/de/xikolo/download/DownloadItem.kt b/app/src/main/java/de/xikolo/download/DownloadItem.kt index 05eb0ad6..ae026fcb 100644 --- a/app/src/main/java/de/xikolo/download/DownloadItem.kt +++ b/app/src/main/java/de/xikolo/download/DownloadItem.kt @@ -40,6 +40,7 @@ interface DownloadItem { /** * Total size of the download. + * Returns 0 per default if the download size cannot be determined. */ val size: Long diff --git a/app/src/main/java/de/xikolo/download/DownloadItemImpl.kt b/app/src/main/java/de/xikolo/download/DownloadItemImpl.kt index 87ff239a..2b11915e 100644 --- a/app/src/main/java/de/xikolo/download/DownloadItemImpl.kt +++ b/app/src/main/java/de/xikolo/download/DownloadItemImpl.kt @@ -106,9 +106,9 @@ abstract class DownloadItemImpl Unit)?> = mutableMapOf() override fun isDownloadingAnything(callback: (Boolean) -> Unit) { - disabledNotificationsManager.hasActiveDownloads(true) { a -> - enabledNotificationsManager.hasActiveDownloads(true) { b -> - callback(a || b) + val mainHandler = Handler(Looper.getMainLooper()) + + handler.post { + disabledNotificationsManager.hasActiveDownloads(true) { a -> + enabledNotificationsManager.hasActiveDownloads(true) { b -> + mainHandler.post { + callback(a || b) + } + } } } } @@ -232,6 +248,8 @@ object FileDownloadHandler : DownloadHandler Unit)?) { + val mainHandler = Handler(Looper.getMainLooper()) + val req = Request(request.url, request.localFile.toUri()).apply { networkType = if (ApplicationPreferences().isDownloadNetworkLimitedOnMobile) { @@ -272,41 +290,57 @@ object FileDownloadHandler : DownloadHandler Unit)?) { - enabledNotificationsManager.getDownload(identifier.get()) { d1 -> - if (d1 != null) { - enabledNotificationsManager.cancel(d1.id) - enabledNotificationsManager.delete(d1.id) - enabledNotificationsManager.remove(d1.id) - callback?.invoke(true) + handler.post { + if (req.extras.getBoolean(REQUEST_EXTRA_SHOW_NOTIFICATION, true)) { + enabledNotificationsManager } else { - disabledNotificationsManager.getDownload(identifier.get()) { d2 -> - if (d2 != null) { - disabledNotificationsManager.cancel(d2.id) - disabledNotificationsManager.delete(d2.id) - disabledNotificationsManager.remove(d2.id) + disabledNotificationsManager + }.enqueue( + req, + { + mainHandler.post { callback?.invoke(true) - } else { + } + }, + { + mainHandler.post { callback?.invoke(false) } } + ) + } + } + + override fun delete(identifier: FileDownloadIdentifier, callback: ((Boolean) -> Unit)?) { + val mainHandler = Handler(Looper.getMainLooper()) + + handler.post { + enabledNotificationsManager.getDownload(identifier.get()) { d1 -> + if (d1 != null) { + enabledNotificationsManager.cancel(d1.id) + enabledNotificationsManager.delete(d1.id) + enabledNotificationsManager.remove(d1.id) + mainHandler.post { + callback?.invoke(true) + } + } else { + disabledNotificationsManager.getDownload(identifier.get()) { d2 -> + if (d2 != null) { + disabledNotificationsManager.cancel(d2.id) + disabledNotificationsManager.delete(d2.id) + disabledNotificationsManager.remove(d2.id) + mainHandler.post { + callback?.invoke(true) + } + } else { + mainHandler.post { + callback?.invoke(false) + } + } + } + } } } } @@ -315,58 +349,51 @@ object FileDownloadHandler : DownloadHandler Unit)? ) { + val mainHandler = Handler(Looper.getMainLooper()) + listeners[identifier.get()] = listener - var called = false - disabledNotificationsManager.getDownload(identifier.get()) { a -> - enabledNotificationsManager.getDownload(identifier.get()) { b -> - listener?.invoke( - getDownloadStatus(a ?: b) - ) - called = true + handler.post { + disabledNotificationsManager.getDownload(identifier.get()) { a -> + enabledNotificationsManager.getDownload(identifier.get()) { b -> + mainHandler.post { + listener?.invoke( + getDownloadStatus(a ?: b) + ) + } + } } } - while (!called) { - Thread.sleep(100) - } } override fun getDownloads( storage: Storage, callback: (Map>) -> Unit ) { - disabledNotificationsManager.getDownloads { a -> - enabledNotificationsManager.getDownloads { b -> - callback.invoke( - a - .filter { it.file.contains(storage.file.absolutePath) } - .associate { - Pair( - FileDownloadIdentifier(it.id), - getDownloadStatus(it) to Gson().fromJson( - it.extras.getString( - REQUEST_EXTRA_CATEGORY, - "" - ), - DownloadCategory::class.java - ) - ) - } + - b - .filter { it.file.contains(storage.file.absolutePath) } - .associate { - Pair( - FileDownloadIdentifier(it.id), - getDownloadStatus(it) to Gson().fromJson( - it.extras.getString( - REQUEST_EXTRA_CATEGORY, - "" - ), - DownloadCategory::class.java + val mainHandler = Handler(Looper.getMainLooper()) + + handler.post { + disabledNotificationsManager.getDownloadsWithStatus(Status.COMPLETED) { a -> + enabledNotificationsManager.getDownloadsWithStatus(Status.COMPLETED) { b -> + mainHandler.post { + callback.invoke( + (a + b) + .filter { it.file.contains(storage.file.absolutePath) } + .associate { + Pair( + FileDownloadIdentifier(it.id), + getDownloadStatus(it) to Gson().fromJson( + it.extras.getString( + REQUEST_EXTRA_CATEGORY, + "" + ), + DownloadCategory::class.java + ) ) - ) - } - ) + } + ) + } + } } } } diff --git a/app/src/main/java/de/xikolo/download/filedownload/FileDownloadItem.kt b/app/src/main/java/de/xikolo/download/filedownload/FileDownloadItem.kt index 27cafed3..204d10c8 100644 --- a/app/src/main/java/de/xikolo/download/filedownload/FileDownloadItem.kt +++ b/app/src/main/java/de/xikolo/download/filedownload/FileDownloadItem.kt @@ -6,7 +6,6 @@ import de.xikolo.download.DownloadCategory import de.xikolo.download.DownloadItemImpl import de.xikolo.download.DownloadStatus import de.xikolo.models.Storage -import de.xikolo.utils.extensions.fileSize import de.xikolo.utils.extensions.internalStorage import de.xikolo.utils.extensions.open import de.xikolo.utils.extensions.preferredStorage @@ -37,7 +36,7 @@ open class FileDownloadItem( get() = fileName override val size: Long - get() = download?.fileSize ?: 0L + get() = download?.length() ?: 0L final override val download: File? get() { @@ -78,7 +77,6 @@ open class FileDownloadItem( ) override fun onStatusChanged(newStatus: DownloadStatus) { - super.onStatusChanged(newStatus) when (newStatus.state) { DownloadStatus.State.DOWNLOADED -> { File("$filePath.tmp").renameTo(File(filePath)) @@ -87,6 +85,7 @@ open class FileDownloadItem( File(filePath).delete() } } + super.onStatusChanged(newStatus) } /** diff --git a/app/src/main/java/de/xikolo/download/hlsvideodownload/HlsVideoDownloadHandler.kt b/app/src/main/java/de/xikolo/download/hlsvideodownload/HlsVideoDownloadHandler.kt index 2f234c39..1a1c5dfe 100644 --- a/app/src/main/java/de/xikolo/download/hlsvideodownload/HlsVideoDownloadHandler.kt +++ b/app/src/main/java/de/xikolo/download/hlsvideodownload/HlsVideoDownloadHandler.kt @@ -4,7 +4,9 @@ import android.content.Context import android.net.Uri import android.os.Handler import android.os.Looper +import android.util.Log import com.google.android.exoplayer2.DefaultRenderersFactory +import com.google.android.exoplayer2.Format import com.google.android.exoplayer2.MediaItem import com.google.android.exoplayer2.database.DatabaseProvider import com.google.android.exoplayer2.database.ExoDatabaseProvider @@ -47,6 +49,8 @@ import kotlin.math.roundToInt object HlsVideoDownloadHandler : DownloadHandler { + val TAG: String = HlsVideoDownloadHandler::class.java.simpleName + private val context: Context get() = App.instance @@ -65,6 +69,7 @@ object HlsVideoDownloadHandler : val dataSourceFactory = DefaultHttpDataSourceFactory(Config.HEADER_USER_AGENT_VALUE) init { + Log.i(TAG, "Starting $TAG") DownloadService.start( context, HlsVideoDownloadForegroundService::class.java @@ -151,10 +156,14 @@ object HlsVideoDownloadHandler : downloadManager: DownloadManager, download: Download ) { + Log.d(TAG, "Download successfully removed: ${download.request.id}") val identifier = download.request.id - notifyStatus(identifier) + val listener = listeners[identifier] listeners.remove(identifier) downloads.remove(identifier) + listener?.invoke( + getDownloadStatus(null) + ) } } ) @@ -184,6 +193,7 @@ object HlsVideoDownloadHandler : } override fun download(request: HlsVideoDownloadRequest, callback: ((Boolean) -> Unit)?) { + Log.i(TAG, "Received download request: ${request.url}") val helper = DownloadHelper.forMediaItem( context, MediaItem.Builder().setUri(Uri.parse(request.url)).build(), @@ -195,18 +205,21 @@ object HlsVideoDownloadHandler : override fun onPrepared(helper: DownloadHelper) { val manifest = helper.manifest as HlsManifest - val bitrates = manifest.masterPlaylist.variants.map { it.format.bitrate } - val lowestBitrate = bitrates.minOrNull() ?: 0 - val highestBitrate = bitrates.maxOrNull() ?: 0 + val formats = manifest.masterPlaylist.variants.map { it.format } + val lowestBitrate = formats.minOfOrNull { it.bitrate } ?: 0 + val highestBitrate = formats.maxOfOrNull { it.bitrate } ?: 0 val targetBitrate = lowestBitrate + request.quality * (highestBitrate - lowestBitrate) - val closestBitrate = bitrates - .map { - abs(it - targetBitrate).roundToInt() - } - .minOrNull() - val estimatedSize = closestBitrate - ?.times(manifest.mediaPlaylist.durationUs * 1000000 / 8) + val closestFormat = formats.minByOrNull { + abs(it.bitrate - targetBitrate).roundToInt() + } + val closestBitrate = closestFormat?.bitrate + + val estimatedSize = (closestFormat?.averageBitrate + ?.takeUnless { it == Format.NO_VALUE } + ?: closestBitrate) + ?.times(manifest.mediaPlaylist.durationUs) + ?.div(8000000) // to bytes and seconds val subtitles = manifest.masterPlaylist.subtitles .mapNotNull { @@ -255,13 +268,21 @@ object HlsVideoDownloadHandler : try { val service = if (request.storage == context.internalStorage) { + Log.i( + TAG, + "Starting downloading to internal storage: ${request.url} aka $identifier" + ) HlsVideoDownloadInternalStorageForegroundService::class.java } else if (request.storage == context.sdcardStorage && getSdcardStorageCache(context) != null ) { + Log.i( + TAG, + "Starting downloading to sdcard storage: ${request.url} aka $identifier" + ) HlsVideoDownloadSdcardStorageForegroundService::class.java } else { - throw Exception() + throw Exception("Error during storage selection") } DownloadService.sendAddDownload( @@ -273,6 +294,10 @@ object HlsVideoDownloadHandler : callback?.invoke(true) } catch (e: Exception) { + Log.e( + TAG, + "Starting downloading failed: ${request.url} aka $identifier ($e)" + ) downloads[identifier] = Download( downloadRequest, Download.STATE_FAILED, @@ -288,6 +313,10 @@ object HlsVideoDownloadHandler : } override fun onPrepareError(helper: DownloadHelper, e: IOException) { + Log.e( + TAG, + "Starting downloading failed in helper preparation: ${request.url} ($e)" + ) callback?.invoke(false) helper.release() } @@ -296,6 +325,7 @@ object HlsVideoDownloadHandler : } override fun delete(identifier: HlsVideoDownloadIdentifier, callback: ((Boolean) -> Unit)?) { + Log.i(TAG, "Received deletion request: $identifier") DownloadService.sendRemoveDownload( context, HlsVideoDownloadInternalStorageBackgroundService::class.java, @@ -321,6 +351,7 @@ object HlsVideoDownloadHandler : identifier: HlsVideoDownloadIdentifier, listener: ((DownloadStatus) -> Unit)? ) { + Log.i(TAG, "Registering listener $listener for $identifier") listeners[identifier.get()] = listener listener?.invoke( getDownloadStatus( @@ -338,6 +369,10 @@ object HlsVideoDownloadHandler : storage: Storage, callback: (Map>) -> Unit ) { + Log.i( + TAG, + "Querying all downloads in ${storage.file.absolutePath}" + ) val sdcardStorageCache = getSdcardStorageCache(context) val cache = if (storage == context.internalStorage) { @@ -345,12 +380,13 @@ object HlsVideoDownloadHandler : } else if (storage == context.sdcardStorage && sdcardStorageCache != null) { sdcardStorageCache } else { + Log.w(TAG, "Storage ${storage.file.absolutePath} not supported") callback.invoke(mapOf()) return } callback.invoke( - getManager(context, cache).downloadIndex.getDownloads().let { + getManager(context, cache).downloadIndex.getDownloads(Download.STATE_COMPLETED).let { val map = mutableMapOf>() it.moveToFirst() @@ -368,36 +404,43 @@ object HlsVideoDownloadHandler : private fun getDownloadStatus(download: Download?): DownloadStatus { if (download == null) { + Log.w( + TAG, "getDownloadStatus(): Download not found, default status is generated: " + + "${download?.request?.id}" + ) return DownloadStatus(null, null, DownloadStatus.State.DELETED, null) } - val estimatedSize = ArgumentWrapper.decode(download.request.data).estimatedSize - val totalSize = download.contentLength.takeUnless { it < 0 } ?: estimatedSize + val totalSize = download.contentLength.takeUnless { it <= 0 } ?: if (download.state == Download.STATE_COMPLETED) { download.bytesDownloaded } else { - download.bytesDownloaded * 100 / download.percentDownloaded + ArgumentWrapper.decode(download.request.data).estimatedSize + ?: download.bytesDownloaded * 100 / download.percentDownloaded }.toLong() - - return DownloadStatus( - totalSize, - download.bytesDownloaded, - when (download.state) { - Download.STATE_QUEUED, Download.STATE_RESTARTING -> DownloadStatus.State.PENDING - Download.STATE_DOWNLOADING -> DownloadStatus.State.RUNNING - Download.STATE_COMPLETED -> DownloadStatus.State.DOWNLOADED - else -> DownloadStatus.State.DELETED - }, - if (download.state == Download.STATE_FAILED) { - Exception("Download failed with reason ${download.failureReason}") - } else null + val state = when (download.state) { + Download.STATE_QUEUED, Download.STATE_RESTARTING, Download.STATE_REMOVING -> DownloadStatus.State.PENDING + Download.STATE_DOWNLOADING -> DownloadStatus.State.RUNNING + Download.STATE_COMPLETED -> DownloadStatus.State.DOWNLOADED + else -> DownloadStatus.State.DELETED + } + val downloaded = if (state == DownloadStatus.State.DELETED) 0 else download.bytesDownloaded + val error = if (download.state == Download.STATE_FAILED) { + Exception("Download failed with reason ${download.failureReason}") + } else null + + Log.d( + TAG, "getDownloadStatus(): Generated download status [${state.name}]" + + "${downloaded}/${totalSize} B (error: ${error}) for ${download.request.id}" ) + return DownloadStatus(totalSize, downloaded, state, error) } private fun notifyStatus(identifier: String) { + Log.d(TAG, "Notifying of new status: $identifier") listeners[identifier]?.invoke( getDownloadStatus(downloads[identifier]) - ) + ) ?: Log.w(TAG, "Listener is null: $identifier") } private fun startNotifierThread(manager: DownloadManager) { diff --git a/app/src/main/java/de/xikolo/download/hlsvideodownload/HlsVideoDownloadItem.kt b/app/src/main/java/de/xikolo/download/hlsvideodownload/HlsVideoDownloadItem.kt index 4671ad46..87b5f0dc 100644 --- a/app/src/main/java/de/xikolo/download/hlsvideodownload/HlsVideoDownloadItem.kt +++ b/app/src/main/java/de/xikolo/download/hlsvideodownload/HlsVideoDownloadItem.kt @@ -27,7 +27,7 @@ open class HlsVideoDownloadItem( get() = url != null override val title: String - get() = url!! + get() = url ?: "" private val cache: Cache get() = if (storage == context.sdcardStorage) { @@ -48,7 +48,11 @@ open class HlsVideoDownloadItem( ).createMediaSource(request.mediaItem) override val size: Long - get() = indexEntry?.bytesDownloaded ?: 0L + get() = try { + indexEntry!!.bytesDownloaded + } catch (e: Exception) { + 0L + } final override val download: MediaSource? get() {