diff --git a/app/src/main/java/org/fnives/tiktokdownloader/data/local/VideoDownloadedLocalSource.kt b/app/src/main/java/org/fnives/tiktokdownloader/data/local/VideoDownloadedLocalSource.kt index 0941333..40cb5a2 100644 --- a/app/src/main/java/org/fnives/tiktokdownloader/data/local/VideoDownloadedLocalSource.kt +++ b/app/src/main/java/org/fnives/tiktokdownloader/data/local/VideoDownloadedLocalSource.kt @@ -58,6 +58,12 @@ class VideoDownloadedLocalSource( .plus(videoDownloaded.asString().addTimeAtStart()) } + fun removeVideo(videoDownloaded: VideoDownloaded) { + sharedPreferencesManagerImpl.downloadedVideos = sharedPreferencesManagerImpl.downloadedVideos + .filterNot { it.getTimeAndOriginal().second.asVideoDownloaded() == videoDownloaded } + .toSet() + } + companion object { private fun VideoDownloaded.asString(): String = diff --git a/app/src/main/java/org/fnives/tiktokdownloader/data/local/VideoInPendingLocalSource.kt b/app/src/main/java/org/fnives/tiktokdownloader/data/local/VideoInPendingLocalSource.kt index 1d23967..6161d2a 100644 --- a/app/src/main/java/org/fnives/tiktokdownloader/data/local/VideoInPendingLocalSource.kt +++ b/app/src/main/java/org/fnives/tiktokdownloader/data/local/VideoInPendingLocalSource.kt @@ -8,6 +8,7 @@ import org.fnives.tiktokdownloader.data.local.persistent.getTimeAndOriginal import org.fnives.tiktokdownloader.data.local.persistent.joinNormalized import org.fnives.tiktokdownloader.data.local.persistent.separateIntoDenormalized import org.fnives.tiktokdownloader.data.model.VideoInPending +import kotlin.math.abs class VideoInPendingLocalSource( private val sharedPreferencesManager: SharedPreferencesManager, @@ -33,6 +34,27 @@ class VideoInPendingLocalSource( .toSet() } + fun moveBy(videoInPending: VideoInPending, positionDifference: Int) { + if (positionDifference == 0) return + + val mutableOrdered = sharedPreferencesManager.pendingVideos + .map { it.getTimeAndOriginal() } + .sortedBy { it.first } + .toMutableList() + + val index = mutableOrdered.indexOfFirst { it.second.asVideoInPending() == videoInPending } + val endTime = mutableOrdered[index + positionDifference].first + + val direction = -positionDifference / abs(positionDifference) + val range = IntProgression.fromClosedRange(index + positionDifference, index - direction, direction) + range.forEach { + mutableOrdered[it] = mutableOrdered[it + direction].first to mutableOrdered[it].second + } + mutableOrdered[index] = endTime to mutableOrdered[index].second + + sharedPreferencesManager.pendingVideos = mutableOrdered.map { it.second.addTimeAtStart(it.first) }.toSet() + } + companion object { private fun VideoInPending.asString(): String = listOf(id, url).joinNormalized() diff --git a/app/src/main/java/org/fnives/tiktokdownloader/data/local/persistent/SerializationHelper.kt b/app/src/main/java/org/fnives/tiktokdownloader/data/local/persistent/SerializationHelper.kt index d34d35b..6340bcb 100644 --- a/app/src/main/java/org/fnives/tiktokdownloader/data/local/persistent/SerializationHelper.kt +++ b/app/src/main/java/org/fnives/tiktokdownloader/data/local/persistent/SerializationHelper.kt @@ -12,8 +12,8 @@ fun List.joinNormalized() : String = fun String.separateIntoDenormalized() : List = split("$SEPARATOR$SEPARATOR").map { it.denormalize() } -fun String.addTimeAtStart(index: Int = 0) = - "${System.currentTimeMillis() + index}$TIME_SEPARATOR$this" +fun String.addTimeAtStart(time: Long = System.currentTimeMillis()) = + "${time}$TIME_SEPARATOR$this" fun String.getTimeAndOriginal(): Pair { val time = takeWhile { it != TIME_SEPARATOR }.toLong() diff --git a/app/src/main/java/org/fnives/tiktokdownloader/data/usecase/MoveVideoInQueue.kt b/app/src/main/java/org/fnives/tiktokdownloader/data/usecase/MoveVideoInQueueUseCase.kt similarity index 58% rename from app/src/main/java/org/fnives/tiktokdownloader/data/usecase/MoveVideoInQueue.kt rename to app/src/main/java/org/fnives/tiktokdownloader/data/usecase/MoveVideoInQueueUseCase.kt index 36d0863..2b1e275 100644 --- a/app/src/main/java/org/fnives/tiktokdownloader/data/usecase/MoveVideoInQueue.kt +++ b/app/src/main/java/org/fnives/tiktokdownloader/data/usecase/MoveVideoInQueueUseCase.kt @@ -3,11 +3,11 @@ package org.fnives.tiktokdownloader.data.usecase import org.fnives.tiktokdownloader.data.local.VideoInPendingLocalSource import org.fnives.tiktokdownloader.data.model.VideoInPending -class MoveVideoInQueue( +class MoveVideoInQueueUseCase( private val videoInPendingLocalSource: VideoInPendingLocalSource ) { - operator fun invoke(videoInPending: VideoInPending, to: VideoInPending) { - + operator fun invoke(videoInPending: VideoInPending, positionDifference: Int) { + videoInPendingLocalSource.moveBy(videoInPending, positionDifference) } } \ No newline at end of file diff --git a/app/src/main/java/org/fnives/tiktokdownloader/data/usecase/RemoveVideoFromQueueUseCase.kt b/app/src/main/java/org/fnives/tiktokdownloader/data/usecase/RemoveVideoFromQueueUseCase.kt index 442b4e5..e84fc4a 100644 --- a/app/src/main/java/org/fnives/tiktokdownloader/data/usecase/RemoveVideoFromQueueUseCase.kt +++ b/app/src/main/java/org/fnives/tiktokdownloader/data/usecase/RemoveVideoFromQueueUseCase.kt @@ -1,13 +1,20 @@ package org.fnives.tiktokdownloader.data.usecase +import org.fnives.tiktokdownloader.data.local.VideoDownloadedLocalSource import org.fnives.tiktokdownloader.data.local.VideoInPendingLocalSource import org.fnives.tiktokdownloader.data.model.VideoInPending +import org.fnives.tiktokdownloader.data.model.VideoState class RemoveVideoFromQueueUseCase( - private val videoInPendingLocalSource: VideoInPendingLocalSource + private val videoInPendingLocalSource: VideoInPendingLocalSource, + private val videoDownloadedLocalSource: VideoDownloadedLocalSource ) { - operator fun invoke(videoInPending: VideoInPending) { - videoInPendingLocalSource.removeVideoFromQueue(videoInPending) + operator fun invoke(videoState: VideoState) { + when(videoState) { + is VideoState.Downloaded -> videoDownloadedLocalSource.removeVideo(videoState.videoDownloaded) + is VideoState.InPending -> videoInPendingLocalSource.removeVideoFromQueue(videoState.videoInPending) + is VideoState.InProcess -> Unit + } } } \ No newline at end of file diff --git a/app/src/main/java/org/fnives/tiktokdownloader/di/module/LocalSourceModule.kt b/app/src/main/java/org/fnives/tiktokdownloader/di/module/LocalSourceModule.kt index 4c311b0..f0ce819 100644 --- a/app/src/main/java/org/fnives/tiktokdownloader/di/module/LocalSourceModule.kt +++ b/app/src/main/java/org/fnives/tiktokdownloader/di/module/LocalSourceModule.kt @@ -8,17 +8,17 @@ import java.util.concurrent.TimeUnit class LocalSourceModule(private val androidFileManagementModule: AndroidFileManagementModule) { - val videoDownloadedLocalSource: VideoDownloadedLocalSource - get() = VideoDownloadedLocalSource( + val videoDownloadedLocalSource: VideoDownloadedLocalSource by lazy { + VideoDownloadedLocalSource( saveVideoFile = androidFileManagementModule.saveVideoFile, sharedPreferencesManagerImpl = androidFileManagementModule.sharedPreferencesManager, verifyFileForUriExists = androidFileManagementModule.verifyFileForUriExists ) + } - val videoInPendingLocalSource: VideoInPendingLocalSource - get() = VideoInPendingLocalSource( - sharedPreferencesManager = androidFileManagementModule.sharedPreferencesManager - ) + val videoInPendingLocalSource: VideoInPendingLocalSource by lazy { + VideoInPendingLocalSource(sharedPreferencesManager = androidFileManagementModule.sharedPreferencesManager) + } val videoInProgressLocalSource: VideoInProgressLocalSource by lazy { VideoInProgressLocalSource() } diff --git a/app/src/main/java/org/fnives/tiktokdownloader/di/module/UseCaseModule.kt b/app/src/main/java/org/fnives/tiktokdownloader/di/module/UseCaseModule.kt index 4602dd7..95261f7 100644 --- a/app/src/main/java/org/fnives/tiktokdownloader/di/module/UseCaseModule.kt +++ b/app/src/main/java/org/fnives/tiktokdownloader/di/module/UseCaseModule.kt @@ -2,7 +2,7 @@ package org.fnives.tiktokdownloader.di.module import kotlinx.coroutines.Dispatchers import org.fnives.tiktokdownloader.data.usecase.AddVideoToQueueUseCase -import org.fnives.tiktokdownloader.data.usecase.MoveVideoInQueue +import org.fnives.tiktokdownloader.data.usecase.MoveVideoInQueueUseCase import org.fnives.tiktokdownloader.data.usecase.RemoveVideoFromQueueUseCase import org.fnives.tiktokdownloader.data.usecase.StateOfVideosObservableUseCase import org.fnives.tiktokdownloader.data.usecase.UrlVerificationUseCase @@ -32,11 +32,12 @@ class UseCaseModule( val removeVideoFromQueueUseCase: RemoveVideoFromQueueUseCase get() = RemoveVideoFromQueueUseCase( - localSourceModule.videoInPendingLocalSource + localSourceModule.videoInPendingLocalSource, + localSourceModule.videoDownloadedLocalSource ) - val moveVideoInQueue: MoveVideoInQueue - get() = MoveVideoInQueue( + val moveVideoInQueueUseCase: MoveVideoInQueueUseCase + get() = MoveVideoInQueueUseCase( localSourceModule.videoInPendingLocalSource ) diff --git a/app/src/main/java/org/fnives/tiktokdownloader/di/module/ViewModelModule.kt b/app/src/main/java/org/fnives/tiktokdownloader/di/module/ViewModelModule.kt index db931be..123b183 100644 --- a/app/src/main/java/org/fnives/tiktokdownloader/di/module/ViewModelModule.kt +++ b/app/src/main/java/org/fnives/tiktokdownloader/di/module/ViewModelModule.kt @@ -25,6 +25,7 @@ class ViewModelModule(private val useCaseModule: UseCaseModule) { useCaseModule.stateOfVideosObservableUseCase, useCaseModule.addVideoToQueueUseCase, useCaseModule.removeVideoFromQueueUseCase, - useCaseModule.videoDownloadingProcessorUseCase + useCaseModule.videoDownloadingProcessorUseCase, + useCaseModule.moveVideoInQueueUseCase ) } \ No newline at end of file diff --git a/app/src/main/java/org/fnives/tiktokdownloader/ui/main/queue/MovingItemCallback.kt b/app/src/main/java/org/fnives/tiktokdownloader/ui/main/queue/MovingItemCallback.kt new file mode 100644 index 0000000..b9f5b20 --- /dev/null +++ b/app/src/main/java/org/fnives/tiktokdownloader/ui/main/queue/MovingItemCallback.kt @@ -0,0 +1,8 @@ +package org.fnives.tiktokdownloader.ui.main.queue + +interface MovingItemCallback { + + fun onMovingStart() + + fun onMovingEnd() +} \ No newline at end of file diff --git a/app/src/main/java/org/fnives/tiktokdownloader/ui/main/queue/QueueFragment.kt b/app/src/main/java/org/fnives/tiktokdownloader/ui/main/queue/QueueFragment.kt index 6b93229..1502e10 100644 --- a/app/src/main/java/org/fnives/tiktokdownloader/ui/main/queue/QueueFragment.kt +++ b/app/src/main/java/org/fnives/tiktokdownloader/ui/main/queue/QueueFragment.kt @@ -57,14 +57,18 @@ class QueueFragment : Fragment(R.layout.fragment_queue) { ) recycler.adapter = adapter - val touchHelper = ItemTouchHelper(PendingItemTouchHelper( + val callback = VideoStateItemTouchHelper( whichItem = { adapter.currentList.getOrNull(it.bindingAdapterPosition) }, onDeleteElement = viewModel::onElementDeleted, - onMovedElement = viewModel::onElementMoved - )) + onUIMoveElement = adapter::swap, + onMoveElement = viewModel::onElementMoved + ) + val touchHelper = ItemTouchHelper(callback) touchHelper.attachToRecyclerView(recycler) viewModel.downloads.observe(viewLifecycleOwner) { videoStates -> + callback.dragEnabled = videoStates.none { it is VideoState.InProcess } + adapter.submitList(videoStates, Runnable { val indexToScrollTo = videoStates.indexOfFirst { it is VideoState.InProcess } .takeIf { it != -1 } ?: return@Runnable @@ -73,36 +77,6 @@ class QueueFragment : Fragment(R.layout.fragment_queue) { } } - class PendingItemTouchHelper( - private val whichItem: (RecyclerView.ViewHolder) -> VideoState?, - private val onDeleteElement: (VideoState) -> Unit, - private val onMovedElement: (VideoState, VideoState) -> Boolean - ) : ItemTouchHelper.Callback() { - override fun getMovementFlags(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder): Int { - val item = whichItem(viewHolder) ?: return 0 - when (item) { - is VideoState.InPending -> Unit - is VideoState.Downloaded, - is VideoState.InProcess -> return 0 - } - - val dragFlags = ItemTouchHelper.UP or ItemTouchHelper.DOWN - val swipeFlags = ItemTouchHelper.START or ItemTouchHelper.END - return makeMovementFlags(dragFlags, swipeFlags) - } - - override fun onMove(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder, target: RecyclerView.ViewHolder): Boolean { - val dragged = whichItem(target) ?: return false - val movedTo = whichItem(viewHolder) ?: return false - return onMovedElement(dragged, movedTo) - } - - override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) { - whichItem(viewHolder)?.let { onDeleteElement(it) } - } - - } - companion object { fun newInstance(): QueueFragment = QueueFragment() diff --git a/app/src/main/java/org/fnives/tiktokdownloader/ui/main/queue/QueueItemAdapter.kt b/app/src/main/java/org/fnives/tiktokdownloader/ui/main/queue/QueueItemAdapter.kt index 2808093..628ff35 100644 --- a/app/src/main/java/org/fnives/tiktokdownloader/ui/main/queue/QueueItemAdapter.kt +++ b/app/src/main/java/org/fnives/tiktokdownloader/ui/main/queue/QueueItemAdapter.kt @@ -4,6 +4,8 @@ import android.view.ViewGroup import android.widget.ImageView import android.widget.ProgressBar import android.widget.TextView +import androidx.cardview.widget.CardView +import androidx.constraintlayout.widget.ConstraintLayout import androidx.core.view.isInvisible import androidx.core.view.isVisible import androidx.recyclerview.widget.DiffUtil @@ -53,12 +55,38 @@ class QueueItemAdapter( } } - class DownloadActionsViewHolder(parent: ViewGroup) : + fun swap(from: VideoState, to: VideoState) { + val mutable = currentList.toMutableList() + val fromIndex = currentList.indexOf(from) + val toIndex = currentList.indexOf(to) + mutable.removeAt(fromIndex) + mutable.add(fromIndex, to) + mutable.removeAt(toIndex) + mutable.add(toIndex, from) + + submitList(mutable) + } + + class DownloadActionsViewHolder(parent: ViewGroup) : MovingItemCallback, RecyclerView.ViewHolder(parent.inflate(R.layout.item_downloaded)) { + val cardView: CardView = itemView as CardView + val content: ConstraintLayout = itemView.findViewById(R.id.content) val urlView: TextView = itemView.findViewById(R.id.url) val statusView: TextView = itemView.findViewById(R.id.status) val thumbNailView: ImageView = itemView.findViewById(R.id.thumbnail) val progress: ProgressBar = itemView.findViewById(R.id.progress) + + override fun onMovingEnd() { + cardView.isEnabled = false + cardView.isPressed = false + content.isPressed = false + } + + override fun onMovingStart() { + cardView.isEnabled = true + cardView.isPressed = true + content.isPressed = true + } } class DiffUtilItemCallback : DiffUtil.ItemCallback() { diff --git a/app/src/main/java/org/fnives/tiktokdownloader/ui/main/queue/QueueViewModel.kt b/app/src/main/java/org/fnives/tiktokdownloader/ui/main/queue/QueueViewModel.kt index dac2c30..c64ff24 100644 --- a/app/src/main/java/org/fnives/tiktokdownloader/ui/main/queue/QueueViewModel.kt +++ b/app/src/main/java/org/fnives/tiktokdownloader/ui/main/queue/QueueViewModel.kt @@ -5,7 +5,7 @@ import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import org.fnives.tiktokdownloader.data.model.VideoState import org.fnives.tiktokdownloader.data.usecase.AddVideoToQueueUseCase -import org.fnives.tiktokdownloader.data.usecase.MoveVideoInQueue +import org.fnives.tiktokdownloader.data.usecase.MoveVideoInQueueUseCase import org.fnives.tiktokdownloader.data.usecase.RemoveVideoFromQueueUseCase import org.fnives.tiktokdownloader.data.usecase.StateOfVideosObservableUseCase import org.fnives.tiktokdownloader.data.usecase.VideoDownloadingProcessorUseCase @@ -17,7 +17,7 @@ class QueueViewModel( private val addVideoToQueueUseCase: AddVideoToQueueUseCase, private val removeVideoFromQueueUseCase: RemoveVideoFromQueueUseCase, private val videoDownloadingProcessorUseCase: VideoDownloadingProcessorUseCase, - private val moveVideoInQueue: MoveVideoInQueue + private val moveVideoInQueueUseCase: MoveVideoInQueueUseCase ) : ViewModel() { val downloads = asLiveData(stateOfVideosObservableUseCase()) @@ -38,17 +38,12 @@ class QueueViewModel( } fun onElementDeleted(videoState: VideoState) { - when (videoState) { - is VideoState.InPending -> removeVideoFromQueueUseCase(videoState.videoInPending) - is VideoState.Downloaded, - is VideoState.InProcess -> Unit - } + removeVideoFromQueueUseCase(videoState) } - fun onElementMoved(moved: VideoState, to: VideoState): Boolean { + fun onElementMoved(moved: VideoState, positionDifference: Int): Boolean { if (moved !is VideoState.InPending) return false - if (to !is VideoState.InPending) return false - moveVideoInQueue(moved.videoInPending, to.videoInPending) + moveVideoInQueueUseCase(moved.videoInPending, positionDifference) return true } diff --git a/app/src/main/java/org/fnives/tiktokdownloader/ui/main/queue/VideoStateItemTouchHelper.kt b/app/src/main/java/org/fnives/tiktokdownloader/ui/main/queue/VideoStateItemTouchHelper.kt new file mode 100644 index 0000000..e5fc119 --- /dev/null +++ b/app/src/main/java/org/fnives/tiktokdownloader/ui/main/queue/VideoStateItemTouchHelper.kt @@ -0,0 +1,72 @@ +package org.fnives.tiktokdownloader.ui.main.queue + +import androidx.recyclerview.widget.ItemTouchHelper +import androidx.recyclerview.widget.RecyclerView +import org.fnives.tiktokdownloader.data.model.VideoState + +class VideoStateItemTouchHelper( + private val whichItem: (RecyclerView.ViewHolder) -> VideoState?, + private val onDeleteElement: (VideoState) -> Unit, + private val onUIMoveElement: (VideoState, VideoState) -> Unit, + private val onMoveElement: (VideoState, Int) -> Unit +) : ItemTouchHelper.Callback() { + + var dragEnabled: Boolean = true + var swipeEnabled: Boolean = true + private var index: Int? = null + + override fun getMovementFlags(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder): Int { + val item = whichItem(viewHolder) ?: return DISABLED_FLAG + var dragEnabled = dragEnabled + var swipeEnabled = swipeEnabled + when (item) { + is VideoState.InPending -> Unit + is VideoState.Downloaded -> { + dragEnabled = false + } + is VideoState.InProcess -> { + dragEnabled = false + swipeEnabled = false + } + } + + val dragFlags = if (dragEnabled) ItemTouchHelper.UP or ItemTouchHelper.DOWN else DISABLED_FLAG + val swipeFlags = if (swipeEnabled) ItemTouchHelper.START or ItemTouchHelper.END else DISABLED_FLAG + return makeMovementFlags(dragFlags, swipeFlags) + } + + override fun onMove(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder, target: RecyclerView.ViewHolder): Boolean { + val dragged = whichItem(target)?.takeIf { it is VideoState.InPending } ?: return false + val movedTo = whichItem(viewHolder)?.takeIf { it is VideoState.InPending } ?: return false + onUIMoveElement(dragged, movedTo) + return true + } + + override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) { + whichItem(viewHolder)?.let { onDeleteElement(it) } + } + + override fun onSelectedChanged(viewHolder: RecyclerView.ViewHolder?, actionState: Int) { + super.onSelectedChanged(viewHolder, actionState) + (viewHolder as? MovingItemCallback)?.onMovingStart() + index = when(actionState) { + ItemTouchHelper.ACTION_STATE_DRAG -> viewHolder?.bindingAdapterPosition + ItemTouchHelper.ACTION_STATE_IDLE -> index + else -> null + } + } + + override fun clearView(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder) { + super.clearView(recyclerView, viewHolder) + (viewHolder as? MovingItemCallback)?.onMovingEnd() + val videoState = whichItem(viewHolder) ?: return + val startIndex = index ?: return + + val endIndex = viewHolder.bindingAdapterPosition + onMoveElement(videoState, endIndex - startIndex) + } + + companion object { + private const val DISABLED_FLAG = 0 + } +} \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_queue.xml b/app/src/main/res/layout/fragment_queue.xml index b390636..684094e 100644 --- a/app/src/main/res/layout/fragment_queue.xml +++ b/app/src/main/res/layout/fragment_queue.xml @@ -3,6 +3,7 @@ xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="match_parent" + xmlns:tools="http://schemas.android.com/tools" app:layoutDescription="@xml/queue_motion_description"> \ No newline at end of file diff --git a/app/src/main/res/layout/item_downloaded.xml b/app/src/main/res/layout/item_downloaded.xml index 3e12a45..69148c2 100644 --- a/app/src/main/res/layout/item_downloaded.xml +++ b/app/src/main/res/layout/item_downloaded.xml @@ -12,6 +12,7 @@ diff --git a/app/src/test/java/org/fnives/tiktokdownloader/data/local/VideoDownloadedLocalSourceTest.kt b/app/src/test/java/org/fnives/tiktokdownloader/data/local/VideoDownloadedLocalSourceTest.kt index f8c629b..5b5d0f9 100644 --- a/app/src/test/java/org/fnives/tiktokdownloader/data/local/VideoDownloadedLocalSourceTest.kt +++ b/app/src/test/java/org/fnives/tiktokdownloader/data/local/VideoDownloadedLocalSourceTest.kt @@ -29,6 +29,7 @@ import org.mockito.kotlin.whenever import java.io.InputStream +@Suppress("TestFunctionName") @Timeout(value = 2) class VideoDownloadedLocalSourceTest { @@ -195,6 +196,50 @@ class VideoDownloadedLocalSourceTest { Assertions.assertEquals(expected, actual.await()) } + @Test + fun GIVEN_two_videos_WHEN_deleting_THEN_observer_is_updated() = runBlocking { + val videoInSavingIntoFile1 = VideoInSavingIntoFile( + id = "id1", + url = "alma1", + contentType = VideoInSavingIntoFile.ContentType("a1", "b1"), + byteStream = FalseInputStream() + ) + val videoInSavingIntoFile2 = VideoInSavingIntoFile( + id = "id2", + url = "alma2", + contentType = VideoInSavingIntoFile.ContentType("a2", "b2"), + byteStream = FalseInputStream() + ) + whenever(mockVerifyFileForUriExists.invoke(anyOrNull())).doReturn(true) + whenever(mockSaveVideoFile.invoke(anyOrNull(), anyOrNull(), anyOrNull())).then { + "uri: " + (it.arguments[1] as String) + } + val expectedModel1 = VideoDownloaded(id = "id1", url = "alma1", uri = "uri: id1.b1") + val expectedModel2 = VideoDownloaded(id = "id2", url = "alma2", uri = "uri: id2.b2") + val expected = listOf( + emptyList(), + listOf(expectedModel1), + listOf(expectedModel2, expectedModel1), + listOf(expectedModel2), + emptyList(), + ) + val actual = async(coroutineContext) { sut.savedVideos.take(expected.size).toList() } + + yield() + sut.saveVideo(videoInSavingIntoFile1) + delay(100) + yield() + sut.saveVideo(videoInSavingIntoFile2) + yield() + + sut.removeVideo(expectedModel1) + yield() + sut.removeVideo(expectedModel2) + yield() + + Assertions.assertIterableEquals(expected, actual.await()) + } + class FalseInputStream : InputStream() { override fun read(): Int = 0 } diff --git a/app/src/test/java/org/fnives/tiktokdownloader/data/local/VideoInPendingLocalSourceTest.kt b/app/src/test/java/org/fnives/tiktokdownloader/data/local/VideoInPendingLocalSourceTest.kt index b65bf87..28299fe 100644 --- a/app/src/test/java/org/fnives/tiktokdownloader/data/local/VideoInPendingLocalSourceTest.kt +++ b/app/src/test/java/org/fnives/tiktokdownloader/data/local/VideoInPendingLocalSourceTest.kt @@ -17,7 +17,7 @@ import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import org.junit.jupiter.api.Timeout - +@Suppress("TestFunctionName") @Timeout(value = 2) class VideoInPendingLocalSourceTest { @@ -76,8 +76,7 @@ class VideoInPendingLocalSourceTest { } @Test - fun GIVEN_observing_PendingVideos_WHEN_2_video_marked_as_pending_THEN_both_of_them_are_sent_out_in_correct_order() = - runBlocking { + fun GIVEN_observing_PendingVideos_WHEN_2_video_marked_as_pending_THEN_both_of_them_are_sent_out_in_correct_order() = runBlocking { val videoInPending1 = VideoInPending("id1", "alma1") val videoInPending2 = VideoInPending("id2", "alma2") val expected = listOf(emptyList(), listOf(videoInPending1), listOf(videoInPending1, videoInPending2)) @@ -94,4 +93,92 @@ class VideoInPendingLocalSourceTest { Assertions.assertEquals(expected, actual.await()) } + @Test + fun GIVEN_2_videos_WHEN_moving_first_one_down_by_one_THEN_it_is_moved_properly() = runBlocking { + val videoInPending1 = VideoInPending("id1", "alma1") + val videoInPending2 = VideoInPending("id2", "alma2") + + val expected = listOf(videoInPending2, videoInPending1) + + yield() + sut.saveUrlIntoQueue(videoInPending1) + delay(10) + sut.saveUrlIntoQueue(videoInPending2) + delay(10) + + sut.moveBy(videoInPending1, 1) + + Assertions.assertEquals(expected, sut.pendingVideos.first()) + } + + @Test + fun GIVEN_2_videos_WHEN_moving_second_one_up_by_one_THEN_it_is_moved_properly() = runBlocking { + val videoInPending1 = VideoInPending("id1", "alma1") + val videoInPending2 = VideoInPending("id2", "alma2") + + val expected = listOf(videoInPending2, videoInPending1) + + yield() + sut.saveUrlIntoQueue(videoInPending1) + delay(10) + sut.saveUrlIntoQueue(videoInPending2) + delay(10) + + sut.moveBy(videoInPending2, -1) + + Assertions.assertEquals(expected, sut.pendingVideos.first()) + } + + @Test + fun GIVEN_3_videos_WHEN_moving_first_moving_around_is_moved_properly() = runBlocking { + val videoInPending1 = VideoInPending("id1", "alma1") + val videoInPending2 = VideoInPending("id2", "alma2") + val videoInPending3 = VideoInPending("id3", "alma3") + + val expected = listOf( + listOf(videoInPending1, videoInPending2, videoInPending3), // start + listOf(videoInPending2, videoInPending1, videoInPending3), // down 1 + listOf(videoInPending2, videoInPending3, videoInPending1), // down 1 + listOf(videoInPending2, videoInPending1, videoInPending3), // ++up 1 + listOf(videoInPending1, videoInPending2, videoInPending3), // ++up 1 + listOf(videoInPending2, videoInPending3, videoInPending1), // down 2 + listOf(videoInPending2, videoInPending1, videoInPending3), // ++up 1 + listOf(videoInPending1, videoInPending2, videoInPending3), // ++up 1 + listOf(videoInPending2, videoInPending3, videoInPending1), // down 2 + listOf(videoInPending1, videoInPending2, videoInPending3), // ++up 1 + ) + + yield() + sut.saveUrlIntoQueue(videoInPending1) + delay(10) + sut.saveUrlIntoQueue(videoInPending2) + delay(10) + sut.saveUrlIntoQueue(videoInPending3) + + val actual = async(coroutineContext) { + sut.pendingVideos.take(expected.size).toList() + } + yield() + + sut.moveBy(videoInPending1, 1) + yield() + sut.moveBy(videoInPending1, 1) + yield() + sut.moveBy(videoInPending1, -1) + yield() + sut.moveBy(videoInPending1, -1) + yield() + sut.moveBy(videoInPending1, 2) + yield() + sut.moveBy(videoInPending1, -1) + yield() + sut.moveBy(videoInPending1, -1) + yield() + sut.moveBy(videoInPending1, 2) + yield() + sut.moveBy(videoInPending1, -2) + yield() + + Assertions.assertIterableEquals(expected, actual.await()) + } } \ No newline at end of file diff --git a/app/src/test/java/org/fnives/tiktokdownloader/data/usecase/MoveVideoInQueueUseCaseTest.kt b/app/src/test/java/org/fnives/tiktokdownloader/data/usecase/MoveVideoInQueueUseCaseTest.kt new file mode 100644 index 0000000..e050581 --- /dev/null +++ b/app/src/test/java/org/fnives/tiktokdownloader/data/usecase/MoveVideoInQueueUseCaseTest.kt @@ -0,0 +1,42 @@ +package org.fnives.tiktokdownloader.data.usecase + +import org.fnives.tiktokdownloader.data.local.VideoInPendingLocalSource +import org.fnives.tiktokdownloader.data.model.VideoInPending +import org.junit.jupiter.api.Assertions.* + +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.Timeout +import org.mockito.kotlin.mock +import org.mockito.kotlin.verify +import org.mockito.kotlin.verifyNoInteractions +import org.mockito.kotlin.verifyNoMoreInteractions + +@Suppress("TestFunctionName") +@Timeout(value = 2) +class MoveVideoInQueueUseCaseTest { + + private lateinit var sut: MoveVideoInQueueUseCase + private lateinit var mockVideoInPendingLocalSource: VideoInPendingLocalSource + + @BeforeEach + fun setup() { + mockVideoInPendingLocalSource = mock() + sut = MoveVideoInQueueUseCase(mockVideoInPendingLocalSource) + } + + @Test + fun WHEN_no_action_THEN_no_delegation() { + verifyNoInteractions(mockVideoInPendingLocalSource) + } + + @Test + fun GIVEN_video_and_position_change_WHEN_invoked_THEN_delegated() { + val video = VideoInPending("1", "url1") + sut.invoke(video, 100) + + verify(mockVideoInPendingLocalSource).moveBy(video, 100) + verifyNoMoreInteractions(mockVideoInPendingLocalSource) + } +} \ No newline at end of file diff --git a/app/src/test/java/org/fnives/tiktokdownloader/data/usecase/RemoveVideoFromQueueUseCaseTest.kt b/app/src/test/java/org/fnives/tiktokdownloader/data/usecase/RemoveVideoFromQueueUseCaseTest.kt new file mode 100644 index 0000000..1190fa5 --- /dev/null +++ b/app/src/test/java/org/fnives/tiktokdownloader/data/usecase/RemoveVideoFromQueueUseCaseTest.kt @@ -0,0 +1,67 @@ +package org.fnives.tiktokdownloader.data.usecase + +import org.fnives.tiktokdownloader.data.local.VideoDownloadedLocalSource +import org.fnives.tiktokdownloader.data.local.VideoInPendingLocalSource +import org.fnives.tiktokdownloader.data.model.VideoDownloaded +import org.fnives.tiktokdownloader.data.model.VideoInPending +import org.fnives.tiktokdownloader.data.model.VideoInProgress +import org.fnives.tiktokdownloader.data.model.VideoState + +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.Timeout +import org.mockito.kotlin.mock +import org.mockito.kotlin.verify +import org.mockito.kotlin.verifyNoInteractions +import org.mockito.kotlin.verifyNoMoreInteractions + +@Suppress("TestFunctionName") +@Timeout(value = 2) +class RemoveVideoFromQueueUseCaseTest { + + private lateinit var sut: RemoveVideoFromQueueUseCase + private lateinit var mockVideoInPendingLocalSource: VideoInPendingLocalSource + private lateinit var mockVideoDownloadedLocalSource: VideoDownloadedLocalSource + + @BeforeEach + fun setup() { + mockVideoInPendingLocalSource = mock() + mockVideoDownloadedLocalSource = mock() + sut = RemoveVideoFromQueueUseCase(mockVideoInPendingLocalSource, mockVideoDownloadedLocalSource) + } + + @Test + fun WHEN_no_action_THEN_no_delegation() { + verifyNoInteractions(mockVideoInPendingLocalSource) + verifyNoInteractions(mockVideoDownloadedLocalSource) + } + + @Test + fun GIVEN_pending_video_WHEN_invoked_THEN_delegated() { + val video = VideoState.InPending(VideoInPending("1", "url1")) + sut.invoke(video) + + verify(mockVideoInPendingLocalSource).removeVideoFromQueue(video.videoInPending) + verifyNoMoreInteractions(mockVideoInPendingLocalSource) + verifyNoInteractions(mockVideoDownloadedLocalSource) + } + + @Test + fun GIVEN_downloaded_video_WHEN_invoked_THEN_delegated() { + val video = VideoState.Downloaded(VideoDownloaded("1", "url1", "img1")) + sut.invoke(video) + + verifyNoInteractions(mockVideoInPendingLocalSource) + verify(mockVideoDownloadedLocalSource).removeVideo(video.videoDownloaded) + verifyNoMoreInteractions(mockVideoDownloadedLocalSource) + } + + @Test + fun GIVEN_in_process_WHEN_invoked_THEN_no_delegation() { + val video = VideoState.InProcess(VideoInProgress("1", "url1")) + sut.invoke(video) + + verifyNoInteractions(mockVideoInPendingLocalSource) + verifyNoInteractions(mockVideoDownloadedLocalSource) + } +} \ No newline at end of file diff --git a/app/src/test/java/org/fnives/tiktokdownloader/ui/main/queue/QueueViewModelTest.kt b/app/src/test/java/org/fnives/tiktokdownloader/ui/main/queue/QueueViewModelTest.kt index 71e7c88..b813765 100644 --- a/app/src/test/java/org/fnives/tiktokdownloader/ui/main/queue/QueueViewModelTest.kt +++ b/app/src/test/java/org/fnives/tiktokdownloader/ui/main/queue/QueueViewModelTest.kt @@ -2,9 +2,13 @@ package org.fnives.tiktokdownloader.ui.main.queue import com.jraska.livedata.test import kotlinx.coroutines.flow.MutableSharedFlow +import org.fnives.tiktokdownloader.data.model.VideoDownloaded import org.fnives.tiktokdownloader.data.model.VideoInPending +import org.fnives.tiktokdownloader.data.model.VideoInProgress import org.fnives.tiktokdownloader.data.model.VideoState import org.fnives.tiktokdownloader.data.usecase.AddVideoToQueueUseCase +import org.fnives.tiktokdownloader.data.usecase.MoveVideoInQueueUseCase +import org.fnives.tiktokdownloader.data.usecase.RemoveVideoFromQueueUseCase import org.fnives.tiktokdownloader.data.usecase.StateOfVideosObservableUseCase import org.fnives.tiktokdownloader.data.usecase.VideoDownloadingProcessorUseCase import org.fnives.tiktokdownloader.helper.junit.rule.InstantExecutorExtension @@ -28,7 +32,9 @@ class QueueViewModelTest { private lateinit var stateOfVideosFlow: MutableSharedFlow> private lateinit var mockStateOfVideosObservableUseCase: StateOfVideosObservableUseCase private lateinit var mockAddVideoToQueueUseCase: AddVideoToQueueUseCase + private lateinit var mockRemoveVideoFromQueueUseCase: RemoveVideoFromQueueUseCase private lateinit var mockVideoDownloadingProcessorUseCase: VideoDownloadingProcessorUseCase + private lateinit var mockMoveVideoInQueueUseCase: MoveVideoInQueueUseCase private lateinit var sut: QueueViewModel @BeforeEach @@ -37,8 +43,16 @@ class QueueViewModelTest { mockStateOfVideosObservableUseCase = mock() whenever(mockStateOfVideosObservableUseCase.invoke()).doReturn(stateOfVideosFlow) mockAddVideoToQueueUseCase = mock() + mockRemoveVideoFromQueueUseCase = mock() mockVideoDownloadingProcessorUseCase = mock() - sut = QueueViewModel(mockStateOfVideosObservableUseCase, mockAddVideoToQueueUseCase, mockVideoDownloadingProcessorUseCase) + mockMoveVideoInQueueUseCase = mock() + sut = QueueViewModel( + mockStateOfVideosObservableUseCase, + mockAddVideoToQueueUseCase, + mockRemoveVideoFromQueueUseCase, + mockVideoDownloadingProcessorUseCase, + mockMoveVideoInQueueUseCase + ) } @Test @@ -48,6 +62,8 @@ class QueueViewModelTest { verifyNoMoreInteractions(mockStateOfVideosObservableUseCase) verifyNoInteractions(mockAddVideoToQueueUseCase) verifyNoInteractions(mockVideoDownloadingProcessorUseCase) + verifyNoInteractions(mockRemoveVideoFromQueueUseCase) + verifyNoInteractions(mockMoveVideoInQueueUseCase) } @Test @@ -60,6 +76,8 @@ class QueueViewModelTest { verifyNoMoreInteractions(mockStateOfVideosObservableUseCase) verifyNoInteractions(mockAddVideoToQueueUseCase) verifyNoInteractions(mockVideoDownloadingProcessorUseCase) + verifyNoInteractions(mockRemoveVideoFromQueueUseCase) + verifyNoInteractions(mockMoveVideoInQueueUseCase) } @Test @@ -77,6 +95,8 @@ class QueueViewModelTest { verifyNoMoreInteractions(mockStateOfVideosObservableUseCase) verifyNoInteractions(mockAddVideoToQueueUseCase) verifyNoInteractions(mockVideoDownloadingProcessorUseCase) + verifyNoInteractions(mockRemoveVideoFromQueueUseCase) + verifyNoInteractions(mockMoveVideoInQueueUseCase) } @Test @@ -89,6 +109,8 @@ class QueueViewModelTest { verifyNoMoreInteractions(mockAddVideoToQueueUseCase) verify(mockVideoDownloadingProcessorUseCase, times(1)).fetchVideoInState() verifyNoMoreInteractions(mockVideoDownloadingProcessorUseCase) + verifyNoInteractions(mockRemoveVideoFromQueueUseCase) + verifyNoInteractions(mockMoveVideoInQueueUseCase) } @Test @@ -103,6 +125,8 @@ class QueueViewModelTest { verifyNoMoreInteractions(mockAddVideoToQueueUseCase) verify(mockVideoDownloadingProcessorUseCase, times(2)).fetchVideoInState() verifyNoMoreInteractions(mockVideoDownloadingProcessorUseCase) + verifyNoInteractions(mockRemoveVideoFromQueueUseCase) + verifyNoInteractions(mockMoveVideoInQueueUseCase) } @Test @@ -125,4 +149,86 @@ class QueueViewModelTest { .assertHistorySize(1) .assertValue(Event(QueueViewModel.NavigationEvent.OpenGallery("alma.com"))) } + + @Test + fun GIVEN_initialized_WHEN_inProgress_onElementDeleted_THEN_remove_called() { + val arg = VideoState.InProcess(VideoInProgress("1","2")) + sut.onElementDeleted(arg) + + verify(mockStateOfVideosObservableUseCase, times(1)).invoke() + verifyNoMoreInteractions(mockStateOfVideosObservableUseCase) + verifyNoInteractions(mockAddVideoToQueueUseCase) + verifyNoInteractions(mockVideoDownloadingProcessorUseCase) + verify(mockRemoveVideoFromQueueUseCase, times(1)).invoke(arg) + verifyNoMoreInteractions(mockRemoveVideoFromQueueUseCase) + verifyNoInteractions(mockMoveVideoInQueueUseCase) + } + + @Test + fun GIVEN_initialized_WHEN_pending_onElementDeleted_THEN_remove_called() { + val arg = VideoState.InPending(VideoInPending("1","2")) + sut.onElementDeleted(arg) + + verify(mockStateOfVideosObservableUseCase, times(1)).invoke() + verifyNoMoreInteractions(mockStateOfVideosObservableUseCase) + verifyNoInteractions(mockAddVideoToQueueUseCase) + verifyNoInteractions(mockVideoDownloadingProcessorUseCase) + verify(mockRemoveVideoFromQueueUseCase, times(1)).invoke(arg) + verifyNoMoreInteractions(mockRemoveVideoFromQueueUseCase) + verifyNoInteractions(mockMoveVideoInQueueUseCase) + } + + @Test + fun GIVEN_initialized_WHEN_download_onElementDeleted_THEN_remove_called() { + val arg = VideoState.Downloaded(VideoDownloaded("1","2","3")) + sut.onElementDeleted(arg) + + verify(mockStateOfVideosObservableUseCase, times(1)).invoke() + verifyNoMoreInteractions(mockStateOfVideosObservableUseCase) + verifyNoInteractions(mockAddVideoToQueueUseCase) + verifyNoInteractions(mockVideoDownloadingProcessorUseCase) + verify(mockRemoveVideoFromQueueUseCase, times(1)).invoke(arg) + verifyNoMoreInteractions(mockRemoveVideoFromQueueUseCase) + verifyNoInteractions(mockMoveVideoInQueueUseCase) + } + + @Test + fun GIVEN_initialized_WHEN_in_progress_moved_THEN_nothing_changes() { + val arg = VideoState.InProcess(VideoInProgress("1","2")) + sut.onElementMoved(arg, 100) + + verify(mockStateOfVideosObservableUseCase, times(1)).invoke() + verifyNoMoreInteractions(mockStateOfVideosObservableUseCase) + verifyNoInteractions(mockAddVideoToQueueUseCase) + verifyNoInteractions(mockVideoDownloadingProcessorUseCase) + verifyNoInteractions(mockRemoveVideoFromQueueUseCase) + verifyNoInteractions(mockMoveVideoInQueueUseCase) + } + + @Test + fun GIVEN_initialized_WHEN_done_moved_THEN_nothing_changes() { + val arg = VideoState.Downloaded(VideoDownloaded("1","2","3")) + sut.onElementMoved(arg, 100) + + verify(mockStateOfVideosObservableUseCase, times(1)).invoke() + verifyNoMoreInteractions(mockStateOfVideosObservableUseCase) + verifyNoInteractions(mockAddVideoToQueueUseCase) + verifyNoInteractions(mockVideoDownloadingProcessorUseCase) + verifyNoInteractions(mockRemoveVideoFromQueueUseCase) + verifyNoInteractions(mockMoveVideoInQueueUseCase) + } + + @Test + fun GIVEN_initialized_WHEN_pending_moved_THEN_nothing_changes() { + val arg = VideoState.InPending(VideoInPending("1","2")) + sut.onElementMoved(arg, 100) + + verify(mockStateOfVideosObservableUseCase, times(1)).invoke() + verifyNoMoreInteractions(mockStateOfVideosObservableUseCase) + verifyNoInteractions(mockAddVideoToQueueUseCase) + verifyNoInteractions(mockVideoDownloadingProcessorUseCase) + verifyNoInteractions(mockRemoveVideoFromQueueUseCase) + verify(mockMoveVideoInQueueUseCase, times(1)).invoke(arg.videoInPending, 100) + verifyNoMoreInteractions(mockMoveVideoInQueueUseCase) + } } \ No newline at end of file