Ram Prakash Bhat
Ram Prakash Bhat

Reputation: 1258

DPAD events handling with Jetpack Compose when Talkback is ON (Media 3 exoplayer)

I am using ExoPlayer (Media3) in Android TV with compose, a streaming application, but when Accessibility/Talkback is ON, DPAD events are not being captured at client side, only back/Enter/Center button is getting captured.

In the normal mode i.e. Accessibility/Talkback is OFF all is working as expected.

But some how in Fire TV am getting all dpad event when Accessibility is ON and OFF

Below is the code is to inflate the media3 exoplayer.

@Composable
private fun CustomPlayerView(
    modifier: Modifier = Modifier,
    areControlsVisible: Boolean,
    isSubtitlesVisible: Boolean,
    isInTv: Boolean = false,
    isInPipMode: Boolean = false,
    resizeMode: Int = AspectRatioFrameLayout.RESIZE_MODE_FIT,
    getPlayerView: (view: View) -> Unit
) {
    val playerManager by rememberLocalPlayer()

    AndroidViewBinding(
        modifier = modifier,
        factory = { inflater, viewGroup, attachToParent ->
            val binding = CustomPlayerBinding.inflate(inflater, viewGroup, attachToParent)

            binding.apply {
                root.isClickable = false
                root.isFocusable = false

                playerView.run {
                    [email protected] = resizeMode
                    [email protected] = playerManager.player

                    // Show the controls forever
                    controllerShowTimeoutMs = Int.MAX_VALUE
                    showController()
                 }
            }

            return@AndroidViewBinding binding
        }

Player screen code:

            CustomPlayerView(
                        areControlsVisible = controlsUIState.controlUIState is ControlUISate.Show,
                        isInPipMode = false,
                        playbackState = playbackState,
                        onPlayerAction = onPlayerControlsAction,
                        onInitialize = {
                            // Initialise player
                        },
                        onRelease = { isForceReleasing ->
                           // Handle release
                        },
                        modifier = Modifier
                            .focusOnInitialVisibility()
                            .focusable()
                            .handleDPadKeyEvents(
                                isSeeking = trickPlayUIState.isSeeking,
                                onEnter = { _, _ ->
                                    handlePlayBack()
                                },
                                onUp = { _, _ ->
                                    //Handle Dpad UP
                                },
                                onLeft = { isLongPress, isReleased ->
                                    //Handle Dpad LEFT
                                },
                                onRight = { isLongPress, isReleased ->
                                    //Handle Dpad RIGHT
                                },
                                onMediaKeyFf = { isLongPress, isReleased ->
                                    //Handle Dpad FF
                                },
                                onMediaKeyRw = { isLongPress, isReleased ->
                                    //Handle Dpad RW
                                },
                                onKeyDown = { _, _ ->
                                    //Handle Dpad DOWN
                                },
                                onMediaKey = {
                                    //Handle Dpad Media Key generic
                                },
                                onMediaKeyPlayPause = { _, _ ->
                                    handlePlayBack()
                                },
                                shouldConsumeRightEvent = true,
                                shouldConsumeLefEvent = true,
                                shouldConsumeEventPlayPauseEvent = true
                            ),
                    )

Dpad ext. fun

      /**
       * Handles horizontal (Left & Right) D-Pad Keys and consumes the event(s) so 
        that the  focus doesn't
       * accidentally move to another element.
      * */

     @Composable
     fun Modifier.handleDPadKeyEvents(
      onLeft: ((isLongPress: Boolean, isReleased: Boolean) -> Unit)? = null,
      onRight: ((isLongPress: Boolean, isReleased: Boolean) -> Unit)? = null,
      onUp: ((isLongPress: Boolean, isReleased: Boolean) -> Unit)? = null,
      onDown: ((isLongPress: Boolean, isReleased: Boolean) -> Unit)? = null,
      onEnter: ((isLongPress: Boolean, isReleased: Boolean) -> Unit)? = null,
      onKeyDown: ((isLongPress: Boolean, isReleased: Boolean) -> Unit)? = null,
      onBack: ((isLongPress: Boolean, isReleased: Boolean) -> Unit)? = null,
      onMediaKey: ((keyCode: Int) -> Unit)? = null, // Callback for media keys
      onMediaKeyFf: ((isLongPress: Boolean, isReleased: Boolean) -> Unit)? = null,
      onMediaKeyRw: ((isLongPress: Boolean, isReleased: Boolean) -> Unit)? = null,
      onMediaKeyPlayPause: ((isLongPress: Boolean, isReleased: Boolean) -> Unit)? = null,
      shouldConsumeEvent: Boolean = false,
      shouldConsumeLefEvent: Boolean = false,
      shouldConsumeRightEvent: Boolean = false,
      shouldConsumeEventPlayPauseEvent: Boolean = false,
      isSeeking: Boolean = false
    ): Modifier {
    val holdThreshold = 5 // Milliseconds for press-and-hold detection
    var pressStartTime by remember { mutableIntStateOf(0) }
    var isLongPressDetected by remember { mutableStateOf(false) } // Flag to track if long press has been detected

LaunchedEffect(isSeeking) {
    // This is required to reset the variables when long press and move focus to another view.
    // This case On Action up of previous focussed view is not called, so that blocking us to reset these variable
    if (!isSeeking) {
        pressStartTime = 0
        isLongPressDetected = false
    }
}

return this.onPreviewKeyEvent { event ->
    val nativeKeyEvent = event.nativeKeyEvent

    fun handleKeyAction(block: (isLongPress: Boolean, isReleased: Boolean) -> Unit) {
        when (nativeKeyEvent.action) {
            KeyEvent.ACTION_UP -> {

                block(isLongPressDetected, true)
                // Reset variables
                pressStartTime = 0
                isLongPressDetected = false
            }

            KeyEvent.ACTION_DOWN -> {
                // only for the first press, as we have requirement, like on key down only we need to show controller.
                if (pressStartTime == 0) onKeyDown?.invoke(false, false)
                pressStartTime += 1
                if (pressStartTime > holdThreshold) {
                    block(true, false)
                    isLongPressDetected = true // Mark that long press was detected
                }
            }
        }
    }

    when {
        // Handle D-Pad Navigation Keys
        DPadEventsKeyCodes.contains(nativeKeyEvent.keyCode) -> {
            when (nativeKeyEvent.keyCode) {
                KeyEvent.KEYCODE_DPAD_LEFT, KeyEvent.KEYCODE_SYSTEM_NAVIGATION_LEFT -> {
                    onLeft?.let {
                        handleKeyAction(it::invoke)
                        return@onPreviewKeyEvent shouldConsumeLefEvent
                    }
                }

                KeyEvent.KEYCODE_DPAD_RIGHT, KeyEvent.KEYCODE_SYSTEM_NAVIGATION_RIGHT -> {
                    onRight?.let {
                        handleKeyAction(it::invoke)
                        return@onPreviewKeyEvent shouldConsumeRightEvent
                    }
                }

                KeyEvent.KEYCODE_DPAD_UP, KeyEvent.KEYCODE_SYSTEM_NAVIGATION_UP -> {
                    onUp?.let { handleKeyAction(it::invoke) }
                }

                KeyEvent.KEYCODE_DPAD_DOWN, KeyEvent.KEYCODE_SYSTEM_NAVIGATION_DOWN -> {
                    onDown?.let { handleKeyAction(it::invoke) }
                }

                KeyEvent.KEYCODE_DPAD_CENTER, KeyEvent.KEYCODE_ENTER, KeyEvent.KEYCODE_NUMPAD_ENTER -> {
                    onEnter?.let { handleKeyAction(it::invoke) }
                }
            }
            shouldConsumeEvent
        }

        // Handle Media Key Events, FF/RW for media keys, rest will be handled by system .
        MediaKeyCodes.contains(nativeKeyEvent.keyCode) -> {
            when (nativeKeyEvent.keyCode) {
                KeyEvent.KEYCODE_MEDIA_FAST_FORWARD -> {
                    onMediaKeyFf?.let {
                        handleKeyAction(it::invoke)
                        return@onPreviewKeyEvent true
                    }
                }

                KeyEvent.KEYCODE_MEDIA_REWIND -> {
                    onMediaKeyRw?.let {
                        handleKeyAction(it::invoke)
                        return@onPreviewKeyEvent true
                    }
                }
                // Handle  Dedicated Media button Play/Pause at client side
                KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE, KeyEvent.KEYCODE_MEDIA_PLAY, KeyEvent.KEYCODE_MEDIA_PAUSE -> {
                    onMediaKeyPlayPause?.let {
                        handleKeyAction(it::invoke)
                        return@onPreviewKeyEvent shouldConsumeEventPlayPauseEvent
                    }
                }

                else -> {
                    onMediaKey?.invoke(nativeKeyEvent.keyCode) // Invoke media key callback
                }
            }
            shouldConsumeEvent
        }

        // Handle System Key Events, we may need to handle like handleKeyAction(onMediaKey?.invoke) for system keys.
        SystemKeyCodes.contains(nativeKeyEvent.keyCode) && nativeKeyEvent.action == KeyEvent.ACTION_DOWN -> {
            when (nativeKeyEvent.keyCode) {
                KeyEvent.KEYCODE_BACK -> onBack?.invoke(false, false)
                else -> onKeyDown?.invoke(false, false)
            }
            shouldConsumeEvent
        }

        else -> shouldConsumeEvent
    }
}

}

Initial focus ext. fun

 /**
  * This modifier can be used to gain focus on a focusable component when it becomes 
    visible
  * for the first time.
  * */
    @Composable
    fun Modifier.focusOnInitialVisibility(
    isVisible: MutableState<Boolean> = remember { mutableStateOf(false) }
   ): Modifier {
    val focusRequester = remember { FocusRequester() }

       return focusRequester(focusRequester)
       .onGloballyPositioned {
          if (!isVisible.value) {
            focusRequester.requestFocus()
            isVisible.value = true
          }
        }
  }

Does anyone have any idea how can I properly handle those events? The controller doesn't show up on top of the video due to this and can't perform any events such as play/pause/seek, etc.

Any help will be greatly appreciated.

Thank you!.

Upvotes: 0

Views: 33

Answers (0)

Related Questions