mirror of
https://github.com/divkit/divkit.git
synced 2026-05-07 20:02:32 +00:00
support preloading for lottie and video
commit_hash:d013f22d82386c3052abfc55f4b154e4f658d50a
This commit is contained in:
@@ -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",
|
||||
|
||||
+27
-2
@@ -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"
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+22
-1
@@ -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 {
|
||||
|
||||
+23
-6
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
+22
-6
@@ -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) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+7
-2
@@ -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)
|
||||
}
|
||||
+28
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
+78
@@ -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)
|
||||
}
|
||||
}
|
||||
+18
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user