GoogleCast 异常问题

项目中集成 Google Cast 功用时, 部分机型遇到一些问题

需求

项目中Cast需求有点特别,默认Cast Button处于不可见状况。 APP进入前台时,查询周围是否有可用Cast设备, 有才显现Cast Button, 否则躲藏

依据流程, 咱们有两种状况需求处理

Fragment/Activity 发动时查询Available Cast Devices,暂命名办法1

代码完成如下: Fragment -> onResume(), checkCastDevices:

    public static boolean checkCastDevice(Context activity) {
        MediaRouter mediaRouter = MediaRouter.getInstance(activity);
        List<mediarouter.routeinfo> routes = mediaRouter.getRoutes();
        if (routes != null &amp;&amp; routes.size() != 0) {
            for (MediaRouter.RouteInfo info : routes) {
                if (info.getDeviceType() != MediaRouter.RouteInfo.CONNECTION_STATE_DISCONNECTED
                        && !info.isDefault() && !info.isBluetooth() && !info.isDeviceSpeaker()) {
                    return true;
                }
            }
        }
        return false;
    }

Fragment/Activity 处于前台时,动态获取Availabe Cast Devices, 暂命名办法2

监听了addCastStateListener CastContext.addCastStateListener(this)


    @Override
    public void onCastStateChanged(int i) {
        stateListenerListTemp.clear();
        stateListenerListTemp.addAll(stateListenerList);
        switch (i) {
            case CastState.CONNECTED:
                for (CastStateListener listener : stateListenerListTemp) {
                    listener.connected();
                }
                break;
            case CastState.CONNECTING:
                for (CastStateListener listener : stateListenerListTemp) {
                    listener.connecting();
                }
                break;
            case CastState.NO_DEVICES_AVAILABLE:  //has not available devices
                for (CastStateListener listener : stateListenerListTemp) {
                    listener.noDevicesAvailable();  // hide cast button
                }
                break;
            case CastState.NOT_CONNECTED:  //has available devices, but not connect
                for (CastStateListener listener : stateListenerListTemp) {
                    listener.notConnected();  // show cast button
                }
                break;
        }
        stateListenerListTemp.clear();
    }

依据上述完成, 能够即时监听Cast Devices的改变,动态更新Cast Button的可见性

问题

新增了一个有个需求, Toolbar上新增了一个视图,需求依据Cast Button的可见性来设置视图可见性,即二者可见性互斥,但在测验中发现一些问题。

部分设备上,例如Remi Note 4, 上述两个办法都无效

  • 问题1,办法1 只有在进程毁掉后第一次发动能够获取周围Available Cast Devices,之后封闭APP重启一向返回false
  • 问题2,办法2 APP处于前台时,断开WIFI时,Cast Button消失,从头衔接WIFI,Cast Button可见,但代码中并没有收到Cast Devices 状况更新的回调, 设置Cast Button Visibility的代码并未履行,这就呈现一种状况:虽然Cast Button不可见,可是新增的视图却并未显现。

分析

  • 问题1

当App进程毁掉后从头发动, 能够成功获取Cast Devices, 阐明初始化CastContext时, 自动扫描了周边Routes, 而App封闭后从头翻开, 部分设备未自动扫描, 所以获取状况失利。

  • 问题2

当Cast Button可见时,断开WIFI衔接, 因为没有收到onCastStateChanged的回调,CastButton.SetVisibility(View.GONE)并未履行, 可是Cast Button 却不可见了, 翻开Developers options–Show layouts bounds开关, 发现Cast Button 在布局中存在,可是图标却不可见,那么有两种可能:
– Cast Button的Drawable 被移除了
– Cast Button被设为View.INVISIBLE

可是onCastStateChanged的回调并未履行, 可是Cast Button 状况却有改变,那只能阐明Cast Button内部监听了衔接状况的改变,并内部处理了展现状况。

项目中Cast Button是一个MediaRouteButton, 查询源码

public class MediaRouteButton extends View {
    private static final String TAG = "MediaRouteButton";
    private static ConnectivityReceiver sConnectivityReceiver;
    private int mVisibility = VISIBLE;
    private boolean mAlwaysVisible;
    public MediaRouteButton(Context context) {
        this(context, null);
    }
    public MediaRouteButton(Context context, AttributeSet attrs) {
        this(context, attrs, R.attr.mediaRouteButtonStyle);
    }
    public MediaRouteButton(Context context, AttributeSet attrs, int defStyleAttr) {
        super(MediaRouterThemeHelper.createThemedButtonContext(context), attrs, defStyleAttr);
        if (sConnectivityReceiver == null) {
            sConnectivityReceiver = new ConnectivityReceiver(context.getApplicationContext());
        }
    }
    @Override
    public void onAttachedToWindow() {
        super.onAttachedToWindow();
        sConnectivityReceiver.registerReceiver(this);
    }
    @Override
    public void onDetachedFromWindow() {
        if (!isInEditMode()) {
            sConnectivityReceiver.unregisterReceiver(this);
        }
        super.onDetachedFromWindow();
    }
    void refreshVisibility() {
        //set VISIBLE or INVISIBLE
        super.setVisibility(mVisibility == VISIBLE
                && !(mAlwaysVisible || sConnectivityReceiver.isConnected())
                ? INVISIBLE : mVisibility);
    }
    /**
    * 监听衔接状况的改变,改写MediaRouteButton可见性
    */
    private static final class ConnectivityReceiver extends BroadcastReceiver {
        private final Context mContext;
        // If we have no information, assume that the device is connected
        private boolean mIsConnected = true;
        private List<MediaRouteButton> mButtons;
        ConnectivityReceiver(Context context) {
            mContext = context;
            mButtons = new ArrayList<MediaRouteButton>();
        }
        public void registerReceiver(MediaRouteButton button) {
            if (mButtons.size() == 0) {
                IntentFilter intentFilter = new IntentFilter();
                intentFilter.addAction(ConnectivityManager.CONNECTIVITY_ACTION);
                mContext.registerReceiver(this, intentFilter);
            }
            mButtons.add(button);
        }
        public void unregisterReceiver(MediaRouteButton button) {
            mButtons.remove(button);
            if (mButtons.size() == 0) {
                mContext.unregisterReceiver(this);
            }
        }
        public boolean isConnected() {
            return mIsConnected;
        }
        @Override
        public void onReceive(Context context, Intent intent) {
            final String action = intent.getAction();
            if (ConnectivityManager.CONNECTIVITY_ACTION.equals(action)) {
                boolean isConnected = !intent.getBooleanExtra(
                        ConnectivityManager.EXTRA_NO_CONNECTIVITY, false);
                if (mIsConnected != isConnected) {
                    mIsConnected = isConnected;
                    for (MediaRouteButton button: mButtons) {
                        button.refreshVisibility();
                    }
                }
            }
        }
    }
}

源码中可见,内部定义了ConnectivityReceiver来监听衔接状况的改变, 然后内部控制MediaRouteButton的显现。

处理

问题1

App发动时,虽然自动扫描周边Routes,来更新能够Cast Devices状况

    //Init Cast
    public static void initCast(Context context) {
        if (context == null)
            return;
        if (initialized) {
            CastContextManager.getInstance().addRouter(context);
            return;
        }
        initialized = true;
        //初始化CastContextManager
        ....
    }
    public void addRouter(Context context) {
        if(castContext !=null){
            MediaRouter mediaRouter = MediaRouter.getInstance(context);
            mediaRouter.removeCallback(callback);
            mediaRouter.addCallback(MediaRouteSelector.EMPTY, callback, MediaRouter.CALLBACK_FLAG_PERFORM_ACTIVE_SCAN);
        }
    }
    private final MediaRouter.Callback callback = new MediaRouter.Callback() {
        @Override
        public void onProviderAdded(MediaRouter router, MediaRouter.ProviderInfo provider) {
            super.onProviderAdded(router, provider);
            ZenLogger.dd("MXMediaRouter", () -> "onProviderAdded  " + provider.getRoutes());
        }
        @Override
        public void onProviderRemoved(MediaRouter router, MediaRouter.ProviderInfo provider) {
            super.onProviderRemoved(router, provider);
            ZenLogger.dd("MXMediaRouter", () -> "onProviderRemoved  " + provider.getRoutes());
        }
        @Override
        public void onProviderChanged(MediaRouter router, MediaRouter.ProviderInfo provider) {
            super.onProviderChanged(router, provider);
            ZenLogger.dd("MXMediaRouter", () -> "onProviderChanged  " + provider.getRoutes());
        }
    };

问题2

因为无法收到onCastStateChanged的回调, 而且MediaRouteButton内部监听了衔接状况的改变来更新显现,那么咱们也效仿相同的完成,通过监听当时衔接状况,结合MediaRouteButton的可见性,来判别Available Cast Devices

  • 当时衔接断开, 直接躲藏MediaRouteButton
  • 当时衔接康复, 查询是否有Available Cast Devices,来设置MediaRouteButton可见
  • 再判别MediaRouteButton是否可见,来设置新视图的可见性
    public static boolean checkCastDevice(Context activity) {
        //如果当时无衔接, 阐明unavailable Cast devices
        if (!NetworkMonitor.isConnected(App.applicationContext())) return false;
        MediaRouter mediaRouter = MediaRouter.getInstance(activity);
        List<MediaRouter.RouteInfo> routes = mediaRouter.getRoutes();
        if (routes != null && routes.size() != 0) {
            for (MediaRouter.RouteInfo info : routes) {
                if (info.getDeviceType() != MediaRouter.RouteInfo.CONNECTION_STATE_DISCONNECTED
                        && !info.isDefault() && !info.isBluetooth() && !info.isDeviceSpeaker()) {
                    return true;
                }
            }
        }
        return false;
    }
    private NetworkMonitor.OnNetworkListener onNetWorkListener = (last, current) -> {
        showMediaRouteButton(CastHelper.checkCastDevice(getActivity()));
        //check MediaRouteButton isVisible
        ...
    };

总结

部分设备当非杀进程从头发动App,不能自动扫描routes, 需求在添加自动扫描,来获取最新的Cast Devices

部分设备无法获取onCastStateChanged回调,且MediaRouteButton内部会监听衔接状况设置显隐性, 所以咱们也同样监听衔接状况的改变,来判别Cast 是否可用