天天看點

Android下實作非啟動界面Wifi連接配接

android的網絡功能和一般的linux并無太大的差別,我原來以為在android上連接配接網絡和普通的linux連接配接網絡沒有很大差別,事實上差別還是有一些的。

由于項目的需要,我的目标是在android的界面沒有啟動之前連接配接wifi,于是本來的期待是直接在init.rc中加入一些腳本調用即可,但研究了一會兒發現沒有那麼簡單。

首先要感謝anly_jun@baidu貼吧的幾篇博文,從http://hi.baidu.com/anly_jun/blog/item/8ecb92d593d144cf50da4b6e.html開始,一共有七篇關于android

wifi子產品分析,其中有大量部落客自己使用uml工具畫的調用講解,對于了解android wifi工作機制還是有很用的。其中最重要的是下面這幾幅圖。

Android下實作非啟動界面Wifi連接配接

(轉自http://hi.baidu.com/anly_jun/blog/item/65e26e117e09ebcda6ef3f56.html)

在想要對wifi硬體動作之前,需要做兩件事情,一是要load wifi的driver,而是要打開wpa_supplicant,其實如果是連接配接沒有加密的wifi,沒有必要打開wpa_supplicant,但是為了講問題化為熟知的問題,此處還是先按照提示調用wifi_load_driver()和wifi_start_supplicant()

按照上面的提示寫出來的初始化代碼如下:

int init_stage() { 

    //

load the wifi driver: insmod .ko 

    int ret = wifi_load_driver(); 

    if(ret < 0) { 

        loge("failed

to load wi-fi driver. %s",strerror(errno)); 

        return -1; 

    } 

start wpa_supplicant 

    ret =  wifi_start_supplicant(); 

to start supplicant daemon. %s",strerror(errno)); 

    return 0; 

接下來,便是連接配接的過程了,經過上面的步驟,wifi的driver已經載入,wpa_supplicant也已經打開,那咱們就可以開始連接配接無線了吧。後來證明這是錯誤的,因為anly_jun的這篇android wifi分析的粒度隻在java層面的函數級别,是以有一些細節并沒有提到。在下面我會提到這些細節。

按照一般的linux中連接配接wifi的步驟,這時候就可以直接調用一個程式來連接配接某個ssid的無線網絡,然後調用dhcpd來配置設定ip了,我之前在eeepc上連接配接wifi就非常簡單,調用iwconfig [ssid],再調用dhcpd就可以了。但很遺憾,android上并沒有iwconfig這樣友善的工具。

這下線索似乎就斷了,天無絕人之路,既然在android的java code中都可以添加一個無線網絡并且連接配接,那我們就去android的java源代碼中找一找。在android中,程式員是使用wifimanager這個類來進行wifi操作的,其中關于添加一個網絡的代碼如下:

/** 

* add a new network description to the set of configured networks. 

* the {@code networkid} field of the supplied configuration object 

* is ignored. 

* <p/> 

* the new network will be marked disabled by default. to enable it, 

* called {@link #enablenetwork}. 

* @param config the set of variables that describe the configuration, 

*            contained in a {@link wificonfiguration} object. 

* @return the id of the newly created network description. this is used in 

*         other operations to specified the network to be acted upon. 

*         returns {@code -1} on failure. 

*/ 

public int addnetwork(wificonfiguration config) { 

    if (config == null) { 

    config.networkid = -1; 

    return addorupdatenetwork(config); 

* internal method for doing the rpc that creates a new network description 

* or updates an existing one. 

* @param config the possibly sparse object containing the variables that 

*         are to set or updated in the network description. 

* @return the id of the network on success, {@code -1} on failure. 

private int addorupdatenetwork(wificonfiguration config) { 

    try { 

        return mservice.addorupdatenetwork(config); 

    } catch (remoteexception e) { 

看其中确實是調用了service的函數,于是又在frameworks/base/services/java/com/android/server/wifiservice.java中找到了有關增加一個網絡配置的相關代碼

* see {@link android.net.wifi.wifimanager#addorupdatenetwork(wificonfiguration)} 

* @return the supplicant-assigned identifier for the new or updated 

* network if the operation succeeds, or {@code -1} if it fails 

public int addorupdatenetwork(wificonfiguration config) { 

    enforcechangepermission(); 

    /* 

     * if the supplied networkid is -1, we create a new empty 

     * network configuration. otherwise, the networkid should 

     * refer to an existing configuration. 

     */ 

    int netid = config.networkid; 

    boolean newnetwork = netid == -1; 

    boolean doreconfig = false; 

    // networkid of -1 means we want to create a new network

    synchronized (mwifistatetracker) { 

        if (newnetwork) { 

            netid = mwifistatetracker.addnetwork(); 

            if (netid < 0) { 

                if (dbg) { 

                    slog.d(tag, "failed to add a network!");

                } 

                return -1; 

            } 

            doreconfig = true; 

        } 

        mneedreconfig = mneedreconfig || doreconfig; 

    setvariables: { 

        /* 

         * note that if a networkid for a non-existent network 

         * was supplied, then the first setnetworkvariable()

         * will fail, so we don't bother to make a separate check 

         * for the validity of the id up front. 

         */ 

        if (config.ssid != null && 

                !mwifistatetracker.setnetworkvariable( 

                    netid, 

                    wificonfiguration.ssidvarname, 

                    config.ssid)) { 

            if (dbg) { 

                slog.d(tag, "failed to set ssid: "+config.ssid); 

            break setvariables; 

        if (config.bssid != null && 

                    wificonfiguration.bssidvarname, 

                    config.bssid)) { 

                slog.d(tag, "failed to set bssid: "+config.bssid); 

        }

我們來到frameworks/base/wifi/java/android/net/wifi/wifistatetracker.java中,看看addnetwork和setnetworkvariable函數都是什麼樣的。

* add a network 

* @return network id of the new network 

public synchronized int addnetwork() { 

    if (mwifistate.get() != wifi_state_enabled) { 

    return wifinative.addnetworkcommand(); 

* set network setting by name 

* @param netid network id of the network 

* @param name network variable key 

* @param value network variable value 

* @return {@code true} if the operation succeeds, {@code false} otherwise 

public synchronized boolean setnetworkvariable(int netid,string name, string value) { 

        return false; 

    return wifinative.setnetworkvariablecommand(netid, name,value); 

似乎wifinative就應該是最終boss了,于是來到frameworks/base/wifi/java/android/net/wifi/wifinative.java中一看,這兩個函數都是這麼定義的。

public native static int addnetworkcommand(); 

public native static boolean setnetworkvariablecommand(intnetid, string name, string value); 

熟悉android ndk開發的朋友都應該知道,這裡的函數都是通過jni連結到c/c++代碼中,于是我們稍微花了一點時間,找到了這個函數所對應的frameworks/base/core/jni/android_net_wifi_wifi.cpp

static int docommand(const char *cmd, char *replybuf, intreplybuflen) 

    size_t reply_len = replybuflen - 1; 

    if (::wifi_command(cmd, replybuf, &reply_len) != 0) 

    else { 

        // strip off trailing newline 

        if (reply_len > 0 && replybuf[reply_len-1] == '\n') 

            replybuf[reply_len-1] = '\0'; 

        else 

            replybuf[reply_len] = '\0'; 

        return 0; 

static jint dointcommand(const char *cmd) 

    char reply[256]; 

    if (docommand(cmd, reply, sizeof(reply)) != 0) { 

        return (jint)-1; 

    } else { 

        return (jint)atoi(reply); 

static jboolean dobooleancommand(const char *cmd, const char*expect) 

        return (jboolean)jni_false; 

        return (jboolean)(strcmp(reply, expect) == 0); 

…… 

static jint android_net_wifi_addnetworkcommand(jnienv* env,jobject clazz) 

    return dointcommand("add_network"); 

static jbooleanandroid_net_wifi_setnetworkvariablecommand(jnienv* env, 

                                                          jobject clazz, 

                                                          jint netid, 

                                                          jstring name, 

                                                          jstring value) 

    char cmdstr[256]; 

    jboolean iscopy; 

    const char *namestr = env->getstringutfchars(name,&iscopy); 

    const char *valuestr = env->getstringutfchars(value,&iscopy); 

    if (namestr == null || valuestr == null) 

        return jni_false; 

    int cmdtoolong = snprintf(cmdstr, sizeof(cmdstr),"set_network %d %s %s", 

                 netid, namestr, valuestr) >=(int)sizeof(cmdstr); 

    env->releasestringutfchars(name, namestr); 

    env->releasestringutfchars(value, valuestr); 

    return (jboolean)!cmdtoolong && dobooleancommand(cmdstr,"ok"); 

// ---------------------------------------------------------------------------- 

/* 

* jni registration. 

static jninativemethod gwifimethods[] = { 

    /* name, signature, funcptr */ 

    { "loaddriver", "()z",  (void*)android_net_wifi_loaddriver }, 

    { "unloaddriver", "()z",  (void*)android_net_wifi_unloaddriver }, 

    { "startsupplicant", "()z",  (void*)android_net_wifi_startsupplicant }, 

    { "stopsupplicant", "()z",  (void*)android_net_wifi_stopsupplicant }, 

    { "connecttosupplicant", "()z",  (void*)android_net_wifi_connecttosupplicant }, 

    { "closesupplicantconnection", "()v",  (void*)android_net_wifi_closesupplicantconnection }, 

    { "listnetworkscommand", "()ljava/lang/string;", 

        (void*) android_net_wifi_listnetworkscommand }, 

    { "addnetworkcommand", "()i", (void*)android_net_wifi_addnetworkcommand }, 

    { "setnetworkvariablecommand", "(iljava/lang/string;ljava/lang/string;)z", 

        (void*) android_net_wifi_setnetworkvariablecommand}, 

    { "getnetworkvariablecommand", "(iljava/lang/string;)ljava/lang/string;", 

        (void*) android_net_wifi_getnetworkvariablecommand}, 

看來這下終于找到幹活兒的函數了,可喜可賀,于是我們得出結論:在android中,要連上一個無線網絡,首先需要添加一個網絡,然後設定這個網絡的配置,而這些步驟實際上都是通過向wpa_supplicant發送指令實作的。指令的格式例外的很簡單,比如添加一個網絡配置是add_network,設定一個網絡配置是set_network [netid] [varname] [var]。于是根據java代碼我寫出了配置環節的代碼。

int config_stage(){    

    // add a network config to supplicant mode 

    int networkid = dointcommand("add_network"); // add a new network id 

    if(networkid < 0) { 

        loge("failed to add a network configuration. %s",strerror(errno)); 

    loge("add a network %d",networkid); 

    // set the ssid of the destination wifi adhoc 

    snprintf(cmdstr, sizeof(cmdstr), "set_network %d %s %s",networkid, ssid_name, ssid); 

    if(!dobooleancommand(cmdstr,"ok")) { 

        loge("failed to set network %d configuration ssid. %s", networkid, strerror(errno)); 

    return networkid; 

添加了一個配置的網絡之後,選擇某個網絡配置的指令是select_network [netid]。在select_network之後,wpa_supplicant就嘗試和該ssid的ap進行associate操作,在associate之後便調用dhcpd擷取ip。select_network是立即傳回的函數,那麼何以得知已經和無線ap連接配接上了呢?android的界面裡面連接配接/斷開wifi都是可以被接受到的事件,在wifistatetracker中我們看到是由wifimonitor來監聽由wpa_supplicant發過來的消息的,于是我們來到framework/base/wifi/java/android/net/wifi/wifimonitor.java,看到其中接受消息的monitor線程

class monitorthread extends thread { 

    public monitorthread() { 

        super("wifimonitor"); 

    public void run() { 

        if (connecttosupplicant()) { 

            // send a message indicating that it is now possible to send commands 

            // to the supplicant 

            mwifistatetracker.notifysupplicantconnection(); 

        } else { 

            mwifistatetracker.notifysupplicantlost(); 

            return; 

        //noinspection infiniteloopstatement 

        for (;;) { 

            string eventstr = wifinative.waitforevent(); 

            // skip logging the common but mostly uninteresting scan-results event 

            if (config.logd &&eventstr.indexof(scanresultsevent) == -1) { 

                log.v(tag, "event [" + eventstr + "]"); 

            if (!eventstr.startswith(eventprefix)) { 

                if (eventstr.startswith(wpaeventprefix) && 

                        0 <eventstr.indexof(passwordkeymaybeincorrectevent)) { 

                    handlepasswordkeymaybeincorrect(); 

                continue; 

            string eventname =eventstr.substring(eventprefixlen); 

            int nameend = eventname.indexof(' '); 

            if (nameend != -1) 

                eventname = eventname.substring(0, nameend);

            if (eventname.length() == 0) { 

                if (config.logd) log.i(tag, "received wpa_supplicant event with empty event

name"); 

wifinative.wairforevent在wifi.c中對應的函數是wifi_wait_event(),用來阻塞的等待wpa_supplicant發來的消息。當wpa_supplicant連接配接上之後,便會發出一個control-event-connected的消息,隻要截獲這個消息,我們便可以進行dhcp的操作了。

基于以上的考慮,我寫了connect階段的代碼。這裡我偷了一下懶,沒有仔細的去分析過來的指令,隻是比對到有connected的指令,就當已經連接配接上了。

#define connected "connected" 

int connect_stage(int networkid) { 

    // enable the network 

    snprintf(cmdstr, sizeof(cmdstr), "select_network

%d",networkid); 

        loge("failed to select network %d. %s", networkid, strerror(errno)); 

    // wait for connect 

    char buf[256]; 

    while(1) { 

        int nread = wifi_wait_for_event(buf, sizeof(buf)); 

        if(nread > 0) { 

            loge("receive buf:\n %s\n",buf); 

            if(strstr(buf,connected) > 0) { 

                break; 

            // xxx danger of not going out of the loop!!! 

        continue; 

}

在wifistatetracer中我還找到了負責dhcp的dhcphandler,按照其中的函數寫了dhcp階段的代碼

int dhcp_stage(){ 

    int result; 

    in_addr_t ipaddr, gateway, mask, dns1, dns2, server; 

    uint32_t lease; 

    char ifname[256]; 

    char mdns1name[256]; 

    char mdns2name[256]; 

    property_get("wifi.interface", ifname ,"eth0"); 

    snprintf(mdns1name, sizeof(mdns1name), "net.%s.dns1",ifname); 

    snprintf(mdns2name, sizeof(mdns2name), "net.%s.dns2",ifname); 

    result = dhcp_do_request(ifname, &ipaddr, &gateway, &mask, &dns1,&dns2, &server, &lease); 

    if(result != 0) { 

to dhcp on interface %s. %s", ifname, strerror(errno)); 

    struct in_addr dns_struct1, dns_struct2; 

    dns_struct1.s_addr = dns1; 

    dns_struct2.s_addr = dns2; 

    property_set(mdns1name,inet_ntoa(dns_struct1)); 

    property_set(mdns2name,inet_ntoa(dns_struct2)); 

于是我用無線路由器建立了一個最簡單的無線網絡,ssid=tsever,沒有密碼,按照上面步驟進行聯網。結果在add_network的步驟就出錯了,無法獲得網絡id,這是怎麼一回事呢?看到dhcp時的代碼要對特定的network interface做操作,突然想起來之前在代碼中似乎看到有對interface做使能操作的代碼——wifiservice在調用mwifistatetracker.enablenetwork之前,還調用了string ifname = mwifistatetracker.getinterfacename();

networkutils.enableinterface(ifname);這兩句。于是在init階段,加上了下面這段。

    ret = ifc_enable(ifname); 

        loge("failed to enable wifi interface %s. %s", ifname ,strerror(errno)); 

    }

但是運作起來還是出同樣的錯誤,不過這次busybox ifconfig之後可以發現eth0的端口了 ,至少沒有做無用功。倒回去看在wifimonitor的monitorthread中有connecttosupplicant()的調用,本以為此調用不是非常關鍵,然後去hardware/libhardware_legacy/wifi/wifi.c檔案裡面看到全局有一個static struct wpa_ctrl *ctrl_conn;并且在wifi_command中需要用到這個ctrl_conn,那麼如果這個ctrl_conn是null的話,所有的wifi_command指令自然都無法完成了,于是又加上了

ret = wifi_connect_to_supplicant(); 

to connect supplicant daemon. %s",strerror(errno)); 

這樣編譯完運作之後,果然順利的配置設定到id了。運作一下程式,還是出現了錯誤,輸出的log如下。

i/wpa_supplicant(  618): ctrl-event-scan-results  ready

i/wpa_supplicant(  618): trying to associate with 00:24:b2:c1:16:b6 (ssid='tserver' freq=2462 mhz)

i/wpa_supplicant(  618): ctrl-event-state-change id=-1 state=3

e/        (  612): finished init stage.

e/        (  612): add a network 2

e/        (  612): finished config stage.

i/wpa_supplicant(  618): ctrl-event-state-change id=1 state=0

e/        (  612): receive buf:

e/        (  612):  ctrl-event-state-change id=1 state=0

i/wpa_supplicant(  618): ctrl-event-state-change id=-1 state=2

e/        (  612):  ctrl-event-state-change id=-1 state=2

e/        (  612):  ctrl-event-scan-results  ready

i/wpa_supplicant(  618): ctrl-event-state-change id=-1 state=4

e/        (  612):  ctrl-event-state-change id=-1 state=4

i/wpa_supplicant(  618): associated with 00:24:b2:c1:16:b6

e/        (  612):  associated with 00:24:b2:c1:16:b6

d/networklocationprovider(  127): oncelllocationchanged [4517,30785]

i/wpa_supplicant(  618): authentication with 00:24:b2:c1:16:b6 timed out.

e/        (  612):  authentication with 00:24:b2:c1:16:b6 timed out.

注意到紅色的那句log,我的wifi并沒有設定密碼,為什麼還需要authentication呢?想一想是不是wpa_supplicant的限制,于是去網上搜了到了這篇wpa_supplicant

工具使用,發現沒有密碼的情況下應該是這樣的配置

bash-4.1# wificonnect

i/wpa_supplicant(  871): ctrl-event-scan-results  ready

i/wpa_supplicant(  871): trying to associate with 00:24:b2:c1:16:b6 (ssid='tserver' freq=2462 mhz)

i/wpa_supplicant(  871): ctrl-event-state-change id=-1 state=3

e/        (  865): finished init stage.

e/        (  865): add a network 2

e/        (  865): finished config stage.

i/wpa_supplicant(  871): ctrl-event-state-change id=1 state=0

e/        (  865): receive buf:

e/        (  865):  ctrl-event-state-change id=1 state=0

i/wpa_supplicant(  871): ctrl-event-state-change id=-1 state=2

e/        (  865):  ctrl-event-state-change id=-1 state=2

e/        (  865):  ctrl-event-scan-results  ready

e/        (  865):  trying to associate with 00:24:b2:c1:16:b6 (ssid='tserver' freq=2462 mhz)

e/        (  865):  ctrl-event-state-change id=-1 state=3

i/wpa_supplicant(  871): ctrl-event-state-change id=2 state=4

e/        (  865):  ctrl-event-state-change id=2 state=4

i/wpa_supplicant(  871): associated with 00:24:b2:c1:16:b6

i/wpa_supplicant(  871): ctrl-event-state-change id=2 state=7

i/wpa_supplicant(  871): ctrl-event-connected - connection to 00:24:b2:c1:16:b6 completed (auth) [id=2 id_str=]

e/        (  865):  associated with 00:24:b2:c1:16:b6

e/        (  865):  ctrl-event-state-change id=2 state=7

e/        (  865):  ctrl-event-connected - connection to 00:24:b2:c1:16:b6 completed (auth) [id=2 id_str=]

e/        (  865): finished dhcp stage.

    snprintf(cmdstr, sizeof(cmdstr), "set_network %d %s %s",networkid, key_mgmt ,pass); 

        loge("failed to set network %d configuration key_mgmr. %s", networkid, strerror(errno)); 

    snprintf(cmdstr, sizeof(cmdstr), "select_network %d",networkid); 

        loge("failed to select network %d. %s", networkid,strerror(errno)); 

    snprintf(mdns1name, sizeof(mdns1name),"net.%s.dns1",ifname); 

    snprintf(mdns2name, sizeof(mdns2name),"net.%s.dns2",ifname); 

    result = dhcp_do_request(ifname, &ipaddr, &gateway,&mask, &dns1, &dns2, &server, &lease); 

        loge("failed to dhcp on interface %s. %s", ifname,strerror(errno)); 

int main(int argc, char *argv[]) 

    int ret = init_stage(); 

        loge("failed init stage. %s",strerror(errno)); 

        exit(-1); 

    loge("finished init stage."); 

    ret = config_stage(); 

        loge("failed config stage. %s",strerror(errno)); 

    loge("finished config stage."); 

    ret = connect_stage(ret); 

        loge("failed connect stage. %s",strerror(errno)); 

    logv("finished connect stage."); 

    ret = dhcp_stage(); 

        loge("failed dhcp stage. %s",strerror(errno)); 

    loge("finished dhcp stage.");