强制安卓在无网络的本地WiFi环境下使用3G网络

15
我有一个Wi-Fi LAN设置,没有互联网访问,只有其他本地Wi-Fi设备连接。DHCP配置为不返回网关或DNS服务器,只有IP和子网掩码。
当我将我的Android连接到这个Wi-Fi接入点时,它会成功连接,但手机上的所有互联网连接都会停止工作。
我本应该期望安卓应该意识到因为Wi-Fi没有网关设置,互联网无法通过该连接,从而应该通过处于5格信号状态的3G连接路由。
我也尝试在Android手机上设置静态IP,但这没有帮助。
这个设置的主要原因是,Android设备可以在此远程网络上与基于互联网的服务器传输数据,因为它可以毫无问题地连接到本地设备。然而,一旦设置了Wi-Fi,3G侧就会出现问题。
对于如何解决这个问题有什么想法吗?

2
你找到解决方案了吗?我也遇到了完全相同的问题。我有一个树莓派,它是一个Airplay服务器,并拥有自己的无线网络。为了将东西流式传输到它,我必须连接到它的网络,但是当我连接时,我没有“运营商互联网连接”。使用iPhone,我可以分配一个静态IP而没有DNS /默认网关,它可以完美地工作。但我在Android上做不到这一点。 - Rhyuk
5个回答

5
经过一些编码和测试,我已经合并了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);
}

使用流程

  1. 检查是否启用了到主机的路由
  2. 如果是,则无论Wi-Fi是否连接,都进行通信,并仅执行第6步(第4步仅检查路由是否已启用,不执行任何相关操作)。否则,临时禁用Wi-Fi。
  3. 线程休眠约3秒,以便3G连接恢复
  4. 将3G路由设置为给定的URL
  5. 重新启用Wi-Fi
  6. 现在可以通过Wi-Fi连接调用给定的URL,即使没有网络访问权限

结论

这有点巧妙,但功能正常。唯一的问题是该路由器具有几秒钟的超时时间(如20-30秒),这会强制您再次执行上述整个过程。将此超时时间设置为更长的值将非常好。


1
谷歌在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; 
    }
});

请注意,在使用此代码片段时要考虑线程安全性。
此外,我建议检查ConnectivityManager#bindProcessToNetwork和这个博客ConnectivityManager.NetworkCallback是一个空类,它有几个方法

0

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

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

(注意:Android在连接WiFi时会断开移动网络的连接,因此没有办法仍然通过移动网络路由互联网访问,尽管Linux可以使用ip route ...工具套件来实现)


谢谢。iPhone似乎与您的LG-P-970相同。然而,对于这种特殊情况,我需要它可以使用Android系统。如果Android与移动网络断开连接,电话是否会通过WiFi连接路由?或者仅在移动网络上禁用数据? - Stephen Hankinson
P-970是安卓系统。当连接Wi-Fi时,只有移动数据会被禁用。可以尝试从169.254范围内分配一个链路本地IP(Apipa)。 - ՕլՁՅԿ
1
谢谢。我尝试了链接本地,但对我的Android没有帮助。不过这是非常有用的信息,因为我正在构建一个产品,并解决了一个问题。谢谢! - Stephen Hankinson

0

我不能保证这会起作用,因为这是我一段时间前尝试过的东西。我有一个类似的需求,需要在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;
}

谢谢。如果我能同时激活两个网络就太理想了。不过我会研究一下的。 - Stephen Hankinson
据我所了解,标准的Android行为是在有wifi连接时禁用移动互联网连接。我的HTC Desire也是如此。我早期学习Android时就想出了上述代码,虽然我在学习过程中没有遇到其他更好的方法。 - Squonk
是的,对于我所针对的特定应用程序来说,这确实是非常糟糕的行为。很遗憾安卓在这种情况下不像PC或Mac那样表现。不过还是谢谢你提供的代码,它在某些时候肯定会派上用场。 - Stephen Hankinson

-2

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