Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Post List: Fix excessive media requests #20939

Merged
merged 4 commits into from
Jun 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -174,8 +174,8 @@ class PostListEventListener(
@Suppress("unused", "SpreadOperator")
@Subscribe(threadMode = BACKGROUND)
fun onMediaChanged(event: OnMediaChanged) {
featuredMediaChanged(*event.mediaList.map { it.mediaId }.toLongArray())
if (!event.isError) {
featuredMediaChanged(*event.mediaList.map { it.mediaId }.toLongArray())
uploadStatusChanged(*event.mediaList.map { it.localPostId }.toIntArray())
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package org.wordpress.android.ui.posts

import android.annotation.SuppressLint
import androidx.annotation.VisibleForTesting
import org.wordpress.android.fluxc.Dispatcher
import org.wordpress.android.fluxc.generated.MediaActionBuilder
import org.wordpress.android.fluxc.model.MediaModel
Expand All @@ -22,21 +23,39 @@ class PostListFeaturedImageTracker(private val dispatcher: Dispatcher, private v
https://github.com/wordpress-mobile/WordPress-Android/issues/11487
*/
@SuppressLint("UseSparseArrays")
private val featuredImageMap = HashMap<Long, String>()
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
internal val featuredImageMap = HashMap<Long, String>()

@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
internal val ongoingRequests = HashSet<Long>()

fun getFeaturedImageUrl(site: SiteModel, featuredImageId: Long): String? {
if (featuredImageId == 0L) {
return null
}
featuredImageMap[featuredImageId]?.let { return it }

featuredImageMap[featuredImageId]?.let {
return it
}

// Check if a request for this image is already ongoing
if (ongoingRequests.contains(featuredImageId)) {
// If the request is ongoing, just return. The callback will be invoked upon completion.
return null
}

mediaStore.getSiteMediaWithId(site, featuredImageId)?.let { media ->
// This should be a pretty rare case, but some media seems to be missing url
return if (media.url.isNotBlank()) {
featuredImageMap[featuredImageId] = media.url
media.url
} else null
}

// Media is not in the Store, we need to download it
// Mark the request as ongoing
ongoingRequests.add(featuredImageId)

val mediaToDownload = MediaModel(
site.id,
featuredImageId
Expand All @@ -47,6 +66,9 @@ class PostListFeaturedImageTracker(private val dispatcher: Dispatcher, private v
}

fun invalidateFeaturedMedia(featuredImageIds: List<Long>) {
featuredImageIds.forEach { featuredImageMap.remove(it) }
featuredImageIds.forEach {
featuredImageMap.remove(it)
ongoingRequests.remove(it)
}
Comment on lines +69 to +72
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will this be called when the featured image request is completed? I mention this because I haven't found any other place where the ongoing request is removed.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @fluiddot
The places where this funcation gets called is PostListEventListenerfeaturedMediaChanged.
The featuredMediaChanged is called when the media is changed or uploaded. Correct me if I am wrong. @zwarm

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the question, which @AjeshRPai answered before me down below #20939 (comment)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The places where this funcation gets called is PostListEventListener → featuredMediaChanged.
The featuredMediaChanged is called when the media is changed or uploaded. Correct me if I am wrong.

This is correct, previously only success calls were removed from the featureImageMap and in the updated version, both success and errors are removed. We need to make sure we don't have requests hanging around when they shouldn't be.

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
package org.wordpress.android.ui.posts

import kotlinx.coroutines.ExperimentalCoroutinesApi
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNull
import org.junit.Before
import org.mockito.kotlin.any
import org.mockito.kotlin.mock
import org.mockito.kotlin.never
import org.mockito.kotlin.verify
import org.mockito.kotlin.whenever
import org.wordpress.android.BaseUnitTest
import org.wordpress.android.fluxc.Dispatcher
import org.wordpress.android.fluxc.model.MediaModel
import org.wordpress.android.fluxc.model.SiteModel
import org.wordpress.android.fluxc.store.MediaStore
import kotlin.test.Test

@Suppress("UNCHECKED_CAST")
@ExperimentalCoroutinesApi
class PostListFeaturedImageTrackerTest : BaseUnitTest() {
private val dispatcher: Dispatcher = mock()
private val mediaStore: MediaStore = mock()

private lateinit var tracker: PostListFeaturedImageTracker

private val site = SiteModel().apply { id = 123 }

@Before
fun setup() {
tracker = PostListFeaturedImageTracker(dispatcher, mediaStore)
}

@Test
fun `given id exists in map, when getFeaturedImageUrl invoked, then return url`() {
val imageId = 123L
val imageUrl = "https://example.com/image.jpg"
tracker.featuredImageMap[imageId] = imageUrl

val result = tracker.getFeaturedImageUrl(site, imageId)

assertEquals(imageUrl, result)
}

@Test
fun `given id is 0, when getFeaturedImageUrl invoked, then return null`() {
val result = tracker.getFeaturedImageUrl(site, 0L)

assertNull(result)
}

@Test
fun `given id not in map and exists in store, when invoked, then return url from media store`() {
val imageId = 456L
val imageUrl = "https://example.com/image.jpg"
val mediaModel = MediaModel(site.id, imageId).apply {
url = imageUrl
}

whenever(mediaStore.getSiteMediaWithId(site, imageId)).thenReturn(mediaModel)

val result = tracker.getFeaturedImageUrl(site, imageId)

assertEquals(imageUrl, result)
assertEquals(imageUrl, tracker.featuredImageMap[imageId])
}

@Test
fun `given id not in map or store, when invoked, then return null and dispatch fetch request`() {
val imageId = 123L

whenever(mediaStore.getSiteMediaWithId(site, imageId)).thenReturn(null)

val result = tracker.getFeaturedImageUrl(site, imageId)

assertNull(result)
verify(dispatcher).dispatch(any())
assert(tracker.ongoingRequests.contains(imageId))
}

@Test
fun `given request ongoing for id, when invoked, should return null`() {
val imageId = 123L

tracker.ongoingRequests.add(imageId)

val result = tracker.getFeaturedImageUrl(site, imageId)

assertNull(result)
verify(mediaStore, never()).getSiteMediaWithId(site, imageId)
verify(dispatcher, never()).dispatch(any())
}

@Test
fun `given id in map and ongoingRequests, when invalidate, then remove id from map and ongoingRequests`() {
val imageId1 = 123L
val imageId2 = 456L

tracker.featuredImageMap[imageId1] = "https://example.com/image1.jpg"
tracker.featuredImageMap[imageId2] = "https://example.com/image2.jpg"
tracker.ongoingRequests.add(imageId1)
tracker.ongoingRequests.add(imageId2)

tracker.invalidateFeaturedMedia(listOf(imageId1, imageId2))

assertNull(tracker.featuredImageMap[imageId1])
assertNull(tracker.featuredImageMap[imageId2])
assert(!tracker.ongoingRequests.contains(imageId1))
assert(!tracker.ongoingRequests.contains(imageId2))
}
}
Loading