Android,在连接到 wifi 时与移动数据通信,无需互联网访问

标签 android networking mobile wifi iot

我有一个需要与 wifi 和移动数据网络通信的汽车配套应用程序。

我的车辆控制单元提供了一个没有互联网访问权限的 wifi 网络,它公开了一个我们可以从应用程序调用的 API 服务。
除此之外,我们需要使用电话移动数据(3G/4G)与另一个可从互联网访问的后端进行通信。

我立即注意到一些 android 手机,当连接到没有互联网的 wifi 网络时 使用 android 设置菜单 , 显示一个系统对话框,通知用户当前网络无法访问互联网。这里用户有两种选择:保持这个 wifi 网络或断开连接并切换到另一个网络。

这里有一些例子:

Samsung J7 - Android 7.0

Motorola moto G7 power - Android 9.0

Xiaomi mi 9T - Android 10

Huawei p9 lite - Android 6.0

经过简短的分析,我了解到如果用户单击“否”选项,系统会断开与当前 wifi 网络的连接,如果另一个网络可用,则连接到该网络。

如果用户点击"is"选项,我们可以有两种不同的行为:

  • 手机在没有互联网访问的情况下保持连接到 wifi 网络,所有应用程序都无法再与互联网通信,因为 android 尝试使用 wifi 接口(interface)。
  • 手机在没有互联网访问的情况下保持连接到 wifi 网络,但 android 系统重新绑定(bind)所有现有的套接字和那些将来会在移动数据网络上打开的套接字(如果 sim 可用)。

  • 然后我尝试了同样的事情,但是 使用编程连接 .
    我的不同 android 版本的示例代码:
    fun connectToWifi(ssid: String, key: String) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
            connectPost10(ssid, key)
        } else {
            connectPre10(ssid, key)
        }
    }
    
    @RequiresApi(Build.VERSION_CODES.O)
    private fun connectPost10(ssid: String, wpa2Passphrase: String) {
    
        val specifier = WifiNetworkSpecifier.Builder()
                .setSsid(ssid)
                .setWpa2Passphrase(wpa2Passphrase)
                .build()
    
        val request = NetworkRequest.Builder()
                .addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
                .setNetworkSpecifier(specifier)
                .build()
    
        val networkCallback = object: ConnectivityManager.NetworkCallback() {
            override fun onAvailable(network: Network) {
                val networkSSID = wifiManager.connectionInfo.ssid
                    .trim()
                    .removeSurrounding("\"")
    
                if (networkSSID == "MY_NETWORK_WITHOUT_INTERNET_SSID") {
                    // i'm connected here
                }
            }
        }
    
        connectivityManager.requestNetwork(request, networkCallback)
    }
    
    private fun connectPre10(ssid: String, key: String) {
    
        // setup wifi configuration
        val config = WifiConfiguration().apply {
            SSID = "\"$ssid\""
            preSharedKey = "\"$key\""
        }
    
        val networkId = wifiManager.addNetwork(config)
    
        wifiManager.disconnect() // disconnect from current (if connected)
        wifiManager.enableNetwork(networkId, true) // enable next attempt
        wifiManager.reconnect()
    }
    

    请注意,为了读取网络 ssid android 需要 ACCESS_FINE_LOCATION 权限并且 GPS 必须处于 Activity 状态。

    我立即注意到使用编程连接时,本地弹出窗口不再出现,但在许多设备上,连接后,移动数据连接被 android“禁用”。

    我想这种行为是系统想要的,并且是由android更喜欢wifi而不是计量连接的事实决定的。
    我对此没有意见,但是在我的 wifi 网络无法访问互联网的情况下会发生什么?其他需要连接的应用程序停止工作,因为它们无法访问互联网。

    我需要一个解决方案,允许我的应用程序通过 wifi 和 4g 进行通信,而不会阻止其他应用程序正常工作。
    我的最小 sdk api 级别是 23 (Marshmallow),目标是 29 (Android 10)。

    我设法通过保存来自连接管理器上注册的回调的网络来解决这个问题。
    val connectivityManager by lazy {
        MyApplication.context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
    }
    
    private val wifiNetworkCallback = object : ConnectivityManager.NetworkCallback() {
        // Called when the framework connects and has declared a new network ready for use.
        override fun onAvailable(network: Network) {
            super.onAvailable(network)
            listener?.onWifiConnected(network)
        }
        // Called when a network disconnects or otherwise no longer satisfies this request or callback.
        override fun onLost(network: Network) {
            super.onLost(network)
            listener?.onWifiDisconnected()
        }
    }
    
    private val mobileNetworkCallback = object : ConnectivityManager.NetworkCallback() {
        // Called when the framework connects and has declared a new network ready for use.
        override fun onAvailable(network: Network) {
            super.onAvailable(network)
            connectivityManager.bindProcessToNetwork(network)
            listener?.onMobileConnected(network)
        }
    
        // Called when a network disconnects or otherwise no longer satisfies this request or callback.
        override fun onLost(network: Network) {
            super.onLost(network)
            connectivityManager.bindProcessToNetwork(null)
            listener?.onMobileDisconnected()
        }
    }
    
    private fun setUpWifiNetworkCallback() {
    
        val request = NetworkRequest.Builder()
            .addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
            .build()
    
        try {
            connectivityManager.unregisterNetworkCallback(wifiNetworkCallback)
        } catch (e: Exception) {
            Log.d(TAG, "WiFi Network Callback was not registered or already unregistered")
        }
    
        connectivityManager.requestNetwork(request, wifiNetworkCallback)
    }
    
    private fun setUpMobileNetworkCallback() {
    
        val request = NetworkRequest.Builder()
            .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
            .addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR)
            .build()
    
        try {
            connectivityManager.unregisterNetworkCallback(mobileNetworkCallback)
        } catch (e: Exception) {
            Log.d(TAG, "Mobile Data Network Callback was not registered or already unregistered")
        }
    
        connectivityManager.requestNetwork(request, mobileNetworkCallback)
    }
    

    将此标记为“connectivityManager.bindProcessToNetwork()”,我们稍后再讨论。

    随后我创建了两种不同的改造服务:
  • Private Retrofit:绑定(bind)在我从 wifi 回调接收到的网络对象上,公开 api 以与本地网络内的车辆进行通信。
  • 公共(public)改造:绑定(bind)到我从移动数据回调收到的网络对象上,公开 api 以与我的后端以及需要互联网的所有其他内容进行通信。

  • 此时在我的应用程序中,我能够重定向通过改造的流量,
    但是我在项目中包含的库如何理解他们应该使用哪个网络?
    答:他们不明白,实际上他们尝试使用wifi网络时出现超时错误。

    当我将谷歌地图添加到我的应用程序时,我注意到了这种行为, Canvas 只显示了一个空网格。
    由于无法通过我之前创建的公共(public)改造服务重定向谷歌地图流量,我不得不寻找另一种解决方案来解决这个问题。

    这里是您之前见过的 ConnectivityManager.bindProcessToNetwork() !
    https://developer.android.com/reference/android/net/ConnectivityManager#bindProcessToNetwork(android.net.Network) .

    通过这种方法,我可以告诉我的应用程序的进程,将来创建的所有套接字都必须使用来自移动数据回调的网络。

    使用这个技巧,谷歌地图和我无法控制连接的所有其他图书馆,他们将使用数据连接与互联网通信,因此他们将正常工作。

    在某些手机中,尤其是旧版本的 android,仍然存在其他应用程序无法连接的问题,因为它们尝试使用 wifi 而不是使用移动数据。
    作为用户,如果使用一个应用程序而所有其他应用程序都不再工作,我会感到非常沮丧。

    所以我想了解是否有一种方法可以满足我的所有要求,无论 Android 版本和手机供应商如何,我的应用程序和其他应用程序都可以正常工作。

    综上所述,我的问题是:
  • 如果以编程方式建立连接,为什么不会出现“没有互联网访问的网络”弹出窗口?
  • 如果Android知道我的WiFi无法上网,为什么其他应用程序不自动使用移动数据网络作为后备?
  • 是否可以告诉 android 其他应用程序必须使用某个网络才能打开 future 的套接字?
  • 每个供应商都有自定义 WiFi 设置以提供增强的互联网体验( Huawei WiFi+Samsung Adaptive WiFiOppo WiFi Assistant 、...)。我注意到在某些手机中激活它可以解决其他应用程序问题,这些功能似乎有权将整个应用程序生态系统重新绑定(bind)到特定网络接口(interface)上。这些功能如何帮助/阻碍我?是否可以编写一些代码来完成与这些功能相同的功能?
  • 最佳答案

    首先,关于你的问题:

  • 此行为保留给系统应用程序。
  • Android 知道与 WiFi 网络的连接状况良好。它不会进一步检查以确认没有与外部世界的连接。顺便说一句,这实际上并不总是理想的行为。
  • 是的,见下文
  • 在某些方面是的,见下文

  • 在我看来,您正在寻找的是更改 Android 的默认路由机制。
    也就是说,您希望所有到 WiFi 网络内服务器的流量都路由到 WiFi 网络,而所有其他流量都通过移动数据接口(interface)路由。有几种方法可以实现这一点:
  • 如果您的应用程序是车辆信息娱乐系统的一部分,并且可以拥有系统权限,或者在有 root 权限的 Android 手机上,您可以使用 ip route 命令直接更改路由表。
  • 您所描述的实际上是虚拟专用网 (VPN) 功能的一部分。您可以基于 OpenVPN 等开源解决方案在服务器端和客户端自己实现 VPN 服务,其中 VPN 服务器将位于 wifi 网络内。 Android 具有用于实现 VPN 客户端的预构建基础架构:https://developer.android.com/guide/topics/connectivity/vpn
  • 您可以使用商业 VPN 解决方案。其中一些允许您正在寻找的配置,我相信会满足您描述的需求。
  • 关于Android,在连接到 wifi 时与移动数据通信,无需互联网访问,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/61773466/

    相关文章:

    ios - 如何通过 iOS 设备向本地主机发出请求(使用 Xcode)?

    macos - 如何在Scala中读取网络流?

    iphone - 用于移动设备的 Twig 模板引擎

    java - 在 J2ME 中通过 http 发送和接收数据以及处理菜单和屏幕

    AMD 处理器上的 Android Studio 硬件加速

    android - Retrofit2 java.lang.IllegalStateException : Expected BEGIN_OBJECT but was STRING at line 1 column 1 path $ 错误

    java - Network Mapper软件的最佳平台和编程语言

    html - 使用 CSS3 剪切内容

    android - 在 Eclipse 上是否有用于 Android 的高质量接口(interface) "painter"

    android - 如何让 EditText 占用它需要的尽可能多的空间,以便与上面的内容一起滚动?