mksteve
mksteve

Reputation: 13073

Getting Google Cast v3 to work on unsupported devices

The Cast V3 framework has features which try to make it possible to run on devices without the Google Play Services required for it to work, but I ran into some issues when testing.

  1. On the Kindle the Google API returns SERVICE_INVALID with a isUserResolvable() true.
  2. On devices with the onActivityResult returning ConnectionResult.SUCCESS after upgrade, the CastContext.getSharedInstance() can throw RuntimeError.
  3. As a side-effect of 2), the XML inflate of items containing MiniControllerFragment will fail.

Some errors I found were

java.lang.RuntimeException: Unable to start activity ComponentInfo{##########.MainActivity}: android.view.InflateException: Binary XML file line #42: Error inflating class fragment

Caused by: java.lang.RuntimeException: 
  com.google.android.gms.dynamite.DynamiteModule$zzc: Remote load failed. No local fallback found.
    at com.google.android.gms.internal.zzauj.zzan(Unknown Source)
    at com.google.android.gms.internal.zzauj.zza(Unknown Source)
    at com.google.android.gms.cast.framework.CastContext.<init>(Unknown Source)
    at com.google.android.gms.cast.framework.CastContext.getSharedInstance(Unknown Source)
    at com.google.android.gms.cast.framework.media.uicontroller.UIMediaController.<init>(Unknown Source)
    at com.google.android.gms.cast.framework.media.widget.MiniControllerFragment.onCreateView(Unknown Source)

This was caused by the inflation of the MiniControllerFragment, on a device where the CastController code wasn't installed. This is similar to the question asked SO : Cast v3 is crashing on devices below 5.0. The answer provided by Kamil Ślesiński helped in my investigation.

and

java.lang.RuntimeException: Failure delivering result ResultInfo{who=null, request=123, result=0, data=null} to activity #####

When I had implemented my ViewStub, I was still crashing in the pre-release test machines, as they were returning SUCCESS, but didn't have the CastContext available. To fix this, I needed another test to check if the CastContext was creatable.

Upvotes: 0

Views: 1380

Answers (1)

mksteve
mksteve

Reputation: 13073

You need a singleton / code in the Application something like below....

boolean gCastable = false;
boolean gCastTested = false;
public boolean isCastAvailable(Activity act, int resultCode ){
    if( gCastTested == true ){
        return gCastable;
    }

    GoogleApiAvailability castApi = GoogleApiAvailability.getInstance();
    int castResult = castApi.isGooglePlayServicesAvailable(act);
    switch( castResult ) {
        case ConnectionResult.SUCCESS:
            gCastable = true;
            gCastTested = true;
            return true;
     /*  This code is needed, so that the user doesn't get a 
      *
      *  your device is incompatible "OK" 
      *
      * message, it isn't really "user actionable"
      */
        case ConnectionResult.SERVICE_INVALID: // Result from Amazon kindle - perhaps check if kindle first??
            gCastable = false;
            gCastTested = true;
            return false;
      ////////////////////////////////////////////////////////////////
        default:
            if (castApi.isUserResolvableError(castResult)) {
                castApi.getErrorDialog(act, castResult, resultCode, new DialogInterface.OnCancelListener() {
                    @Override
                    public void onCancel(DialogInterface dialog) {
                        gCastable = false;
                        gCastTested = false;
                        return;
                    }
                }).show();
            } else {
                gCastTested = true;
                gCastable = false;
                return false;
            }
    }
    return gCastable;
}

public void setCastOK(Activity mainActivity, boolean result ) {
    gCastTested = true;
    gCastable = result;
}

and a helper function to check if we know the state of the cast.

public boolean isCastAvailableKnown() {
    return gCastable;
}

However to cope with devices which return SUCCESS, I also needed the following code in the App / singleton.

When the Activity receives the cast result, we create a CastContext. The "hope" is, if the Application can create the CastContext, then the framework will succeed in the same way (the cause of the crash).

public boolean onCastResultReceived( Activity act, int result ) {
    boolean wasOk = false;
    if( result == ConnectionResult.SUCCESS ){
        try {
            CastContext ctx = CastContext.getSharedInstance(act );
            wasOk = true;
        } catch ( RuntimeException e ){
            wasOk = false;
        }
    }
    if( wasOk ) {
        setCastOK(act, true);
        return true;
    }else {
        setCastOK(act, false );
        return false;
    }
}

The inflation of the mini controller is disabled by using a ViewStub and a fragment...

Fragment mini_controller_fragment.xml

<?xml version="1.0" encoding="utf-8"?>
<fragment
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/cast_mini_controller"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_alignParentBottom="true"
    android:visibility="gone"
    app:castShowImageThumbnail="true"
    class="com.google.android.gms.cast.framework.media.widget.MiniControllerFragment" />

With usage something like this....

    <ViewStub
        android:id="@+id/cast_mini_controller"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_alignParentBottom="true"
        android:layout="@layout/mini_controller_fragment"
        />

Activity

An Activity's interaction with the cast components looks something like this...

/* called when we have found out that cast is compatible. */
private void onCastAvailable() {
    ViewStub miniControllerStub = (ViewStub) findViewById(R.id.cast_mini_controller);
    miniControllerStub.inflate();    // only inflated if Cast is compatible.
    mCastStateListener = new CastStateListener() {
        @Override
        public void onCastStateChanged(int newState) {
            if (newState != CastState.NO_DEVICES_AVAILABLE) {
                showIntroductoryOverlay();
            }
            if (mQueueMenuItem != null) {
                mQueueMenuItem.setVisible(
                        (mCastSession != null) && mCastSession.isConnected());
            }
        }
    };
    mCastContext = CastContext.getSharedInstance(this);
    if (mCastSession == null) {
        mCastSession = mCastContext.getSessionManager()
                .getCurrentCastSession();
    }
    if (mQueueMenuItem != null) {
        mQueueMenuItem.setVisible(
                (mCastSession != null) && mCastSession.isConnected());
    }
}


private void showIntroductoryOverlay() {
    if (mOverlay != null) {
        mOverlay.remove();
    }
    if ((mediaRouteMenuItem != null) && mediaRouteMenuItem.isVisible()) {
        new Handler().post(new Runnable() {
            @Override
            public void run() {
                mOverlay = new IntroductoryOverlay.Builder(
                        MainActivity.this, mediaRouteMenuItem)
                        .setTitleText(getString(R.string.introducing_cast))
                        .setOverlayColor(R.color.primary)
                        .setSingleTime()
                        .setOnOverlayDismissedListener(
                                new IntroductoryOverlay.OnOverlayDismissedListener() {
                                    @Override
                                    public void onOverlayDismissed() {
                                        mOverlay = null;
                                    }
                                })
                        .build();
                mOverlay.show();
            }
        });
    }

}

onCreate modified as below...

protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    setContentView(R.layout.activity_main);
    mApp = (MyApplication)getApplication();
    if( mApp.isCastAvailable( (Activity)this, GPS_RESULT )) {
        onCastAvailable();
    }

    ...
}

onActivityResult needs to cope with the result from the Google Play Services upgrade...

protected void onActivityResult(int requestCode, int resultCode, Intent data) {
    if( requestCode == GPS_RESULT ) {
        if(mApp.onCastResultReceived( this, resultCode ) ){
            onCastAvailable();
        }

onResume

protected void onResume() {
    if( mCastContext != null && mCastStateListener != null ) {
        mCastContext.addCastStateListener(mCastStateListener);
        mCastContext.getSessionManager().addSessionManagerListener(
                mSessionManagerListener, CastSession.class);
        if (mCastSession == null) {
            mCastSession = CastContext.getSharedInstance(this).getSessionManager()
                    .getCurrentCastSession();
        }
        if (mQueueMenuItem != null) {
            mQueueMenuItem.setVisible(
                    (mCastSession != null) && mCastSession.isConnected());
        }
    }
    super.onResume();
}

onPause

protected void onPause() {
    super.onPause();
    if( mCastContext != null && mCastStateListener != null ) {
        mCastContext.removeCastStateListener(mCastStateListener);
        mCastContext.getSessionManager().removeSessionManagerListener(
                mSessionManagerListener, CastSession.class);
    }
}

The session Manager listener in the class...

private final SessionManagerListener<CastSession> mSessionManagerListener =
        new MySessionManagerListener();
private class MySessionManagerListener implements SessionManagerListener<CastSession> {

    @Override
    public void onSessionEnded(CastSession session, int error) {
        if (session == mCastSession) {
            mCastSession = null;
        }
        invalidateOptionsMenu();
    }

    @Override
    public void onSessionResumed(CastSession session, boolean wasSuspended) {
        mCastSession = session;
        invalidateOptionsMenu();
    }

    @Override
    public void onSessionStarted(CastSession session, String sessionId) {
        mCastSession = session;
        invalidateOptionsMenu();
    }

    @Override
    public void onSessionStarting(CastSession session) {
    }

    @Override
    public void onSessionStartFailed(CastSession session, int error) {
    }

    @Override
    public void onSessionEnding(CastSession session) {
    }

    @Override
    public void onSessionResuming(CastSession session, String sessionId) {
    }

    @Override
    public void onSessionResumeFailed(CastSession session, int error) {
    }

    @Override
    public void onSessionSuspended(CastSession session, int reason) {
    }
}

UI interaction

Finally I could change the UI when cast was available by calling the "known" function in my Application...

    int visibility = View.GONE;
    if( mApplication.isCastAvailableKnown( ) ) {
        CastSession castSession = CastContext.getSharedInstance(mApplication).getSessionManager()
                .getCurrentCastSession();
        if( castSession != null && castSession.isConnected() ){
            visibility = View.VISIBLE;
        }
    }
    viewHolder.mMenu.setVisibility( visibility);

Upvotes: 1

Related Questions