diff --git a/app/src/main/java/com/navi/uitron/demo/UiTronDependencyProvider.kt b/app/src/main/java/com/navi/uitron/demo/UiTronDependencyProvider.kt index 2a387b6..a2331e8 100644 --- a/app/src/main/java/com/navi/uitron/demo/UiTronDependencyProvider.kt +++ b/app/src/main/java/com/navi/uitron/demo/UiTronDependencyProvider.kt @@ -13,6 +13,9 @@ import androidx.compose.ui.graphics.Shape import androidx.compose.ui.text.font.Font import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontWeight +import androidx.media3.database.StandaloneDatabaseProvider +import androidx.media3.datasource.cache.LeastRecentlyUsedCacheEvictor +import androidx.media3.datasource.cache.SimpleCache import com.navi.alfred.AlfredManager import com.navi.uitron.IUiTronDependencyProvider import com.navi.uitron.model.UiTronConfig @@ -20,8 +23,19 @@ import com.navi.uitron.model.ui.OutlinedTextFieldValueTransformation import com.navi.uitron.model.ui.UiTronShape import com.navi.uitron.utils.EMPTY import com.navi.uitron.utils.SPACE +import java.io.File class UiTronDependencyProvider : IUiTronDependencyProvider { + + private val media3SimpleCache by lazy { + val downloadContentDirectory = File(getContext().getExternalFilesDir(null), "downloads") + SimpleCache( + downloadContentDirectory, + LeastRecentlyUsedCacheEvictor(100_000_000), + StandaloneDatabaseProvider(getContext()) + ) + } + private val ttFontFamily = FontFamily( Font( @@ -218,6 +232,10 @@ class UiTronDependencyProvider : IUiTronDependencyProvider { return null } + override fun getMedia3SimpleCache(): SimpleCache { + return media3SimpleCache + } + override fun maskSensitiveUiTronComposable( id: String, left: Float?, diff --git a/app/src/main/res/raw/mock.json b/app/src/main/res/raw/mock.json index 03a8184..8e56f1c 100644 --- a/app/src/main/res/raw/mock.json +++ b/app/src/main/res/raw/mock.json @@ -1,5 +1,37 @@ { - "movingBoxes":{ + "videoMock": { + "data": { + "productIcon": { + "videoUrl": "https://public-assets.prod.navi-sa.in/shivam/video-view/coverr-close-up-of-unboxing-iphone-15-1080p.mp4", + "viewType": "Video" + } + }, + "view": [ + { + "property": { + "width": "MATCH_PARENT", + "height": "MATCH_PARENT", + "layoutId": "productIcon", + "viewType": "Video", + "placeHolderView": { + "data": {}, + "view": [ + { + "property": { + "width": "MATCH_PARENT", + "height": "MATCH_PARENT", + "layoutId": "placeHolderView", + "viewType": "Box", + "backgroundColor": "#000000" + } + } + ] + } + } + } + ] + }, + "movingBoxes": { "view": [ { "property": { @@ -1504,7 +1536,7 @@ } } }, - "spannableV2": { + "spannableV2": { "view": [ { "property": { @@ -4635,7 +4667,7 @@ } } }, - "cardShadowTemplate":{ + "cardShadowTemplate": { "parentComposeView": [ { "property": { @@ -4659,7 +4691,6 @@ }, "repeatMode": "Repeat" }, - "targetColor": "#011627" } ], @@ -10981,7 +11012,6 @@ "width": "MATCH_PARENT", "height": "WRAP_CONTENT", "layoutId": "parent_container", - "borderStrokeData": { "brushData": { "brushType": "LINEAR", @@ -18245,7 +18275,7 @@ ], "data": { "simple_text": { - "text":"NAVI", + "text": "NAVI", "viewType": "Text" }, "click_me_trigger": { diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index d1bd118..b593aa5 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -9,6 +9,7 @@ androidx-constraintlayoutCompose = "1.1.0-alpha10" androidx-core-ktx = "1.8.0" androidx-hilt = "1.0.0" androidx-lifecycle = "2.6.1" +androidx-media3 = "1.4.1" androidx-pagingCompose = "3.2.0" androidx-pagingRuntimeKtx = "3.1.1" androidx-profileinstaller = "1.3.1" @@ -65,6 +66,9 @@ androidx-lifecycle-runtime-compose = { module = "androidx.lifecycle:lifecycle-ru androidx-lifecycle-runtime-ktx = { module = "androidx.lifecycle:lifecycle-runtime-ktx", version.ref = "androidx-lifecycle" } androidx-lifecycle-viewmodel-ktx = { module = "androidx.lifecycle:lifecycle-viewmodel-ktx", version.ref = "androidx-lifecycle" } +androidx-media3-exoplayer = { module = "androidx.media3:media3-exoplayer", version.ref = "androidx-media3" } +androidx-media3-ui = { module = "androidx.media3:media3-ui", version.ref = "androidx-media3" } + androidx-navigation-compose = { module = "androidx.navigation:navigation-compose", version.ref = "navigation-compose" } androidx-paging-compose = { module = "androidx.paging:paging-compose", version.ref = "androidx-pagingCompose" } diff --git a/lint.xml b/lint.xml new file mode 100644 index 0000000..58a15f6 --- /dev/null +++ b/lint.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/navi-uitron/build.gradle b/navi-uitron/build.gradle index dc50ce3..9e951ce 100644 --- a/navi-uitron/build.gradle +++ b/navi-uitron/build.gradle @@ -84,6 +84,12 @@ publishing { } dependencies { + api(libs.androidx.media3.exoplayer) { + exclude group: "com.google.guava", module: "guava" + } + api(libs.androidx.media3.ui) { + exclude group: "com.google.guava", module: "guava" + } api libs.coil.compose implementation platform(libs.androidx.compose.bom) diff --git a/navi-uitron/src/main/java/com/navi/uitron/IUiTronDependencyProvider.kt b/navi-uitron/src/main/java/com/navi/uitron/IUiTronDependencyProvider.kt index d5a8285..fef6980 100644 --- a/navi-uitron/src/main/java/com/navi/uitron/IUiTronDependencyProvider.kt +++ b/navi-uitron/src/main/java/com/navi/uitron/IUiTronDependencyProvider.kt @@ -12,6 +12,7 @@ import android.view.View import androidx.compose.ui.graphics.Shape import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontWeight +import androidx.media3.datasource.cache.SimpleCache import com.navi.uitron.model.UiTronConfig import com.navi.uitron.model.ui.OutlinedTextFieldValueTransformation import com.navi.uitron.model.ui.UiTronShape @@ -60,4 +61,6 @@ interface IUiTronDependencyProvider { fun getUiTronConfig(): UiTronConfig fun getShape(shape: UiTronShape?): Shape? + + fun getMedia3SimpleCache(): SimpleCache } diff --git a/navi-uitron/src/main/java/com/navi/uitron/deserializer/ComposePropertyDeserializer.kt b/navi-uitron/src/main/java/com/navi/uitron/deserializer/ComposePropertyDeserializer.kt index f4acebc..5dc6d0d 100644 --- a/navi-uitron/src/main/java/com/navi/uitron/deserializer/ComposePropertyDeserializer.kt +++ b/navi-uitron/src/main/java/com/navi/uitron/deserializer/ComposePropertyDeserializer.kt @@ -162,6 +162,9 @@ open class ComposePropertyDeserializer : JsonDeserializer { LinearProgressIndicatorWithThumbProperty::class.java ) } + ComposeViewType.Video.name -> { + context?.deserialize(jsonObject, VideoProperty::class.java) + } else -> null } } diff --git a/navi-uitron/src/main/java/com/navi/uitron/deserializer/UiTronDataDeserializer.kt b/navi-uitron/src/main/java/com/navi/uitron/deserializer/UiTronDataDeserializer.kt index 9c8d48e..26eccda 100644 --- a/navi-uitron/src/main/java/com/navi/uitron/deserializer/UiTronDataDeserializer.kt +++ b/navi-uitron/src/main/java/com/navi/uitron/deserializer/UiTronDataDeserializer.kt @@ -40,6 +40,7 @@ import com.navi.uitron.model.data.TextData import com.navi.uitron.model.data.ToastData import com.navi.uitron.model.data.UiTronData import com.navi.uitron.model.data.UiTronSliderData +import com.navi.uitron.model.data.VideoData import com.navi.uitron.model.ui.ComposeViewType import java.lang.reflect.Type @@ -162,6 +163,9 @@ open class UiTronDataDeserializer : JsonDeserializer { LinearProgressIndicatorWithThumbData::class.java ) } + ComposeViewType.Video.name -> { + context?.deserialize(jsonObject, VideoData::class.java) + } else -> null } } diff --git a/navi-uitron/src/main/java/com/navi/uitron/model/data/VideoData.kt b/navi-uitron/src/main/java/com/navi/uitron/model/data/VideoData.kt new file mode 100644 index 0000000..a43f93c --- /dev/null +++ b/navi-uitron/src/main/java/com/navi/uitron/model/data/VideoData.kt @@ -0,0 +1,12 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.uitron.model.data + +data class VideoData( + val videoUrl: String? = null, +) : UiTronData() diff --git a/navi-uitron/src/main/java/com/navi/uitron/model/event/UiTronDataProviderFactory.kt b/navi-uitron/src/main/java/com/navi/uitron/model/event/UiTronDataProviderFactory.kt index cbf2c11..238f560 100644 --- a/navi-uitron/src/main/java/com/navi/uitron/model/event/UiTronDataProviderFactory.kt +++ b/navi-uitron/src/main/java/com/navi/uitron/model/event/UiTronDataProviderFactory.kt @@ -37,6 +37,7 @@ import com.navi.uitron.model.data.TextData import com.navi.uitron.model.data.ToastData import com.navi.uitron.model.data.UiTronData import com.navi.uitron.model.data.UiTronSliderData +import com.navi.uitron.model.data.VideoData import com.navi.uitron.model.ui.ComposeViewType class UiTronDataProviderFactory { @@ -73,6 +74,7 @@ class UiTronDataProviderFactory { ComposeViewType.RadioGroup.name -> RadioGroupData() ComposeViewType.LinearProgressIndicatorWithThumb.name -> LinearProgressIndicatorWithThumbData() + ComposeViewType.Video.name -> VideoData() else -> EmptyData() } } diff --git a/navi-uitron/src/main/java/com/navi/uitron/model/ui/UiTronView.kt b/navi-uitron/src/main/java/com/navi/uitron/model/ui/UiTronView.kt index d5c39a7..b0de308 100644 --- a/navi-uitron/src/main/java/com/navi/uitron/model/ui/UiTronView.kt +++ b/navi-uitron/src/main/java/com/navi/uitron/model/ui/UiTronView.kt @@ -261,7 +261,8 @@ enum class ComposeViewType { FloatingActionButton, ToolTipPopUp, RadioGroup, - LinearProgressIndicatorWithThumb + LinearProgressIndicatorWithThumb, + Video } val containerRenderers = @@ -314,7 +315,8 @@ val elementRenderers = ComposeViewType.CustomTextField.name, ComposeViewType.OtpBox.name, ComposeViewType.SlideToActButton.name, - ComposeViewType.LinearProgressIndicatorWithThumb.name + ComposeViewType.LinearProgressIndicatorWithThumb.name, + ComposeViewType.Video.name ) enum class HorizontalArrangementType { diff --git a/navi-uitron/src/main/java/com/navi/uitron/model/ui/VideoProperty.kt b/navi-uitron/src/main/java/com/navi/uitron/model/ui/VideoProperty.kt new file mode 100644 index 0000000..ebeba7c --- /dev/null +++ b/navi-uitron/src/main/java/com/navi/uitron/model/ui/VideoProperty.kt @@ -0,0 +1,21 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.uitron.model.ui + +import com.navi.uitron.model.UiTronResponse + +data class VideoProperty( + var placeHolderView: UiTronResponse? = null, +) : BaseProperty() { + + override fun copyNonNullFrom(property: BaseProperty?) { + super.copyNonNullFrom(property) + val videoProperty = property as? VideoProperty + videoProperty?.placeHolderView?.let { placeHolderView = it } + } +} diff --git a/navi-uitron/src/main/java/com/navi/uitron/provider/ElementRendererProviderImpl.kt b/navi-uitron/src/main/java/com/navi/uitron/provider/ElementRendererProviderImpl.kt index 5ddd6ce..67aa438 100644 --- a/navi-uitron/src/main/java/com/navi/uitron/provider/ElementRendererProviderImpl.kt +++ b/navi-uitron/src/main/java/com/navi/uitron/provider/ElementRendererProviderImpl.kt @@ -31,6 +31,7 @@ import com.navi.uitron.provider.element.SpacerViewRenderer import com.navi.uitron.provider.element.SpannableTextViewRenderer import com.navi.uitron.provider.element.SwitchViewRenderer import com.navi.uitron.provider.element.TextViewRenderer +import com.navi.uitron.provider.element.VideoViewRenderer class ElementRendererProviderImpl : IUiTronRendererProvider { @@ -61,6 +62,7 @@ class ElementRendererProviderImpl : IUiTronRendererProvider { ComposeViewType.SlideToActButton.name -> SlideToActButtonViewRenderer() ComposeViewType.LinearProgressIndicatorWithThumb.name -> LinearProgressIndicatorWithThumbViewRenderer() + ComposeViewType.Video.name -> VideoViewRenderer() else -> null } } diff --git a/navi-uitron/src/main/java/com/navi/uitron/provider/element/VideoViewRenderer.kt b/navi-uitron/src/main/java/com/navi/uitron/provider/element/VideoViewRenderer.kt new file mode 100644 index 0000000..c3f26df --- /dev/null +++ b/navi-uitron/src/main/java/com/navi/uitron/provider/element/VideoViewRenderer.kt @@ -0,0 +1,31 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.uitron.provider.element + +import androidx.compose.runtime.Composable +import com.navi.uitron.model.UiTronRenderConfig +import com.navi.uitron.model.ui.VideoProperty +import com.navi.uitron.provider.IUiTronRenderer +import com.navi.uitron.render.VideoRenderer + +class VideoViewRenderer : IUiTronRenderer { + + @Composable + override fun RenderUiTronView(uiTronRenderConfig: UiTronRenderConfig) { + (uiTronRenderConfig.composeView.property as? VideoProperty)?.let { + VideoRenderer(uiTronRenderer = uiTronRenderConfig.uiTronRenderer) + .Render( + property = it, + uiTronData = + uiTronRenderConfig.dataMap?.getOrElse(it.layoutId.orEmpty()) { null }, + uiTronViewModel = uiTronRenderConfig.uiTronViewModel, + modifier = uiTronRenderConfig.modifier + ) + } + } +} diff --git a/navi-uitron/src/main/java/com/navi/uitron/render/VideoRenderer.kt b/navi-uitron/src/main/java/com/navi/uitron/render/VideoRenderer.kt new file mode 100644 index 0000000..5b0006d --- /dev/null +++ b/navi-uitron/src/main/java/com/navi/uitron/render/VideoRenderer.kt @@ -0,0 +1,192 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.uitron.render + +import android.content.Context +import android.net.Uri +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberUpdatedState +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.layout.layoutId +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalLifecycleOwner +import androidx.compose.ui.viewinterop.AndroidView +import androidx.media3.common.MediaItem +import androidx.media3.common.Player +import androidx.media3.common.Player.STATE_READY +import androidx.media3.datasource.DefaultDataSource +import androidx.media3.datasource.DefaultHttpDataSource +import androidx.media3.datasource.FileDataSource +import androidx.media3.datasource.cache.CacheDataSink +import androidx.media3.datasource.cache.CacheDataSource +import androidx.media3.exoplayer.ExoPlayer +import androidx.media3.exoplayer.source.MediaSource +import androidx.media3.exoplayer.source.ProgressiveMediaSource +import androidx.media3.ui.AspectRatioFrameLayout.RESIZE_MODE_ZOOM +import androidx.media3.ui.PlayerView +import com.navi.uitron.UiTronSdkManager +import com.navi.uitron.model.data.UiTronData +import com.navi.uitron.model.data.VideoData +import com.navi.uitron.model.ui.VideoProperty +import com.navi.uitron.utils.clip +import com.navi.uitron.utils.customClickable +import com.navi.uitron.utils.customCombinedClick +import com.navi.uitron.utils.customOffset +import com.navi.uitron.utils.orTrue +import com.navi.uitron.utils.setBackground +import com.navi.uitron.utils.setBlur +import com.navi.uitron.utils.setHeight +import com.navi.uitron.utils.setHeightRange +import com.navi.uitron.utils.setPadding +import com.navi.uitron.utils.setTag +import com.navi.uitron.utils.setWidth +import com.navi.uitron.utils.setWidthRange +import com.navi.uitron.viewmodel.UiTronViewModel + +class VideoRenderer(private val uiTronRenderer: UiTronRenderer) : Renderer { + + @Composable + override fun Render( + property: VideoProperty, + uiTronData: UiTronData?, + uiTronViewModel: UiTronViewModel, + modifier: Modifier? + ) { + super.Render(property, uiTronData, uiTronViewModel, modifier) + val videoData = uiTronData as? VideoData + + if (property.visible.orTrue()) { + val viewModifier = + (modifier ?: Modifier) + .setTag(property) + .layoutId(property.layoutId.orEmpty()) + .customOffset(property.offset) + .setWidth(property.width) + .setHeight(property.height) + .setWidthRange(property.widthRange) + .setHeightRange(property.heightRange) + .setPadding(property.margin) + .setBackground( + property.backgroundColor, + property.shape, + property.backGroundBrushData + ) + .clip(property.clipShape) + .setPadding(property.padding) + .customClickable( + { uiTronViewModel.handleActions(uiTronData?.onClick) }, + actions = uiTronData?.onClick?.actions, + property = property + ) + .customCombinedClick(property, uiTronData) { uiTronViewModel.handleActions(it) } + .graphicsLayer { alpha = property.alpha ?: 1.0f } + .setBlur(property.blurData) + + val localContext = LocalContext.current + + val mediaUrl = remember(videoData?.videoUrl) { videoData?.videoUrl.orEmpty() } + + val placeholderVisibility = remember { mutableStateOf(true) } + + exoPlayer = + remember(mediaUrl) { + ExoPlayer.Builder(localContext).build().apply { + setMediaSource(getProgressiveMediaSource(localContext, mediaUrl)) + repeatMode = ExoPlayer.REPEAT_MODE_ALL + volume = 0f + prepare() + addListener( + object : Player.Listener { + override fun onPlaybackStateChanged(playbackState: Int) { + super.onPlaybackStateChanged(playbackState) + if (playbackState == STATE_READY) { + play() + } + } + + override fun onRenderedFirstFrame() { + super.onRenderedFirstFrame() + if (placeholderVisibility.value) { + placeholderVisibility.value = false + } + } + } + ) + } + } + + val playerView = + remember(exoPlayer) { + PlayerView(localContext).apply { + useController = false + player = exoPlayer + resizeMode = RESIZE_MODE_ZOOM + } + } + + Box { + AndroidView(modifier = viewModifier, factory = { playerView }) + if (placeholderVisibility.value) { + property.placeHolderView?.let { + UiTronRenderer( + it.data, + uiTronViewModel, + uiTronRenderer.customUiTronRenderer, + ) + .Render( + composeViews = it.parentComposeView.orEmpty(), + modifier = viewModifier + ) + } ?: run { Box(viewModifier.background(Color.White)) } + } + } + + val lifecycleOwner = rememberUpdatedState(LocalLifecycleOwner.current) + + DisposableEffect(lifecycleOwner.value) { onDispose { releasePlayer() } } + } + } + + private var exoPlayer: ExoPlayer? = null + + private fun releasePlayer() { + exoPlayer?.apply { + playWhenReady = false + release() + } + exoPlayer = null + } + + companion object { + private fun getProgressiveMediaSource(context: Context, mediaUrl: String): MediaSource { + val downloadCache = UiTronSdkManager.getDependencyProvider().getMedia3SimpleCache() + val cacheSink = CacheDataSink.Factory().setCache(downloadCache) + val downstreamFactory = FileDataSource.Factory() + val upstreamFactory = + DefaultDataSource.Factory(context, DefaultHttpDataSource.Factory()) + + val dataSourceFactory = + CacheDataSource.Factory() + .setCache(downloadCache) + .setCacheWriteDataSinkFactory(cacheSink) + .setCacheReadDataSourceFactory(downstreamFactory) + .setUpstreamDataSourceFactory(upstreamFactory) + .setFlags(CacheDataSource.FLAG_IGNORE_CACHE_ON_ERROR) + + return ProgressiveMediaSource.Factory(dataSourceFactory) + .createMediaSource(MediaItem.fromUri(Uri.parse(mediaUrl))) + } + } +} diff --git a/navi-uitron/src/main/java/com/navi/uitron/serializer/ComposePropertySerializer.kt b/navi-uitron/src/main/java/com/navi/uitron/serializer/ComposePropertySerializer.kt index 8761e9a..a1ba45a 100644 --- a/navi-uitron/src/main/java/com/navi/uitron/serializer/ComposePropertySerializer.kt +++ b/navi-uitron/src/main/java/com/navi/uitron/serializer/ComposePropertySerializer.kt @@ -177,6 +177,9 @@ open class ComposePropertySerializer : JsonSerializer { LinearProgressIndicatorWithThumbProperty::class.java ) } + ComposeViewType.Video.name -> { + context?.serialize(src as VideoProperty, VideoProperty::class.java) + } else -> null } } diff --git a/navi-uitron/src/main/java/com/navi/uitron/serializer/UiTronDataSerializer.kt b/navi-uitron/src/main/java/com/navi/uitron/serializer/UiTronDataSerializer.kt index 142567f..098c2ea 100644 --- a/navi-uitron/src/main/java/com/navi/uitron/serializer/UiTronDataSerializer.kt +++ b/navi-uitron/src/main/java/com/navi/uitron/serializer/UiTronDataSerializer.kt @@ -38,6 +38,7 @@ import com.navi.uitron.model.data.TextData import com.navi.uitron.model.data.ToastData import com.navi.uitron.model.data.UiTronData import com.navi.uitron.model.data.UiTronSliderData +import com.navi.uitron.model.data.VideoData import com.navi.uitron.model.ui.ComposeViewType import java.lang.reflect.Type @@ -154,6 +155,9 @@ open class UiTronDataSerializer : JsonSerializer { LinearProgressIndicatorWithThumbData::class.java ) } + ComposeViewType.Video.name -> { + context?.serialize(src as VideoData, VideoData::class.java) + } else -> null } }