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();
}
}