如何处理WiFi和移动数据之间的网络切换?

26
我正在开发一个VoIP应用程序。在VoIP通话过程中,当用户从WiFi切换到移动数据时,我处理场景时遇到了问题。
在我的呼叫屏幕活动中,我已经注册了一个接收器,可以帮助我获得有关网络更改情况的通知。
以下是我用于检测onRecieve方法中网络变化的代码。 conn_name 是私有类级别变量,保存先前的连接名称。
ConnectivityManager connectivity_mgr = ((ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE));
NetworkInfo net_info = connectivity_mgr.getActiveNetworkInfo();

if (net_info != null && net_info.isConnectedOrConnecting() && !conn_name.equalsIgnoreCase("")) {
    new_con = net_info.getExtraInfo();
    if (new_con != null && !new_con.equalsIgnoreCase(conn_name))
        network_changed = true;
    conn_name = (new_con == null) ? "" : new_con;
    connectionStatus ="connected";
} else {
    if (net_info != null && conn_name.equalsIgnoreCase("")) {
        conn_name = net_info.getExtraInfo();
        connectionStatus ="connected";
        network_changed = true;
    } else if(!new_con.equals(conn_name)) {
        conn_name = "";
        connectionStatus ="disconnected";
        network_changed = true;
    }
}

使用上述方法,我能够检测到网络变化。但是当我连接WiFi时,会发生一件奇怪的事情。当我的应用程序最初启动时,它连接了移动数据。当用户进入自己所知道的WiFi区域时,他会连接到自己所知道的WiFi。由于WiFi始终被选择为默认路由,Android切换到WiFi并且我收到了网络通知,提示WiFi已经打开。
因此,我将我的应用程序IP地址更新为WiFi IP地址,所以这里没有问题。但是同时移动数据仍然连接着,但是getActiveNetworkInfo()明确告诉我我已连接到WiFi,即使我早先连接到移动数据。
所以问题在于当用户关闭WiFi按钮时,移动数据仍然连接着,但我仍然收到WiFi关闭的通知。 这表明即使手机仍然连接到移动数据,网络也已经断开连接。
但是一秒钟后,我收到一个移动数据已连接的通知。 但是一旦我收到网络断开连接的通知,我就关闭了VoIP电话。 所以当我收到WiFi关闭的通知时,我如何确保移动数据仍然连接着。
我尝试了getActiveNetworkInfo(),但当我收到WiFi关闭的通知时,它会变成null。
我遵循了这些链接: Android API调用以确定用户设置的“数据启用” 如何判断“移动网络数据”是否已启用或禁用(即使通过WiFi连接)? 使用上述链接,当用户连接到移动数据时,我能够检测到移动数据按钮已启用,它会给我true。 但是当出现特定情况时,问题就会发生。
现在当wifi被禁用时,我收到通知,但它显示移动数据已禁用,即使我的移动数据已启用。我无法处理这种情况,因为当我收到断开连接的通知时,我会断开我的电话。

当wifi断开连接时,您可以暂停1-2秒钟。然后在1-2秒钟后尝试重新检查移动数据是否可用。 - Rahulrr2602
@Rahulrr2602 如果没有其他解决方案的话,我计划选择这个。 - Jeeva
4个回答

35

您可以使用 ConnectivityManager 的 API:特别是在您的用例中,您对 registerDefaultNetworkCallback() 感兴趣:


    public class TestActivity extends AppCompatActivity {
        
        // 连接管理器
        private ConnectivityManager manager;
        
        // 网络回调
        private final ConnectivityManager.NetworkCallback networkCallback = new ConnectivityManager.NetworkCallback() {
// 网络连接可用时 @Override public void onAvailable(Network network) { super.onAvailable(network); // 使用三元运算符检查网络是否为按流量计费的,如果是,则连接至LTE,否则连接至WIFI // 请注意,这种方法可能不完全准确,因为非按流量计费的网络并不意味着一定是WIFI Log.i("vvv", "已连接至" + (manager.isActiveNetworkMetered() ? "LTE" : "WIFI")); }
// 网络连接丢失时 @Override public void onLost(Network network) { super.onLost(network); Log.i("vvv", "正在失去活动连接"); } };
// Activity创建时 @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState);
// 获取连接管理器 manager = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE); // 注册默认网络回调 manager.registerDefaultNetworkCallback(networkCallback); }
// Activity销毁时 @Override protected void onDestroy() { super.onDestroy(); // 取消注册网络回调 manager.unregisterNetworkCallback(networkCallback); } }

我的设备连接到LTE只需要大约半秒钟。

输入图像描述

这意味着,当WIFI断开连接时,您无法预先知道设备是否最终会连接到LTE。因此,可以采用以下方法:在处理程序上发布一个动作,在一秒钟内取消调用。如果连接很快出现,请取消预先发布的动作。如果您最终处于Runnable代码中,则说明连接没有快速建立,这意味着您应该结束通话。


    public class TestActivity extends AppCompatActivity {
private ConnectivityManager manager; // 连接管理器
private final Handler handler = new Handler(); // 处理器 private final ConnectivityManager.NetworkCallback networkCallback = new ConnectivityManager.NetworkCallback() { // 网络回调 @Override public void onAvailable(Network network) { // 网络可用时 super.onAvailable(network); Log.i("vvv", "connected to " + (manager.isActiveNetworkMetered() ? "LTE" : "WIFI")); // 输出网络类型
// 移除回调 handler.removeCallbacks(endCall); }
@Override public void onLost(Network network) { // 网络丢失时 super.onLost(network); Log.i("vvv", "losing active connection"); // 输出提示信息
// 延迟一秒后执行任务 handler.postDelayed(endCall, 1000); } };
// 取消通话的任务 private final Runnable endCall = new Runnable() { @Override public void run() { // 如果执行到这里 - 可以取消通话,因为在一秒内未建立连接 } };
@Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState);
manager = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE); // 获取连接管理器 manager.registerDefaultNetworkCallback(networkCallback); // 注册网络回调 }
@Override protected void onDestroy() { super.onDestroy(); manager.unregisterNetworkCallback(networkCallback); // 取消注册网络回调 handler.removeCallbacks(endCall); // 移除任务 } }

这种方法的缺点是,registerDefaultNetworkCallback() 仅从API 24开始可用。在 ConnectivityManagerCompat 中也没有替代方法。相反,您可以使用从API 21开始提供的registerNetworkCallback()


让我试一试。 - Jeeva
2
通过使用registerDefaultNetworkCallback,您事先不会知道。作为替代方案,您可以注册两个单独的回调,一个用于TRANSPORT_WIFI,另一个用于TRANSPORT_CELLULAR。以这种方式,如果它们可用,您将有对两个网络的引用。大多数OEM始终在后台保持蜂窝网络处于活动状态。如果他们没有这样做,您可以通过使用CM#requestNetwork来请求蜂窝网络。 - AllThatICode

4

使用RxJava的实现方式

class ConnectivityMonitor : ConnectivityManager.NetworkCallback() {
    var networkTimeout: Disposable? = null

    override fun onAvailable(network: Network?) {
        super.onAvailable(network)
        Timber.d("Network available")
        networkTimeout?.dispose()
    }

    override fun onLosing(network: Network?, maxMsToLive: Int) {
        super.onLosing(network, maxMsToLive)
        Timber.d("onLosing")
    }

    override fun onLost(network: Network?) {
        super.onLost(network)
        Timber.d("onLost")

        networkTimeout = Single.timer(5, TimeUnit.SECONDS)
            .subscribeOn(Schedulers.io())
            .observeOn(AndroidSchedulers.mainThread())
            .subscribe { _ -> Timber.d("Network lost") }
    }

    override fun onUnavailable() {
        super.onUnavailable()
        Timber.d("Network unavailable")
    }
}

监听器设置:

    private fun setupListeners() {
        // connection listener
        val connectivityManager = getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
            connectivityManager.registerDefaultNetworkCallback(connectivityMonitor)
        } else {
            val builder = NetworkRequest.Builder()
            connectivityManager.registerNetworkCallback(builder.build(), connectivityMonitor)
        }
    }

计时器/一次性使用的使用可以在切换连接类型之间引入延迟。

3
您可以使用 BroadcastReceiver 并注册 NETWORK_STATE_CHANGED_ACTIONWIFI_STATE_CHANGED_ACTION 来实现。
private boolean isConnected;

final BroadcastReceiver broadcastReceiver = new BroadcastReceiver() {
    @Override
    public void onReceive(Context context, Intent intent) {
        if (intent == null || intent.getAction() == null)
            return;
        switch (intent.getAction()){
            case WifiManager.NETWORK_STATE_CHANGED_ACTION :
            case WifiManager.WIFI_STATE_CHANGED_ACTION :
                if (!isConnected && isOnline(BaseActivity.this)) {
                    isConnected = true;
                    // do stuff when connected
                    Log.i("Network status: ","Connected");
                }else{
                    isConnected = isOnline(BaseActivity.this);
                    Log.i("Network status: ","Disconnected");
                }
                break;
        }
    }
};



@Override
protected void onCreate(Bundle savedInstanceState) {
    isConnected = isOnline(this);
    final IntentFilter filters = new IntentFilter();
    filters.addAction(WifiManager.WIFI_STATE_CHANGED_ACTION);
    filters.addAction(WifiManager.NETWORK_STATE_CHANGED_ACTION);
    registerReceiver(broadcastReceiver, filters);
}


public static boolean isOnline(Context ctx) {
    ConnectivityManager cm = (ConnectivityManager) ctx
            .getSystemService(Context.CONNECTIVITY_SERVICE);
    NetworkInfo netInfo = cm != null
            ? cm.getActiveNetworkInfo()
            : null;
    return netInfo != null && netInfo.isConnectedOrConnecting();
}

更新onDestroy方法中不要忘记取消注册BroadcastReceiver

@Override
protected void onDestroy() {
    unregisterReceiver(broadcastReceiver);
    super.onDestroy();
} 

当移动数据连接时,同时接收到网络通知以关闭WiFi时会发生什么? - Jeeva
@Jeeva isConnected 标志位在 onCreate 方法中使用在线实用函数进行初始化,而该标志位将判断旧状态和当前状态的 onReceive 动作。 - Khaled Lela
1
@Jeeva,你可以使用带有延迟的Handler来发布结束通话的内容,以便在Wifi和移动数据之间进行切换,就像@azizbekian的回答中所提到的那样。 - Khaled Lela

0

感谢您的贡献 @azizbiken,我已经在Compose中的ProducerScope中实现了同样的功能。

fun networkCallback(producer: ProducerScope<ConnectionState>, context: Context, callback: (ConnectionState) -> Unit): ConnectivityManager.NetworkCallback {
return object : ConnectivityManager.NetworkCallback() {
    var unAvailableJob: Job? = null
    override fun onAvailable(network: Network) {
        //cancel the coroutine once you get network instead of updating no network status
        unAvailableJob?.cancel()
            callback(ConnectionState.Available)
    }

    override fun onLost(network: Network) {
        unAvailableJob = producer.launch {
            delay(1000)
            callback(ConnectionState.Unavailable)
        }
    }
}
}

这里是调用代码

    val callback = networkCallback(this,context = applicationContext, callback = { connectionState -> trySend(connectionState) })

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接