Reputation: 13073
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.
ConnectionResult.SUCCESS
after upgrade, the CastContext.getSharedInstance()
can throw RuntimeError
.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
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"
/>
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) {
}
}
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