X

WiFi DirectでP2P通信を行う(1)

WiFi DirectとはWiFi対応機器同士を直接接続するP2P通信方式です。
P2P通信の名前の通り、アクセスポイントを必要としないことから、手軽に無線LANを使って通信できるAndorid 4.0の注目の新機能です。
(無線LANでは以前から1対1通信を行うアドホックモードが存在していましたが、WiFi Directではさらに発展させた1対複数通信が可能です)

Android SDK 4.0にはWiFi DirectのSampleが付属しています。

android-sdk-windows\samples\android-14\WiFiDirectDemo

WiFiDirectDemoは複雑ですが、WiFi Directを理解するにはとてもよい題材です。今回はこのSampleを例にWiFi Directについて解説します。
WiFiDirectDemo http://developer.android.com/resources/samples/WiFiDirectDemo/index.html

注意:Android SDKのAPI ReferenceではWiFi Directを”Wi-Fi p2p”、WiFi Direct対応デバイスを”Peers”(ピア)とP2Pで用いる名称で表記しています。記事中でも気をつけて触れていますが随時読み替えてください。
(まだ実機での動作確認ができていませんので記事の記載に誤りがあった場合、随時アップデートします!)

WiFi Direct機能はandroid.net.wifi.p2pパッケージとして新たに追加されました。
主なポイントは以下の通りです

WiFi Directを使うためには

クラス等説明
WiFi Direct用のBroadcastReceiverWiFi Directの状態変化はブロードキャストされるため、BroadcastReceiverで受け取ること
WifiP2pManagerWiFi Direct対応デバイス(P2Pデバイス)の情報、接続状態を管理するクラス
WifiP2pInfo接続状態が格納されている
WifiP2pDeviceデバイス情報が格納されている、接続先情報など。

今回は、ブロードキャストレシーバとWiFi Directに関わるintentのハンドリング方法について紹介します。
WiFi Directの接続方法や解除、キャンセルの仕方を(2)にて取り扱います。

では、さっそくブロードキャストレシーバの登録方法から確認してみましょう。
非常に長い記事になってしまっていますので、各センテンスの最後に大事なポイントをまとめています。
Reference:
http://developer.android.com/reference/android/net/wifi/p2p/WifiP2pManager.html
http://developer.android.com/reference/android/net/wifi/p2p/WifiP2pInfo.html
http://developer.android.com/reference/android/net/wifi/p2p/WifiP2pDevice.html

ブロードキャストレシーバの登録

WiFi Direct機能に関わる情報は、intentというかたちで通知されます。WiFi Directの有効/無効状態が切り替わるなど、状態変化に応じたintentがブロードキャストされます。
WiFi Directを使うためには、ブロードキャストされたintentの情報が必須になるため、WifiP2pManagerを取得してBroadcastReceiverを登録します。
レシーバ(以下のサンプルではWiFiDirectBroadcastReceiverです)は接続状態に応じてデバイス一覧の更新、デバイス情報の取得、通信状態の確認など、通信制御を行うために利用します。

WiFiDirectActivity.java

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);

        // add necessary intent values to be matched.

        intentFilter.addAction(WifiP2pManager.WIFI_P2P_STATE_CHANGED_ACTION);
        intentFilter.addAction(WifiP2pManager.WIFI_P2P_PEERS_CHANGED_ACTION);
        intentFilter.addAction(WifiP2pManager.WIFI_P2P_CONNECTION_CHANGED_ACTION);
        intentFilter.addAction(WifiP2pManager.WIFI_P2P_THIS_DEVICE_CHANGED_ACTION);

        manager = (WifiP2pManager) getSystemService(Context.WIFI_P2P_SERVICE);
        channel = manager.initialize(this, getMainLooper(), null);
    }

    /** register the BroadcastReceiver with the intent values to be matched */
    @Override
    public void onResume() {
        super.onResume();
        receiver = new WiFiDirectBroadcastReceiver(manager, channel, this);
        registerReceiver(receiver, intentFilter);
    }

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

8-11行目:intentFilterを作成し、4つのActionを監視します。これらイベント(Action)が発生したときはintentが通知されます。
13行目:WifiP2pManagerを取得し、initializeメソッドで初期化します。WiFi機能を使うContext(ソースコードでは第1引数のthis)とスレッド(getMainLooper()、UIスレッド)を引数に、channelを取得します。返り値のchannelは、P2P機能を利用するのに必要なインスタンスです。ここではinitializeメソッドの第3引数はnullですが、リスナーを登録でき、必要に応じてWiFi Direct接続が切れた場合(channelが失われた時)の通知を受け取れます。

21行目:WiFiDirectBroadcastReceiverを作成して、イベントを受け取っています(WiFiDirectBroadcastReceiverについては後述)。
22行目:onResumeメソッド内でブロードキャストレシーバーを登録、28行目onPauseメソッド内で解除しています。

大事なポイント

ポイントは初期化処理とブロードキャストレシーバで受け取る4つのActionと初期化処理です。初期化時に受け取るchannelはWiFi Direct制御で重要なクラスです。

  • public WifiP2pManager.Channel initialize (Context srcContext, Looper srcLooper, WifiP2pManager.ChannelListener listener)

WifiP2pManagerで定義されているアクション

アクション名説明
WIFI_P2P_STATE_CHANGED_ACTIONWiFi Directの有効/無効状態
WIFI_P2P_PEERS_CHANGED_ACTIONデバイス情報の変更通知(通信可能なデバイスの発見・ロストなど)
WIFI_P2P_CONNECTION_CHANGED_ACTIONIPアドレスなどコネクション情報。通信状態の変更通知
WIFI_P2P_THIS_DEVICE_CHANGED_ACTION自分自身のデバイス状態の変更通知(相手デバイスではないことに注意)

ブロードキャストされた情報を受け取る

先ほどの4つのActionについて、レシーバー内部処理を記載していきます

  1. WIFI_P2P_STATE_CHANGED_ACTION
  2. WIFI_P2P_PEERS_CHANGED_ACTION
  3. WIFI_P2P_CONNECTION_CHANGED_ACTION
  4. WIFI_P2P_THIS_DEVICE_CHANGED_ACTION

デバイス一覧の更新、デバイス情報の取得、通信状態の変更通知など、Actionに応じて必要な処理が異なるため、
1つずつソースコードを確認してみましょう。

1.WIFI_P2P_STATE_CHANGED_ACTION:WiFi Directの有効/無効

WIFI_P2P_STATE_CHANGED_ACTIONはWiFi Directの有効/無効に応じて通知されます。本体デバイスの機能そのものの利用可能・不可を表すため、機能制限やWiFi Direct有効化を促すのに利用します。

WiFiDirectBroadcastReceiver.java

   public void onReceive(Context context, Intent intent) {
        String action = intent.getAction();
        if (WifiP2pManager.WIFI_P2P_STATE_CHANGED_ACTION.equals(action)) {

            // UI update to indicate wifi p2p status.
            int state = intent.getIntExtra(WifiP2pManager.EXTRA_WIFI_STATE, -1);
            if (state == WifiP2pManager.WIFI_P2P_STATE_ENABLED) {
                // Wifi Direct mode is enabled
                activity.setIsWifiP2pEnabled(true);
            } else {
                activity.setIsWifiP2pEnabled(false);
                activity.resetData();

            }
            Log.d(WiFiDirectActivity.TAG, "P2P state changed - " + state);
        }
   ...(省略)...

Sampleでは、9/11行目で有効/無効情報をactivity(WiFiDirectActivity.java)に通知しています。
もしアプリを起動している本体でWifi Directが無効であれば設定アプリ(Setting)で有効に促す必要があります。
もしくはDirect機能を使わないようにアプリ側の設定を変更する、などが最もわかりやすい処理例でしょう。
Reference:
http://developer.android.com/reference/android/net/wifi/p2p/WifiP2pManager.html

2.WIFI_P2P_PEERS_CHANGED_ACTION:デバイス一覧を取得

WiFi Direct通信が出来るデバイス(たびたびPeersと呼ばれます)に変更があったときに呼ばれます。相手側デバイスの検出に利用します。
利用シーンはデバイスが接続可能範囲から外れたときや、新しく見つけたときです。
デバイス一覧を更新したり、通信相手を切り替えるトリガとして利用します。

WiFiDirectBroadcastReceiver.java

    public void onReceive(Context context, Intent intent) {
        String action = intent.getAction();
   ...(省略)...
        } else if (WifiP2pManager.WIFI_P2P_PEERS_CHANGED_ACTION.equals(action)) {

            // request available peers from the wifi p2p manager. This is an
            // asynchronous call and the calling activity is notified with a
            // callback on PeerListListener.onPeersAvailable()
            if (manager != null) {
                manager.requestPeers(channel, (PeerListListener) activity.getFragmentManager()
                        .findFragmentById(R.id.frag_list));
            }
            Log.d(WiFiDirectActivity.TAG, "P2P peers changed");
        }
   ...(省略)...

WifiP2pManagerクラスのrequestPeersメソッドの書式はrequestPeers(WifiP2pManager.Channel, WifiP2pManager.PeerListListener) です。
ここでは、PeerListListenerにデバイス(Peers)情報を通知するようリクエストしています。

サンプルではアプリでデバイス一覧を表示しているので以下のような処理が続きます。

デバイス一覧の更新

WifiP2pManagerクラスのrequestPeersメソッドにWifiP2pManager.PeerListListenerを登録 することで通信可能な相手デバイス一覧を得ることができます。
PeerListListenerインターフェイスでは、onPeersAvailableメソッド で有効なデバイス情報(リスト)を取得できます。

以下で紹介するサンプルアプリケーションでは、デバイスの変更通知を受けてデバイス一覧リストを更新し、常に最新の情報を表示するように作られています。
DeviceListFragment.java

public class DeviceListFragment extends ListFragment implements PeerListListener {

    private List<WifiP2pDevice> peers = new ArrayList<WifiP2pDevice>();
    ...省略...

    public void onPeersAvailable(WifiP2pDeviceList peerList) {
        if (progressDialog != null && progressDialog.isShowing()) {
            progressDialog.dismiss();
        }
        peers.clear();
        peers.addAll(peerList.getDeviceList());
        ((WiFiPeerListAdapter) getListAdapter()).notifyDataSetChanged();
        if (peers.size() == 0) {
            Log.d(WiFiDirectActivity.TAG, "No devices found");
            return;
        }
    }

6行目:onPeersAvailableメソッドで受け取ったWifiP2pDeviceListを private List peersとして保存します。
12行目:ListAdapterに対してデータ更新を通知することでListViewを適切に更新していることがわかります。

大事なポイント

ポイントはWifiP2pManagerクラスのrequestPeersメソッドの動きです。Channelとリスナーを登録し、
デバイス一覧はリスナーのonPeersAvailableメソッドで受け取ります

  • public WifiP2pManager.requestPeers(WifiP2pManager.Channel, WifiP2pManager.PeerListListener)

Reference:
http://developer.android.com/reference/android/net/wifi/p2p/WifiP2pManager.html

3.WIFI_P2P_CONNECTION_CHANGED_ACTION:コネクション情報

WIFI_P2P_CONNECTION_CHANGED_ACTIONはWiFi Direct通信状態の変更通知です。
相手デバイスとのコネクションが繋がれば接続処理を、切れた場合は終了処理を行います。サンプルでは、接続した場合にIPアドレスをTextViewに表示しています。

WiFiDirectBroadcastReceiver.java

    public void onReceive(Context context, Intent intent) {
        String action = intent.getAction();
   ...(省略)...
        } else if (WifiP2pManager.WIFI_P2P_CONNECTION_CHANGED_ACTION.equals(action)) {

            if (manager == null) {
                return;
            }

            NetworkInfo networkInfo = (NetworkInfo) intent
                    .getParcelableExtra(WifiP2pManager.EXTRA_NETWORK_INFO);

            if (networkInfo.isConnected()) {

                // we are connected with the other device, request connection
                // info to find group owner IP

                DeviceDetailFragment fragment = (DeviceDetailFragment) activity
                        .getFragmentManager().findFragmentById(R.id.frag_detail);
                manager.requestConnectionInfo(channel, fragment);
            } else {
                // It's a disconnect
                activity.resetData();
            }
        }
   ...(省略)...

WIFI_P2P_CONNECTION_CHANGED_ACTIONが通知されるとき、intentにはNetworkInfoが含まれます。getParcelableExtraメソッドを使ってネットワーク情報を取り出します。
10行目:NetworkInfoクラスのisConnectedメソッドで接続状態が確認できます。
20行目:IPアドレス情報を表示するため、WifiP2pManagerクラスのrequestConnectionInfoメソッドを使い、フラグメントに情報通知しています。

  • public WifiP2pManager.requestConnectionInfo(Channel c, ConnectionInfoListener listener)

接続情報の表示

フラグメント側の処理を見てみましょう。WifiP2pManagerクラスのrequestConnectionInfoメソッドで登録したConnectionInfoListenerは、onConnectionInfoAvailableメソッドを実装する必要があります。
このメソッドは接続情報WifiP2pInfoクラスを引数に与えられ、WifiP2pInfoクラス内にIP情報やP2P接続のオーナー(親側、WifiP2pInfoクラスのisGroupOwnerメンバ)などを含んでいます。
onConnectionInfoAvailableメソッドは、Androidフレームワーク側で、接続情報が準備出来た段階で呼び出されます。

以下のサンプルは、WifiP2pInfoクラスからIPアドレスを取得、表示しています。
DeviceDetailFragment.java

public class DeviceDetailFragment extends Fragment implements ConnectionInfoListener {
   ...(省略)...
    public void onConnectionInfoAvailable(final WifiP2pInfo info) {
        if (progressDialog != null && progressDialog.isShowing()) {
            progressDialog.dismiss();
        }
        this.info = info;
        this.getView().setVisibility(View.VISIBLE);
        ...(省略)...

        // InetAddress from WifiP2pInfo struct.
        view = (TextView) mContentView.findViewById(R.id.device_info);
        view.setText("Group Owner IP - " + info.groupOwnerAddress.getHostAddress());

        ...(省略)...
        // hide the connect button
        mContentView.findViewById(R.id.btn_connect).setVisibility(View.GONE);
    }

13行目、ホストアドレスをTextViewに表示しています。
(WiFiDirectDemoアプリでは、onConnectionInfoAvailableメソッドで接続完了を検出し、ファイル受信動作に入りますが説明を省略しています)

大事なポイント

WifiP2pManagerクラスのrequestConnectionInfoメソッドによる接続情報の取得です。WifiP2pManagerのrequestXXXメソッドはリスナー登録が必要なものばかりなので定型文として覚えておくと後々便利です。

  • public WifiP2pManager.requestConnectionInfo(Channel c, ConnectionInfoListener listener)
  • ConnectionInfoListener.onConnectionInfoAvailable(final WifiP2pInfo info)

Reference:
http://developer.android.com/reference/android/net/wifi/p2p/WifiP2pManager.html
http://developer.android.com/reference/android/net/wifi/p2p/WifiP2pInfo.html

4.WIFI_P2P_THIS_DEVICE_CHANGED_ACTION:デバイス状態

WIFI_P2P_THIS_DEVICE_CHANGED_ACTIONは(相手ではなく)自分自身のデバイス状態の変更通知です。
ユーザー操作、設定アプリ(エアプレーンモード等)により、状態が変わってた際に通知されます。サンプルではデバイス表示を最新に更新しています。

WiFiDirectBroadcastReceiver.java

    public void onReceive(Context context, Intent intent) {
        String action = intent.getAction();
   ...(省略)...
        } 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));

        }
    }

7行目:WIFI_P2P_THIS_DEVICE_CHANGED_ACTIONが通知されるとき、intentにはWifiP2pDeviceが含まれます。getParcelableExtraメソッドを使ってデバイス情報を取り出します。
サンプルではDeviceListFragmentにデバイス情報を渡して、表示を更新しています。以下にフラグメント(DeviceListFragment)のコードを記載しました。

本体WiFi Direct状態の更新

WIFI_P2P_THIS_DEVICE_CHANGED_ACTION通知を受けて、サンプルではDeviceListFragmentクラスのupdateThisDeviceメソッドを呼んでいます。
このメソッドでは、WifiP2pDeviceをつかってデバイス情報を最新に更新しています。

DeviceListFragment.java

public class DeviceListFragment extends ListFragment implements PeerListListener {
   ...(省略)...

    public void updateThisDevice(WifiP2pDevice device) {
        this.device = device;
        TextView view = (TextView) mContentView.findViewById(R.id.my_name);
        view.setText(device.deviceName);
        view = (TextView) mContentView.findViewById(R.id.my_status);
        view.setText(getDeviceStatus(device.status));
    }
   ...(省略)...
    private static String getDeviceStatus(int deviceStatus) {
        Log.d(WiFiDirectActivity.TAG, "Peer status :" + deviceStatus);
        switch (deviceStatus) {
            case WifiP2pDevice.AVAILABLE:
                return "Available";
            case WifiP2pDevice.INVITED:
                return "Invited";
            case WifiP2pDevice.CONNECTED:
                return "Connected";
            case WifiP2pDevice.FAILED:
                return "Failed";
            case WifiP2pDevice.UNAVAILABLE:
                return "Unavailable";
            default:
                return "Unknown";

        }
    }

9行目:WifiP2pDeviceクラスdeviceインスタンスを保存し、statusをTextViewに反映しています。
12行目:getDeviceStatusメソッド内部でstatusを判断しています。statusにはAVAILABLE、INVITED、CONNECTED、FAILED、UNAVAILABLEがあります。

Reference:
http://developer.android.com/reference/android/net/wifi/p2p/WifiP2pManager.html
http://developer.android.com/reference/android/net/wifi/p2p/WifiP2pDevice.html

おつかれさまでした!
今回は、WiFiDirectDemoを題材にブロードキャストレシーバーの作成とそれらの処理例を紹介しました。
複雑な処理体系になっていますが、個別でみると理解しやすく作られています。処理内容を順番に追ってみてください。
最後のおさらいでActionを確認してみましょう。

WifiP2pManagerで定義されているアクションと対応する処理

アクション名処理
WIFI_P2P_STATE_CHANGED_ACTIONWiFi Directの有効/無効状態が通知される。機能制限やユーザへの通知に利用
WIFI_P2P_PEERS_CHANGED_ACTIONWifiP2pManagerクラスのrequestPeersメソッドをつかってデバイス一覧WifiP2pDeviceListを取得。個々のデバイス情報はWifiP2pDevice
WIFI_P2P_CONNECTION_CHANGED_ACTIONWifiP2pManagerクラスのrequestConnectionInfoメソッドを使って接続状態WiFiP2pInfoを取得する
WIFI_P2P_THIS_DEVICE_CHANGED_ACTIONintentから自分自身のデバイス状態の変更通知WifiP2pDeviceを取り出して、接続状態にあわせた処理を行う

次回は、WiFi Direct接続と切断を解説します。

mhidaka: Software Engineerだよ。DroidKaigi Organizer / Androidと組込とRe:VIEW。techbooster主宰。mhidaka's writings http://booklog.jp/users/mhidaka 技術書典! http://techbookfest.org