获取 Google Cast v3 以在不受支持的设备上运行

Getting Google Cast v3 to work on unsupported devices

Cast V3 框架的一些特性试图让 运行 在没有 Google Play Services 工作所需的设备上成为可能,但我 运行 在测试时遇到了一些问题.

  1. 在 Kindle 上 Google API returns SERVICE_INVALID isUserResolvable() 为真。
  2. 在升级后 onActivityResult 返回 ConnectionResult.SUCCESS 的设备上,CastContext.getSharedInstance() 可以抛出 RuntimeError
  3. 作为 2) 的副作用,包含 MiniControllerFragment 的项目的 XML 膨胀将失败。

我发现的一些错误是

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)

这是由 MiniControllerFragment 的 inflation 在未安装 CastController 代码的设备上引起的。这类似于 提出的问题。 Kamil Ślesiński 提供的答案对我的调查很有帮助。

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

当我实现我的 ViewStub 时,我仍然在预发布测试机器中崩溃,因为它们返回成功,但没有可用的 CastContext。为了解决这个问题,我需要另一个测试来检查 CastContext 是否可创建。

您需要在应用程序中使用如下所示的单例/代码....

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;
}

还有一个辅助函数来检查我们是否知道转换的状态。

public boolean isCastAvailableKnown() {
    return gCastable;
}

但是为了应对 return 成功的设备,我还需要在 App / singleton 中添加以下代码。

当 Activity 收到转换结果时,我们创建一个 CastContext。 "hope"是,如果Application可以创建CastContext,那么框架也会以同样的方式成功(崩溃的原因)。

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;
    }
}

使用ViewStub和片段禁用了迷你控制器的inflation...

片段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" />

像这样使用....

    <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

Activity 与 cast 组件的交互看起来像这样...

/* 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 修改如下...

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 需要处理来自 Google Play Services 升级的结果...

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();
}

暂停

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

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 互动

最后,我可以通过在我的应用程序中调用 "known" 函数来更改 UI 当转换可用时...

    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);