標籤雲

搜尋此網誌

2016/02/23

Wi-Fi Peer-to-Peer (Wi-Fi P2P)

Android 的 Wi-Fi P2P framework 符合 Wi-Fi Direct™ 認證
它可以讓應用程式在藍牙有效範圍內快速找到附近的裝置並與之互動

Wi-Fi P2P API 包含以下部分:
- WifiP2pManager 包含了讓你發現、要求,及連接至 peer 的 method
(initialize(), connect(), cancelConnect(), requestConnectInfo(), createGroup(), removeGroup(), requestGroupInfo(), discoverPeers(), requestPeers())

- 不同的 Wi-Fi P2P Listener 則讓你獲得 WifiP2pManager method的成功或失敗通知
當我們呼叫 WifiP2pManager 的 method 也需要傳入 Listener
(Wi-Fi P2P Listeners: ActionListener, ChannelListener, ConnectionInfoListener, GroupInfoListener, PeerListListener)

- 還有 Intent,通知我們 Wi-Fi P2P framework 的特定事件,例如連線中斷或發現新的 peer
(Wi-Fi P2P Intents: WIFI_P2P_DISCOVERY_CHANGED_ACTION, WIFI_P2P_PEERS_CHANGED_ACTION, WIFI_P2P_STATE_CHANGED_ACTION, WIFI_P2P_THIS_DEVICE_CHANGED_ACTION)

以上這三個部分會經常同時用到
(例如我們呼叫 discoverPeers() 時需要傳入 WifiP2pManager.ActionListener
才能接收 ActionListener.onSuccess() 和 ActionListener.onFailure() 的通知
而 discoverPeers() 找到有 peer 改變時也會發出 WIFI_P2P_PEERS_CHANGED_ACTION intent)

* Set Up Application Permissions

首先我們必須宣告需要用到的 permission
雖然不會用到網路連線,但是因為 Wi-Fi P2P 有使用到 standard Java sockets
而這需要 INTERNET 的 permission 所以除了 ACCESS_WIFI_STATE, CHANGE_WIFI_STATE 之外我們還需要 INTERNET 這個 permission
<uses-permission
        android:required="true"
        android:name="android.permission.ACCESS_WIFI_STATE"/>
    <uses-permission
        android:required="true"
        android:name="android.permission.CHANGE_WIFI_STATE"/>
    <uses-permission
        android:required="true"
        android:name="android.permission.INTERNET"/>

* Set Up a Broadcast Receiver and Peer-to-Peer Manager

為了接收 Wi-Fi P2P 的 broadcast 我們必須過濾以下 Intent Filter
private final IntentFilter intentFilter = new IntentFilter();
WiFiDirectBroadcastReceiver receiver;
Channel mChannel;
...
@Override
public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.main);

    //  Wi-Fi P2P 狀態已改變(啟用或停用)
    intentFilter.addAction(WifiP2pManager.WIFI_P2P_STATE_CHANGED_ACTION);
    // 可用 peers 列表已改變
    intentFilter.addAction(WifiP2pManager.WIFI_P2P_PEERS_CHANGED_ACTION);
    // Wi-Fi P2P 連接狀態已改變
    intentFilter.addAction(WifiP2pManager.WIFI_P2P_CONNECTION_CHANGED_ACTION);
    // 裝置細節已改變
    intentFilter.addAction(WifiP2pManager.WIFI_P2P_THIS_DEVICE_CHANGED_ACTION);
    ...
    mManager = (WifiP2pManager) getSystemService(Context.WIFI_P2P_SERVICE);
    mChannel = mManager.initialize(this, getMainLooper(), null);
}
/** 註冊 BroadcastReceiver */
@Override
public void onResume() {
    super.onResume();
    receiver = new WiFiDirectBroadcastReceiver(mManager, mChannel, this);
    registerReceiver(receiver, intentFilter);
}

@Override
public void onPause() {
    super.onPause();
    unregisterReceiver(receiver);
}

/**
 * A BroadcastReceiver that notifies of important Wi-Fi p2p events.
 */
public class WiFiDirectBroadcastReceiver extends BroadcastReceiver {

    private WifiP2pManager mManager;
    private Channel mChannel;
    private MyWiFiActivity mActivity;

    public WiFiDirectBroadcastReceiver(WifiP2pManager manager, Channel channel,
            MyWifiActivity activity) {
        super();
        this.mManager = manager;
        this.mChannel = channel;
        this.mActivity = activity;
    }

    @Override
    public void onReceive(Context context, Intent intent) {
        String action = intent.getAction();

        if (WifiP2pManager.WIFI_P2P_STATE_CHANGED_ACTION.equals(action)) {
            // 取得 WifiP2pManager.EXTRA_WIFI_STATE 並傳給 activity
            int state = intent.getIntExtra(WifiP2pManager.EXTRA_WIFI_STATE, -1);
            if (state == WifiP2pManager.WIFI_P2P_STATE_ENABLED) {
                activity.setIsWifiP2pEnabled(true);
            } else {
                activity.setIsWifiP2pEnabled(false);
            }
        } else if (WifiP2pManager.WIFI_P2P_PEERS_CHANGED_ACTION.equals(action)) {
            // peer list 改變了,用 WifiP2pManager 要求可用的 peer。這是非同步的呼叫而且
            // activity 將透過 PeerListListener.onPeersAvailable() 這個 callback 被通知
            if (mManager != null) {
                mManager.requestPeers(mChannel, peerListListener);
            }
            Log.d(WiFiDirectActivity.TAG, "P2P peers changed");
        } else if (WifiP2pManager.WIFI_P2P_CONNECTION_CHANGED_ACTION.equals(action)) {
            // 連線狀態改變了
            if (mManager == null) { return; }
            NetworkInfo networkInfo = (NetworkInfo) intent
                    .getParcelableExtra(WifiP2pManager.EXTRA_NETWORK_INFO);
            if (networkInfo.isConnected()) {
                // 與其他裝置連接了,要求連接資訊以取得 group owner IP
                //呼叫非同步的 requestConnectionInfo() 並傳入 connectionListener 參數
                mManager.requestConnectionInfo(mChannel, connectionListener);
            }
        } else if (WifiP2pManager.WIFI_P2P_THIS_DEVICE_CHANGED_ACTION.equals(action)) {
            DeviceListFragment fragment = (DeviceListFragment) activity.getFragmentManager()
                    .findFragmentById(R.id.frag_list);
            fragment.updateThisDevice((WifiP2pDevice) intent.getParcelableExtra(
                    WifiP2pManager.EXTRA_WIFI_P2P_DEVICE));
        }
    }
}

* Initiate Peer Discovery

mManager.discoverPeers(mChannel, new WifiP2pManager.ActionListener() {
        @Override
        public void onSuccess() {
            // discovery 初始成功。discoverPeers 動作已開始
            // 但如果有發現 service 會收到 broadcast (在 onReceive 裡),所以這裡空白也可以
        }
        @Override
        public void onFailure(int reasonCode) {
            // discovery 初始失敗。錯誤處理
        }
});

* Fetch the List of Peers

實作 WifiP2pManager.PeerListListener 可以接收 peer list
private List peers = new ArrayList();
    ...
    private PeerListListener peerListListener = new PeerListListener() {
        @Override
        public void onPeersAvailable(WifiP2pDeviceList peerList) {
            // 舊的退出或新的加入
            peers.clear();
            peers.addAll(peerList.getDeviceList());

            // 如果我們有用 AdapterView 顯示 peer list,記得要 notifyDataSetChanged()
            ((WiFiPeerListAdapter) getListAdapter()).notifyDataSetChanged();
            if (peers.size() == 0) {
                Log.d(WiFiDirectActivity.TAG, "No devices found");
                return;
            }
        }
    }
如果收到 WIFI_P2P_PEERS_CHANGED_ACTION 的 broadcast
我們可以呼叫 requestPeers() 並傳入 peerListListener (或是在 broadcast 建構時就傳入也是一個方法)
public void onReceive(Context context, Intent intent) {
    ...
    else if (WifiP2pManager.WIFI_P2P_PEERS_CHANGED_ACTION.equals(action)) {
        // 用 WifiP2pManager 要求可用的 peer。這是非同步的呼叫而且
        // activity 將透過 PeerListListener.onPeersAvailable() 這個 callback 被通知
        if (mManager != null) {
            mManager.requestPeers(mChannel, peerListListener);
        }
        Log.d(WiFiDirectActivity.TAG, "P2P peers changed");
    }...
}

* Connect to a Peer

建立一個 WifiP2pConfig 物件連線到 peer
並從 WifiP2pDevice 複製資料進去
然後呼叫 connect()
@Override
    public void connect() {
        // 選取網路上找到的第一個裝置
        WifiP2pDevice device = peers.get(0);

        WifiP2pConfig config = new WifiP2pConfig();
        config.deviceAddress = device.deviceAddress;
        config.wps.setup = WpsInfo.PBC;

        mManager.connect(mChannel, config, new ActionListener() {
            @Override
            public void onSuccess() {
                // WiFiDirectBroadcastReceiver 會通知我們所以這裡可以略過。
            }
            @Override
            public void onFailure(int reason) {
                Toast.makeText(WiFiDirectActivity.this, "Connect failed. Retry.",
                        Toast.LENGTH_SHORT).show();
            }
        });
    }

實作 WifiP2pManager.ActionListener 只能通知我們初始化的成功或失敗
實作 WifiP2pManager.ConnectionInfoListener 的 onConnectionInfoAvailable() callback 則可以通知我們連線狀態的改變
為了讓不同裝置連接到同一個裝置(例如多人遊戲或聊天 app ),其中一個裝置會被指定為 "group owner"
@Override
    public void onConnectionInfoAvailable(final WifiP2pInfo info) {
        // 來自 WifiP2pInfo 結構的 InetAddress
        InetAddress groupOwnerAddress = info.groupOwnerAddress.getHostAddress());
        // group 交涉動作後就可以知道誰是 group owner
        if (info.groupFormed && info.isGroupOwner) {
            // group owner 做該進行的事,例如建立 server thread 並接收傳入連接
        } else if (info.groupFormed) {
            // 其他裝置則成為 client。例如建立 client thread 並與 group owner 連接
        }
    }

現在我們再回到 BroadcastReceiver 的 onReceive
看看 WIFI_P2P_CONNECTION_CHANGED_ACTION 的處理
...
        } else if (WifiP2pManager.WIFI_P2P_CONNECTION_CHANGED_ACTION.equals(action)) {
            // 連線狀態改變了
            if (mManager == null) { return; }
            NetworkInfo networkInfo = (NetworkInfo) intent
                    .getParcelableExtra(WifiP2pManager.EXTRA_NETWORK_INFO);
            if (networkInfo.isConnected()) {
                // 與其他裝置連接了,要求連接資訊以取得 group owner IP
                //呼叫非同步的 requestConnectionInfo() 並傳入 connectionListener 參數
                mManager.requestConnectionInfo(mChannel, connectionListener);
            }
        } ...

* NSD & Wi-Fi P2P in combination

前篇已經看過了 NSD
就來看看如何使用 Wi-Fi P2P 來進行 NSD
達到就算沒有連接到網路或熱點也可以直接探索附近裝置的功能

manifest 的部分一樣需要宣告 ACCESS_WIFI_STATE, CHANGE_WIFI_STATE 及 INTERNET 這三個 permission

- Add a Local Service

我們需要為 service discovery 註冊我們提供的 local service
這樣 framework 才會在 service discovery 自動回應
而建立一個 local service 需要三個步驟:
1. 建立 WifiP2pServiceInfo object
2. 將 local service 的資訊填到 WifiP2pServiceInfo 裡
3. 呼叫 addLocalService() 為 service discovery 註冊 local service
private void startRegistration() {
        //  用一個 string map 存放我們的 service 訊息
        Map record = new HashMap();
        record.put("listenport", String.valueOf(SERVER_PORT));
        record.put("buddyname", "John Doe" + (int) (Math.random() * 1000));
        record.put("available", "visible");

        // Service information. 傳入 service name, service type, 及其他裝置連接時要提供的訊息
        WifiP2pDnsSdServiceInfo serviceInfo =
                WifiP2pDnsSdServiceInfo.newInstance("_test", "_presence._tcp", record);

        // 呼叫 addLocalService, 傳送 service info, network channel, 及 ActionListener
        mManager.addLocalService(channel, serviceInfo, new ActionListener() {
            @Override
            public void onSuccess() {
                // 加入成功!除非你想更新 UI 或紀錄狀態否則不用做其他事
            }
            @Override
            public void onFailure(int arg0) {
                // 加入失敗. 錯誤碼可能是 P2P_UNSUPPORTED, ERROR, or BUSY
            }
        });
    }

- Discover Nearby Services

android 使用 callback 機制通知應用程式有哪些可用的 service
使用 WifiP2pManager.DnsSdTxtRecordListener 查看紀錄
這紀錄可以被其他裝置 broadcast
當一個連線進來,複製裝置位置與其他任何有關的資訊到這個 method 以便稍後存取
final HashMap<String, String> buddies = new HashMap<String, String>();
...
private void discoverService() {
    DnsSdTxtRecordListener txtListener = new DnsSdTxtRecordListener() {
        @Override
        /* Callback 包含:
         * fullDomain: full domain name: 例如 "printer._ipp._tcp.local."
         * record: key/value 配對的文字紀錄資料
         * device: 執行 service 的裝置
         */
        public void onDnsSdTxtRecordAvailable(String fullDomain, Map record, WifiP2pDevice device) {
                Log.d(TAG, "DnsSdTxtRecord available -" + record.toString());
                //從自訂欄位 "buddyname" 取出資訊
                buddies.put(device.deviceAddress, record.get("buddyname"));
            }
        };
    ...
}
創建一個 WifiP2pManager.DnsSdServiceResponseListener 來接收 service 的相關訊息
前面的 code 例子用了一個 Map 物件把 buddyname 存入

一旦 DnsSdTxtRecordListener 與 DnsSdServiceResponseListener 這兩個 Listener 都實作了
用 setDnsSdResponseListeners() 把它們加到 WifiP2pManager 中
private void discoverService() {
...
    DnsSdServiceResponseListener servListener = new DnsSdServiceResponseListener() {
        @Override
        public void onDnsSdServiceAvailable(String instanceName, String registrationType,
                WifiP2pDevice resourceType) {

                // 從 DnsTxtRecord 更新裝置名稱為讓人看得懂的名稱
                resourceType.deviceName = buddies
                        .containsKey(resourceType.deviceAddress) ? buddies
                        .get(resourceType.deviceAddress) : resourceType.deviceName;

                // 把 WifiP2pDevice 加入自訂的 adapter 用來顯示 wifi 裝置
                WiFiDirectServicesList fragment = (WiFiDirectServicesList) getFragmentManager()
                        .findFragmentById(R.id.frag_peerlist);
                WiFiDevicesAdapter adapter = ((WiFiDevicesAdapter) fragment
                        .getListAdapter());

                adapter.add(resourceType);
                adapter.notifyDataSetChanged();
                Log.d(TAG, "onBonjourServiceAvailable " + instanceName);
        }
    };
    mManager.setDnsSdResponseListeners(channel, servListener, txtListener);
    ...
}

現在建立一個 serviceRequest 並且呼叫 addServiceRequest()
serviceRequest = WifiP2pDnsSdServiceRequest.newInstance();
        mManager.addServiceRequest(channel,
                serviceRequest,
                new ActionListener() {
                    @Override
                    public void onSuccess() {
                        // 成功!
                    }
                    @Override
                    public void onFailure(int code) {
                        // 失敗。確認錯誤碼並進行相關處理
                        // P2P_UNSUPPORTED - Wi-Fi P2P 不支援在此裝置上執行該應用程式
                        // ERROR - 因為內部錯誤造成操作失敗
                        // BUSY - 系統忙碌無法處理請求
                    }
                });
最後呼叫 discoverServices()
mManager.discoverServices(channel, new ActionListener() {
            @Override
            public void onSuccess() {
                // 成功!
            }
            @Override
            public void onFailure(int code) {
                // 失敗。確認錯誤碼並進行相關處理
                // P2P_UNSUPPORTED - Wi-Fi P2P 不支援在此裝置上執行該應用程式
                // ERROR - 因為內部錯誤造成操作失敗
                // BUSY - 系統忙碌無法處理請求
                if (code == WifiP2pManager.P2P_UNSUPPORTED) {
                    Log.d(TAG, "P2P isn't supported on this device.");
                else if(...)
                    ...
            }
        });

相關資料:
Connecting Devices Wirelessly
Wi-Fi P2P
Creating P2P Connections with Wi-Fi
Using Wi-Fi P2P for Service Discovery

沒有留言: