package com.fireworksdk.bridge.utils import android.annotation.SuppressLint import android.content.Context import android.graphics.Typeface import android.graphics.drawable.GradientDrawable import com.firework.common.PlayerMode import com.firework.common.Theme import com.firework.common.ad.AdBadgeOption import com.firework.common.ad.AdBadgeTextType import com.firework.common.ad.AdOption import com.firework.common.chat.ChatStyle import com.firework.common.chat.TextShadow import com.firework.common.cta.CtaDelay import com.firework.common.cta.CtaDelayUnit import com.firework.common.cta.CtaStyle import com.firework.common.feed.FeedLayout import com.firework.common.feed.FeedPadding import com.firework.common.feed.FeedResource import com.firework.common.feed.FeedTitlePosition import com.firework.common.player.ActionButtonOption import com.firework.common.player.CloseButtonOption import com.firework.common.player.FeedCompleteAction import com.firework.common.player.LivestreamCountDownOption import com.firework.common.player.MuteButtonOption import com.firework.common.player.PipButtonOption import com.firework.common.player.PlaybackButtonOption import com.firework.common.player.PlayerUiOption import com.firework.common.player.ScrollingOrientation import com.firework.common.player.VideoDetailsOption import com.firework.common.widget.ActionButton import com.firework.common.widget.Shape import com.firework.common.widget.WidgetImage import com.firework.viewoptions.BaseOption import com.firework.viewoptions.ChatOption import com.firework.viewoptions.CtaOption import com.firework.viewoptions.ImmersiveParam import com.firework.viewoptions.LayoutOption import com.firework.viewoptions.LogoConfig import com.firework.viewoptions.PlayIcon import com.firework.viewoptions.PlayerOption import com.firework.viewoptions.StoryBlockOption import com.firework.viewoptions.TitleOption import com.firework.viewoptions.ViewOptions import com.fireworksdk.bridge.FWInitializationProvider import com.fireworksdk.bridge.models.FWAdConfigurationModel import com.fireworksdk.bridge.models.FWButtonInfoModel import com.fireworksdk.bridge.models.FWFontInfoModel import com.fireworksdk.bridge.models.FWShape import com.fireworksdk.bridge.models.FWVideoFeedConfigModel import com.fireworksdk.bridge.models.FWVideoFeedPropsModel import com.fireworksdk.bridge.models.isAutosizedGrid import com.fireworksdk.bridge.models.enums.FWAppearanceMode import com.fireworksdk.bridge.models.enums.FWBadgeTextType import com.fireworksdk.bridge.models.enums.FWCtaDelayType import com.fireworksdk.bridge.models.enums.FWFeedCompleteAction import com.fireworksdk.bridge.models.enums.FWGradientDrawableOrientation import com.fireworksdk.bridge.models.enums.FWPlayerStyle import com.fireworksdk.bridge.models.enums.FWSystemTypeface import com.fireworksdk.bridge.models.enums.FWVideoCompleteAction import com.fireworksdk.bridge.models.enums.FWVideoFeedMode import com.fireworksdk.bridge.models.enums.FWVideoFeedSource import com.fireworksdk.bridge.models.enums.FWVideoFeedTitlePosition import com.fireworksdk.bridge.models.enums.FWVideoPlayerCTAWidth import com.fireworksdk.bridge.models.enums.FWVideoPlayerLogoOption import com.fireworksdk.bridge.models.enums.FWShareButtonPosition import com.fireworksdk.bridge.models.enums.FWVideoPlayerScrollDirection import com.firework.common.player.ShareButtonPosition enum class FWComponentType { VIDEO_FEED, STORY_BLOCK, VIDEO_PLAYER, CIRCLE_STORY, PLAYER_DECK, } object FWConfigUtil { fun generateViewOptionsBuilder(context: Context?, videoFeedPropsModel: FWVideoFeedPropsModel?, componentType: FWComponentType): ViewOptions.Builder { context ?: return ViewOptions.Builder() videoFeedPropsModel ?: return ViewOptions.Builder() val layoutOptionBuilder = getDefaultLayoutOptionBuilder(componentType) val titleOptionBuilder = getDefaultTitleOptionBuilder() val playerOptionBuilder = getDefaultPlayerOptionBuilder() val playerUiOptionBuilder = getDefaultPlayerUiOptionBuilder() val videoDetailsOptionBuilder = getDefaultVideoDetailsOptionBuilder() val closeButtonOptionBuilder = getDefaultCloseButtonOptionBuilder() val pipButtonOptionBuilder = getDefaultPipButtonOptionBuilder() val muteButtonOptionBuilder = getDefaultMuteButtonOptionBuilder() val playbackButtonOptionBuilder = getDefaultPlaybackButtonOptionBuilder() val baseOptionBuilder = getDefaultBaseOptionBuilder() val adBadgeOptionBuilder = getDefaultAdBadgeOptionBuilder() val ctaOptionBuilder = getDefaultCtaOptionBuilder() val chatOptionBuilder = getDefaultChatOptionBuilder() val adOptionBuilder = getDefaultAdOptionBuilder() val countDownOptionBuilder = getDefaultPlayerCountdownTimerOptionBuilder() val actionButtonOption = getDefaultActionButtonOptionBuilder() val storyblockOptionBuilder = getDefaultStoryblockOptionBuilder() when (videoFeedPropsModel.source) { FWVideoFeedSource.Discover -> { baseOptionBuilder.feedResource(FeedResource.Discovery) } FWVideoFeedSource.Channel -> { videoFeedPropsModel.channel?.let { channel -> baseOptionBuilder.feedResource(FeedResource.Channel(channel)) } } FWVideoFeedSource.Playlist -> { videoFeedPropsModel.channel?.let { channel -> videoFeedPropsModel.playlist?.let { playlist -> baseOptionBuilder.feedResource(FeedResource.Playlist(channel, playlist)) } } } FWVideoFeedSource.DynamicContent -> { videoFeedPropsModel.channel?.let { channel -> baseOptionBuilder.feedResource(FeedResource.DynamicContent(channel, videoFeedPropsModel.dynamicContentParameters?:hashMapOf())) } } FWVideoFeedSource.PlaylistGroup -> { videoFeedPropsModel.playlistGroup?.let { playlistGroupId -> baseOptionBuilder.feedResource(FeedResource.PlaylistGroup(playlistGroupId)) } } FWVideoFeedSource.HashtagPlaylist -> { videoFeedPropsModel.channel?.let { channel -> if (!videoFeedPropsModel.hashtagFilterExpression.isNullOrBlank()) { baseOptionBuilder.feedResource(FeedResource.ChannelHashtag(channel, videoFeedPropsModel.hashtagFilterExpression)) } else { baseOptionBuilder.feedResource(FeedResource.Channel(channel)) } } } FWVideoFeedSource.Sku -> { videoFeedPropsModel.channel?.let { channel -> baseOptionBuilder.feedResource(FeedResource.Sku(channel, videoFeedPropsModel.productIds ?: emptyList())) } } FWVideoFeedSource.ShareUrl -> { videoFeedPropsModel.shareUrl?.let { shareUrl -> baseOptionBuilder.feedResource(FeedResource.ShareUrl(shareUrl)) } } FWVideoFeedSource.SingleContent -> { videoFeedPropsModel.contentId?.let { contentId -> baseOptionBuilder.feedResource(FeedResource.SingleContent(contentId)) } } else -> {} } when (videoFeedPropsModel.mode) { FWVideoFeedMode.Row -> { layoutOptionBuilder.feedLayout(FeedLayout.HORIZONTAL) } FWVideoFeedMode.Column -> { layoutOptionBuilder.feedLayout(FeedLayout.VERTICAL) } FWVideoFeedMode.Grid -> { layoutOptionBuilder.feedLayout(FeedLayout.GRID) } else -> {} } // The native SDK has no separate autosizing switch: it expands a GRID feed to its content // height exactly when `maxItemCount` is set. So the cap is forwarded only for an autosized grid // feed -- sending it for a fixed-height one would silently turn that feed into an expanding one // -- and an autosized grid without an explicit cap still needs one, which is the SDK maximum. if (componentType == FWComponentType.VIDEO_FEED && videoFeedPropsModel.isAutosizedGrid) { baseOptionBuilder.maxItemCount(videoFeedPropsModel.maxVideos ?: BaseOption.MAX_ITEM_COUNT) } if (videoFeedPropsModel.circleShape) { layoutOptionBuilder.circleShape(true) } val backgroundColor = videoFeedPropsModel.videoFeedConfiguration?.backgroundColor if (!backgroundColor.isNullOrBlank()) { layoutOptionBuilder.backgroundColor(FWColorUtil.parseColor(backgroundColor)) } val cornerRadius = videoFeedPropsModel.videoFeedConfiguration?.cornerRadius if (cornerRadius != null && cornerRadius >= 0) { layoutOptionBuilder.roundedCorner(true) .roundedCornerRadius(FWCommonUtil.dpToPx(cornerRadius.toFloat(), context)) } val aspectRatio = videoFeedPropsModel.videoFeedConfiguration?.aspectRatio if (aspectRatio != null && aspectRatio > 0) { layoutOptionBuilder.aspectRatio(aspectRatio) } val title = videoFeedPropsModel.videoFeedConfiguration?.title if (title != null) { val titleHidden = title.hidden val titleTextColor = title.textColor val titleFontSize = title.fontSize val titleNumberOfLines = title.numberOfLines val titleBackgroundColor = title.backgroundColor val titleGradientDrawable = title.gradientDrawable val typeface = getTypeface(context, title.fontInfo) if (titleHidden != null) { titleOptionBuilder.showFeedTitle(!titleHidden) } if (!titleTextColor.isNullOrBlank()) { titleOptionBuilder.feedTitleTextColor(FWColorUtil.parseColor(titleTextColor)) } if (titleFontSize != null && titleFontSize > 0) { titleOptionBuilder.feedTitleTextSize(FWCommonUtil.spToPx(titleFontSize.toFloat(), context)) } if (titleNumberOfLines != null && titleNumberOfLines > 0 && titleNumberOfLines < 6) { titleOptionBuilder.feedTitleTextNumberOfLines(titleNumberOfLines) } typeface?.let { titleOptionBuilder.feedTitleTextTypeface(it) } if (!titleBackgroundColor.isNullOrBlank()) { titleOptionBuilder.feedTitleBackgroundColor(FWColorUtil.parseColor(titleBackgroundColor)) } if (titleGradientDrawable != null) { val orientation = titleGradientDrawable.orientation val colors = titleGradientDrawable.colors if (orientation != null && !colors.isNullOrEmpty()) { var gradientDrawableOrientation = GradientDrawable.Orientation.LEFT_RIGHT when { orientation === FWGradientDrawableOrientation.TOP_BOTTOM -> { gradientDrawableOrientation = GradientDrawable.Orientation.TOP_BOTTOM } orientation === FWGradientDrawableOrientation.TR_BL -> { gradientDrawableOrientation = GradientDrawable.Orientation.TR_BL } orientation === FWGradientDrawableOrientation.RIGHT_LEFT -> { gradientDrawableOrientation = GradientDrawable.Orientation.RIGHT_LEFT } orientation === FWGradientDrawableOrientation.BR_TL -> { gradientDrawableOrientation = GradientDrawable.Orientation.BR_TL } orientation === FWGradientDrawableOrientation.BOTTOM_TOP -> { gradientDrawableOrientation = GradientDrawable.Orientation.BOTTOM_TOP } orientation === FWGradientDrawableOrientation.BL_TR -> { gradientDrawableOrientation = GradientDrawable.Orientation.BL_TR } orientation === FWGradientDrawableOrientation.LEFT_RIGHT -> { gradientDrawableOrientation = GradientDrawable.Orientation.BL_TR } orientation === FWGradientDrawableOrientation.TL_BR -> { gradientDrawableOrientation = GradientDrawable.Orientation.TL_BR } else -> {} } val intColors = colors.map { FWColorUtil.parseColor(it) } titleOptionBuilder.feedTitleBackgroundDrawable( GradientDrawable( gradientDrawableOrientation, intColors.toIntArray(), ) ) } } } when (videoFeedPropsModel.videoFeedConfiguration?.titlePosition) { FWVideoFeedTitlePosition.Nested -> { layoutOptionBuilder.feedTitlePosition(FeedTitlePosition.NESTED) } FWVideoFeedTitlePosition.Stacked -> { layoutOptionBuilder.feedTitlePosition(FeedTitlePosition.STACKED) } else -> {} } val playIcon = videoFeedPropsModel.videoFeedConfiguration?.playIcon if (playIcon != null) { layoutOptionBuilder.showPlayIcon(playIcon.hidden != true) // Sizing is independent of the artwork: it applies to the default icon as well. playIcon.iconWidth?.takeIf { it >= 0 }?.let { layoutOptionBuilder.playIconWidth(FWCommonUtil.dpToPx(it.toFloat(), context)) } getPlayIcon(context, playIcon)?.let { layoutOptionBuilder.playIcon(it) } } val showAdBadge = videoFeedPropsModel.videoFeedConfiguration?.showAdBadge if (showAdBadge != null) { adBadgeOptionBuilder.adBadgeShowOnThumbnails(showAdBadge) } val adBadgeConfigModel = FWGlobalDataUtil.adBadgeConfigModel val adBadgeTextType = adBadgeConfigModel.badgeTextType val adBadgeBackgroundColor = adBadgeConfigModel.backgroundColor val adBadgeTextColor = adBadgeConfigModel.textColor val adBadgeTypeface = getTypeface(context, adBadgeConfigModel.fontInfo) when (adBadgeTextType) { FWBadgeTextType.Ad -> { adBadgeOptionBuilder.adBadgeLabel(AdBadgeTextType.AD) } FWBadgeTextType.Sponsored -> { adBadgeOptionBuilder.adBadgeLabel(AdBadgeTextType.SPONSORED) } else -> {} } if (!adBadgeBackgroundColor.isNullOrBlank()) { adBadgeOptionBuilder.adBadgeBackColor(FWColorUtil.parseColor(adBadgeBackgroundColor)) } if (!adBadgeTextColor.isNullOrBlank()) { adBadgeOptionBuilder.adBadgeTextColor(FWColorUtil.parseColor(adBadgeTextColor)) } adBadgeTypeface?.let { adBadgeOptionBuilder.adBadgeTypeface(it) } val enableAutoplay = videoFeedPropsModel.videoFeedConfiguration?.enableAutoplay enableAutoplay?.let { playerOptionBuilder.autoplay(it) } FWGlobalDataUtil.shareBaseUrl?.let { playerOptionBuilder.shareBaseUrl(it) } val shareBaseUrl = videoFeedPropsModel.videoPlayerConfiguration?.shareBaseUrl shareBaseUrl?.let { playerOptionBuilder.shareBaseUrl(it) } playerOptionBuilder.sdkHandleCtaButtonClick(!FWGlobalDataUtil.customCTAClickEnabled) val gridColumns = videoFeedPropsModel.videoFeedConfiguration?.gridColumns if (gridColumns != null && gridColumns > 0) { layoutOptionBuilder.columnCount(gridColumns) } val itemSpacing = videoFeedPropsModel.videoFeedConfiguration?.itemSpacing if (itemSpacing != null && itemSpacing >= 0) { layoutOptionBuilder.itemSpacing(FWCommonUtil.dpToPx(itemSpacing.toFloat(), context) / 2) } val contentPadding = videoFeedPropsModel.videoFeedConfiguration?.contentPadding if (contentPadding != null) { val top = contentPadding.top?.let { FWCommonUtil.dpToPx(it.toFloat(), context) } ?: 0 val right = contentPadding.right?.let { FWCommonUtil.dpToPx(it.toFloat(), context) } ?: 0 val bottom = contentPadding.bottom?.let { FWCommonUtil.dpToPx(it.toFloat(), context) } ?: 0 val left = contentPadding.left?.let { FWCommonUtil.dpToPx(it.toFloat(), context) } ?: 0 layoutOptionBuilder.feedPadding(FeedPadding(start = left, top = top, end = right, bottom = bottom)) } // player config when (videoFeedPropsModel.videoPlayerConfiguration?.playerStyle) { FWPlayerStyle.Fit -> { playerOptionBuilder.playerMode(PlayerMode.FIT_MODE) } FWPlayerStyle.Full -> { playerOptionBuilder.playerMode(PlayerMode.FULL_BLEED_MODE) } else -> {} } when (videoFeedPropsModel.videoPlayerConfiguration?.scrollDirection) { FWVideoPlayerScrollDirection.Vertical -> { playerOptionBuilder.scrollingOrientation(ScrollingOrientation.VERTICAL) } FWVideoPlayerScrollDirection.Horizontal -> { playerOptionBuilder.scrollingOrientation(ScrollingOrientation.HORIZONTAL) } else -> {} } when (videoFeedPropsModel.videoPlayerConfiguration?.videoCompleteAction) { FWVideoCompleteAction.Loop -> { playerOptionBuilder.autoPlayOnComplete(false) } FWVideoCompleteAction.AdvanceToNext -> { playerOptionBuilder.autoPlayOnComplete(true) } else -> {} } when (videoFeedPropsModel.videoPlayerConfiguration?.feedCompleteAction) { FWFeedCompleteAction.Loop -> { playerOptionBuilder.feedCompleteAction(FeedCompleteAction.LOOP) } FWFeedCompleteAction.Dismiss -> { playerOptionBuilder.feedCompleteAction(FeedCompleteAction.DISMISS) } else -> {} } val showShareButton = videoFeedPropsModel.videoPlayerConfiguration?.showShareButton showShareButton?.let { playerOptionBuilder.showShareButton(it) } val enablePipMode = videoFeedPropsModel.videoPlayerConfiguration?.enablePictureInPicture enablePipMode?.let { playerOptionBuilder.enablePipMode(it) } when (videoFeedPropsModel.videoPlayerConfiguration?.ctaWidth) { FWVideoPlayerCTAWidth.FullWidth -> { ctaOptionBuilder.ctaMode(CtaOption.CtaMode.FULL_WIDTH) } FWVideoPlayerCTAWidth.Compact -> { ctaOptionBuilder.ctaMode(CtaOption.CtaMode.COMPACT) } FWVideoPlayerCTAWidth.SizeToFit -> { ctaOptionBuilder.ctaMode(CtaOption.CtaMode.SIZE_TO_FIT) } else -> {} } val ctaButtonStyle = videoFeedPropsModel.videoPlayerConfiguration?.ctaButtonStyle ctaButtonStyle?.let { style -> val background = if (!style.backgroundColor.isNullOrBlank()) { FWColorUtil.parseColor(style.backgroundColor) } else { null } val fontSize = if (style.fontSize != null && style.fontSize > 0) { FWCommonUtil.spToPx(style.fontSize.toFloat(), context).toFloat() } else { null } val textColor = if (!style.textColor.isNullOrBlank()) { FWColorUtil.parseColor(style.textColor) } else { null } val shape = when (style.shape) { FWShape.ROUND_RECTANGLE -> { Shape.SHAPE_ROUND_RECTANGLE } FWShape.OVAL -> { Shape.SHAPE_OVAL } else -> null } ctaOptionBuilder.ctaStyle(CtaStyle( shape = shape, backgroundColor = background, textColor = textColor, fontSize = fontSize )) } val actionButtonStyle = videoFeedPropsModel.videoPlayerConfiguration?.actionButtonStyle actionButtonStyle?.let { style -> val background = if (!style.backgroundColor.isNullOrBlank()) { FWColorUtil.parseColor(style.backgroundColor) } else { null } val textColor = if (!style.textColor.isNullOrBlank()) { FWColorUtil.parseColor(style.textColor) } else { null } val dividingLineColor = if (!style.dividingLineColor.isNullOrBlank()) { FWColorUtil.parseColor(style.dividingLineColor) } else { null } val shape = when (style.shape) { FWShape.ROUND_RECTANGLE -> { Shape.SHAPE_ROUND_RECTANGLE } FWShape.OVAL -> { Shape.SHAPE_OVAL } else -> null } actionButtonOption.actionButton(ActionButton( backgroundColor = background, textColor = textColor, shape = shape, dividingLineColor = dividingLineColor )) } val cancelButtonStyle = videoFeedPropsModel.videoPlayerConfiguration?.cancelButtonStyle cancelButtonStyle?.let { style -> val background = if (!style.backgroundColor.isNullOrBlank()) { FWColorUtil.parseColor(style.backgroundColor) } else { null } val textColor = if (!style.textColor.isNullOrBlank()) { FWColorUtil.parseColor(style.textColor) } else { null } val shape = when (style.shape) { FWShape.ROUND_RECTANGLE -> { Shape.SHAPE_ROUND_RECTANGLE } FWShape.OVAL -> { Shape.SHAPE_OVAL } else -> null } actionButtonOption.cancelButton(ActionButton( backgroundColor = background, textColor = textColor, shape = shape, )) } playerOptionBuilder.actionButtonOption(actionButtonOption.build()) val chatStyle = videoFeedPropsModel.videoPlayerConfiguration?.chatStyle chatStyle?.let { style -> var shadowColor: Int? = null var shadowOffsetX: Float? = null var shadowOffsetY: Float? = null var shadowRadius: Float? = null // Use textColor directly val textColor: Int? = if (!style.textColor.isNullOrBlank()) { FWColorUtil.parseColor(style.textColor) } else { null } style.textShadow?.let { shadow -> shadowColor = if (!shadow.color.isNullOrBlank()) { if (shadow.opacity != null) { FWColorUtil.parseColorWithAlpha(shadow.opacity, shadow.color) } else { FWColorUtil.parseColor(shadow.color) } } else { null } shadowOffsetX = shadow.offsetX?.toFloat() shadowOffsetY = shadow.offsetY?.toFloat() shadowRadius = shadow.radius?.toFloat() } chatOptionBuilder.chatStyle(ChatStyle( textColor = textColor, textShadow = TextShadow( color = shadowColor, offsetX = shadowOffsetX, offsetY = shadowOffsetY, radius = shadowRadius, ) )) } val ctaDelay = videoFeedPropsModel.videoPlayerConfiguration?.ctaDelay ctaDelay?.let { delay -> delay.value ?: return@let when (delay.type) { FWCtaDelayType.Constant -> { ctaOptionBuilder.ctaDelay( CtaDelay(delay.value.toFloat(), CtaDelayUnit.SECONDS) ) } FWCtaDelayType.Percentage -> { ctaOptionBuilder.ctaDelay( CtaDelay(delay.value.toFloat(), CtaDelayUnit.PERCENTAGE) ) } else -> {} } } val ctaHighlightDelay = videoFeedPropsModel.videoPlayerConfiguration?.ctaHighlightDelay ctaHighlightDelay?.let { delay -> delay.value ?: return@let when (delay.type) { FWCtaDelayType.Constant -> { ctaOptionBuilder.ctaHighlightDelay( CtaDelay(delay.value.toFloat(), CtaDelayUnit.SECONDS) ) } FWCtaDelayType.Percentage -> { ctaOptionBuilder.ctaHighlightDelay( CtaDelay(delay.value.toFloat(), CtaDelayUnit.PERCENTAGE) ) } else -> {} } } val showMuteButton = videoFeedPropsModel.videoPlayerConfiguration?.showMuteButton showMuteButton?.let { playerOptionBuilder.showMuteButton(it) } val showPlaybackButton = videoFeedPropsModel.videoPlayerConfiguration?.showPlaybackButton showPlaybackButton?.let { playerOptionBuilder.showPlayPauseButtonInVideo(it) } val showBranding = videoFeedPropsModel.videoPlayerConfiguration?.showBranding showBranding?.let { playerOptionBuilder.showFireworkLogo(it) } val showVideoDetailTitle = videoFeedPropsModel.videoPlayerConfiguration?.showVideoDetailTitle showVideoDetailTitle?.let { videoDetailsOptionBuilder.showCaption(showVideoDetailTitle) } videoFeedPropsModel.videoPlayerConfiguration?.showMoreButton?.let { playerOptionBuilder.showMoreButton(it) } videoFeedPropsModel.videoPlayerConfiguration?.shareButtonPosition?.let { position -> when (position) { FWShareButtonPosition.VideoDetailPage -> playerOptionBuilder.shareButtonPosition(ShareButtonPosition.MORE_MENU) FWShareButtonPosition.PlayerTopArea -> playerOptionBuilder.shareButtonPosition(ShareButtonPosition.MAIN_VIDEO_VIEW) } } val buttonConfiguration = videoFeedPropsModel.videoPlayerConfiguration?.buttonConfiguration videoDetailsOptionBuilder.buttonIcon(getWidgetImage(context, buttonConfiguration?.videoDetailButton)) closeButtonOptionBuilder.icon(getWidgetImage(context, buttonConfiguration?.closeButton)) closeButtonOptionBuilder.shouldShowWhenPiPEnabled(buttonConfiguration?.shouldShowCloseButtonWhenPiPEnabled ?: false) pipButtonOptionBuilder.icon(getWidgetImage(context, buttonConfiguration?.pipButton)) muteButtonOptionBuilder.muteIcon(getWidgetImage(context, buttonConfiguration?.muteButton)) .unmuteIcon(getWidgetImage(context, buttonConfiguration?.unmuteButton)) playbackButtonOptionBuilder.playIcon(getWidgetImage(context, buttonConfiguration?.playButton)) .pauseIcon(getWidgetImage(context, buttonConfiguration?.pauseButton)) playerUiOptionBuilder.videoDetailsOption(videoDetailsOptionBuilder.build()) .closeButtonOption(closeButtonOptionBuilder.build()) .pipButtonOption(pipButtonOptionBuilder.build()) .muteButtonOption(muteButtonOptionBuilder.build()) .playbackButtonOption(playbackButtonOptionBuilder.build()) playerOptionBuilder.playerUiOption(playerUiOptionBuilder.build()) val adConfiguration = videoFeedPropsModel.adConfiguration if (adConfiguration != null) { adOptionBuilder.vastAttributes(listVastAttributes2Map(adConfiguration.vastAttributes)) if (adConfiguration.adsFetchTimeout != null) { adOptionBuilder.adsFetchTimeoutInSeconds(adConfiguration.adsFetchTimeout) } } val logoConfigOption = videoFeedPropsModel.videoPlayerConfiguration?.videoPlayerLogoConfiguration?.option val logoConfigChannelId = videoFeedPropsModel.videoPlayerConfiguration?.videoPlayerLogoConfiguration?.encodedId val isLogoClickable = videoFeedPropsModel.videoPlayerConfiguration?.videoPlayerLogoConfiguration?.isClickable ?: true when { logoConfigOption == FWVideoPlayerLogoOption.Creator && !logoConfigChannelId.isNullOrBlank() -> { playerOptionBuilder.logoConfig(LogoConfig.Logo.CreatorLogo(logoConfigChannelId, isLogoClickable)) } logoConfigOption == FWVideoPlayerLogoOption.ChannelAggregator && !logoConfigChannelId.isNullOrBlank() -> { playerOptionBuilder.logoConfig(LogoConfig.Logo.AggregatorLogo(logoConfigChannelId, isLogoClickable)) } else -> {} } val countdownTimerAppearance = videoFeedPropsModel.videoPlayerConfiguration?.countdownTimerConfiguration?.appearance val isCountdownTimerHidden = videoFeedPropsModel.videoPlayerConfiguration?.countdownTimerConfiguration?.isHidden ?: true countDownOptionBuilder.isHidden(isCountdownTimerHidden) when (countdownTimerAppearance) { FWAppearanceMode.Light -> countDownOptionBuilder.theme(Theme.LIGHT) FWAppearanceMode.Dark -> countDownOptionBuilder.theme(Theme.DARK) else -> {} } playerOptionBuilder.livestreamCountDownOption(countDownOptionBuilder.build()) videoFeedPropsModel.videoPlayerConfiguration?.enableScrollForVertical?.let { enableScrollForVertical -> storyblockOptionBuilder.enableVerticalSwipe(enableScrollForVertical)} videoFeedPropsModel.videoPlayerConfiguration?.enableAutoPlay?.let { enableAutoplayOnStoryblock -> storyblockOptionBuilder.enableAutoPlay(enableAutoplayOnStoryblock)} videoFeedPropsModel.videoPlayerConfiguration?.showFullScreenIcon?.let { showFullScreenIcon -> storyblockOptionBuilder.showFullScreenIcon(showFullScreenIcon)} videoFeedPropsModel.videoPlayerConfiguration?.enableAutoPause?.let { enableAutoPause -> storyblockOptionBuilder.enableAutoPause(enableAutoPause)} videoFeedPropsModel.videoPlayerConfiguration?.handleAppearanceManually?.let { handleAppearanceManually -> storyblockOptionBuilder.handleAppearanceManually(handleAppearanceManually)} videoFeedPropsModel.videoPlayerConfiguration?.enableSmallSizeInCompact?.let { enableSmallSizeInCompact -> storyblockOptionBuilder.enableSmallSizeInCompact(enableSmallSizeInCompact)} videoFeedPropsModel.videoPlayerConfiguration?.additionalControlsInset?.let { additionalControlsInset -> storyblockOptionBuilder.setImmersiveParam( ImmersiveParam( topControlsInset = FWCommonUtil.dpToPx( (additionalControlsInset.top ?: 0).toFloat(), context ), bottomControlsInset = FWCommonUtil.dpToPx( (additionalControlsInset.bottom ?: 0).toFloat(), context ), ) ) } videoFeedPropsModel.videoPlayerConfiguration?.shouldExtendMediaOutsideSafeArea?.let { shouldExtendMediaOutsideSafeArea -> playerOptionBuilder.enableImmersiveMode(shouldExtendMediaOutsideSafeArea) } when (componentType) { FWComponentType.VIDEO_FEED, FWComponentType.CIRCLE_STORY, FWComponentType.PLAYER_DECK -> { videoFeedPropsModel.videoPlayerConfiguration?.isArrowButtonVisible?.let { isArrowButtonVisible -> playerOptionBuilder.isArrowButtonVisible(isArrowButtonVisible) } } FWComponentType.STORY_BLOCK -> { videoFeedPropsModel.videoPlayerConfiguration?.isArrowButtonVisible?.let { isArrowButtonVisible -> storyblockOptionBuilder.isArrowButtonVisible(isArrowButtonVisible) } videoFeedPropsModel.videoPlayerConfiguration?.isFullscreenArrowButtonVisible?.let { isFullscreenArrowButtonVisible -> playerOptionBuilder.isArrowButtonVisible(isFullscreenArrowButtonVisible) } } FWComponentType.VIDEO_PLAYER -> { videoFeedPropsModel.videoPlayerConfiguration?.isArrowButtonVisible?.let { isArrowButtonVisible -> playerOptionBuilder.isArrowButtonVisible(isArrowButtonVisible) } } } playerOptionBuilder.shareUrlCustomCallBack(FWGlobalDataUtil.customShareUrlCallback) return ViewOptions.Builder() .layoutOption(layoutOptionBuilder.build()) .titleOption(titleOptionBuilder.build()) .playerOption(playerOptionBuilder.build()) .baseOption(baseOptionBuilder.build()) .adBadgeOption(adBadgeOptionBuilder.build()) .ctaOption(ctaOptionBuilder.build()) .adOption(adOptionBuilder.build()) .chatOption(chatOptionBuilder.build()) .storyBlockOption(storyblockOptionBuilder.build()) } private fun getDefaultStoryblockOptionBuilder(): StoryBlockOption.Builder { return StoryBlockOption.Builder() } private fun getDefaultLayoutOptionBuilder(componentType: FWComponentType): LayoutOption.Builder { val context = FWInitializationProvider.INSTANCE.resumedActivity ?: return LayoutOption.Builder() return LayoutOption.Builder() .playIconWidth(FWCommonUtil.dpToPx(defaultPlayIconWidthDp(componentType), context)) .itemSpacing(FWCommonUtil.dpToPx(4.0f, context)) } /** * The play icon width the bridge seeds when the host sets none, in dp. * * The bridge owns these defaults rather than deferring to the native ones, which differ per * component (24dp for a feed, 50dp for a player deck). Player deck keeps 50 so it matches iOS. */ internal fun defaultPlayIconWidthDp(componentType: FWComponentType): Float { return when (componentType) { FWComponentType.PLAYER_DECK -> 50f else -> 36f } } private fun getDefaultTitleOptionBuilder(): TitleOption.Builder { return TitleOption.Builder() } private fun getDefaultPlayerOptionBuilder(): PlayerOption.Builder { return PlayerOption.Builder() .enablePipMode(false) } private fun getDefaultPlayerUiOptionBuilder(): PlayerUiOption.Builder { return PlayerUiOption.Builder() } private fun getDefaultPlayerCountdownTimerOptionBuilder(): LivestreamCountDownOption.Builder { return LivestreamCountDownOption.Builder() } private fun getDefaultActionButtonOptionBuilder(): ActionButtonOption.Builder { return ActionButtonOption.Builder() } private fun getDefaultVideoDetailsOptionBuilder(): VideoDetailsOption.Builder { return VideoDetailsOption.Builder() } private fun getDefaultCloseButtonOptionBuilder(): CloseButtonOption.Builder { return CloseButtonOption.Builder() } private fun getDefaultPipButtonOptionBuilder(): PipButtonOption.Builder { return PipButtonOption.Builder() } private fun getDefaultMuteButtonOptionBuilder(): MuteButtonOption.Builder { return MuteButtonOption.Builder() } private fun getDefaultPlaybackButtonOptionBuilder(): PlaybackButtonOption.Builder { return PlaybackButtonOption.Builder() } private fun getDefaultBaseOptionBuilder(): BaseOption.Builder { return BaseOption.Builder() } private fun getDefaultAdBadgeOptionBuilder(): AdBadgeOption.Builder { return AdBadgeOption.Builder() } private fun getDefaultCtaOptionBuilder(): CtaOption.Builder { return CtaOption.Builder() } private fun getDefaultChatOptionBuilder(): ChatOption.Builder { return ChatOption.Builder() } private fun getDefaultAdOptionBuilder(): AdOption.Builder { return AdOption.Builder() } fun getTypeface(context: Context, font: FWFontInfoModel?): Typeface? { val typefaceName = font?.typefaceName typefaceName ?: return null var typeface = Typeface.DEFAULT if (font.isCustom == true) { typeface = Typeface.createFromAsset(context.assets, typefaceName.rawValue) } else { when (typefaceName) { FWSystemTypeface.DefaultBold -> { typeface = Typeface.DEFAULT_BOLD } FWSystemTypeface.SansSerif -> { typeface = Typeface.SANS_SERIF } FWSystemTypeface.Serif -> { typeface = Typeface.SERIF } FWSystemTypeface.Monospace -> { typeface = Typeface.MONOSPACE } else -> {} } } return typeface } private fun listVastAttributes2Map(list: List?): Map? { list ?: return null val map = mutableMapOf() for (item in list) { if (!item.name.isNullOrBlank() && !item.value.isNullOrBlank()) { map[item.name] = item.value } } return map } /** * Maps a host-supplied play icon image onto [PlayIcon]. Returns null when no usable drawable was * given so the component keeps its own default icon. Sizing is handled separately by * playIconWidth. */ private fun getPlayIcon( context: Context, playIcon: FWVideoFeedConfigModel.FWPlayIconModel, ): PlayIcon? { // No tintColor is passed: the thumbnail play icon is rendered as-is so a multi-color image // keeps its own colors. val widgetImage = getWidgetImage(context, FWButtonInfoModel(imageName = playIcon.imageName)) ?: return null return PlayIcon.CustomIcon(image = widgetImage) } private fun getWidgetImage(context: Context, buttonInfo: FWButtonInfoModel?): WidgetImage? { if (buttonInfo == null) { return null } val drawableRes = getDrawableByImageName(context, buttonInfo.imageName) ?: return null return WidgetImage( drawableRes = drawableRes, tintColor = if (buttonInfo.tintColor.isNullOrBlank()) null else FWColorUtil.parseColor(buttonInfo.tintColor) ) } @SuppressLint("DiscouragedApi") private fun getDrawableByImageName(context: Context, imageName: String?): Int? { if (imageName.isNullOrBlank()) { return null } return try { // getIdentifier returns 0, not an exception, when the drawable is missing. context.resources.getIdentifier(imageName, "drawable", context.packageName).takeIf { it != 0 } } catch (_: Exception) { null } } }