package com.fireworksdk.bridge.reactnative.manager import android.view.Choreographer import android.view.View import androidx.fragment.app.FragmentActivity import com.facebook.react.bridge.ReadableArray import com.facebook.react.bridge.ReadableMap import com.facebook.react.common.MapBuilder import com.facebook.react.uimanager.SimpleViewManager import com.facebook.react.uimanager.ThemedReactContext import com.facebook.react.uimanager.annotations.ReactProp import com.firework.error.videofeed.VideoFeedError import com.firework.videofeed.FeedViewState import com.fireworksdk.bridge.components.playerdeck.PlayerDeckContainerView import com.fireworksdk.bridge.models.* import com.fireworksdk.bridge.models.enums.FWPlayerDeckEventName import com.fireworksdk.bridge.models.enums.FWVideoFeedSource import com.fireworksdk.bridge.reactnative.utils.FWEventUtils import com.fireworksdk.bridge.utils.FWLogUtils import com.fireworksdk.bridge.utils.FWModelUtils.internalLiveStreamStatus import com.fireworksdk.bridge.utils.FWModelUtils.internalVideoType import java.util.WeakHashMap // SimpleViewManager (not ViewGroupManager) because PlayerDeck has no React children. // ViewGroupManager causes RN to recursively drop internal native children during // manageChildren/dropView, which interferes with RecyclerView's attach state. class FWPlayerDeckManager : SimpleViewManager() { private val viewContextMap = WeakHashMap() private val viewLayoutCallbackMap = WeakHashMap() private val viewNeedsForceLayoutMap = WeakHashMap() private val viewInitializedMap = WeakHashMap() override fun getName(): String = REACT_CLASS override fun createViewInstance(reactContext: ThemedReactContext): PlayerDeckContainerView { val activity = reactContext.currentActivity as? FragmentActivity ?: throw IllegalStateException("PlayerDeck requires a FragmentActivity") val containerView = PlayerDeckContainerView(activity) viewContextMap[containerView] = reactContext containerView.addOnAttachStateChangeListener(object : View.OnAttachStateChangeListener { override fun onViewAttachedToWindow(v: View) { viewNeedsForceLayoutMap[containerView] = true startLayoutUpdates(containerView) } override fun onViewDetachedFromWindow(v: View) { stopLayoutUpdates(containerView) } }) return containerView } override fun onAfterUpdateTransaction(view: PlayerDeckContainerView) { super.onAfterUpdateTransaction(view) initPlayerDeck(view) } private fun initPlayerDeck(containerView: PlayerDeckContainerView) { if (viewInitializedMap[containerView] == true) return val reactContext = viewContextMap[containerView] ?: return val activity = reactContext.currentActivity as? FragmentActivity ?: return addPlayerDeckListeners(reactContext, containerView) containerView.initPlayerDeck(activity.supportFragmentManager) viewInitializedMap[containerView] = true FWEventUtils.receivePlayerDeckGetFeedIdEvent( reactContext, containerView.id, containerView.getFeedId() ) } private fun startLayoutUpdates(containerView: PlayerDeckContainerView) { if (viewLayoutCallbackMap.containsKey(containerView)) return val callback = object : Choreographer.FrameCallback { override fun doFrame(frameTimeNanos: Long) { manuallyLayoutChildren(containerView) if (containerView.viewTreeObserver.isAlive) { containerView.viewTreeObserver.dispatchOnGlobalLayout() } if (viewLayoutCallbackMap.containsKey(containerView)) { Choreographer.getInstance().postFrameCallback(this) } } } viewLayoutCallbackMap[containerView] = callback Choreographer.getInstance().postFrameCallback(callback) } private fun stopLayoutUpdates(containerView: PlayerDeckContainerView) { viewLayoutCallbackMap.remove(containerView)?.let { Choreographer.getInstance().removeFrameCallback(it) } } private fun manuallyLayoutChildren(containerView: PlayerDeckContainerView) { val needsForce = viewNeedsForceLayoutMap.remove(containerView) == true val width = containerView.width val height = containerView.height for (i in 0 until containerView.childCount) { val child = containerView.getChildAt(i) if (needsForce) { child.forceLayout() } child.measure( View.MeasureSpec.makeMeasureSpec(width, View.MeasureSpec.EXACTLY), View.MeasureSpec.makeMeasureSpec(height, View.MeasureSpec.EXACTLY) ) child.layout(0, 0, width, height) } } override fun getCommandsMap(): Map { return mapOf( "refresh" to COMMAND_REFRESH, ) } override fun receiveCommand( root: PlayerDeckContainerView, commandId: Int, args: ReadableArray? ) { super.receiveCommand(root, commandId, args) FWLogUtils.d { "FWPlayerDeckManager receiveCommand (Int), commandId: $commandId" } handleCommand(root, commandId) } override fun receiveCommand( root: PlayerDeckContainerView, commandId: String?, args: ReadableArray? ) { super.receiveCommand(root, commandId, args) FWLogUtils.d { "FWPlayerDeckManager receiveCommand (String), commandId: $commandId" } handleCommand(root, commandId?.toIntOrNull() ?: return) } private fun handleCommand(root: PlayerDeckContainerView, commandId: Int) { when (commandId) { COMMAND_REFRESH -> root.refresh() } } @ReactProp(name = "source") fun setSource(view: PlayerDeckContainerView, source: String?) { view.videoFeedPropsModel = view.videoFeedPropsModel.copy( source = FWVideoFeedSource.deserialize(source) ) } @ReactProp(name = "channel") fun setChannel(view: PlayerDeckContainerView, channel: String?) { view.videoFeedPropsModel = view.videoFeedPropsModel.copy(channel = channel) } @ReactProp(name = "playlist") fun setPlaylist(view: PlayerDeckContainerView, playlist: String?) { view.videoFeedPropsModel = view.videoFeedPropsModel.copy(playlist = playlist) } @ReactProp(name = "playlistGroup") fun setPlaylistGroup(view: PlayerDeckContainerView, playlistGroup: String?) { view.videoFeedPropsModel = view.videoFeedPropsModel.copy(playlistGroup = playlistGroup) } @ReactProp(name = "hashtagFilterExpression") fun setHashtagFilterExpression(view: PlayerDeckContainerView, hashtagFilterExpression: String?) { view.videoFeedPropsModel = view.videoFeedPropsModel.copy( hashtagFilterExpression = hashtagFilterExpression ) } @Suppress("UNCHECKED_CAST") @ReactProp(name = "productIds") fun setProductIds(view: PlayerDeckContainerView, productIdArray: ReadableArray?) { view.videoFeedPropsModel = view.videoFeedPropsModel.copy( productIds = productIdArray?.toArrayList() as ArrayList? ) } @ReactProp(name = "contentId") fun setContentId(view: PlayerDeckContainerView, contentId: String?) { view.videoFeedPropsModel = view.videoFeedPropsModel.copy(contentId = contentId) } @ReactProp(name = "shareUrl") fun setShareUrl(view: PlayerDeckContainerView, shareUrl: String?) { view.videoFeedPropsModel = view.videoFeedPropsModel.copy(shareUrl = shareUrl) } @Suppress("UNCHECKED_CAST") @ReactProp(name = "dynamicContentParameters") fun setDynamicContentParameters(view: PlayerDeckContainerView, parameters: ReadableMap?) { val parametersMap = parameters?.toHashMap() as? HashMap> view.videoFeedPropsModel = view.videoFeedPropsModel.copy( dynamicContentParameters = parametersMap ) } @ReactProp(name = "videoPlayerConfiguration") fun setVideoPlayerConfig(view: PlayerDeckContainerView, config: ReadableMap?) { val configMap = config?.toHashMap() ?: hashMapOf() val jsonObject = org.json.JSONObject(configMap) val videoPlayerConfigModel = FWVideoPlayerConfigModelDeserializer.deserialize(jsonObject) view.videoFeedPropsModel = view.videoFeedPropsModel.copy( videoPlayerConfiguration = videoPlayerConfigModel ) } @ReactProp(name = "adConfiguration") fun setAdConfiguration(view: PlayerDeckContainerView, config: ReadableMap?) { val configMap = config?.toHashMap() ?: hashMapOf() val jsonObject = org.json.JSONObject(configMap) val adConfigurationModel = FWAdConfigurationDeserializer.deserialize(jsonObject) view.videoFeedPropsModel = view.videoFeedPropsModel.copy( adConfiguration = adConfigurationModel ) } @Suppress("UNCHECKED_CAST") @ReactProp(name = "playerDeckConfiguration") fun setPlayerDeckConfiguration(view: PlayerDeckContainerView, config: ReadableMap?) { view.playerDeckConfigMap = config?.toHashMap() as? Map } @ReactProp(name = "cornerRadius") fun setCornerRadius(view: PlayerDeckContainerView, cornerRadius: Int?) { view.videoFeedPropsModel = view.videoFeedPropsModel.copy(cornerRadius = cornerRadius) } private fun addPlayerDeckListeners( reactContext: ThemedReactContext, containerView: PlayerDeckContainerView, ) { containerView.setShoppingOverlayListener { isOpen -> containerView.isShoppingOverlayOpen = isOpen } containerView.setFeedViewStateListener { feedViewState -> when (feedViewState) { is FeedViewState.Loading -> { FWLogUtils.d { "PlayerDeck.FeedViewState.Loading" } } is FeedViewState.LoadData -> { FWLogUtils.d { "PlayerDeck.FeedViewState.LoadData" } FWEventUtils.receivePlayerDeckLoadFinishedSuccessEvent( reactContext, containerView.id ) } is FeedViewState.EmptyFeed -> { FWLogUtils.d { "PlayerDeck.FeedViewState.EmptyFeed" } FWEventUtils.receivePlayerDeckEmptyEvent(reactContext, containerView.id) } is FeedViewState.EndOfFeed -> { FWLogUtils.d { "PlayerDeck.FeedViewState.EndOfFeed" } FWEventUtils.receivePlayerDeckLoadFinishedSuccessEvent( reactContext, containerView.id ) } else -> {} } } containerView.setOnErrorListener { error -> when (error) { is VideoFeedError.LoadingFirstPageFailed -> { FWEventUtils.receivePlayerDeckLoadFinishedFailedEvent( reactContext, containerView.id, error.message, error.message ) } is VideoFeedError.LoadingNextPageFailed -> { FWEventUtils.receivePlayerDeckLoadFinishedFailedEvent( reactContext, containerView.id, error.message, error.message ) } else -> {} } } containerView.setOnFeedLoadListener { videos -> FWLogUtils.d { "PlayerDeck onFeedLoad, ${videos.size} video(s)" } FWEventUtils.receivePlayerDeckVideosLoadedEvent( reactContext, containerView.id, videos ) } containerView.setOnFeedItemClickListener { feedItem -> val fwVideoFeedItemDetailsModel = FWVideoFeedItemDetailsModel( containerView.getFeedId(), feedItem.indexInTheList, feedItem.id, feedItem.duration.toInt(), containerView.videoFeedPropsModel.source?.rawValue, feedItem.title, containerView.videoFeedPropsModel.playlistGroup, containerView.videoFeedPropsModel.playlist, containerView.videoFeedPropsModel.channel, feedItem.videoInfo.internalVideoType, feedItem.videoInfo.internalLiveStreamStatus, containerView.videoFeedPropsModel.dynamicContentParameters ) FWEventUtils.sendVideoFeedClickEvent(reactContext, fwVideoFeedItemDetailsModel) } } override fun onDropViewInstance(containerView: PlayerDeckContainerView) { stopLayoutUpdates(containerView) viewContextMap.remove(containerView) viewNeedsForceLayoutMap.remove(containerView) viewInitializedMap.remove(containerView) super.onDropViewInstance(containerView) containerView.destroy() } override fun getExportedCustomBubblingEventTypeConstants(): Map? { return MapBuilder.builder() .put( FWPlayerDeckEventName.PlayerDeckLoadFinished.rawValue, MapBuilder.of( "phasedRegistrationNames", MapBuilder.of("bubbled", FWPlayerDeckEventName.PlayerDeckLoadFinished.rawValue) ) ) .put( FWPlayerDeckEventName.PlayerDeckEmpty.rawValue, MapBuilder.of( "phasedRegistrationNames", MapBuilder.of("bubbled", FWPlayerDeckEventName.PlayerDeckEmpty.rawValue) ) ) .put( FWPlayerDeckEventName.PlayerDeckGetFeedId.rawValue, MapBuilder.of( "phasedRegistrationNames", MapBuilder.of("bubbled", FWPlayerDeckEventName.PlayerDeckGetFeedId.rawValue) ) ) .put( FWPlayerDeckEventName.PlayerDeckVideosLoaded.rawValue, MapBuilder.of( "phasedRegistrationNames", MapBuilder.of("bubbled", FWPlayerDeckEventName.PlayerDeckVideosLoaded.rawValue) ) ) .build() } companion object { private const val REACT_CLASS = "FWPlayerDeck" private const val COMMAND_REFRESH = 1000000 } }