Android多网络环境(wifi,mobile)下强制在某个网络(mobile)访问服务端以及适配

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/u010019468/article/details/72886859
本篇文章已授权微信公众号 guolin_blog (郭霖)独家发布

这个功能如标题所述:在wifi和移动数据网络同时开启之下,在Android5.0之前系统并没有很好地提供这样的api来实现这样的功能。现在需要wifi开着的情况下,强制通过移动数据网络发送网络请求,可能会觉得哪会有这样的蛋疼需求,认为只要能访问就行了,还要特地移动网络,那我只能讲你们的业务发展中没有这样的需求。好了废话不多说,实现如下:

Wifi下指定移动网络访问服务端

首先注意权限申请,需要如下权限才能切换:

<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE"/>
<uses-permission android:name="android.permission.WRITE_SETTINGS"/>
 @TargetApi(21)
    private void forceSendRequestByMobileData() {
        ConnectivityManager connectivityManager = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE);
        NetworkRequest.Builder builder = new NetworkRequest.Builder();
        builder.addCapability(NET_CAPABILITY_INTERNET);
        //强制使用蜂窝数据网络-移动数据
        builder.addTransportType(TRANSPORT_CELLULAR);
        NetworkRequest build = builder.build();
        connectivityManager.requestNetwork(build, new ConnectivityManager.NetworkCallback() {
            @Override
            public void onAvailable(Network network) {
                super.onAvailable(network);
                try {
                    URL url = new URL("");
                    HttpURLConnection connection = (HttpURLConnection)network.openConnection(url);
                    /*******省略参数配置*******/
                    connection.connect();
                    /*******数据流处理*******/
                } catch (Exception e) {

                }

            }
        });
    }

其中
builder.addCapability(NET_CAPABILITY_INTERNET);
//强制使用蜂窝数据网络-移动数据
builder.addTransportType(TRANSPORT_CELLULAR);
通过配置TransportType参数TRANSPORT_CELLULAR来指定移动网络,其值可以为int有如下几种:分别为移动数据网络、wifi、蓝牙、以太网、Vpn等5中传输通道。这时就不论当前手机有多少网络是处于连接中,都可以指定单个。

 /**
     * Indicates this network uses a Cellular transport.
     */
    public static final int TRANSPORT_CELLULAR = 0;

    /**
     * Indicates this network uses a Wi-Fi transport.
     */
    public static final int TRANSPORT_WIFI = 1;

    /**
     * Indicates this network uses a Bluetooth transport.
     */
    public static final int TRANSPORT_BLUETOOTH = 2;

    /**
     * Indicates this network uses an Ethernet transport.
     */
    public static final int TRANSPORT_ETHERNET = 3;

    /**
     * Indicates this network uses a VPN transport.
     */
    public static final int TRANSPORT_VPN = 4;

上文中addCapability(NET_CAPABILITY_INTERNET)其参数配置也是固定范围可选的,不能乱配,共有19个参数可配:

    public static final int NET_CAPABILITY_MMS            = 0;
    public static final int NET_CAPABILITY_SUPL           = 1;
    public static final int NET_CAPABILITY_DUN            = 2;
    public static final int NET_CAPABILITY_FOTA           = 3;
    public static final int NET_CAPABILITY_IMS            = 4;
    public static final int NET_CAPABILITY_CBS            = 5;
    public static final int NET_CAPABILITY_WIFI_P2P       = 6;
    public static final int NET_CAPABILITY_IA             = 7;
    public static final int NET_CAPABILITY_RCS            = 8;
    public static final int NET_CAPABILITY_XCAP           = 9;
    public static final int NET_CAPABILITY_EIMS           = 10;
    public static final int NET_CAPABILITY_NOT_METERED    = 11;

    /**
     * Indicates that this network should be able to reach the internet.
     */
    public static final int NET_CAPABILITY_INTERNET       = 12;
    public static final int NET_CAPABILITY_NOT_RESTRICTED = 13;
    public static final int NET_CAPABILITY_TRUSTED        = 14;
    public static final int NET_CAPABILITY_NOT_VPN        = 15;
    public static final int NET_CAPABILITY_VALIDATED      = 16;
    public static final int NET_CAPABILITY_CAPTIVE_PORTAL = 17;
    public static final int NET_CAPABILITY_FOREGROUND = 18;

其中有默认如下配置能力,故若要访问网络需要加上NET_CAPABILITY_INTERNET

private static final long DEFAULT_CAPABILITIES =
            (1 << NET_CAPABILITY_NOT_RESTRICTED) |
            (1 << NET_CAPABILITY_TRUSTED) |
            (1 << NET_CAPABILITY_NOT_VPN);

在connectivityManager.requestNetwork请求之后如果此次netWork是可以使用的,就会回调ConnectivityManager.NetworkCallback中onAvailable(Network network)函数,这时候利用返回的Network来进行http连接了network.openConnection(url)。其源码如下:

 public void requestNetwork(NetworkRequest request, NetworkCallback networkCallback,
            int timeoutMs, int legacyType) {
        sendRequestForNetwork(request.networkCapabilities, networkCallback, timeoutMs, REQUEST,
                legacyType);
    }
private NetworkRequest sendRequestForNetwork(NetworkCapabilities need,
            NetworkCallback networkCallback, int timeoutSec, int action,
            int legacyType) {
        if (networkCallback == null) {
            throw new IllegalArgumentException("null NetworkCallback");
        }
        if (need == null && action != REQUEST) {
            throw new IllegalArgumentException("null NetworkCapabilities");
        }
        try {
            incCallbackHandlerRefCount();
            synchronized(sNetworkCallback) {
                if (action == LISTEN) {
                    networkCallback.networkRequest = mService.listenForNetwork(need,
                            new Messenger(sCallbackHandler), new Binder());
                } else {
                    networkCallback.networkRequest = mService.requestNetwork(need,
                            new Messenger(sCallbackHandler), timeoutSec, new Binder(), legacyType);
                }
                if (networkCallback.networkRequest != null) {
                    sNetworkCallback.put(networkCallback.networkRequest, networkCallback);
                }
            }
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
        if (networkCallback.networkRequest == null) decCallbackHandlerRefCount();
        return networkCallback.networkRequest;
    }

可以看到 connectivityManager.requestNetwork()

requestNetwork(NetworkRequest request, NetworkCallback networkCallback) 

又调用到
sendRequestForNetwork(request.networkCapabilities,networkCallback, timeoutMs, REQUEST,legacyType)
来实现请求,其源码可以看到requestNetwork的请求类型是REQUEST,继而走到:

networkCallback.networkRequest = mService.requestNetwork(need,
                            new Messenger(sCallbackHandler), timeoutSec, new Binder(), legacyType);

最后是通过mService这个是aidl进程通信的IConnectivityManager.Sub内部类prox代理对象,通过binder机制通知远端进行requestNetwork方法。

最后不要忘记了注销监听NetworkCallback:

mConnectivityManager.unregisterNetworkCallback(networkCallback);

适配问题

介绍使用以及原理就是这么多了,但是并不会实际中并不会就那么完美,首先想到就是适配情况

  • 适配机型问题
    在多个品牌手机中oppo,小米,魅族几个机型中发现可用能够实现这个功能,但在华为p系列p7以及荣耀手机中可以在回调中onAvailable(Network network)正常返回Network,也能进行连接,但读取网络结果时就一直出现超时。

    解决办法:先检查是否有权限,特别注意oppo手机wifi第一次用户安装登录默认是没有数据网络权限的,所以总会导致接口访问失败超时,需要用户切换到移动数据网络连接状态下,让用户授权。

    若权限存在的话,可以用下面提到的startUsingNetworkFeature低版本的兼容方法尝试,目前自测是可以的。区别在于这个不使用NetWork,直接在打开的移动网络中进行连接访问。

  • 适配低版本
    上述也提到在5.0之前要实现这样的功能呢,在低版本中可以使用startUsingNetworkFeature(int networkType, String feature)这个被抛弃方法实现,其实和上述实现原理差不多,只是使用起来性能以及效率问题,这个老Api在某些机型上不能立即采用移动网络进行请求,需要等待些时间重试。

    public int startUsingNetworkFeature(int networkType, String feature) {
        checkLegacyRoutingApiAccess();
        NetworkCapabilities netCap = networkCapabilitiesForFeature(networkType, feature);
        if (netCap == null) {
            Log.d(TAG, "Can't satisfy startUsingNetworkFeature for " + networkType + ", " +
                    feature);
            return PhoneConstants.APN_REQUEST_FAILED;
        }

        NetworkRequest request = null;
        synchronized (sLegacyRequests) {
            LegacyRequest l = sLegacyRequests.get(netCap);
            if (l != null) {
                Log.d(TAG, "renewing startUsingNetworkFeature request " + l.networkRequest);
                renewRequestLocked(l);
                if (l.currentNetwork != null) {
                    return PhoneConstants.APN_ALREADY_ACTIVE;
                } else {
                    return PhoneConstants.APN_REQUEST_STARTED;
                }
            }

            request = requestNetworkForFeatureLocked(netCap);
        }
        if (request != null) {
            Log.d(TAG, "starting startUsingNetworkFeature for request " + request);
            return PhoneConstants.APN_REQUEST_STARTED;
        } else {
            Log.d(TAG, " request Failed");
            return PhoneConstants.APN_REQUEST_FAILED;
        }
    }

继续看requestNetworkForFeatureLocked(netCap)实现

    private NetworkRequest requestNetworkForFeatureLocked(NetworkCapabilities netCap) {
        int delay = -1;
        int type = legacyTypeForNetworkCapabilities(netCap);
        try {
            delay = mService.getRestoreDefaultNetworkDelay(type);
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
        LegacyRequest l = new LegacyRequest();
        l.networkCapabilities = netCap;
        l.delay = delay;
        l.expireSequenceNumber = 0;
        l.networkRequest = sendRequestForNetwork(netCap, l.networkCallback, 0,
                REQUEST, type);
        if (l.networkRequest == null) return null;
        sLegacyRequests.put(netCap, l);
        sendExpireMsgForFeature(netCap, l.expireSequenceNumber, delay);
        return l.networkRequest;
    }

可以看到最终也走到lsendRequestForNetwork(netCap, l.networkCallback, 0,REQUEST, type);这个方法和5.0之后的requestNetwork(NetworkRequest request, NetworkCallback networkCallback)是一个效果。

  • 甚至有人可能会想到先断开wifi,再通过移动网络来访问,如下:
ConnectivityManager cm = (ConnectivityManager)Context.getSystemService(Context.CONNECTIVITY_SERVICE);
NetworkInfo ni = cm.getActiveNetworkInfo();
if(ni == null)
  //no connectivity, abort
if(ni.getType() == ConnectivityManager.TYPE_WIFI || ni.getType() == ConnectivityManager.TYPE_WIMAX) {
  WifiManager wm = (WifiManager)Context.getSystemService(Context.WIFI_SERVICE);
  if( wm != null)
    wm.disconnect();
  //this will force android to fallback to other available n/w which is 3G
}
while(true) {
  NetworkInfo ni = cm.getActiveNetworkInfo();
  if( ni != null && ni.getType() == ConnectivityManager.TYPE_MOBILE && ni.isConnected()) {
    //send your http request
    break;
  }
  //sleep for some time, so that android can connect you to other n/w
}

但是并不好用,好多手机都不能通过代码api来实现开关wifi以及移动网络,相比较而言可以用前者更合适。

展开阅读全文

没有更多推荐了,返回首页