1. 功能介绍

以太网的功能是允许设备提供硬件接口通过插入网线的形式访问互联网的功能。
接入网线之后,设备可以动态的获取IP,DNS,Gateway等一系列网络属性,我们也可以手动配置设备的网络属性,使用静态配置参数。

Google已经有一套现成的机制使用有线网,但没有涉及有线网配置的功能,
本文主要介绍如何Google现有机制的基础上实现静态网络的配置。

本文基于高通MSM8953 Android 7.1平台进行开发,通过配置eth0网口的IP,DNS,Gateway三个参数,实现上网功能,
若是其他平台或者非高通平台,可以当作参考。


2. 动态获取网络参数

此部分Google已经做好,当接入网线之后,在SystemBar中会出现有线网介入图标(<—>),此时设备已经接入有线网络,可以正常上网。


3. 手动配置网络参数(重点)

首先先来介绍一下相关java类:

(1)frameworks/base/core/java/android/net/IpConfiguration.java
IP状态配置,动态或者是静态,之后会介绍

(2)frameworks/base/core/java/android/net/StaticIpConfiguration.java
静态IP配置相关类,主要用于配置静态IP。

(3)frameworks/base/core/java/android/net/EthernetManager.java
上层配置IP的管理类,可以通过context.getSystemService(Context.ETHERNET_SERVICE)获得。

(4)frameworks/opt/net/ethernet/java/com/android/server/ethernet/EthernetServiceImpl.java
通过实现IEthernetManager.aidl接口来处理一些远程的以太网请求。

(5)frameworks/opt/net/ethernet/java/com/android/server/ethernet/EthernetNetworkFactory.java
以太网网络链接的管理类。


具体介绍之前,先来看一张简单配置UML的流程图,方便接下来的讲解.

在这里插入图片描述

接下来对照流程图逐步进行讲解。


3.1 输入相关配置信息

我们自己的项目中是通过配置eth0的 IP,DNS,Gateway来配置静态网络参数的。
可以自己开发相应界面,让用户手动输入相关信息即可。

这一步不涉及配置代码,仅仅是获取用户的想要设置的配置信息。


3.2 获取 IpConfiguration 配置参数

首先我们需要将相关配置信息转化为 StaticIpConfiguration,转化之前,先介绍两个枚举类:

public enum IpAssignment { 
        /* Use statically configured IP settings. Configuration can be accessed 
         * with staticIpConfiguration */ 
        STATIC, 
        /* Use dynamically configured IP settigns */ 
        DHCP, 
        /* no IP details are assigned, this is used to indicate 
         * that any existing IP settings should be retained */ 
        UNASSIGNED 
    }

    public enum ProxySettings { 
        /* No proxy is to be used. Any existing proxy settings 
         * should be cleared. */ 
        NONE, 
        /* Use statically configured proxy. Configuration can be accessed 
         * with httpProxy. */ 
        STATIC, 
        /* no proxy details are assigned, this is used to indicate 
         * that any existing proxy settings should be retained */ 
        UNASSIGNED, 
        /* Use a Pac based proxy. 
         */ 
        PAC 
    }

这两个枚举类型在IpConfiguration类中,具体作用上面代码部分的注释也写明了。
下面是将配置信息转化为StaticIpConfiguration的方法:

   private StaticIpConfiguration validateIpConfigFields(String ip,String dns,String gateway) { 
        StaticIpConfiguration  staticIpConfiguration = new StaticIpConfiguration();       
 
        //analysis ip address  
        Inet4Address inetAddr = getIPv4Address(ip); 
        if (inetAddr == null || inetAddr.equals(Inet4Address.ANY)) { 
            return -1; 
        } 
        staticIpConfiguration.ipAddress = new LinkAddress(inetAddr, DEFAULT_PREFIX_LENGTH); 
 
        //analysis gateway address 
        InetAddress gatewayAddr = getIPv4Address(gateway); 
        if (gatewayAddr == null) { 
              return -1; 
        } 
        if (gatewayAddr.isMulticastAddress()) { 
            return -1; 
        } 
        staticIpConfiguration.gateway = gatewayAddr; 
 
        //analysis dns address 
        InetAddress dnsAddr = getIPv4Address(dns); 
        if (dnsAddr == null) { 
            return -1; 
        } 
        staticIpConfiguration.dnsServers.add(dnsAddr); 
 
        return staticIpConfiguration; 
    }

    private Inet4Address getIPv4Address(String text) { 
        try { 
            return (Inet4Address) NetworkUtils.numericToInetAddress(text); 
        } catch (IllegalArgumentException | ClassCastException e) { 
            Log.e(TAG,"getIPv4Address fail"); 
            return null; 
        } 
    }

其中DEFAULT_PREFIX_LENGTH默认值是24,参考来自于Wifi模块。
至此,我们就将用户输入的IP,DNS,Gateway转化为需要的StaticIpConfiguration。

由于最终调用EthernetManager的setConfiguration函数时传递的参数类型是IpConfiguration,
查看StaticIpConfiguration,发现StaticIpConfiguration并不是IpConfiguration的子类,
所以我们需要在将StaticIpConfiguration转化为IpConfiguration,

查看IpConfiguration代码,发现IpConfiguration的构造函数中含有StaticIpConfiguration参数,
另外,我们可以通过setStaticIpConfiguration改变IpConfiguration。
这里我们选择前者,直接使用StaticIpConfiguration传入IpConfiguration的构造函数创建IpConfiguration对象,

先看一下IpConfiguration的构造函数:

    private void init(IpAssignment ipAssignment, 
                      ProxySettings proxySettings, 
                      StaticIpConfiguration staticIpConfiguration, 
                      ProxyInfo httpProxy) { 
        this.ipAssignment = ipAssignment; 
        this.proxySettings = proxySettings; 
        this.staticIpConfiguration = (staticIpConfiguration == null) ? 
                null : new StaticIpConfiguration(staticIpConfiguration); 
        this.httpProxy = (httpProxy == null) ? 
                null : new ProxyInfo(httpProxy); 
    } 
 
    public IpConfiguration() { 
        init(IpAssignment.UNASSIGNED, ProxySettings.UNASSIGNED, null, null); 
    } 
 
    public IpConfiguration(IpAssignment ipAssignment, 
                           ProxySettings proxySettings, 
                           StaticIpConfiguration staticIpConfiguration, 
                           ProxyInfo httpProxy) { 
        init(ipAssignment, proxySettings, staticIpConfiguration, httpProxy); 
    }

可以看出,无论是有参的构造函数还是无参的构造函数,最终都会调用IpConfiguration的init函数进行初始化配置。
我们使用的是IpConfiguration中有参的构造函数,
其中参数IpAssignment和ProxySettings是枚举类型,

我们需要配置静态地址,所以应该传入IpAssignment.STATIC和ProxySettings.STATIC,
第三个参数传入StaticIpConfiguration,
第四个参数ProxyInfo传入空即可,不需要设置代理。

mIpAssignment = IpAssignment.STATIC; 
mProxySettings = ProxySettings.STATIC;
mStaticIpConfiguration =  validateIpConfigFields(ip,dns,gateway); // 注意此处的参数应正确配置
 
IpConfiguration ipconfig = new IpConfiguration(mIpAssignment,mProxySettings,mStaticIpConfiguration,null);


3.3 通过Ethernet发出配置命令

获取IpConfiguration之后,我们就可以调用EthernetManager的setConfiguration开始进行静态网络配置:

    /** 
     * Set Ethernet configuration. 
     */ 
    public void setConfiguration(IpConfiguration config) { 
        try { 
            mService.setConfiguration(config); 
        } catch (RemoteException e) { 
            throw e.rethrowFromSystemServer(); 
        } 
    }

查看设置代码,函数会调用mService的setConfiguration并且可能会抛出RemoteException。
说明这一操作应该是远程aidl的调用,跟踪代码发现mService的类型为EthernetServiceImpl,
并且实现了IEthernetManager.aidl接口。


3.4 EthernetServiceImpl处理请求

查看EthernetServiceImpl中的设置函数:

   /** 
     * Set Ethernet configuration 
     */ 
    @Override 
    public void setConfiguration(IpConfiguration config) { 
        if (!mStarted.get()) { 
            Log.w(TAG, "System isn't ready enough to change ethernet configuration"); 
        } 
 
        enforceConnectivityInternalPermission(); 
 
        synchronized (mIpConfiguration) { 
            mEthernetConfigStore.writeIpAndProxyConfigurations(config); 
 
            // TODO: this does not check proxy settings, gateways, etc. 
            // Fix this by making IpConfiguration a complete representation of static configuration. 
            if (!config.equals(mIpConfiguration)) { 
                mIpConfiguration = new IpConfiguration(config); 
                mTracker.stop(); 
                mTracker.start(mContext, mHandler); 
            } 
        } 
    }

代码中EthernetConfigStore将会把IpConfiguration的配置信息写入配置文件。
进入EthernetConfigStore发现writeIpAndProxyConfigurations最后会调用EthernetConfigStore父类writeIpAndProxyConfigurations方法将配置信息写入配置文件。

之后判断当前地址是否跟配置地址一样,若不一样,则进行新地址的配置,
由于配置信息已经通过EthernetConfigStore写入配置文件,mTracker也即EthernetNetworkFactory就会重启当前网络。
这部分的逻辑代码都是Google已有的代码,这里不继续跟踪。

注:有时候EthernetNetworkFactory重启网络之后发现配置信息没有生效,
我遇到这种情况后,发现此时需要重启eth0网口。

重启网口的功能将会在下面的文章里介绍。


4 监听网线的插拔

开发时,我们需要见监听以太网的状态变化,根据状态变化更新界面显示或者是做一些其他的操作。
由于上层代码实际操作的是EthernetManager类,所以理所应该先去EthernetManger中查看有没有类似接口,
或者回调函数之类可以监听网口变化的功能,查看EthernetManager,

我们发现有如下的接口:

 /**
     * A listener interface to receive notification on changes in Ethernet.
     */
    public interface Listener {
        /**
         * Called when Ethernet port's availability is changed.
         * @param isAvailable {@code true} if one or more Ethernet port exists.
         */
        public void onAvailabilityChanged(boolean isAvailable);
    }

解释中说此接口可以接受以太网变化的通知。
在实际应用时,发现插入网线和拔出网线确实能够接受到通知,说明这个接口正是我们需要的。
查看代码发现,要使用这个接口,应该先调用addListener将实现该接口的子类加入到一个ArrayList的通知列表里面,
这说明我们可以在不同的地方接受以太网状态变化的通知。

    /** 
     * Adds a listener. 
     * @param listener A {@link Listener} to add. 
     * @throws IllegalArgumentException If the listener is null. 
     */ 
    public void addListener(Listener listener) { 
        if (listener == null) { 
            throw new IllegalArgumentException("listener must not be null"); 
        } 
        mListeners.add(listener); 
        if (mListeners.size() == 1) { 
            try { 
                mService.addListener(mServiceListener); 
            } catch (RemoteException e) { 
                throw e.rethrowFromSystemServer(); 
            } 
        } 
    }

从上面代码可以看到,传递的参数被添加到了mListeners中,
并且将mServiceListener添加到EthernetServiceImpl的远程监听接口中去。

mServiceListener代码如下:

    private final IEthernetServiceListener.Stub mServiceListener =
            new IEthernetServiceListener.Stub() {
                @Override
                public void onAvailabilityChanged(boolean isAvailable) {
                    mHandler.obtainMessage(
                            MSG_AVAILABILITY_CHANGED, isAvailable ? 1 : 0, 0, null).sendToTarget();
                }
            };

若系统检测到以太网状态发生变化,则会通过调用mServiceListener来进行广播通知,
接着在mHandler中会循坏遍历mListeners列表中的监听对象,凡是注册了监听接口的类都会收到通知消息。


5. 网口的打开与关闭

有时候我们需要在不拔出网线的同时关闭上网的功能,这个时候解决方安就是将网口关闭,等到允许上网时再打开网口,接下来就来介绍网口的打开与关闭操作。

查看EthernetNetworkFactory的代码可以发现有这样一个函数和之前一样,
我们先到EthernetManager中查看有没有已经做好的功能可以供我们调用。

很遗憾的是,EthernetManager并没有实现开关网口的功能。
由于EthernetManager不是最终管理以太网的管理类,只是一个提供上层接口的一个中间类,
所以要想查看以太网的所以功能,我们应该去查找以太网的管理类EthernetNetworkFactory。

查看EthernetNetworkFactory的代码可以发现有这样一个函数:

    /** 
     * Updates interface state variables. 
     * Called on link state changes or on startup. 
     */ 
    private void updateInterfaceState(String iface, boolean up) { 
        Log.d(TAG, "updateInterface: " + iface + " link " + (up ? "up" : "down")+" , mIface : "+mIface); 
        if (!mIface.equals(iface)) { 
            return; 
        } 
        Log.d(TAG, "updateInterface: " + iface + " link " + (up ? "up" : "down")); 
 
        synchronized(this) { 
            mLinkUp = up; 
            mNetworkInfo.setIsAvailable(up); 
            if (!up) { 
                // Tell the agent we're disconnected. It will call disconnect(). 
                mNetworkInfo.setDetailedState(DetailedState.DISCONNECTED, null, mHwAddr); 
                stopIpProvisioningThreadLocked(); 
            } 
            updateAgent(); 
            // set our score lower than any network could go 
            // so we get dropped.  TODO - just unregister the factory 
            // when link goes down. 
            mFactory.setScoreFilter(up ? NETWORK_SCORE : -1); 
        } 
    }

这正是我们想要的功能,所以这个功能其实也是已经做好的,但是他是private类型的函数,说明Google并不想将这个功能公开出来。

函数的第一个参数是想要打开或关闭的网口名称,第二个参数表明是打开还是关闭,true表示打开,false表示关闭。

既然已经有了功能,我们只需要调用即可,具体如何调用,我们可以模仿配置静态IP的方法,从EthernetManager开始,到EthernetNetworkFactory结束,将这个过程做成一个标准的功能。

首先我们在EthernetManager中添加一个函数updateIface:

 /* 
    * up and down eth0 
    */ 
    public void updateIface(String iface,boolean up){ 
        try { 
            mService.updateIfaceState(iface,up); 
        } catch (RemoteException e) { 
            throw e.rethrowFromSystemServer(); 
        } 
    }

此时需要在EthernetServiceImpl中添加函数updateIfaceState:

    @Override 
    public void updateIfaceState(String iface,boolean up){ 
        mTracker.changeEthernetState(iface,up); 
    }

Override注解说明这是重写函数,EthernetServiceImpl继承自IEthernetManager.Stub,
所以我们需要在对应的IEthernetManager.aidl接口文件中加入updateIfaceState声明,

如下所示:

// IethernetManager.aidl
 
interface IEthernetManager 
{ 
    IpConfiguration getConfiguration(); 
    void setConfiguration(in IpConfiguration config); 
    boolean isAvailable(); 
    void addListener(in IEthernetServiceListener listener); 
    void removeListener(in IEthernetServiceListener listener); 
    void updateIfaceState(String iface,boolean up); 
}

这里之后会调用mTracker.changeEthernetState函数在EthernetNetworkFactory创建函数changeEthernetState:

    public void changeEthernetState(String iface,boolean state){ 
        Log.i(TAG,"changeEthernetState : iface : "+iface+" , state : "+state); 
        updateInterfaceState(iface,state); 
    }

到此结束,从EthernetManager到EthernetNetworkFactory中关于网口开关的功能就做完了,
我们只需要调用EthernetManager中的updateIface函数就能实现网口的打开与关闭功能。


6 监听网线拔出
之前在第四节中介绍过监听网线的插拔功能,
第五节中介绍了在不拔网线的情况下打开与关闭网口,
这个时候就会遇到一个问题,那就是我无法正确的监听到网线的拔出。

实际操作中会发现,打开与关闭网口是,监听器监听同样会被调用,但是此时我并没有拔出网线。
换一个说法就是,网口的打开与关闭实际上模拟的就是网线的插拔功能。

那此时我就需要正确区分开网口的关闭与网线的拔出这两种情况。

查询EthernetNetworkFactory代码可以发现有一个内部类可以监听到网线的插入与拔出:

   private class InterfaceObserver extends BaseNetworkObserver { 
        @Override 
        public void interfaceLinkStateChanged(String iface, boolean up) { 
            updateInterfaceState(iface, up); 
        } 
 
        @Override 
        public void interfaceAdded(String iface) { 
            maybeTrackInterface(iface); 
        } 
 
        @Override 
        public void interfaceRemoved(String iface) { 
            stopTrackingInterface(iface); 
        } 
    }

其中interfaceAdded表示网线的插入,interfaceRemoved表示网线的拔出。
为了配合系统的原生代码结构,我们可以在EthernetManager的Listener接口中添加一个新函数声明,添加后的Listener接口如下:

    /** 
     * A listener interface to receive notification on changes in Ethernet. 
     */ 
    public interface Listener { 
        /** 
         * Called when Ethernet port's availability is changed. 
         * @param isAvailable {@code true} if one or more Ethernet port exists. 
         */ 
        public void onAvailabilityChanged(boolean isAvailable); 
 
        /* 
         *Called when network wire take out 
         */ 
        public void onEthernetIfaceRemove(); 
    }

监听的注册流程重EthernetManager的addListener,到EthernetServiceImpl中的addListener时,
已经将其注册到了一个RemoteCallList的列表中,在通过构造mTacker是将监听列表传给了EthernetNetworkFactory。

所以我们只需要在EthernetNetworkFactory实现通知网线拔出就可以了。

具体代码如下:

private void notifyListenersRemoved(){ 
        int n = mListeners.beginBroadcast(); 
        Log.i("SIMCOMIP","notifyListenersRemoved state listener size : "+n); 
            for (int i = 0; i < n; i++) { 
                try { 
                    mListeners.getBroadcastItem(i).onEthernetIfaceRemove(); 
                } catch (RemoteException e) { 
                    // Do nothing here. 
                } 
            } 
        mListeners.finishBroadcast(); 
    }

首先,添加一个通知所有监听者网线拔出的函数,之后我们就可以在前面提到的InterfaceObserver的interfaceRemoved函数中调用一下就可以了:

    private class InterfaceObserver extends BaseNetworkObserver { 
        @Override 
        public void interfaceLinkStateChanged(String iface, boolean up) { 
            updateInterfaceState(iface, up); 
        } 
 
        @Override 
        public void interfaceAdded(String iface) { 
            maybeTrackInterface(iface); 
        } 
 
        @Override 
        public void interfaceRemoved(String iface) { 
            stopTrackingInterface(iface); 
            notifyListenersRemoved(); 
        } 
    }


7. SystsemUI同步更新

上层的SystemUI显示图标及更新是通过NetworkControllerImpl.java文件完成,具体可以自己查看代码,这里不做解析了。

Logo

权威|前沿|技术|干货|国内首个API全生命周期开发者社区

更多推荐