Wi-Fi Direct で P2P 接続を作成する

Wi-Fi Direct(ピアツーピアまたは P2P とも呼ばれます)を使用すると、アプリは Bluetooth より広い範囲で、近くのデバイスをすばやく見つけて通信できます。

アプリは、Wi-Fi ピアツーピア(P2P)API を使用して、ネットワークまたはアクセス ポイントに接続する手間をかけずに、近くのデバイスに接続できます。アプリが安全な近距離ネットワークの一部として機能するように設計されている場合、従来の Wi-Fi アドホック ネットワークより Wi-Fi Direct のほうが適切です。それは、以下の理由によります。

  • Wi-Fi Direct は WPA2 暗号化をサポートします(一部のアドホック ネットワークは WEP 暗号化のみをサポートします)。
  • デバイスは、自らが提供するサービスをブロードキャストできます。これにより、他のデバイスは適切なピアをたやすく発見できます。
  • Wi-Fi Direct は、どのデバイスをネットワークのグループ オーナーにするかを決定するにあたって、各デバイスの電源管理、UI、サービス機能を調べ、この情報を使用して、サーバーの責任を最も効果的に果たすことができるデバイスを選択します。
  • Android は Wi-Fi アドホック モードをサポートしていません。

このレッスンでは、Wi-Fi P2P を使用して、近くのデバイスを見つけて接続する方法を示します。

アプリの権限をセットアップする

Wi-Fi P2P を使用するには、ACCESS_FINE_LOCATIONCHANGE_WIFI_STATEACCESS_WIFI_STATEINTERNET 権限をマニフェストに追加します。Wi-Fi P2P はインターネット接続を必要としませんが、INTERNET 権限を必要とする標準の Java ソケットを使用します。したがって、Wi-Fi P2P を使用するには、次の権限が必要です。

    <manifest xmlns:android="http://schemas.android.com/apk/res/android"
        package="com.example.android.nsdchat"
        ...
        <uses-permission
            android:required="true"
            android:name="android.permission.ACCESS_FINE_LOCATION"/>
        <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"/>
        ...
    

上記の権限に加えて、次の API で位置情報モードを有効にすることも必要です。

ブロードキャスト レシーバとピアツーピア マネージャーをセットアップする

Wi-Fi P2P を使用するには、特定のイベントが発生したときにアプリに知らせるブロードキャスト インテントをリッスンする必要があります。アプリ内で IntentFilter をインスタンス化して、以下をリッスンするように設定します。

WIFI_P2P_STATE_CHANGED_ACTION
Wi-Fi P2P が有効かどうかを示します。
WIFI_P2P_PEERS_CHANGED_ACTION
利用可能なピアのリストが変更されたことを示します。
WIFI_P2P_CONNECTION_CHANGED_ACTION
Wi-Fi P2P 接続の状態が変更されたことを示します。Android 10 以降、この情報はスティッキーではなくなりました。この情報がスティッキーであったために、登録時にアプリがこれらのブロードキャストを受信することに依存していた場合は、その代わりとして、初期化時に適切な get メソッドを使用して情報を取得してください。
WIFI_P2P_THIS_DEVICE_CHANGED_ACTION
このデバイスの構成の詳細が変更されたことを示します。Android 10 以降、この情報はスティッキーではなくなりました。この情報がスティッキーであったために、登録時にアプリがこれらのブロードキャストを受信することに依存していた場合は、その代わりとして、初期化時に適切な get メソッドを使用して情報を取得してください。

Kotlin

    private val intentFilter = IntentFilter()
    ...
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.main)

        // Indicates a change in the Wi-Fi P2P status.
        intentFilter.addAction(WifiP2pManager.WIFI_P2P_STATE_CHANGED_ACTION)

        // Indicates a change in the list of available peers.
        intentFilter.addAction(WifiP2pManager.WIFI_P2P_PEERS_CHANGED_ACTION)

        // Indicates the state of Wi-Fi P2P connectivity has changed.
        intentFilter.addAction(WifiP2pManager.WIFI_P2P_CONNECTION_CHANGED_ACTION)

        // Indicates this device's details have changed.
        intentFilter.addAction(WifiP2pManager.WIFI_P2P_THIS_DEVICE_CHANGED_ACTION)
        ...
    }
    

Java

    private final IntentFilter intentFilter = new IntentFilter();
    ...
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);

        // Indicates a change in the Wi-Fi P2P status.
        intentFilter.addAction(WifiP2pManager.WIFI_P2P_STATE_CHANGED_ACTION);

        // Indicates a change in the list of available peers.
        intentFilter.addAction(WifiP2pManager.WIFI_P2P_PEERS_CHANGED_ACTION);

        // Indicates the state of Wi-Fi P2P connectivity has changed.
        intentFilter.addAction(WifiP2pManager.WIFI_P2P_CONNECTION_CHANGED_ACTION);

        // Indicates this device's details have changed.
        intentFilter.addAction(WifiP2pManager.WIFI_P2P_THIS_DEVICE_CHANGED_ACTION);
        ...
    }
    

onCreate() メソッドの終わりで WifiP2pManager のインスタンスを取得し、その initialize() メソッドを呼び出します。このメソッドは WifiP2pManager.Channel オブジェクトを返します。このオブジェクトは、後でアプリを Wi-Fi P2P フレームワークに接続するために使用します。

Kotlin

    private lateinit var channel: WifiP2pManager.Channel
    private lateinit var manager: WifiP2pManager

    override fun onCreate(savedInstanceState: Bundle?) {
        ...
        manager = getSystemService(Context.WIFI_P2P_SERVICE) as WifiP2pManager
        channel = manager.initialize(this, mainLooper, null)
    }
    

Java

    Channel channel;
    WifiP2pManager manager;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        ...
        manager = (WifiP2pManager) getSystemService(Context.WIFI_P2P_SERVICE);
        channel = manager.initialize(this, getMainLooper(), null);
    }
    

次に、システムの Wi-Fi P2P 状態の変化をリッスンするために使用する新しい BroadcastReceiver クラスを作成します。onReceive() メソッドに、上記の各 P2P 状態の変化を処理するための条件を追加します。

Kotlin

    override fun onReceive(context: Context, intent: Intent) {
        when(intent.action) {
            WifiP2pManager.WIFI_P2P_STATE_CHANGED_ACTION -> {
                // Determine if Wifi P2P mode is enabled or not, alert
                // the Activity.
                val state = intent.getIntExtra(WifiP2pManager.EXTRA_WIFI_STATE, -1)
                activity.isWifiP2pEnabled = state == WifiP2pManager.WIFI_P2P_STATE_ENABLED
            }
            WifiP2pManager.WIFI_P2P_PEERS_CHANGED_ACTION -> {

                // The peer list has changed! We should probably do something about
                // that.

            }
            WifiP2pManager.WIFI_P2P_CONNECTION_CHANGED_ACTION -> {

                // Connection state changed! We should probably do something about
                // that.

            }
            WifiP2pManager.WIFI_P2P_THIS_DEVICE_CHANGED_ACTION -> {
                (activity.supportFragmentManager.findFragmentById(R.id.frag_list) as DeviceListFragment)
                        .apply {
                            updateThisDevice(
                                    intent.getParcelableExtra(
                                            WifiP2pManager.EXTRA_WIFI_P2P_DEVICE) as WifiP2pDevice
                            )
                        }
            }
        }
    }
    

Java

    @Override
    public void onReceive(Context context, Intent intent) {
        String action = intent.getAction();
        if (WifiP2pManager.WIFI_P2P_STATE_CHANGED_ACTION.equals(action)) {
            // Determine if Wifi P2P mode is enabled or not, alert
            // the 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)) {

            // The peer list has changed! We should probably do something about
            // that.

        } else if (WifiP2pManager.WIFI_P2P_CONNECTION_CHANGED_ACTION.equals(action)) {

            // Connection state changed! We should probably do something about
            // that.

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

        }
    }
    

最後に、メイン アクティビティがアクティブになったときにインテント フィルタとブロードキャスト レシーバを登録し、アクティビティが一時停止されたときに登録解除するコードを追加します。これを行うのに最適なメソッドは、onResume()onPause() です。

Kotlin

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

    public override fun onPause() {
        super.onPause()
        unregisterReceiver(receiver)
    }
    

Java

    /** 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);
    }
    

ピアの検出を開始する

Wi-Fi P2P を使用して近くのデバイスの検索を開始するには、discoverPeers() を呼び出します。このメソッドは、次の引数を受け取ります。

Kotlin

    manager.discoverPeers(channel, object : WifiP2pManager.ActionListener {

        override fun onSuccess() {
            // Code for when the discovery initiation is successful goes here.
            // No services have actually been discovered yet, so this method
            // can often be left blank. Code for peer discovery goes in the
            // onReceive method, detailed below.
        }

        override fun onFailure(reasonCode: Int) {
            // Code for when the discovery initiation fails goes here.
            // Alert the user that something went wrong.
        }
    })
    

Java

    manager.discoverPeers(channel, new WifiP2pManager.ActionListener() {

        @Override
        public void onSuccess() {
            // Code for when the discovery initiation is successful goes here.
            // No services have actually been discovered yet, so this method
            // can often be left blank. Code for peer discovery goes in the
            // onReceive method, detailed below.
        }

        @Override
        public void onFailure(int reasonCode) {
            // Code for when the discovery initiation fails goes here.
            // Alert the user that something went wrong.
        }
    });
    

このコードはピア検出を開始するのみであることにご注意ください。discoverPeers() メソッドは、検出プロセスを開始すると、すぐに結果を返します。システムは、指定されたアクション リスナーのメソッドを呼び出すことにより、ピア検出プロセスが正常に開始されたかどうかをアプリに通知します。また、接続が開始されるか P2P グループが形成されるまで、検出はアクティブなままです。

ピアのリストを取得する

次に、ピアのリストを取得して処理するコードを作成します。最初に、WifiP2pManager.PeerListListener インターフェースを実装します。このインターフェースは、Wi-Fi P2P が検出したピアに関する情報を提供します。この情報により、アプリはピアがネットワークに参加するかまたはネットワークを離脱するタイミングを判断することもできます。次のコード スニペットは、ピアに関する上記の操作を示しています。

Kotlin

    private val peers = mutableListOf<WifiP2pDevice>()
    ...

    private val peerListListener = WifiP2pManager.PeerListListener { peerList ->
        val refreshedPeers = peerList.deviceList
        if (refreshedPeers != peers) {
            peers.clear()
            peers.addAll(refreshedPeers)

            // If an AdapterView is backed by this data, notify it
            // of the change. For instance, if you have a ListView of
            // available peers, trigger an update.
            (listAdapter as WiFiPeerListAdapter).notifyDataSetChanged()

            // Perform any other updates needed based on the new list of
            // peers connected to the Wi-Fi P2P network.
        }

        if (peers.isEmpty()) {
            Log.d(TAG, "No devices found")
            return@PeerListListener
        }
    }
    

Java

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

    private PeerListListener peerListListener = new PeerListListener() {
        @Override
        public void onPeersAvailable(WifiP2pDeviceList peerList) {

            List<WifiP2pDevice> refreshedPeers = peerList.getDeviceList();
            if (!refreshedPeers.equals(peers)) {
                peers.clear();
                peers.addAll(refreshedPeers);

                // If an AdapterView is backed by this data, notify it
                // of the change. For instance, if you have a ListView of
                // available peers, trigger an update.
                ((WiFiPeerListAdapter) getListAdapter()).notifyDataSetChanged();

                // Perform any other updates needed based on the new list of
                // peers connected to the Wi-Fi P2P network.
            }

            if (peers.size() == 0) {
                Log.d(WiFiDirectActivity.TAG, "No devices found");
                return;
            }
        }
    }
    

次に、ブロードキャスト レシーバの onReceive() メソッドを変更して、アクション WIFI_P2P_PEERS_CHANGED_ACTION を含むインテントが受信されたときに requestPeers()を呼び出すようにします。このリスナーをなんらかの方法でレシーバに渡す必要があります。その方法の 1 つは、ブロードキャスト レシーバのコンストラクタへの引数としてリスナーを送信することです。

Kotlin

    fun onReceive(context: Context, intent: Intent) {
        when (intent.action) {
            ...
            WifiP2pManager.WIFI_P2P_PEERS_CHANGED_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()
                mManager?.requestPeers(channel, peerListListener)
                Log.d(TAG, "P2P peers changed")

            }
            ...
        }
    }
    

Java

    public void onReceive(Context context, Intent intent) {
        ...
        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 (mManager != null) {
                mManager.requestPeers(channel, peerListListener);
            }
            Log.d(WiFiDirectActivity.TAG, "P2P peers changed");
        }...
    }
    

これにより、アクション WIFI_P2P_PEERS_CHANGED_ACTION を含むインテントが、更新されたピアリストに対するリクエストをトリガーします。

ピアに接続する

ピアに接続するには、新しい WifiP2pConfig オブジェクトを作成し、接続先のデバイスを表す WifiP2pDevice からそのオブジェクトにデータをコピーします。次に、connect() メソッドを呼び出します。

Kotlin

    override fun connect() {
        // Picking the first device found on the network.
        val device = peers[0]

        val config = WifiP2pConfig().apply {
            deviceAddress = device.deviceAddress
            wps.setup = WpsInfo.PBC
        }

        manager.connect(channel, config, object : WifiP2pManager.ActionListener {

            override fun onSuccess() {
                // WiFiDirectBroadcastReceiver notifies us. Ignore for now.
            }

            override fun onFailure(reason: Int) {
                Toast.makeText(
                        this@WiFiDirectActivity,
                        "Connect failed. Retry.",
                        Toast.LENGTH_SHORT
                ).show()
            }
        })
    }
    

Java

    @Override
    public void connect() {
        // Picking the first device found on the network.
        WifiP2pDevice device = peers.get(0);

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

        manager.connect(channel, config, new ActionListener() {

            @Override
            public void onSuccess() {
                // WiFiDirectBroadcastReceiver notifies us. Ignore for now.
            }

            @Override
            public void onFailure(int reason) {
                Toast.makeText(WiFiDirectActivity.this, "Connect failed. Retry.",
                        Toast.LENGTH_SHORT).show();
            }
        });
    }
    

グループ内の各デバイスが Wi-Fi Direct をサポートしている場合、接続時にグループのパスワードを明示的に要求する必要はありません。ただし、Wi-Fi Direct をサポートしていないデバイスがグループに参加できるようにするには、requestGroupInfo() を呼び出してこのパスワードを取得する必要があります。次のコード スニペットをご覧ください。

Kotlin

    manager.requestGroupInfo(channel) { group ->
        val groupPassword = group.passphrase
    }
    

Java

    manager.requestGroupInfo(channel, new GroupInfoListener() {
      @Override
      public void onGroupInfoAvailable(WifiP2pGroup group) {
          String groupPassword = group.getPassphrase();
      }
    });
    

WifiP2pManager.ActionListenerconnect() メソッドに実装されています)は、開始が成功または失敗したときにだけ通知することに注意してください。接続状態の変化をリッスンするには、WifiP2pManager.ConnectionInfoListener インターフェースを実装します。このインターフェースの onConnectionInfoAvailable() コールバックは、接続の状態が変化したときに通知します。複数のデバイスが 1 つのデバイスに接続される場合(3 人以上のプレイヤーが参加するゲームやチャットアプリなど)、1 つのデバイスが「グループ オーナー」に指名されます。特定のデバイスをネットワークのグループ オーナーに指名するには、グループを作成するセクションの手順に従います。

Kotlin

    private val connectionListener = WifiP2pManager.ConnectionInfoListener { info ->

        // InetAddress from WifiP2pInfo struct.
        val groupOwnerAddress: String = info.groupOwnerAddress.hostAddress

        // After the group negotiation, we can determine the group owner
        // (server).
        if (info.groupFormed && info.isGroupOwner) {
            // Do whatever tasks are specific to the group owner.
            // One common case is creating a group owner thread and accepting
            // incoming connections.
        } else if (info.groupFormed) {
            // The other device acts as the peer (client). In this case,
            // you'll want to create a peer thread that connects
            // to the group owner.
        }
    }
    

Java

    @Override
    public void onConnectionInfoAvailable(final WifiP2pInfo info) {

        // InetAddress from WifiP2pInfo struct.
        InetAddress groupOwnerAddress = info.groupOwnerAddress.getHostAddress();

        // After the group negotiation, we can determine the group owner
        // (server).
        if (info.groupFormed && info.isGroupOwner) {
            // Do whatever tasks are specific to the group owner.
            // One common case is creating a group owner thread and accepting
            // incoming connections.
        } else if (info.groupFormed) {
            // The other device acts as the peer (client). In this case,
            // you'll want to create a peer thread that connects
            // to the group owner.
        }
    }
    

次に、ブロードキャスト レシーバの onReceive() メソッドに戻って、WIFI_P2P_CONNECTION_CHANGED_ACTION インテントをリッスンするセクションを変更します。このインテントが受信されたら、requestConnectionInfo() を呼び出します。これは非同期呼び出しであるため、パラメータとして指定した接続情報リスナーによって結果が受信されます。

Kotlin

    when (intent.action) {
        ...
        WifiP2pManager.WIFI_P2P_CONNECTION_CHANGED_ACTION -> {

            // Connection state changed! We should probably do something about
            // that.

            mManager?.let { manager ->

                val networkInfo: NetworkInfo? = intent
                        .getParcelableExtra(WifiP2pManager.EXTRA_NETWORK_INFO) as NetworkInfo

                if (networkInfo?.isConnected == true) {

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

                    manager.requestConnectionInfo(channel, connectionListener)
                }
            }
        }
        ...
    }
    

Java

        ...
        } 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

                manager.requestConnectionInfo(channel, connectionListener);
            }
            ...
    

グループを作成する

あなたのアプリを実行しているデバイスが、旧式のデバイス(つまり、Wi-Fi Direct をサポートしていないデバイス)を含むネットワークのグループ オーナーとして機能するようにしたい場合は、ピアに接続するセクションと同様の手順を順序どおりに実施します(ただし、connect() の代わりに createGroup() を使用して新しい WifiP2pManager.ActionListener を作成する点が異なります)。次のコード スニペットに示すように、WifiP2pManager.ActionListener 内のコールバック処理は同じです。

Kotlin

    manager.createGroup(channel, object : WifiP2pManager.ActionListener {
        override fun onSuccess() {
            // Device is ready to accept incoming connections from peers.
        }

        override fun onFailure(reason: Int) {
            Toast.makeText(
                    this@WiFiDirectActivity,
                    "P2P group creation failed. Retry.",
                    Toast.LENGTH_SHORT
            ).show()
        }
    })
    

Java

    manager.createGroup(channel, new WifiP2pManager.ActionListener() {
        @Override
        public void onSuccess() {
            // Device is ready to accept incoming connections from peers.
        }

        @Override
        public void onFailure(int reason) {
            Toast.makeText(WiFiDirectActivity.this, "P2P group creation failed. Retry.",
                    Toast.LENGTH_SHORT).show();
        }
    });
    

注: ネットワーク内のすべてのデバイスが Wi-Fi Direct をサポートしている場合、各デバイスで connect() メソッドを使用できます。このメソッドは、接続した後でグループを作成し、グループ オーナーを自動的に選択するからです。

グループを作成したら、requestGroupInfo() を呼び出して、ネットワーク上のピアに関する詳細情報(デバイス名や接続ステータスなど)を取得できます。