support preloading for lottie and video

commit_hash:d013f22d82386c3052abfc55f4b154e4f658d50a
This commit is contained in:
i-ts
2026-03-18 14:28:11 +03:00
parent a53cf048dc
commit c768674d8f
20 changed files with 494 additions and 36 deletions
+5
View File
@@ -1190,6 +1190,10 @@
"client/android/div/src/main/java/com/yandex/div/core/player/DivVideoSource.kt":"divkit/public/client/android/div/src/main/java/com/yandex/div/core/player/DivVideoSource.kt",
"client/android/div/src/main/java/com/yandex/div/core/player/DivVideoViewMapper.kt":"divkit/public/client/android/div/src/main/java/com/yandex/div/core/player/DivVideoViewMapper.kt",
"client/android/div/src/main/java/com/yandex/div/core/player/PlayerViewsVisibilityChangeListener.kt":"divkit/public/client/android/div/src/main/java/com/yandex/div/core/player/PlayerViewsVisibilityChangeListener.kt",
"client/android/div/src/main/java/com/yandex/div/core/preload/PreloadResult.kt":"divkit/public/client/android/div/src/main/java/com/yandex/div/core/preload/PreloadResult.kt",
"client/android/div/src/main/java/com/yandex/div/core/preload/PreloadingCompletion.kt":"divkit/public/client/android/div/src/main/java/com/yandex/div/core/preload/PreloadingCompletion.kt",
"client/android/div/src/main/java/com/yandex/div/core/preload/PreloadingCompletionImpl.kt":"divkit/public/client/android/div/src/main/java/com/yandex/div/core/preload/PreloadingCompletionImpl.kt",
"client/android/div/src/main/java/com/yandex/div/core/preload/PreloadingRegistry.kt":"divkit/public/client/android/div/src/main/java/com/yandex/div/core/preload/PreloadingRegistry.kt",
"client/android/div/src/main/java/com/yandex/div/core/resources/ContextThemeWrapperWithResourceCache.kt":"divkit/public/client/android/div/src/main/java/com/yandex/div/core/resources/ContextThemeWrapperWithResourceCache.kt",
"client/android/div/src/main/java/com/yandex/div/core/resources/PrimitiveResourceCache.kt":"divkit/public/client/android/div/src/main/java/com/yandex/div/core/resources/PrimitiveResourceCache.kt",
"client/android/div/src/main/java/com/yandex/div/core/resources/ResourcesWrapper.kt":"divkit/public/client/android/div/src/main/java/com/yandex/div/core/resources/ResourcesWrapper.kt",
@@ -1625,6 +1629,7 @@
"client/android/div/src/test/java/com/yandex/div/core/expression/variables/DivVariablesParserTest.kt":"divkit/public/client/android/div/src/test/java/com/yandex/div/core/expression/variables/DivVariablesParserTest.kt",
"client/android/div/src/test/java/com/yandex/div/core/expression/variables/TwoWayVariableBinderTest.kt":"divkit/public/client/android/div/src/test/java/com/yandex/div/core/expression/variables/TwoWayVariableBinderTest.kt",
"client/android/div/src/test/java/com/yandex/div/core/expression/variables/VariableControllerTest.kt":"divkit/public/client/android/div/src/test/java/com/yandex/div/core/expression/variables/VariableControllerTest.kt",
"client/android/div/src/test/java/com/yandex/div/core/preload/PreloadingCompletionImplTest.kt":"divkit/public/client/android/div/src/test/java/com/yandex/div/core/preload/PreloadingCompletionImplTest.kt",
"client/android/div/src/test/java/com/yandex/div/core/resources/ResourcesWrapperTest.kt":"divkit/public/client/android/div/src/test/java/com/yandex/div/core/resources/ResourcesWrapperTest.kt",
"client/android/div/src/test/java/com/yandex/div/core/state/DivStatePathTest.kt":"divkit/public/client/android/div/src/test/java/com/yandex/div/core/state/DivStatePathTest.kt",
"client/android/div/src/test/java/com/yandex/div/core/state/DivStatePathUtilsTest.kt":"divkit/public/client/android/div/src/test/java/com/yandex/div/core/state/DivStatePathUtilsTest.kt",
@@ -5,6 +5,9 @@ import android.net.Uri
import com.airbnb.lottie.LottieComposition
import com.airbnb.lottie.LottieCompositionFactory
import com.airbnb.lottie.LottieResult
import com.yandex.div.core.preload.PreloadResult
import com.yandex.div.core.preload.UriPreloadResult
import com.yandex.div.internal.Assert
import com.yandex.div.lottie.DivLottieRawResProvider.Companion.ASSET_SCHEME
import com.yandex.div.lottie.DivLottieRawResProvider.Companion.DIVKIT_ASSET_SCHEME
import com.yandex.div.lottie.DivLottieRawResProvider.Companion.HTTPS_SCHEME
@@ -36,10 +39,32 @@ internal class DivLottieCompositionRepository(
}
}
internal fun preloadLottieComposition(url: Uri) {
internal fun preloadLottieComposition(url: Uri, onComplete: (PreloadResult) -> Unit = { }) {
when (url.scheme) {
HTTP_SCHEME, HTTPS_SCHEME -> {
networkCache.cacheComposition(url.toString())
val supported = networkCache.cacheComposition(url.toString()) { error ->
onComplete(UriPreloadResult(url, error))
}
if (!supported) {
Assert.fail("Lottie preloading works unstable! " +
"Please implement DivLottieNetworkCache.cacheComposition(String, onComplete)!")
networkCache.cacheComposition(url.toString())
onComplete(UriPreloadResult(url, null))
}
}
RES_SCHEME, ASSET_SCHEME, DIVKIT_ASSET_SCHEME -> {
onComplete(UriPreloadResult(url, null))
}
else -> {
onComplete(
UriPreloadResult(
url, RuntimeException(
"Unsupported scheme '${url.scheme}'. Preload cancelled"
)
)
)
}
}
}
@@ -7,6 +7,7 @@ import com.airbnb.lottie.LottieDrawable
import com.airbnb.lottie.LottieResult
import com.yandex.div.core.Disposable
import com.yandex.div.core.extension.DivExtensionHandler
import com.yandex.div.core.preload.PreloadingRegistry
import com.yandex.div.core.util.toIntSafely
import com.yandex.div.core.view2.Div2View
import com.yandex.div.core.widget.LoadableImageView
@@ -48,10 +49,30 @@ open class DivLottieExtensionHandler(
override val subscriptions: MutableList<Disposable> = mutableListOf()
override fun preprocess(div: DivBase, expressionResolver: ExpressionResolver) {
preprocessInternal(div, expressionResolver, null)
}
override fun preprocess(
div: DivBase,
expressionResolver: ExpressionResolver,
preloadingRegistry: PreloadingRegistry,
) {
preprocessInternal(div, expressionResolver, preloadingRegistry)
}
private fun preprocessInternal(
div: DivBase,
expressionResolver: ExpressionResolver,
preloadingRegistry: PreloadingRegistry?,
) {
val lottieUrl = div.extensions
?.find { extension -> extension.id == EXTENSION_ID }
?.params?.lottieUrl ?: return
repo.preloadLottieComposition(lottieUrl.evaluate(expressionResolver))
val url = lottieUrl.evaluate(expressionResolver)
val preloading = preloadingRegistry?.registerPreloading("lottie")
repo.preloadLottieComposition(url) { result ->
preloading?.onCompleted(result)
}
}
override fun beforeBindView(divView: Div2View, expressionResolver: ExpressionResolver, view: View, div: DivBase) {
@@ -16,6 +16,14 @@ interface DivLottieNetworkCache {
*/
fun cacheComposition(url: String)
/**
* Requests caching lottie composition from network with result.
* @return true if caching callbacks supported.
*/
fun cacheComposition(url: String, onComplete: ((Throwable?) -> Unit)): Boolean {
return false
}
companion object {
@JvmStatic
val STUB = object : DivLottieNetworkCache {
@@ -9,6 +9,8 @@ import androidx.media3.datasource.cache.CacheWriter
import com.yandex.div.core.DivPreloader
import com.yandex.div.core.player.DivPlayerFactory
import com.yandex.div.core.player.DivPlayerPreloader
import com.yandex.div.core.preload.PreloadResult
import com.yandex.div.core.preload.UriPreloadResult
import com.yandex.div.internal.KLog
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
@@ -25,8 +27,19 @@ public class ExoPlayerVideoPreloader(
private val cache: ExoPlayerCache = ExoPlayerCache(context)
): DivPlayerPreloader {
override fun preloadVideo(src: List<Uri>): DivPreloader.PreloadReference {
return preloadVideoInternal(src) {}
}
override fun preloadVideo(src: List<Uri>, callback: (List<PreloadResult>) -> Unit): DivPreloader.PreloadReference {
return preloadVideoInternal(src, callback)
}
private fun preloadVideoInternal(
src: List<Uri>,
callback: (List<PreloadResult>) -> Unit,
): DivPreloader.PreloadReference {
if (src.isEmpty()) return DivPreloader.PreloadReference.EMPTY
val job = GlobalScope.launch(Dispatchers.IO) { preCacheVideo(src) }
val job = GlobalScope.launch(Dispatchers.IO) { preCacheVideo(src, callback) }
return DivPreloader.PreloadReference {
job.cancel()
}
@@ -36,7 +49,9 @@ public class ExoPlayerVideoPreloader(
return ExoDivPlayerFactory(context)
}
private fun preCacheVideo(src: List<Uri>) {
private fun preCacheVideo(src: List<Uri>, callback: (List<PreloadResult>) -> Unit) {
val results = mutableListOf<PreloadResult>()
src.forEach { videoUri ->
val dataSpec = DataSpec(videoUri)
@@ -46,15 +61,17 @@ public class ExoPlayerVideoPreloader(
KLog.d(TAG) { "downloadPercentage $downloadPercentage videoUri: $videoUri" }
}
cacheVideo(dataSpec, progressListener)
val error = cacheVideo(dataSpec, progressListener)
results.add(UriPreloadResult(videoUri, error))
}
callback(results)
}
private fun cacheVideo(
dataSpec: DataSpec,
progressListener: CacheWriter.ProgressListener
) {
runCatching {
): Throwable? {
return runCatching {
CacheWriter(
cache.cacheDataSourceFactory.createDataSource(),
dataSpec,
@@ -64,6 +81,6 @@ public class ExoPlayerVideoPreloader(
}.onFailure {
KLog.e(TAG) { "error on loading video with URL = \"${dataSpec.uri}\"" }
it.printStackTrace()
}
}.exceptionOrNull()
}
}
@@ -7,6 +7,8 @@ import com.google.android.exoplayer2.upstream.cache.CacheWriter
import com.yandex.div.core.DivPreloader
import com.yandex.div.core.player.DivPlayerFactory
import com.yandex.div.core.player.DivPlayerPreloader
import com.yandex.div.core.preload.PreloadResult
import com.yandex.div.core.preload.UriPreloadResult
import com.yandex.div.internal.KLog
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
@@ -23,8 +25,21 @@ public class ExoPlayerVideoPreloader(
private val cache: ExoPlayerCache = ExoPlayerCache(context)
): DivPlayerPreloader {
override fun preloadVideo(src: List<Uri>): DivPreloader.PreloadReference {
return preloadVideoInternal(src) {}
}
override fun preloadVideo(src: List<Uri>, callback: (List<PreloadResult>) -> Unit): DivPreloader.PreloadReference {
return preloadVideoInternal(src, callback)
}
private fun preloadVideoInternal(src: List<Uri>, callback: (List<PreloadResult>) -> Unit): DivPreloader.PreloadReference {
if (src.isEmpty()) return DivPreloader.PreloadReference.EMPTY
val job = GlobalScope.launch(Dispatchers.IO) { preCacheVideo(src) }
val job = GlobalScope.launch(Dispatchers.IO) {
val results = mutableListOf<PreloadResult>()
preCacheVideo(src) { uri, error ->
results.add(UriPreloadResult(uri, error))
}
callback(results)
}
return DivPreloader.PreloadReference {
job.cancel()
}
@@ -34,7 +49,7 @@ public class ExoPlayerVideoPreloader(
return ExoDivPlayerFactory(context)
}
private fun preCacheVideo(src: List<Uri>) {
private fun preCacheVideo(src: List<Uri>, callback: (Uri, Throwable?) -> Unit) {
src.forEach { videoUri ->
val dataSpec = DataSpec(videoUri)
@@ -44,15 +59,16 @@ public class ExoPlayerVideoPreloader(
KLog.d(TAG) { "downloadPercentage $downloadPercentage videoUri: $videoUri" }
}
cacheVideo(dataSpec, progressListener)
val error = cacheVideo(dataSpec, progressListener)
callback(videoUri, error)
}
}
private fun cacheVideo(
dataSpec: DataSpec,
progressListener: CacheWriter.ProgressListener
) {
runCatching {
): Throwable? {
return runCatching {
CacheWriter(
cache.cacheDataSourceFactory.createDataSource(),
dataSpec,
@@ -62,6 +78,6 @@ public class ExoPlayerVideoPreloader(
}.onFailure {
KLog.e(TAG) { "error on loading video with URL = \"${dataSpec.uri}\"" }
it.printStackTrace()
}
}.exceptionOrNull()
}
}
@@ -10,8 +10,17 @@ import com.yandex.div.core.images.CachedBitmap
import com.yandex.div.core.images.DivCachedImage
import com.yandex.div.core.images.DivImageDownloadCallback
import com.yandex.div.core.images.LoadReference
import com.yandex.div.core.preload.CompositeResult
import com.yandex.div.core.player.DivPlayerPreloader
import com.yandex.div.core.preload.PreloadResult
import com.yandex.div.core.preload.PreloadingCompletion
import com.yandex.div.core.preload.PreloadingCompletionImpl
import com.yandex.div.core.preload.PreloadingRegistry
import com.yandex.div.core.preload.filterErrorResults
import com.yandex.div.core.view2.DivImagePreloader
import com.yandex.div.internal.Assert
import com.yandex.div.internal.KLog
import com.yandex.div.internal.Log
import com.yandex.div.internal.core.DivVisitor
import com.yandex.div.internal.core.buildItems
import com.yandex.div.internal.core.nonNullItems
@@ -85,7 +94,7 @@ class DivPreloader internal constructor(
override fun defaultVisit(data: Div, resolver: ExpressionResolver) {
imagePreloader?.preloadImage(data, resolver, preloadFilter, downloadCallback)
?.forEach { ticket.addImageReference(it) }
extensionController.preprocessExtensions(data.value(), resolver)
extensionController.preprocessExtensions(data.value(), resolver, downloadCallback)
}
override fun visit(data: Div.Container, resolver: ExpressionResolver) {
@@ -126,12 +135,16 @@ class DivPreloader internal constructor(
override fun visit(data: Div.Video, resolver: ExpressionResolver) {
defaultVisit(data, resolver)
if (preloadFilter.shouldPreloadContent(data, resolver)) {
val shouldPreloadContent = preloadFilter.shouldPreloadContent(data, resolver)
if (shouldPreloadContent) {
val preloading = downloadCallback.registerPreloading("video")
val sources = mutableListOf<Uri>()
data.value.videoSources?.forEach {
sources.add(it.url.evaluate(resolver))
}
videoPreloader.preloadVideo(sources).also { ticket.addReference(it) }
videoPreloader.preloadVideo(sources, callback = { results: List<PreloadResult> ->
preloading.onCompleted(CompositeResult(results))
}).also { ticket.addReference(it) }
}
}
}
@@ -164,13 +177,23 @@ class DivPreloader internal constructor(
}
}
class DownloadCallback(private val callback: Callback) : DivImageDownloadCallback() {
private var downloadsLeftCount = 0
private var failures = 0
class DownloadCallback(private val callback: Callback) : DivImageDownloadCallback(), PreloadingRegistry {
private val activePreloads = mutableMapOf<String, PreloadingCompletionImpl>()
private val legacyPreloading = LegacyPreloading()
private var started = false
override fun registerPreloading(tag: String): PreloadingCompletion {
val key = "preload#${Any().hashCode()}/$tag"
val completion = PreloadingCompletionImpl(tag) {
tryFinish()
}
activePreloads[key] = completion
return completion
}
@Deprecated("Use registerPreloading")
fun onSingleLoadingStarted() = runOnUiThread {
downloadsLeftCount++
legacyPreloading.onSingleLoadingStarted()
}
override fun onSuccess(cachedImage: DivCachedImage) {
@@ -198,23 +221,54 @@ class DivPreloader internal constructor(
override fun onError(e: Throwable?) {
runOnUiThread {
failures++
done()
legacyPreloading.onSingleLoadingFailed(e ?: UnknownError("No stack provided"))
tryFinish()
}
}
private fun done() = runOnUiThread {
downloadsLeftCount--
legacyPreloading.onSingleLoadingCompleted()
tryFinish()
}
private fun tryFinish() = runOnUiThread {
val downloadsLeftCount =
(activePreloads.size - activePreloads.count { it.value.isCompleted }) +
legacyPreloading.downloadsLeftCount
val failures =
activePreloads.count { it.value.isFailed } + legacyPreloading.failures.size
if (downloadsLeftCount == 0 && started) {
if (failures > 0 && Log.isEnabled) {
val errors = gatherPreloadErrors()
errors.forEachIndexed { index, throwable ->
KLog.e(TAG, throwable) { "Preload error ${index + 1} / ${errors.size}" }
}
}
callback.finish(failures != 0)
}
}
private fun gatherPreloadErrors(): List<Throwable> {
val results = mutableListOf<Throwable>()
results.addAll(legacyPreloading.failures)
activePreloads.values.forEach { completion ->
val errors: Sequence<RuntimeException> = completion.result
?.filterErrorResults()
?.map { RuntimeException("Preload of '${it.uri}' failed!", it.error) }
?: return@forEach
results.addAll(errors)
}
return results
}
fun onFullPreloadStarted() = runOnUiThread {
started = true
if (downloadsLeftCount == 0) {
callback.finish(failures != 0)
}
tryFinish()
}
private inline fun runOnUiThread(crossinline action: () -> Unit) {
@@ -270,3 +324,31 @@ class DivPreloader internal constructor(
}
}
}
private class LegacyPreloading {
val failures = mutableListOf<Throwable>()
private var downloadsLeftCountRaw = 0
val downloadsLeftCount
get() = downloadsLeftCountRaw.coerceAtLeast(0)
fun onSingleLoadingStarted() {
downloadsLeftCountRaw++
}
fun onSingleLoadingCompleted() {
downloadsLeftCountRaw--
if (downloadsLeftCountRaw < 0) {
Assert.fail("Got more downloads than started! Is there a race?")
}
}
fun onSingleLoadingFailed(t: Throwable) {
failures.add(t)
onSingleLoadingCompleted()
}
}
private const val TAG = "DivPreloader"
@@ -6,6 +6,7 @@ import com.yandex.div.core.DivPreloader.Companion.NO_CALLBACK
import com.yandex.div.core.annotations.Mockable
import com.yandex.div.core.annotations.PublicApi
import com.yandex.div.core.extension.DivExtensionController
import com.yandex.div.core.preload.CompositeResult
import com.yandex.div.core.player.DivPlayerPreloader
import com.yandex.div.core.state.DivStatePath
import com.yandex.div.core.view2.BindingContext
@@ -50,7 +51,7 @@ internal class DivViewDataPreloader internal constructor(
override fun defaultVisit(data: Div, context: BindingContext, path: DivStatePath) {
imagePreloader?.preloadImage(data, context.expressionResolver, preloadFilter, downloadCallback)
?.forEach { ticket.addImageReference(it) }
extensionController.preprocessExtensions(data.value(), context.expressionResolver)
extensionController.preprocessExtensions(data.value(), context.expressionResolver, downloadCallback)
}
override fun visit(data: Div.Custom, context: BindingContext, path: DivStatePath) {
@@ -65,7 +66,10 @@ internal class DivViewDataPreloader internal constructor(
data.value.videoSources?.forEach {
sources.add(it.url.evaluate(context.expressionResolver))
}
videoPreloader.preloadVideo(sources).also { ticket.addReference(it) }
val preloading = downloadCallback.registerPreloading("video")
videoPreloader.preloadVideo(sources, callback = { results ->
preloading.onCompleted(CompositeResult(results))
}).also { ticket.addReference(it) }
}
}
}
@@ -1,6 +1,7 @@
package com.yandex.div.core.extension
import android.view.View
import com.yandex.div.core.DivPreloader
import com.yandex.div.core.annotations.Mockable
import com.yandex.div.core.dagger.DivScope
import com.yandex.div.core.view2.Div2View
@@ -15,13 +16,17 @@ internal class DivExtensionController @Inject constructor(
private val extensionHandlers: List<DivExtensionHandler>,
) {
fun preprocessExtensions(div: DivBase, resolver: ExpressionResolver) {
fun preprocessExtensions(
div: DivBase,
resolver: ExpressionResolver,
downloadCallback: DivPreloader.DownloadCallback,
) {
if (!hasExtensions(div)) {
return
}
extensionHandlers.forEach { handler ->
if (handler.matches(div)) {
handler.preprocess(div, resolver)
handler.preprocess(div, resolver, downloadCallback)
}
}
}
@@ -1,6 +1,10 @@
package com.yandex.div.core.extension
import android.view.View
import com.yandex.div.core.preload.CompositeResult
import com.yandex.div.core.preload.PreloadResult
import com.yandex.div.core.preload.PreloadingCompletion
import com.yandex.div.core.preload.PreloadingRegistry
import com.yandex.div.core.view2.Div2View
import com.yandex.div.json.expressions.ExpressionResolver
import com.yandex.div2.DivBase
@@ -11,6 +15,16 @@ interface DivExtensionHandler {
fun preprocess(div: DivBase, expressionResolver: ExpressionResolver) = Unit
fun preprocess(
div: DivBase,
expressionResolver: ExpressionResolver,
preloadingRegistry: PreloadingRegistry,
) {
val completion = preloadingRegistry.registerPreloading(div.toString())
preprocess(div, expressionResolver)
completion.onCompleted(CompositeResult(emptyList()))
}
fun beforeBindView(divView: Div2View, expressionResolver: ExpressionResolver, view: View, div: DivBase) = Unit
fun bindView(divView: Div2View, expressionResolver: ExpressionResolver, view: View, div: DivBase)
@@ -2,6 +2,8 @@ package com.yandex.div.core.player
import android.net.Uri
import com.yandex.div.core.DivPreloader
import com.yandex.div.core.preload.PreloadResult
import com.yandex.div.core.preload.UriPreloadResult
/**
* Describes the interface for preloading video for your player.
@@ -9,6 +11,18 @@ import com.yandex.div.core.DivPreloader
interface DivPlayerPreloader {
fun preloadVideo(src: List<Uri>): DivPreloader.PreloadReference
fun preloadVideo(
src: List<Uri>,
callback: (List<PreloadResult>) -> Unit,
): DivPreloader.PreloadReference {
val ref = preloadVideo(src)
val error = NotImplementedError("Please implement DivPlayerPreloader.preloadVideo(src, callback)!")
callback(
src.map { uri -> UriPreloadResult(uri, error) }
)
return ref
}
companion object {
@JvmField
val STUB: DivPlayerPreloader = object : DivPlayerPreloader {
@@ -0,0 +1,40 @@
package com.yandex.div.core.preload
import android.net.Uri
/**
* Result of a preload.
*/
sealed interface PreloadResult
/**
* Result of a preload for a single URI.
*/
class UriPreloadResult(
val uri: Uri,
val error: Throwable?
): PreloadResult
/**
* Holds composition of preload results.
*/
class CompositeResult(
val results: List<PreloadResult>,
): PreloadResult
internal fun PreloadResult.filterErrorResults(): Sequence<UriPreloadResult> {
val result = this
return sequence {
when (result) {
is CompositeResult -> result.results.forEach {
yieldAll(it.filterErrorResults())
}
is UriPreloadResult -> {
if (result.error != null) {
yield(result)
}
}
}
}
}
@@ -0,0 +1,12 @@
package com.yandex.div.core.preload
/**
* Callback for reporting completion of an async preload
* registered via [PreloadingRegistry.registerPreloading].
*/
interface PreloadingCompletion {
/**
* Reports that the preload has finished.
*/
fun onCompleted(preloadResult: PreloadResult)
}
@@ -0,0 +1,28 @@
package com.yandex.div.core.preload
import com.yandex.div.internal.Assert
internal class PreloadingCompletionImpl(
private val id: String,
private val onComplete: () -> Unit,
) : PreloadingCompletion {
var isFailed: Boolean = false
private set
var isCompleted = false
private set
var result: PreloadResult? = null
private set
override fun onCompleted(preloadResult: PreloadResult) {
if (isCompleted) {
Assert.fail("Preloading '$id' is already completed!")
return
}
isCompleted = true
result = preloadResult
isFailed = result?.filterErrorResults()?.firstOrNull() != null
onComplete()
}
}
@@ -0,0 +1,12 @@
package com.yandex.div.core.preload
/**
* Registry for reporting async preload completion. Used by extension handlers (e.g. lottie)
* so that [DivPreloader] callback is notified when all preloads including extensions are done.
*/
interface PreloadingRegistry {
/**
* Register that new prelaad
*/
fun registerPreloading(tag: String): PreloadingCompletion
}
@@ -364,6 +364,8 @@ internal class DivTooltipController @VisibleForTesting constructor(
hideTooltip(divTooltip.id, div2View)
}
}
} else {
tooltips.remove(divTooltip.id)
}
}
tooltipData.ticket = ticket
@@ -34,13 +34,13 @@ class DivImagePreloader @Inject constructor(
}
private fun preloadImage(url: String, callback: DivPreloader.DownloadCallback, references: ArrayList<LoadReference>) {
references.add(imageLoader.loadImage(url, callback, DivImagePriority.IMAGES_PRIORITY_PRELOAD))
callback.onSingleLoadingStarted()
references.add(imageLoader.loadImage(url, callback, DivImagePriority.IMAGES_PRIORITY_PRELOAD))
}
private fun preloadImageBytes(url: String, callback: DivPreloader.DownloadCallback, references: ArrayList<LoadReference>) {
references.add(imageLoader.loadImageBytes(url, callback, DivImagePriority.IMAGES_PRIORITY_PRELOAD))
callback.onSingleLoadingStarted()
references.add(imageLoader.loadImageBytes(url, callback, DivImagePriority.IMAGES_PRIORITY_PRELOAD))
}
private inner class PreloadVisitor(
@@ -1,21 +1,28 @@
package com.yandex.div.core
import android.net.Uri
import com.yandex.div.core.extension.DivExtensionController
import com.yandex.div.core.extension.DivExtensionHandler
import com.yandex.div.core.images.LoadReference
import com.yandex.div.core.player.DivPlayerPreloader
import com.yandex.div.core.preload.PreloadResult
import com.yandex.div.core.view2.DivImagePreloader
import com.yandex.div.json.expressions.Expression
import com.yandex.div.json.expressions.ExpressionResolver
import com.yandex.div2.Div
import com.yandex.div2.DivContainer
import com.yandex.div2.DivImage
import com.yandex.div2.DivCustom
import com.yandex.div2.DivExtension
import com.yandex.div2.DivInput
import com.yandex.div2.DivSeparator
import com.yandex.div2.DivText
import com.yandex.div2.DivVideo
import com.yandex.div2.DivVideoSource
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.kotlin.any
import org.mockito.kotlin.doAnswer
import org.mockito.kotlin.doReturn
import org.mockito.kotlin.eq
import org.mockito.kotlin.mock
@@ -25,6 +32,9 @@ import org.mockito.kotlin.whenever
import org.robolectric.RobolectricTestRunner
import java.util.Arrays
/**
* Tests for [DivPreloader].
*/
@RunWith(RobolectricTestRunner::class)
class DivPreloaderTest {
@@ -54,6 +64,29 @@ class DivPreloaderTest {
private val divContainer = Div.Container(container)
private val videoPreloader = mock<DivPlayerPreloader>()
private val videoSourceUrl = Uri.parse("https://example.com/video.mp4")
private val videoSource = DivVideoSource(
mimeType = Expression.constant("video/mp4"),
url = Expression.constant(videoSourceUrl)
)
private val videoWithPreload = DivVideo(
videoSources = listOf(videoSource),
preloadRequired = Expression.constant(true)
)
private val divVideoWithPreload = Div.Video(videoWithPreload)
private val videoWithoutPreload = DivVideo(videoSources = listOf(videoSource))
private val divVideoWithoutPreload = Div.Video(videoWithoutPreload)
private val imageWithPreload = DivImage(
imageUrl = Expression.constant(Uri.parse("https://example.com/img.png")),
preloadRequired = Expression.constant(true)
)
private val divImageWithPreload = Div.Image(imageWithPreload)
private val containerWithVideoAndImage = DivContainer(
items = Arrays.asList(divVideoWithPreload, divImageWithPreload)
)
private val divContainerWithVideoAndImage = Div.Container(containerWithVideoAndImage)
private val underTest: DivPreloader = DivPreloader(
divImagePreloader,
divCustomContainerViewAdapter,
@@ -124,7 +157,31 @@ class DivPreloaderTest {
underTest.preload(divContainer, mock())
verify(extensionHandlers[0], times(1)).preprocess(eq(container), any())
verify(extensionHandlers[1], times(1)).preprocess(eq(separator), any())
verify(extensionHandlers[0], times(1)).preprocess(eq(container), any(), any())
verify(extensionHandlers[1], times(1)).preprocess(eq(separator), any(), any())
}
@Test
fun `preload div video calls video preloader when preload required`() {
whenever(videoPreloader.preloadVideo(any(), any())).doAnswer { invocation ->
(invocation.getArgument<(List<PreloadResult>) -> Unit>(1)).invoke(emptyList())
DivPreloader.PreloadReference.EMPTY
}
underTest.preload(divVideoWithPreload, ExpressionResolver.EMPTY)
verify(videoPreloader).preloadVideo(eq(listOf(videoSourceUrl)), any())
}
@Test
fun `video preload reference cancelled when ticket cancelled`() {
val videoPreloadReference = mock<DivPreloader.PreloadReference>()
whenever(videoPreloader.preloadVideo(any(), any())).doReturn(videoPreloadReference)
val ticket = underTest.preload(divVideoWithPreload, ExpressionResolver.EMPTY)
ticket.cancel()
verify(videoPreloadReference).cancel()
}
}
@@ -0,0 +1,78 @@
package com.yandex.div.core.preload
import android.net.Uri
import com.yandex.div.core.util.EnableAssertsRule
import org.junit.Assert
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
/**
* Tests for [PreloadingCompletionImpl].
*/
@RunWith(RobolectricTestRunner::class)
class PreloadingCompletionImplTest {
@get:Rule
val enableAssertsRule = EnableAssertsRule(true)
private val preloadId = "test_preload_id"
private var onCompleteInvocationCount = 0
private val onComplete: () -> Unit = { onCompleteInvocationCount++ }
private val underTest = PreloadingCompletionImpl(preloadId, onComplete)
private val successResult = UriPreloadResult(Uri.parse("https://example.com/res"), null)
private val errorResult = UriPreloadResult(
Uri.parse("https://example.com/fail"),
RuntimeException("load failed")
)
@Test
fun `onCompleted sets isCompleted and result and invokes onComplete`() {
underTest.onCompleted(successResult)
Assert.assertTrue(underTest.isCompleted)
Assert.assertSame(successResult, underTest.result)
Assert.assertEquals(1, onCompleteInvocationCount)
}
@Test
fun `isFailed is false when result has no errors`() {
underTest.onCompleted(successResult)
Assert.assertFalse(underTest.isFailed)
}
@Test
fun `isFailed is false when result is null`() {
Assert.assertFalse(underTest.isFailed)
}
@Test
fun `isFailed is true when CompositeResult contains error`() {
val compositeResult = CompositeResult(
listOf(successResult, errorResult)
)
underTest.onCompleted(compositeResult)
Assert.assertTrue(underTest.isFailed)
}
@Test
fun `isFailed is false when CompositeResult has no errors`() {
val compositeResult = CompositeResult(
listOf(successResult, UriPreloadResult(Uri.parse("https://other.com"), null))
)
underTest.onCompleted(compositeResult)
Assert.assertFalse(underTest.isFailed)
}
@Test(expected = AssertionError::class)
fun `second onCompleted throws when assertions enabled`() {
underTest.onCompleted(successResult)
underTest.onCompleted(successResult)
}
}
@@ -46,6 +46,9 @@ import org.robolectric.Robolectric
import org.robolectric.RobolectricTestRunner
import org.robolectric.shadows.ShadowLooper
/**
* Tests for [DivTooltipController].
*/
@RunWith(RobolectricTestRunner::class)
class DivTooltipControllerTest {
@@ -281,6 +284,21 @@ class DivTooltipControllerTest {
Assert.assertTrue(underTest.captureCurrentTooltips().isEmpty())
}
@Test
fun `when preload completes with failures tooltip can be shown again`() {
val preloadCallback = argumentCaptor<DivPreloader.Callback>()
val preloadTicket = mock<DivPreloader.Ticket>()
whenever(divPreloader.preload(any(), any(), preloadCallback.capture()))
.doReturn(preloadTicket)
prepareDiv()
underTest.showTooltip("tooltip_id", bindingContext)
preloadCallback.lastValue.finish(true)
Assert.assertTrue(underTest.captureCurrentTooltips().isEmpty())
verify(popupWindow, never()).showAtLocation(any(), anyInt(), anyInt(), anyInt())
}
private fun prepareDiv(duration: Long = 5000, offset: DivPoint? = null) {
tooltips.add(
DivTooltip(