Wi-Fi Direct로 P2P 연결 만들기

Wi-Fi Direct (P2P라고도 함)를 사용하면 애플리케이션이 블루투스 기능 범위를 벗어난 범위에서 근처 기기를 빠르게 찾고 상호작용할 수 있습니다.

Wi-Fi Direct (P2P) API를 사용하면 애플리케이션이 네트워크나 핫스팟에 연결할 필요 없이 근처 기기에 연결할 수 있습니다. 앱이 안전한 근거리 네트워크의 일부로 설계된 경우 다음과 같은 이유로 Wi-Fi Direct가 기존 Wi-Fi 애드혹 네트워킹보다 더 적합한 옵션입니다.

  • Wi-Fi Direct는 WPA2 암호화를 지원합니다. 일부 애드혹 네트워크는 WEP 암호화만 지원합니다.
  • 기기는 제공하는 서비스를 브로드캐스트할 수 있으므로 다른 기기가 적합한 피어를 더 쉽게 검색할 수 있습니다.
  • 네트워크의 그룹 소유자가 될 기기를 결정할 때 Wi-Fi Direct는 각 기기의 전원 관리, UI, 서비스 기능을 검사하고 이 정보를 사용하여 서버 책임을 가장 효과적으로 처리할 수 있는 기기를 선택합니다.
  • Android는 Wi-Fi 임시 모드를 지원하지 않습니다.

이 과정에서는 Wi-Fi P2P를 사용하여 주변 기기를 찾고 연결하는 방법을 보여줍니다.

애플리케이션 권한 설정

Wi-Fi Direct를 사용하려면 ACCESS_FINE_LOCATION, CHANGE_WIFI_STATE, ACCESS_WIFI_STATE, INTERNET 권한을 매니페스트에 추가합니다. 앱이 Android 13 (API 수준 33) 이상을 타겟팅하는 경우 매니페스트에 NEARBY_WIFI_DEVICES 권한도 추가합니다. Wi-Fi Direct는 인터넷 연결이 필요하지 않지만 표준 자바 소켓을 사용하므로 INTERNET 권한이 필요합니다. 따라서 Wi-Fi Direct를 사용하려면 다음 권한이 필요합니다.

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.android.nsdchat"
    ...
    <!-- If your app targets Android 13 (API level 33)
         or higher, you must declare the NEARBY_WIFI_DEVICES permission. -->
        <uses-permission android:name="android.permission.NEARBY_WIFI_DEVICES"
        <!-- If your app derives location information from Wi-Fi APIs,
             don't include the "usesPermissionFlags" attribute. -->
        android:usesPermissionFlags="neverForLocation" />
        
    <uses-permission
        android:required="true"
        android:name="android.permission.ACCESS_FINE_LOCATION"
        <!-- If any feature in your app relies on precise location information,
             don't include the "maxSdkVersion" attribute. -->
        android:maxSdkVersion="32" />
    <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도 위치 모드를 사용하도록 설정해야 합니다.

broadcast receiver 및 P2P 관리자 설정

Wi-Fi Direct를 사용하려면 특정 이벤트가 발생할 때 애플리케이션에 알리는 브로드캐스트 인텐트를 수신 대기해야 합니다. 애플리케이션에서 IntentFilter를 인스턴스화하고 다음을 수신 대기하도록 설정합니다.

WIFI_P2P_STATE_CHANGED_ACTION
Wi-Fi Direct가 사용 설정되었는지 여부를 나타냅니다.
WIFI_P2P_PEERS_CHANGED_ACTION
사용 가능한 피어 목록이 변경되었음을 나타냅니다.
WIFI_P2P_CONNECTION_CHANGED_ACTION
Wi-Fi Direct 연결 상태가 변경되었음을 나타냅니다. 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 Direct 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 Direct 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)
    ...
}

자바

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 Direct 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 Direct 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() 메서드를 호출합니다. 이 메서드는 나중에 앱을 Wi-Fi Direct 프레임워크에 연결하는 데 사용할 WifiP2pManager.Channel 객체를 반환합니다.

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

자바

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 상태 변경사항을 수신하는 데 사용할 새 BroadcastReceiver 클래스를 만듭니다. onReceive() 메서드에서 위에 나열된 각 상태 변경을 처리하는 조건을 추가합니다.

Kotlin

override fun onReceive(context: Context, intent: Intent) {
    when(intent.action) {
        WifiP2pManager.WIFI_P2P_STATE_CHANGED_ACTION -> {
            // Determine if Wi-Fi Direct 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
                        )
                    }
        }
    }
}

자바

@Override
public void onReceive(Context context, Intent intent) {
    String action = intent.getAction();
    if (WifiP2pManager.WIFI_P2P_STATE_CHANGED_ACTION.equals(action)) {
        // Determine if Wi-Fi Direct 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));

    }
}

마지막으로, 기본 활동이 활성 상태일 때 인텐트 필터와 broadcast receiver를 등록하고 활동이 일시중지될 때 등록을 취소하는 코드를 추가합니다. 이 작업을 수행하기에 가장 좋은 위치는 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)
}

자바

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

자바

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 그룹이 형성될 때까지 검색이 활성 상태로 유지됩니다.

동종 기기 목록 가져오기

이제 동종 기기 목록을 가져오고 처리하는 코드를 작성합니다. 먼저 Wi-Fi Direct가 감지한 피어에 관한 정보를 제공하는 WifiP2pManager.PeerListListener 인터페이스를 구현합니다. 또한 이 정보를 통해 앱은 피어가 네트워크에 참여하거나 네트워크에서 나가는 시점을 확인할 수 있습니다. 다음 코드 스니펫은 피어와 관련된 작업을 보여줍니다.

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

자바

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

이제 작업 WIFI_P2P_PEERS_CHANGED_ACTION가 있는 인텐트가 수신될 때 requestPeers()를 호출하도록 broadcast receiver의 onReceive() 메서드를 수정합니다. 이 리스너를 수신기에 전달해야 합니다. 한 가지 방법은 broadcast receiver의 생성자에 인수로 전송하는 것입니다.

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")


        }
        ...
    }
}

자바

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

자바

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

자바

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

connect() 메서드에 구현된 WifiP2pManager.ActionListener는 초기화가 성공하거나 실패할 때만 알림을 보냅니다. 연결 상태의 변경사항을 수신 대기하려면 WifiP2pManager.ConnectionInfoListener 인터페이스를 구현합니다. onConnectionInfoAvailable() 콜백은 연결 상태가 변경되면 알려줍니다. 여러 기기가 단일 기기에 연결되는 경우 (예: 플레이어가 3명 이상인 게임 또는 채팅 앱) 한 기기가 '그룹 소유자'로 지정됩니다. 그룹 만들기 섹션의 단계에 따라 특정 기기를 네트워크의 그룹 소유자로 지정할 수 있습니다.

Kotlin

private val connectionListener = WifiP2pManager.ConnectionInfoListener { info ->

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

자바

@Override
public void onConnectionInfoAvailable(final WifiP2pInfo info) {

    // String from WifiP2pInfo struct
    String 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.
    }
}

이제 broadcast receiver의 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)
            }
        }
    }
    ...
}

자바

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

자바

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()를 호출하여 기기 이름 및 연결 상태를 포함한 네트워크의 피어에 관한 세부정보를 가져올 수 있습니다.