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_STATEINTERNET 권한을 매니페스트에 추가합니다. 앱이 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)
    ...
}

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

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 상태 변경사항을 수신 대기하는 데 사용할 새 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
                        )
                    }
        }
    }
}

Java

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

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

동종 기기 목록 가져오기

이제 동종 기기 목록을 가져오고 처리하는 코드를 작성합니다. 먼저 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
    }
}

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

이제 WIFI_P2P_PEERS_CHANGED_ACTION 작업이 있는 인텐트가 수신되면 requestPeers()를 호출하도록 broadcast receiver의 onReceive() 메서드를 수정합니다. 어떻게든 이 리스너를 broadcast receiver에 전달해야 합니다. 한 가지 방법은 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")


        }
        ...
    }
}

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

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

Java

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

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()를 호출하여 기기 이름, 연결 상태 등 네트워크의 동종 기기에 관한 세부정보를 검색할 수 있습니다.