1. 程式人生 > >Android連線指定Wifi的方法

Android連線指定Wifi的方法

本篇部落格主要記錄一下Android中開啟Wifi、獲取Wifi接入點資訊及連線指接入點的方法。

自己寫的demo主要用於測試介面的基本功能,因此介面及底層邏輯比較粗糙。

demo的整體介面如下所示:

上圖中的OPEN按鍵負責開啟Wifi;
GET按鍵負責獲取掃描到的接入點資訊。

當獲取到接入點資訊後,我選取了其中的名稱及訊號強度,以列表的形式顯示在主介面下方,如下圖:

當點選列表中的Item時,就會去連線對應的接入點。
自己的邏輯比較簡單,測試時的程式碼,假定連線的是不許要密碼或密碼已知的接入點。

demo的佈局檔案就不介紹了,就是Button和RecyclerView。
主要記錄一下,使用到的核心程式碼。

        ....................
        //Open按鍵點選後的邏輯
        mOpenWifiButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                //WifiManager的isWifiEnabled介面,用於判斷Wifi開關是否已經開啟
                if (!mWifiManager.isWifiEnabled()) {
                    //setWifiEnabled介面用於開啟Wifi
mWifiManager.setWifiEnabled(true); mMainHandler.post(mMainRunnable); } } }); ....................

mMainRunnable的程式碼如下,主要用於判斷Wifi是否開啟成功。

    ................
    private Runnable mMainRunnable = new Runnable() {
        @Override
        public
void run() { if (mWifiManager.isWifiEnabled()) { //開啟成功後,使能Get按鍵 mGetWifiInfoButton.setEnabled(true); } else { mMainHandler.postDelayed(mMainRunnable, 1000); } } }; ...............

這部分程式碼,主要使用了WifiManager的公有介面,開啟Wifi開關及判斷開啟狀態。
這部分操作需要的許可權是:

    <uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
    <uses-permission android:name="android.permission.CHANGE_WIFI_STATE"/>

Get按鍵被點選後,對應的程式碼如下:

        .................
        mGetWifiInfoButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                if (mWifiManager.isWifiEnabled()) {
                    //getScanResults介面將返回List<ScanResult>
                    //ScanResult中保留了每個接入點的基本資訊
                    mScanResultList = mWifiManager.getScanResults();

                    //多個接入點可能攜帶相同的資訊,形成一個整體的Wifi覆蓋網路
                    //因此,篩除一些冗餘資訊
                    sortList(mScanResultList);

                    //我使用的是RecyclerView,得到資料後,重新整理介面進行顯示
                    mWifiInfoRecyclerView.getAdapter().notifyDataSetChanged();
                }
            }
        });
        .................

上面這部分程式碼也比較簡單,主要利用WifiManager的getScanResults介面,獲取終端探索到的接入點資訊。
其中,sortList的程式碼如下:

    ..............
    private void sortList(List<ScanResult> list) {
        TreeMap<String, ScanResult> map = new TreeMap<>();
        //demo中僅按照SSID進行篩選
        //實際使用時,還可以參考訊號強度等條件
        for (ScanResult scanResult : list) {
            map.put(scanResult.SSID, scanResult);
        }
        list.clear();
        list.addAll(map.values());
    }
    .............

這部分程式碼唯一需要注意的地方是,需要申明許可權:

    <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>

同時,在高版本中還需要主動獲取執行時許可權。

許可權的要求,是由WifiServiceImpl的實現決定的,我們以Android 7.0為例,看看對應的程式碼:

public List<ScanResult> getScanResults(String callingPackage) {
    //這裡要求的是ACCESS_WIFI_STATE
    enforceAccessPermission();
    ............
    try {
        ...........
        if (!canReadPeerMacAddresses && !isActiveNetworkScorer
                //在checkCallerCanAccessScanResults中檢查了ACCESS_FINE_LOCATION和ACCESS_COARSE_LOCATION
                //如果沒有這兩個許可權,就會返回一個empty List
                && !checkCallerCanAccessScanResults(callingPackage, uid)) {
            return new ArrayList<ScanResult>();
        }
        ...........
    } fianlly {
        ..........
    }
}

獲取到資訊後,就可以顯示和點選列表中的Item了。
由於自己使用的是RecyclerView,因此這部分工作全部交給了對應ViewHolder:

    ...............
    private class ScanResultViewHolder extends RecyclerView.ViewHolder {
        private View mView;
        private TextView mWifiName;
        private TextView mWifiLevel;

        ScanResultViewHolder(View itemView) {
            super(itemView);

            mView = itemView;
            mWifiName = (TextView) itemView.findViewById(R.id.ssid);
            mWifiLevel = (TextView) itemView.findViewById(R.id.level);
        }

        void bindScanResult(final ScanResult scanResult) {
            //將接入點的名稱和強度顯示到介面上
            mWifiName.setText(
                    getString(R.string.scan_wifi_name, "" + scanResult.SSID));
            mWifiLevel.setText(
                    getString(R.string.scan_wifi_level, "" + scanResult.level));

            //點選Item後,就連線對應的接入點
            mView.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    //createWifiConfig主要用於構建一個WifiConfiguration,程式碼中的例子主要用於連線不需要密碼的Wifi
                    //WifiManager的addNetwork介面,傳入WifiConfiguration後,得到對應的NetworkId
                    int netId = mWifiManager.addNetwork(createWifiConfig(scanResult.SSID, "", WIFICIPHER_NOPASS));

                    //WifiManager的enableNetwork介面,就可以連線到netId對應的wifi了
                    //其中boolean引數,主要用於指定是否需要斷開其它Wifi網路
                    boolean enable = mWifiManager.enableNetwork(netId, true);
                    Log.d("ZJTest", "enable: " + enable);

                    //可選操作,讓Wifi重新連線最近使用過的接入點
                    //如果上文的enableNetwork成功,那麼reconnect同樣連線netId對應的網路
                    //若失敗,則連線之前成功過的網路
                    boolean reconnect = mWifiManager.reconnect();
                    Log.d("ZJTest", "reconnect: " + reconnect);
                }
            });
        }
    }
    .................

以上就是連線指定Wifi的基本套路,從程式碼中容易看出,關鍵問題是如何創建出有效的WifiConfiguration。
自己測試時,初始建立WifiConfiguration失敗,手機怎麼都沒法連線到熱點上,後來修改後,基本功能終於能夠實現:

    ....................
    private static final int WIFICIPHER_NOPASS = 0;
    private static final int WIFICIPHER_WEP = 1;
    private static final int WIFICIPHER_WPA = 2;

    private WifiConfiguration createWifiConfig(String ssid, String password, int type) {
        //初始化WifiConfiguration
        WifiConfiguration config = new WifiConfiguration();
        config.allowedAuthAlgorithms.clear();
        config.allowedGroupCiphers.clear();
        config.allowedKeyManagement.clear();
        config.allowedPairwiseCiphers.clear();
        config.allowedProtocols.clear();

        //指定對應的SSID
        config.SSID = "\"" + ssid + "\"";

        //如果之前有類似的配置
        WifiConfiguration tempConfig = isExist(ssid);
        if(tempConfig != null) {
            //則清除舊有配置
            mWifiManager.removeNetwork(tempConfig.networkId);
        }

        //不需要密碼的場景
        if(type == WIFICIPHER_NOPASS) {
            config.allowedKeyManagement.set(WifiConfiguration.KeyMgmt.NONE);
        //以WEP加密的場景
        } else if(type == WIFICIPHER_WEP) {
            config.hiddenSSID = true;
            config.wepKeys[0]= "\""+password+"\"";
            config.allowedAuthAlgorithms.set(WifiConfiguration.AuthAlgorithm.OPEN);
            config.allowedAuthAlgorithms.set(WifiConfiguration.AuthAlgorithm.SHARED);
            config.allowedKeyManagement.set(WifiConfiguration.KeyMgmt.NONE);
            config.wepTxKeyIndex = 0;
        //以WPA加密的場景,自己測試時,發現熱點以WPA2建立時,同樣可以用這種配置連線
        } else if(type == WIFICIPHER_WPA) {
            config.preSharedKey = "\""+password+"\"";
            config.hiddenSSID = true;
            config.allowedAuthAlgorithms.set(WifiConfiguration.AuthAlgorithm.OPEN);
            config.allowedGroupCiphers.set(WifiConfiguration.GroupCipher.TKIP);
            config.allowedKeyManagement.set(WifiConfiguration.KeyMgmt.WPA_PSK);
            config.allowedPairwiseCiphers.set(WifiConfiguration.PairwiseCipher.TKIP);
            config.allowedGroupCiphers.set(WifiConfiguration.GroupCipher.CCMP);
            config.allowedPairwiseCiphers.set(WifiConfiguration.PairwiseCipher.CCMP);
            config.status = WifiConfiguration.Status.ENABLED;
        }

        return config;
    }
    .................
    private WifiConfiguration isExist(String ssid) {
        List<WifiConfiguration> configs = mWifiManager.getConfiguredNetworks();

        for (WifiConfiguration config : configs) {
            if (config.SSID.equals("\""+ssid+"\"")) {
                return config;
            }
        }
        return null;
    }
    .................

自己寫完demo後,以一個手機建立熱點,分別測試了有密碼和無密碼的場景(對應的,需要修改createWifiConfig的傳入引數)。
發現demo執行的手機在兩種場景下,均能夠連線到指定熱點。

在本文的最後,補充一下終端作為熱點時的介面。

public boolean isWifiApEnabled()

具有@SystemApi、@hide註解的公有介面,判斷手機的熱點是否開啟。

在Android 5.1之前,這個介面沒有@SystemApi註解,
於是有很多程式碼會利用Java發射機制,獲取該方法並判斷手機熱點是否開啟。
現在那些老程式碼已經沒法使用了。

現在的做法(以5.1以上為例),應該利用廣播接收器監聽WifiManager中定義的WIFI_AP_STATE_CHANGED_ACTION。
注意到該Action也有@SystemApi註解,所以要直接監聽對應的字串,示例如下(上面連結中的demo也有涉及):

    ...................
    private BroadcastReceiver mBroadcastReceiver;
    private void registerBroadcastReceiver() {
        mBroadcastReceiver = new BroadcastReceiver() {
            @Override
            public void onReceive(Context context, Intent intent) {
                //收到廣播後,利用"wifi_state"的欄位,得到AP的狀態
                int state = intent.getIntExtra("wifi_state", 11);
                Log.d("ZJTest", "AP state: " + state);
            }
        };

        IntentFilter intentFilter = new IntentFilter();
        //新增Action對應的字元資訊
        intentFilter.addAction("android.net.wifi.WIFI_AP_STATE_CHANGED");
        this.registerReceiver(mBroadcastReceiver, intentFilter);
    }
    .........
    private void unregisterBroadcastReceiver() {
        this.unregisterReceiver(mBroadcastReceiver);
    }
    ..........

我暫時沒有深究Wifi模組開啟AP的流程。
不過從自己的測試結果來看,Wifi開啟或關閉AP時,推測傳送的應該是Sticky型別的廣播。
於是,只要APK註冊了廣播監聽器,立馬就會得到回覆,明白當前AP的狀態。

例如,我在開啟AP後,再開啟自己的測試Demo,立馬會收到如下資訊:

//對應WIFI_AP_STATE_ENABLED,定義於WifiManager中,@SystemApi
02-20 17:48:52.470 12773-12773/? D/ZJTest: AP state: 13

手動關閉AP後可以得到如下結果:

//WIFI_AP_STATE_DISABLING
02-20 17:49:35.803 12773-12773/stark.a.is.zhang.wifitest D/ZJTest: AP state: 10

//WIFI_AP_STATE_DISABLED
02-20 17:49:36.960 12773-12773/stark.a.is.zhang.wifitest D/ZJTest: AP state: 11

public boolean setWifiApConfiguration(WifiConfiguration wifiConfig)

public WifiConfiguration getWifiApConfiguration()

@SystemApi,設定和獲取Wifi-AP的配置資訊。

可以看出不論手機作為AP還是STA,在Framework中均利用WifiConfiguration抽象對應的配置資訊,包括鑑權演算法、密碼、SSID、協議等。
這種設計是符合802.11協議精神的,畢竟在物理裝置的角度上,AP和STA是完全對等的。只不過在實際情況中,根據各自的需求,特質化了一些元件。

實際上從底層協議來看,僅在傳輸這個角度上,AP和STA的主要區別僅在於收到資料幀後的處理流程不同。AP收到資料幀後,發現目的地址不是自己,就會進入轉發流程;而STA可能就直接丟棄該資料幀了。當然如果從控制的角度來看,即考慮通訊信令,AP和STA還是主從的關係。

public boolean setWifiApEnabled(WifiConfiguration wifiConfig, boolean enabled)

@SystemApi,改變Wifi-AP的開關狀態。開啟的AP,將使用引數定義的WifiConfiguration資訊。

可以看出,手機熱點對應介面全部變成了SystemApi,因此在android的高版本上,應用基本上是無法再操作熱點了。