在没有网络访问的本地 wifi 上强制 Android 使用 3G

Posted

技术标签:

【中文标题】在没有网络访问的本地 wifi 上强制 Android 使用 3G【英文标题】:Force Android to use 3G when on local area wifi without net access 【发布时间】:2012-02-09 07:38:42 【问题描述】:

我有一个无法访问互联网的 wifi LAN 设置。只是连接到它的各种其他本地wifi设备。 DHCP 配置为不返回网关或 dns 服务器。只有一个 IP 和网络掩码。

当我将我的 android 连接到这个 wifi AP 时,它连接正常,但手机上的所有互联网连接都停止工作。

我希望由于 wifi 没有网关设置,android 应该意识到互联网无法通过该连接,而是应该通过 5 格的 3G 连接进行路由。

我也试过在安卓手机上设置一个静态IP,但这没有帮助。

此设置的主要原因是安卓设备可以将此远程网络上的数据传输到基于互联网的服务器,因为它可以毫无问题地连接到本地设备。然而,一旦设置了 wifi,3G 端就坏了。

关于如何解决此问题的任何想法?

【问题讨论】:

您找到解决方案了吗?我有这个完全相同的问题。我有一个 Raspberry Pi,它是一个 Airplay 服务器,并且有自己的无线网络。为了向它传输内容,我必须连接到它的网络,但是当我连接时,我没有“运营商互联网连接”。使用 iPhone,我可以分配一个没有 DNS/默认网关的静态 IP,而且效果很好。我只是无法在 Android 中实现。 【参考方案1】:

经过一些编码和测试后,我合并了 Squonk 和 this 解决方案。这是我创建的类:

package it.helian.exampleprj.network;

import java.net.InetAddress;
import java.net.UnknownHostException;

import android.content.Context;
import android.net.ConnectivityManager;
import android.net.NetworkInfo.State;
import android.net.wifi.WifiManager;
import android.text.TextUtils;
import android.util.Log;

public class NetworkUtils 
    private static final String TAG_LOG = "ExamplePrj";

    Context context;
    WifiManager wifiMan = null;
    WifiManager.WifiLock wifiLock = null;

    public NetworkUtils(Context context) 
        super();
        this.context = context;
    

    /**
     * Enable mobile connection for a specific address
     * @param context a Context (application or activity)
     * @param address the address to enable
     * @return true for success, else false
     */
    public boolean forceMobileConnectionForAddress(Context context, String address) 
        ConnectivityManager connectivityManager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
        if (null == connectivityManager) 
            Log.d(TAG_LOG, "ConnectivityManager is null, cannot try to force a mobile connection");
            return false;
        

        //check if mobile connection is available and connected
        State state = connectivityManager.getNetworkInfo(ConnectivityManager.TYPE_MOBILE_HIPRI).getState();
        Log.d(TAG_LOG, "TYPE_MOBILE_HIPRI network state: " + state);
        if (0 == state.compareTo(State.CONNECTED) || 0 == state.compareTo(State.CONNECTING)) 
            return true;
        

        //activate mobile connection in addition to other connection already activated
        int resultInt = connectivityManager.startUsingNetworkFeature(ConnectivityManager.TYPE_MOBILE, "enableHIPRI");
        Log.d(TAG_LOG, "startUsingNetworkFeature for enableHIPRI result: " + resultInt);

        //-1 means errors
        // 0 means already enabled
        // 1 means enabled
        // other values can be returned, because this method is vendor specific
        if (-1 == resultInt) 
            Log.e(TAG_LOG, "Wrong result of startUsingNetworkFeature, maybe problems");
            return false;
        
        if (0 == resultInt) 
            Log.d(TAG_LOG, "No need to perform additional network settings");
            return true;
        

        //find the host name to route
        String hostName = extractAddressFromUrl(address);
        Log.d(TAG_LOG, "Source address: " + address);
        Log.d(TAG_LOG, "Destination host address to route: " + hostName);
        if (TextUtils.isEmpty(hostName)) hostName = address;

        //create a route for the specified address
        int hostAddress = lookupHost(hostName);
        if (-1 == hostAddress) 
            Log.e(TAG_LOG, "Wrong host address transformation, result was -1");
            return false;
        
        //wait some time needed to connection manager for waking up
        try 
            for (int counter=0; counter<30; counter++) 
                State checkState = connectivityManager.getNetworkInfo(ConnectivityManager.TYPE_MOBILE_HIPRI).getState();
                if (0 == checkState.compareTo(State.CONNECTED))
                    break;
                Thread.sleep(1000);
            
         catch (InterruptedException e) 
            //nothing to do
        
        boolean resultBool = connectivityManager.requestRouteToHost(ConnectivityManager.TYPE_MOBILE_HIPRI, hostAddress);
        Log.d(TAG_LOG, "requestRouteToHost result: " + resultBool);
        if (!resultBool)
            Log.e(TAG_LOG, "Wrong requestRouteToHost result: expected true, but was false");

        state = connectivityManager.getNetworkInfo(ConnectivityManager.TYPE_MOBILE_HIPRI).getState();
        Log.d(TAG_LOG, "TYPE_MOBILE_HIPRI network state after routing: " + state);

        return resultBool;
    

    /**
     * This method extracts from address the hostname
     * @param url eg. http://some.where.com:8080/sync
     * @return some.where.com
     */
    public String extractAddressFromUrl(String url) 
        String urlToProcess = null;

        //find protocol
        int protocolEndIndex = url.indexOf("://");
        if(protocolEndIndex>0) 
            urlToProcess = url.substring(protocolEndIndex + 3);
         else 
            urlToProcess = url;
        

        // If we have port number in the address we strip everything
        // after the port number
        int pos = urlToProcess.indexOf(':');
        if (pos >= 0) 
            urlToProcess = urlToProcess.substring(0, pos);
        

        // If we have resource location in the address then we strip
        // everything after the '/'
        pos = urlToProcess.indexOf('/');
        if (pos >= 0) 
            urlToProcess = urlToProcess.substring(0, pos);
        

        // If we have ? in the address then we strip
        // everything after the '?'
        pos = urlToProcess.indexOf('?');
        if (pos >= 0) 
            urlToProcess = urlToProcess.substring(0, pos);
        
        return urlToProcess;
    

    /**
     * Transform host name in int value used by @link ConnectivityManager.requestRouteToHost
     * method
     *
     * @param hostname
     * @return -1 if the host doesn't exists, elsewhere its translation
     * to an integer
     */
    private int lookupHost(String hostname) 
        InetAddress inetAddress;
        try 
            inetAddress = InetAddress.getByName(hostname);
         catch (UnknownHostException e) 
            return -1;
        
        byte[] addrBytes;
        int addr;
        addrBytes = inetAddress.getAddress();
        addr = ((addrBytes[3] & 0xff) << 24)
                | ((addrBytes[2] & 0xff) << 16)
                | ((addrBytes[1] & 0xff) << 8 )
                |  (addrBytes[0] & 0xff);
        return addr;
    

    @SuppressWarnings("unused")
    private int lookupHost2(String hostname) 
        InetAddress inetAddress;
        try 
            inetAddress = InetAddress.getByName(hostname);
         catch (UnknownHostException e) 
            return -1;
        
        byte[] addrBytes;
        int addr;
        addrBytes = inetAddress.getAddress();
        addr = ((addrBytes[3] & 0xff) << 24)


        | ((addrBytes[2] & 0xff) << 16)
            | ((addrBytes[1] & 0xff) << 8 )
            |  (addrBytes[0] & 0xff);
        return addr;
    

    public Boolean disableWifi() 
        wifiMan = (WifiManager) context.getSystemService(Context.WIFI_SERVICE);
        if (wifiMan != null) 
            wifiLock = wifiMan.createWifiLock(WifiManager.WIFI_MODE_SCAN_ONLY, "HelianRCAWifiLock");
        
        return wifiMan.setWifiEnabled(false);
    

    public Boolean enableWifi() 
        Boolean success = false;

        if (wifiLock != null && wifiLock.isHeld())
            wifiLock.release();
        if (wifiMan != null)
        success = wifiMan.setWifiEnabled(true);
        return success;
    

这是用法

使用代码

            boolean mobileRoutingEnabled = checkMobileInternetRouting();

            if(!mobileRoutingEnabled) 
                networkUtils.disableWifi();

                try 
                    Thread.sleep(3000);
                 catch (InterruptedException e) 
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                
            

            networkUtils.forceMobileConnectionForAddress(context, RCA_URL);

            if(!mobileRoutingEnabled) 
                networkUtils.enableWifi();
            

            // This second check is for testing purpose
            checkMobileInternetRouting();

            return callWebService(RCA_COMPLETE_URL, _plate);

checkMobileInternetRouting 是:

private boolean checkMobileInternetRouting() 
    ConnectivityManager cm = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);

    State state = cm.getNetworkInfo(ConnectivityManager.TYPE_MOBILE_HIPRI).getState();
    return 0 == state.compareTo(State.CONNECTED) || 0 == state.compareTo(State.CONNECTING);

使用程序

    检查是否启用了到主机的路由 如果是,则无论 wifi 是否连接,都继续通信,只执行第 6 点(第 4 点只会检查路由是否已启用,而不执行任何相关操作)。否则会暂时禁用 wifi。 线程休眠约 3 秒,让 3g 连接恢复 设置3g路由到给定的url 重新启用 wifi 现在即使在没有网络访问的 wifi 连接下也可以调用给定的 url

结论

这有点hacky,但可以正常工作。唯一的问题是此路由有几秒钟的超时(如 20-30),这迫使您再次执行上述整个过程。将此超时设置为更高的值会非常好。

【讨论】:

【参考方案2】:

Google 为此在 Android SDK 21 中添加了一些有用的方法。

你可以创建NetworkRequest:

NetworkRequest networkRequest = new NetworkRequest.Builder()
    .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
    .build();

然后您可以使用ConnectivityManager 请求此类网络。例如,您希望确保所有 HTTP 请求都将通过具有 Internet 访问权限的网络传递。您可以通过这种方式构建您的 Retrofit API:

ApiConfig apiConfig;

ConnectivityManager connectivityManager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);

connectivityManager.requestNetwork(networkRequest, new ConnectivityManager.NetworkCallback() 
    @Override
    public void onAvailable(Network network) 
        apiConfig = new Retrofit.Builder()
            .baseUrl("https://api.imatrix.io/")
            .client(new OkHttpClient.Builder()
                .socketFactory(network.getSocketFactory())
                .build())
            .build()
            .create(ApiConfig.class);
    

    @Override
    public void onLost(Network network) 
        apiConfig = null; 
    
);

当您使用这样的 sn-p 代码时,请注意线程安全。

另外,我建议检查ConnectivityManager#bindProcessToNetwork 和这个blog。

ConnectivityManager.NetworkCallback 是一个空类,它有several methods。

【讨论】:

【参考方案3】:

从代码中,当您检测到没有连接时,您可以关闭 WiFi...

至于设置,没有(没有很好的方法来检查是否确实存在普遍且可靠的连接)。但是有些手机会自动执行您描述的操作,例如我的 LG P-970。

(注意:Android 连接到 WiFi 时会断开与移动网络的连接,因此无法继续连接到 WiFi,而是通过移动设备路由互联网访问,即使 Linux 可以做到这一点(使用 ip route ... 套件工具))

【讨论】:

谢谢。 iPhone 的工作方式似乎与您的 LG-P-970 相同。但是对于这种特定情况,我需要它与 Android 一起使用。如果 android 与移动网络断开连接,呼叫是否会通过 wifi 连接进行路由?还是仅在移动网络上禁用了数据? P-970 是安卓系统。当然,只有在使用 wifi 时才禁用移动数据。可能值得尝试从 169.254 范围分配链路本地 IP (Apipa)。 谢谢。我尝试了本地链接,但这对我的 Android 没有帮助。这是非常有用的信息,因为我已经在我正在构建的产品中使用它并且它解决了一个问题。谢谢!【参考方案4】:

我不能保证这会奏效,因为这是我不久前才尝试过的。当 wifi 连接的网络无法通往外界时,我也有类似的需要使用 3G(或其他移动网络)。

以下代码应断开 wifi 连接以允许移动网络进入播放。您需要在此过程中进行各种测试,然后再次重新建立 wifi 连接...

WifiManager wifiMan = null;
WifiManager.WifiLock wifiLock = null;

private Boolean disableWifi() 
    wifiMan = (WifiManager) getSystemService(Context.WIFI_SERVICE);
    if (wifiMan != null) 
        wifiLock = wifiMan.createWifiLock(WifiManager.WIFI_MODE_SCAN_ONLY, "MyWifiLock");
    
    return wifiMan.setWifiEnabled(false);


private Boolean enableWifi() 
    Boolean success;

    if (wifiLock != null)
        wifiLock.release();
    if (wifiMan != null)
        success = wifiMan.setWifiEnabled(true);
    return success;

【讨论】:

谢谢。如果我可以同时激活两个网络,那将是理想的。不过我会调查一下。 根据我的阅读,标准的 Android 行为是在 wifi 连接可用时禁用移动互联网连接。我的 HTC Desire 肯定就是这种情况。我在 Android 学习的早期就想出了上面的代码,所以可能会有更好的做事方式,尽管我在此过程中没有遇到任何其他事情。 是的,对于我所针对的特定应用程序,这确实是一种糟糕的行为。在这种情况下,android 的行为不像 PC 或 Mac,这太糟糕了。不过感谢您提供的代码,它很可能会在某个时候派上用场。【参考方案5】:

您无需编写任何代码。我找到了一个可以做这件事的应用程序。如果此连接没有互联网,您可以配置为自动断开与 wifi 的连接。

https://play.google.com/store/apps/details?id=com.nLabs.internetconnectivity&hl=en

【讨论】:

以上是关于在没有网络访问的本地 wifi 上强制 Android 使用 3G的主要内容,如果未能解决你的问题,请参考以下文章

在android中通过wifi访问连接在本地网络上的打印机

强制 Android 应用连接到某个 WiFi 网络

打开移动数据时如何从wifi网络访问本地url?

Android 127.0.0.1(环回)连接:在 WiFi 上工作,在 3G/4G 上失败

UniFi 外部强制网络门户 - 身份验证时移动 WiFi 断开连接

是否可以通过 Wifi 或 TMobile 网络强制网络流量?