Issue#7 Enable Deleting Downloaded Video by swiping and add possibility to reordering pending videos when not loading

This commit is contained in:
Gergely Hegedus 2022-04-22 00:06:21 +03:00
parent 136530b927
commit db58234f74
20 changed files with 532 additions and 67 deletions

View file

@ -58,6 +58,12 @@ class VideoDownloadedLocalSource(
.plus(videoDownloaded.asString().addTimeAtStart()) .plus(videoDownloaded.asString().addTimeAtStart())
} }
fun removeVideo(videoDownloaded: VideoDownloaded) {
sharedPreferencesManagerImpl.downloadedVideos = sharedPreferencesManagerImpl.downloadedVideos
.filterNot { it.getTimeAndOriginal().second.asVideoDownloaded() == videoDownloaded }
.toSet()
}
companion object { companion object {
private fun VideoDownloaded.asString(): String = private fun VideoDownloaded.asString(): String =

View file

@ -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.joinNormalized
import org.fnives.tiktokdownloader.data.local.persistent.separateIntoDenormalized import org.fnives.tiktokdownloader.data.local.persistent.separateIntoDenormalized
import org.fnives.tiktokdownloader.data.model.VideoInPending import org.fnives.tiktokdownloader.data.model.VideoInPending
import kotlin.math.abs
class VideoInPendingLocalSource( class VideoInPendingLocalSource(
private val sharedPreferencesManager: SharedPreferencesManager, private val sharedPreferencesManager: SharedPreferencesManager,
@ -33,6 +34,27 @@ class VideoInPendingLocalSource(
.toSet() .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 { companion object {
private fun VideoInPending.asString(): String = listOf(id, url).joinNormalized() private fun VideoInPending.asString(): String = listOf(id, url).joinNormalized()

View file

@ -12,8 +12,8 @@ fun List<String>.joinNormalized() : String =
fun String.separateIntoDenormalized() : List<String> = fun String.separateIntoDenormalized() : List<String> =
split("$SEPARATOR$SEPARATOR").map { it.denormalize() } split("$SEPARATOR$SEPARATOR").map { it.denormalize() }
fun String.addTimeAtStart(index: Int = 0) = fun String.addTimeAtStart(time: Long = System.currentTimeMillis()) =
"${System.currentTimeMillis() + index}$TIME_SEPARATOR$this" "${time}$TIME_SEPARATOR$this"
fun String.getTimeAndOriginal(): Pair<Long, String> { fun String.getTimeAndOriginal(): Pair<Long, String> {
val time = takeWhile { it != TIME_SEPARATOR }.toLong() val time = takeWhile { it != TIME_SEPARATOR }.toLong()

View file

@ -3,11 +3,11 @@ package org.fnives.tiktokdownloader.data.usecase
import org.fnives.tiktokdownloader.data.local.VideoInPendingLocalSource import org.fnives.tiktokdownloader.data.local.VideoInPendingLocalSource
import org.fnives.tiktokdownloader.data.model.VideoInPending import org.fnives.tiktokdownloader.data.model.VideoInPending
class MoveVideoInQueue( class MoveVideoInQueueUseCase(
private val videoInPendingLocalSource: VideoInPendingLocalSource private val videoInPendingLocalSource: VideoInPendingLocalSource
) { ) {
operator fun invoke(videoInPending: VideoInPending, to: VideoInPending) { operator fun invoke(videoInPending: VideoInPending, positionDifference: Int) {
videoInPendingLocalSource.moveBy(videoInPending, positionDifference)
} }
} }

View file

@ -1,13 +1,20 @@
package org.fnives.tiktokdownloader.data.usecase 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.local.VideoInPendingLocalSource
import org.fnives.tiktokdownloader.data.model.VideoInPending import org.fnives.tiktokdownloader.data.model.VideoInPending
import org.fnives.tiktokdownloader.data.model.VideoState
class RemoveVideoFromQueueUseCase( class RemoveVideoFromQueueUseCase(
private val videoInPendingLocalSource: VideoInPendingLocalSource private val videoInPendingLocalSource: VideoInPendingLocalSource,
private val videoDownloadedLocalSource: VideoDownloadedLocalSource
) { ) {
operator fun invoke(videoInPending: VideoInPending) { operator fun invoke(videoState: VideoState) {
videoInPendingLocalSource.removeVideoFromQueue(videoInPending) when(videoState) {
is VideoState.Downloaded -> videoDownloadedLocalSource.removeVideo(videoState.videoDownloaded)
is VideoState.InPending -> videoInPendingLocalSource.removeVideoFromQueue(videoState.videoInPending)
is VideoState.InProcess -> Unit
}
} }
} }

View file

@ -8,17 +8,17 @@ import java.util.concurrent.TimeUnit
class LocalSourceModule(private val androidFileManagementModule: AndroidFileManagementModule) { class LocalSourceModule(private val androidFileManagementModule: AndroidFileManagementModule) {
val videoDownloadedLocalSource: VideoDownloadedLocalSource val videoDownloadedLocalSource: VideoDownloadedLocalSource by lazy {
get() = VideoDownloadedLocalSource( VideoDownloadedLocalSource(
saveVideoFile = androidFileManagementModule.saveVideoFile, saveVideoFile = androidFileManagementModule.saveVideoFile,
sharedPreferencesManagerImpl = androidFileManagementModule.sharedPreferencesManager, sharedPreferencesManagerImpl = androidFileManagementModule.sharedPreferencesManager,
verifyFileForUriExists = androidFileManagementModule.verifyFileForUriExists verifyFileForUriExists = androidFileManagementModule.verifyFileForUriExists
) )
}
val videoInPendingLocalSource: VideoInPendingLocalSource val videoInPendingLocalSource: VideoInPendingLocalSource by lazy {
get() = VideoInPendingLocalSource( VideoInPendingLocalSource(sharedPreferencesManager = androidFileManagementModule.sharedPreferencesManager)
sharedPreferencesManager = androidFileManagementModule.sharedPreferencesManager }
)
val videoInProgressLocalSource: VideoInProgressLocalSource by lazy { VideoInProgressLocalSource() } val videoInProgressLocalSource: VideoInProgressLocalSource by lazy { VideoInProgressLocalSource() }

View file

@ -2,7 +2,7 @@ package org.fnives.tiktokdownloader.di.module
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import org.fnives.tiktokdownloader.data.usecase.AddVideoToQueueUseCase 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.RemoveVideoFromQueueUseCase
import org.fnives.tiktokdownloader.data.usecase.StateOfVideosObservableUseCase import org.fnives.tiktokdownloader.data.usecase.StateOfVideosObservableUseCase
import org.fnives.tiktokdownloader.data.usecase.UrlVerificationUseCase import org.fnives.tiktokdownloader.data.usecase.UrlVerificationUseCase
@ -32,11 +32,12 @@ class UseCaseModule(
val removeVideoFromQueueUseCase: RemoveVideoFromQueueUseCase val removeVideoFromQueueUseCase: RemoveVideoFromQueueUseCase
get() = RemoveVideoFromQueueUseCase( get() = RemoveVideoFromQueueUseCase(
localSourceModule.videoInPendingLocalSource localSourceModule.videoInPendingLocalSource,
localSourceModule.videoDownloadedLocalSource
) )
val moveVideoInQueue: MoveVideoInQueue val moveVideoInQueueUseCase: MoveVideoInQueueUseCase
get() = MoveVideoInQueue( get() = MoveVideoInQueueUseCase(
localSourceModule.videoInPendingLocalSource localSourceModule.videoInPendingLocalSource
) )

View file

@ -25,6 +25,7 @@ class ViewModelModule(private val useCaseModule: UseCaseModule) {
useCaseModule.stateOfVideosObservableUseCase, useCaseModule.stateOfVideosObservableUseCase,
useCaseModule.addVideoToQueueUseCase, useCaseModule.addVideoToQueueUseCase,
useCaseModule.removeVideoFromQueueUseCase, useCaseModule.removeVideoFromQueueUseCase,
useCaseModule.videoDownloadingProcessorUseCase useCaseModule.videoDownloadingProcessorUseCase,
useCaseModule.moveVideoInQueueUseCase
) )
} }

View file

@ -0,0 +1,8 @@
package org.fnives.tiktokdownloader.ui.main.queue
interface MovingItemCallback {
fun onMovingStart()
fun onMovingEnd()
}

View file

@ -57,14 +57,18 @@ class QueueFragment : Fragment(R.layout.fragment_queue) {
) )
recycler.adapter = adapter recycler.adapter = adapter
val touchHelper = ItemTouchHelper(PendingItemTouchHelper( val callback = VideoStateItemTouchHelper(
whichItem = { adapter.currentList.getOrNull(it.bindingAdapterPosition) }, whichItem = { adapter.currentList.getOrNull(it.bindingAdapterPosition) },
onDeleteElement = viewModel::onElementDeleted, onDeleteElement = viewModel::onElementDeleted,
onMovedElement = viewModel::onElementMoved onUIMoveElement = adapter::swap,
)) onMoveElement = viewModel::onElementMoved
)
val touchHelper = ItemTouchHelper(callback)
touchHelper.attachToRecyclerView(recycler) touchHelper.attachToRecyclerView(recycler)
viewModel.downloads.observe(viewLifecycleOwner) { videoStates -> viewModel.downloads.observe(viewLifecycleOwner) { videoStates ->
callback.dragEnabled = videoStates.none { it is VideoState.InProcess }
adapter.submitList(videoStates, Runnable { adapter.submitList(videoStates, Runnable {
val indexToScrollTo = videoStates.indexOfFirst { it is VideoState.InProcess } val indexToScrollTo = videoStates.indexOfFirst { it is VideoState.InProcess }
.takeIf { it != -1 } ?: return@Runnable .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 { companion object {
fun newInstance(): QueueFragment = QueueFragment() fun newInstance(): QueueFragment = QueueFragment()

View file

@ -4,6 +4,8 @@ import android.view.ViewGroup
import android.widget.ImageView import android.widget.ImageView
import android.widget.ProgressBar import android.widget.ProgressBar
import android.widget.TextView import android.widget.TextView
import androidx.cardview.widget.CardView
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.view.isInvisible import androidx.core.view.isInvisible
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.recyclerview.widget.DiffUtil 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)) { 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 urlView: TextView = itemView.findViewById(R.id.url)
val statusView: TextView = itemView.findViewById(R.id.status) val statusView: TextView = itemView.findViewById(R.id.status)
val thumbNailView: ImageView = itemView.findViewById(R.id.thumbnail) val thumbNailView: ImageView = itemView.findViewById(R.id.thumbnail)
val progress: ProgressBar = itemView.findViewById(R.id.progress) 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<VideoState>() { class DiffUtilItemCallback : DiffUtil.ItemCallback<VideoState>() {

View file

@ -5,7 +5,7 @@ import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import org.fnives.tiktokdownloader.data.model.VideoState import org.fnives.tiktokdownloader.data.model.VideoState
import org.fnives.tiktokdownloader.data.usecase.AddVideoToQueueUseCase 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.RemoveVideoFromQueueUseCase
import org.fnives.tiktokdownloader.data.usecase.StateOfVideosObservableUseCase import org.fnives.tiktokdownloader.data.usecase.StateOfVideosObservableUseCase
import org.fnives.tiktokdownloader.data.usecase.VideoDownloadingProcessorUseCase import org.fnives.tiktokdownloader.data.usecase.VideoDownloadingProcessorUseCase
@ -17,7 +17,7 @@ class QueueViewModel(
private val addVideoToQueueUseCase: AddVideoToQueueUseCase, private val addVideoToQueueUseCase: AddVideoToQueueUseCase,
private val removeVideoFromQueueUseCase: RemoveVideoFromQueueUseCase, private val removeVideoFromQueueUseCase: RemoveVideoFromQueueUseCase,
private val videoDownloadingProcessorUseCase: VideoDownloadingProcessorUseCase, private val videoDownloadingProcessorUseCase: VideoDownloadingProcessorUseCase,
private val moveVideoInQueue: MoveVideoInQueue private val moveVideoInQueueUseCase: MoveVideoInQueueUseCase
) : ViewModel() { ) : ViewModel() {
val downloads = asLiveData(stateOfVideosObservableUseCase()) val downloads = asLiveData(stateOfVideosObservableUseCase())
@ -38,17 +38,12 @@ class QueueViewModel(
} }
fun onElementDeleted(videoState: VideoState) { fun onElementDeleted(videoState: VideoState) {
when (videoState) { removeVideoFromQueueUseCase(videoState)
is VideoState.InPending -> removeVideoFromQueueUseCase(videoState.videoInPending)
is VideoState.Downloaded,
is VideoState.InProcess -> Unit
}
} }
fun onElementMoved(moved: VideoState, to: VideoState): Boolean { fun onElementMoved(moved: VideoState, positionDifference: Int): Boolean {
if (moved !is VideoState.InPending) return false if (moved !is VideoState.InPending) return false
if (to !is VideoState.InPending) return false moveVideoInQueueUseCase(moved.videoInPending, positionDifference)
moveVideoInQueue(moved.videoInPending, to.videoInPending)
return true return true
} }

View file

@ -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
}
}

View file

@ -3,6 +3,7 @@
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
xmlns:tools="http://schemas.android.com/tools"
app:layoutDescription="@xml/queue_motion_description"> app:layoutDescription="@xml/queue_motion_description">
<View <View
@ -91,6 +92,8 @@
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
tools:itemCount="3"
tools:listitem="@layout/item_downloaded"
app:layout_constraintTop_toBottomOf="@id/toolbar_background" /> app:layout_constraintTop_toBottomOf="@id/toolbar_background" />
</androidx.constraintlayout.motion.widget.MotionLayout> </androidx.constraintlayout.motion.widget.MotionLayout>

View file

@ -12,6 +12,7 @@
<androidx.constraintlayout.widget.ConstraintLayout <androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:id="@+id/content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:background="?attr/selectableItemBackground" android:background="?attr/selectableItemBackground"
android:padding="@dimen/medium_padding"> android:padding="@dimen/medium_padding">

View file

@ -29,6 +29,7 @@ import org.mockito.kotlin.whenever
import java.io.InputStream import java.io.InputStream
@Suppress("TestFunctionName")
@Timeout(value = 2) @Timeout(value = 2)
class VideoDownloadedLocalSourceTest { class VideoDownloadedLocalSourceTest {
@ -195,6 +196,50 @@ class VideoDownloadedLocalSourceTest {
Assertions.assertEquals(expected, actual.await()) 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() { class FalseInputStream : InputStream() {
override fun read(): Int = 0 override fun read(): Int = 0
} }

View file

@ -17,7 +17,7 @@ import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import org.junit.jupiter.api.Timeout import org.junit.jupiter.api.Timeout
@Suppress("TestFunctionName")
@Timeout(value = 2) @Timeout(value = 2)
class VideoInPendingLocalSourceTest { class VideoInPendingLocalSourceTest {
@ -76,8 +76,7 @@ class VideoInPendingLocalSourceTest {
} }
@Test @Test
fun GIVEN_observing_PendingVideos_WHEN_2_video_marked_as_pending_THEN_both_of_them_are_sent_out_in_correct_order() = fun GIVEN_observing_PendingVideos_WHEN_2_video_marked_as_pending_THEN_both_of_them_are_sent_out_in_correct_order() = runBlocking<Unit> {
runBlocking<Unit> {
val videoInPending1 = VideoInPending("id1", "alma1") val videoInPending1 = VideoInPending("id1", "alma1")
val videoInPending2 = VideoInPending("id2", "alma2") val videoInPending2 = VideoInPending("id2", "alma2")
val expected = listOf(emptyList(), listOf(videoInPending1), listOf(videoInPending1, videoInPending2)) val expected = listOf(emptyList(), listOf(videoInPending1), listOf(videoInPending1, videoInPending2))
@ -94,4 +93,92 @@ class VideoInPendingLocalSourceTest {
Assertions.assertEquals(expected, actual.await()) Assertions.assertEquals(expected, actual.await())
} }
@Test
fun GIVEN_2_videos_WHEN_moving_first_one_down_by_one_THEN_it_is_moved_properly() = runBlocking<Unit> {
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<Unit> {
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<Unit> {
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())
}
} }

View file

@ -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)
}
}

View file

@ -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)
}
}

View file

@ -2,9 +2,13 @@ package org.fnives.tiktokdownloader.ui.main.queue
import com.jraska.livedata.test import com.jraska.livedata.test
import kotlinx.coroutines.flow.MutableSharedFlow 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.VideoInPending
import org.fnives.tiktokdownloader.data.model.VideoInProgress
import org.fnives.tiktokdownloader.data.model.VideoState import org.fnives.tiktokdownloader.data.model.VideoState
import org.fnives.tiktokdownloader.data.usecase.AddVideoToQueueUseCase 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.StateOfVideosObservableUseCase
import org.fnives.tiktokdownloader.data.usecase.VideoDownloadingProcessorUseCase import org.fnives.tiktokdownloader.data.usecase.VideoDownloadingProcessorUseCase
import org.fnives.tiktokdownloader.helper.junit.rule.InstantExecutorExtension import org.fnives.tiktokdownloader.helper.junit.rule.InstantExecutorExtension
@ -28,7 +32,9 @@ class QueueViewModelTest {
private lateinit var stateOfVideosFlow: MutableSharedFlow<List<VideoState>> private lateinit var stateOfVideosFlow: MutableSharedFlow<List<VideoState>>
private lateinit var mockStateOfVideosObservableUseCase: StateOfVideosObservableUseCase private lateinit var mockStateOfVideosObservableUseCase: StateOfVideosObservableUseCase
private lateinit var mockAddVideoToQueueUseCase: AddVideoToQueueUseCase private lateinit var mockAddVideoToQueueUseCase: AddVideoToQueueUseCase
private lateinit var mockRemoveVideoFromQueueUseCase: RemoveVideoFromQueueUseCase
private lateinit var mockVideoDownloadingProcessorUseCase: VideoDownloadingProcessorUseCase private lateinit var mockVideoDownloadingProcessorUseCase: VideoDownloadingProcessorUseCase
private lateinit var mockMoveVideoInQueueUseCase: MoveVideoInQueueUseCase
private lateinit var sut: QueueViewModel private lateinit var sut: QueueViewModel
@BeforeEach @BeforeEach
@ -37,8 +43,16 @@ class QueueViewModelTest {
mockStateOfVideosObservableUseCase = mock() mockStateOfVideosObservableUseCase = mock()
whenever(mockStateOfVideosObservableUseCase.invoke()).doReturn(stateOfVideosFlow) whenever(mockStateOfVideosObservableUseCase.invoke()).doReturn(stateOfVideosFlow)
mockAddVideoToQueueUseCase = mock() mockAddVideoToQueueUseCase = mock()
mockRemoveVideoFromQueueUseCase = mock()
mockVideoDownloadingProcessorUseCase = mock() mockVideoDownloadingProcessorUseCase = mock()
sut = QueueViewModel(mockStateOfVideosObservableUseCase, mockAddVideoToQueueUseCase, mockVideoDownloadingProcessorUseCase) mockMoveVideoInQueueUseCase = mock()
sut = QueueViewModel(
mockStateOfVideosObservableUseCase,
mockAddVideoToQueueUseCase,
mockRemoveVideoFromQueueUseCase,
mockVideoDownloadingProcessorUseCase,
mockMoveVideoInQueueUseCase
)
} }
@Test @Test
@ -48,6 +62,8 @@ class QueueViewModelTest {
verifyNoMoreInteractions(mockStateOfVideosObservableUseCase) verifyNoMoreInteractions(mockStateOfVideosObservableUseCase)
verifyNoInteractions(mockAddVideoToQueueUseCase) verifyNoInteractions(mockAddVideoToQueueUseCase)
verifyNoInteractions(mockVideoDownloadingProcessorUseCase) verifyNoInteractions(mockVideoDownloadingProcessorUseCase)
verifyNoInteractions(mockRemoveVideoFromQueueUseCase)
verifyNoInteractions(mockMoveVideoInQueueUseCase)
} }
@Test @Test
@ -60,6 +76,8 @@ class QueueViewModelTest {
verifyNoMoreInteractions(mockStateOfVideosObservableUseCase) verifyNoMoreInteractions(mockStateOfVideosObservableUseCase)
verifyNoInteractions(mockAddVideoToQueueUseCase) verifyNoInteractions(mockAddVideoToQueueUseCase)
verifyNoInteractions(mockVideoDownloadingProcessorUseCase) verifyNoInteractions(mockVideoDownloadingProcessorUseCase)
verifyNoInteractions(mockRemoveVideoFromQueueUseCase)
verifyNoInteractions(mockMoveVideoInQueueUseCase)
} }
@Test @Test
@ -77,6 +95,8 @@ class QueueViewModelTest {
verifyNoMoreInteractions(mockStateOfVideosObservableUseCase) verifyNoMoreInteractions(mockStateOfVideosObservableUseCase)
verifyNoInteractions(mockAddVideoToQueueUseCase) verifyNoInteractions(mockAddVideoToQueueUseCase)
verifyNoInteractions(mockVideoDownloadingProcessorUseCase) verifyNoInteractions(mockVideoDownloadingProcessorUseCase)
verifyNoInteractions(mockRemoveVideoFromQueueUseCase)
verifyNoInteractions(mockMoveVideoInQueueUseCase)
} }
@Test @Test
@ -89,6 +109,8 @@ class QueueViewModelTest {
verifyNoMoreInteractions(mockAddVideoToQueueUseCase) verifyNoMoreInteractions(mockAddVideoToQueueUseCase)
verify(mockVideoDownloadingProcessorUseCase, times(1)).fetchVideoInState() verify(mockVideoDownloadingProcessorUseCase, times(1)).fetchVideoInState()
verifyNoMoreInteractions(mockVideoDownloadingProcessorUseCase) verifyNoMoreInteractions(mockVideoDownloadingProcessorUseCase)
verifyNoInteractions(mockRemoveVideoFromQueueUseCase)
verifyNoInteractions(mockMoveVideoInQueueUseCase)
} }
@Test @Test
@ -103,6 +125,8 @@ class QueueViewModelTest {
verifyNoMoreInteractions(mockAddVideoToQueueUseCase) verifyNoMoreInteractions(mockAddVideoToQueueUseCase)
verify(mockVideoDownloadingProcessorUseCase, times(2)).fetchVideoInState() verify(mockVideoDownloadingProcessorUseCase, times(2)).fetchVideoInState()
verifyNoMoreInteractions(mockVideoDownloadingProcessorUseCase) verifyNoMoreInteractions(mockVideoDownloadingProcessorUseCase)
verifyNoInteractions(mockRemoveVideoFromQueueUseCase)
verifyNoInteractions(mockMoveVideoInQueueUseCase)
} }
@Test @Test
@ -125,4 +149,86 @@ class QueueViewModelTest {
.assertHistorySize(1) .assertHistorySize(1)
.assertValue(Event(QueueViewModel.NavigationEvent.OpenGallery("alma.com"))) .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)
}
} }