Search code examples
androidandroid-jetpack-composechromecast

Jetpack Composable with MediaRouteActionProvider


I am currently integrating Chromcast into our app. We are using Jetpack Compose for all out UI elements. We are using a TopAppBar composable and I am trying to add the Chromecast button to it using the MediaRouteActionProvider. The only way I can find to use the MediaRouteActionProvider is by using a menu.xml and inflating the menu in onCreateOptionsMenu.

Does anyone know of a way to use an ActionProvider outside the context of a menu with Jetpack Compose or am I stuck with using a menu for now?


Solution

  • UPDATE: Decided to take another crack at this.

    import android.content.Context
    import androidx.lifecycle.MutableLiveData
    import androidx.lifecycle.ViewModel
    import androidx.lifecycle.ViewModelProvider
    import androidx.mediarouter.app.MediaRouteDialogFactory
    import androidx.mediarouter.media.MediaControlIntent
    import androidx.mediarouter.media.MediaRouteSelector
    import com.google.android.gms.cast.framework.CastContext
    import com.google.android.gms.cast.framework.CastState
    import com.google.android.gms.cast.framework.CastStateListener
    
    class MediaRouteViewModel(context: Context) : ViewModel(), CastStateListener {
    
        var isCastingAvailable = false
            private set
    
        val castingStateLiveData = MutableLiveData(CastingState.UNKNOWN)
    
        val mDialogFactory = MediaRouteDialogFactory.getDefault()
    
        val mSelector: MediaRouteSelector = MediaRouteSelector.Builder()
            .addControlCategory(MediaControlIntent.CATEGORY_LIVE_AUDIO)
            .addControlCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK)
            .build()
    
        init {
            try {
                isCastingAvailable = true
    
                val castingContext = CastContext.getSharedInstance(context)
                castingContext.addCastStateListener(this)
            } catch (e: Exception) {
                // handle me please
            }
        }
    
        fun shouldShowChooserFragment(): Boolean {
            return when (castingStateLiveData.value) {
                CastingState.NOT_CONNECTED -> true
                CastingState.UNKNOWN -> true
                CastingState.NO_DEVICES_AVAILABLE -> true
                CastingState.CONNECTING -> false
                CastingState.CONNECTED -> false
                else -> false
            }
        }
    
        enum class CastingState {
            CONNECTED,
            CONNECTING,
            NOT_CONNECTED,
            NO_DEVICES_AVAILABLE,
            UNKNOWN
        }
    
        class Factory(val context: Context) : ViewModelProvider.Factory {
            override fun <T : ViewModel?> create(modelClass: Class<T>): T {
                return MediaRouteViewModel(context = context) as T
            }
        }
    
        override fun onCastStateChanged(p0: Int) {
            val castingState = when (p0) {
                CastState.CONNECTED -> CastingState.CONNECTED
                CastState.CONNECTING -> CastingState.CONNECTING
                CastState.NOT_CONNECTED -> CastingState.NOT_CONNECTED
                CastState.NO_DEVICES_AVAILABLE -> CastingState.NO_DEVICES_AVAILABLE
                else -> CastingState.UNKNOWN
            }
    
            castingStateLiveData.postValue(castingState)
        }
    }
    
    @Composable
    fun MediaRouter(
        modifier: Modifier,
        iconWidth: Dp,
        fragmentManager: FragmentManager
    ) {
        val context = LocalContext.current
    
        val mediaRouteProviderViewModel: MediaRouteViewModel =
            viewModel(factory = MediaRouteViewModel.Factory(context))
    
        // Can use the casting state to change the painter accordingly
        val castingState = mediaRouteProviderViewModel.castingStateLiveData.observeAsState()
    
        if (mediaRouteProviderViewModel.isCastingAvailable) {
            Box(modifier = modifier.size(iconWidth)) {
                Image(
                    painter = painterResource(id = R.drawable.ic_baseline_cast_connected_24),
                    contentDescription = null,
                    modifier = Modifier
                        .clickable {
                            val shouldShowChooserFragment = mediaRouteProviderViewModel.shouldShowChooserFragment()
                            val fragmentTag = if (shouldShowChooserFragment) "MediaRouteChooserDialogFragment" else "MediaRouteControllerDialogFragment"
                            
                            val fragment = if (shouldShowChooserFragment) {
                                mediaRouteProviderViewModel.mDialogFactory
                                    .onCreateChooserDialogFragment()
                                    .apply {
                                        routeSelector = mediaRouteProviderViewModel.mSelector
                                    }
                            } else {
                                mediaRouteProviderViewModel.mDialogFactory.onCreateControllerDialogFragment()
                            }
    
                            val transaction = fragmentManager.beginTransaction()
                            transaction.add(fragment, fragmentTag)
                            transaction.commitAllowingStateLoss()
                        },
                    contentScale = ContentScale.FillBounds
                )
            }
        }
    }
    

    Old Post:

    I'm sure there's a better way of doing this in compose but this at least gets the job done.

    class MediaRouteViewModel(context: Context) : ViewModel() {
        private val mediaRouteActionProvider = MediaRouteActionProvider(context)
    
        val buttonView: View
    
        init {
            buttonView = mediaRouteActionProvider.onCreateActionView()
            mediaRouteActionProvider.routeSelector = MediaRouteSelector.Builder()
                .addControlCategory(MediaControlIntent.CATEGORY_LIVE_AUDIO)
                .addControlCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK)
                .build()
        }
    
    
        fun onClick() {
            mediaRouteActionProvider.onPerformDefaultAction()
        }
    }
    
    @Composable
    fun MediaRouter(modifier: Modifier, iconWidth: Dp) {
        val context = LocalContext.current
    
        val mediaRouteProviderViewModel = MediaRouteViewModel(context)
    
        Box(modifier = modifier.size(iconWidth)) {
            AndroidView(factory = {
                mediaRouteProviderViewModel.buttonView
            }, modifier = Modifier.fillMaxSize())
    
            Box(modifier = Modifier
                .fillMaxSize()
                .clickable { mediaRouteProviderViewModel.onClick() })
        }
    }