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의 생성자에 인수로 이를 전송하는 것입니다.

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