Wi-Fi Direct (又稱點對點或 P2P) 可讓應用程式快速尋找附近的裝置並與之互動,範圍超出藍牙功能。
應用程式可透過 Wi-Fi Direct (P2P) API 連線至附近的裝置,不必連線至網路或無線基地台。如果您的應用程式是安全近距離網路的一部分,Wi-Fi Direct 比傳統 Wi-Fi 隨意網路更適合,原因如下:
- Wi-Fi Direct 支援 WPA2 加密。(部分臨機操作網路僅支援 WEP 加密)。
- 裝置可以廣播提供的服務,協助其他裝置更輕鬆地找到合適的對等互連裝置。
- Wi-Fi Direct 會檢查每個裝置的電源管理、使用者介面和服務功能,並根據這些資訊選擇最能有效處理伺服器責任的裝置,做為網路的群組擁有者。
- 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 不需要網際網路連線,但會使用標準 Java 插座,因此需要 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 也必須啟用「定位模式」:
設定廣播接收器和對等互連管理員
如要使用 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()
方法。這個方法會傳回 WifiP2pManager.Channel
物件,您稍後會使用該物件將應用程式連線至 Wi-Fi Direct 架構。
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); }
現在請建立新的 BroadcastReceiver
類別,用來監聽系統 Wi-Fi 狀態的變更。在 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)); } }
最後,請新增程式碼,在主要活動處於啟用狀態時註冊意圖篩選器和廣播接收器,並在活動暫停時取消註冊。最佳做法是使用 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()
。這個方法會採用下列引數:
- 您在初始化對等互連 mManager 時收到的
WifiP2pManager.Channel
- 實作
WifiP2pManager.ActionListener
,其中包含系統在探索成功和失敗時呼叫的方法。
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 Direct 偵測到的對等互連裝置相關資訊。應用程式也能透過這項資訊,判斷同層級裝置何時加入或離開網路。下列程式碼片段說明與對等互連相關的作業:
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
動作的 Intent 時呼叫 requestPeers()
。您需要以某種方式將這個監聽器傳遞至接收器。其中一種方法是將其做為引數傳送至廣播接收器的建構函式。
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
intent
動作,就會觸發更新後的對等互連清單要求。
與同業人士交流互動
如要連線至對等互連裝置,請建立新的 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()
回呼會通知您。如果多部裝置要連線至單一裝置 (例如有三名以上玩家的遊戲,或是即時通訊應用程式),其中一部裝置會指定為「群組擁有者」。按照「建立群組」一節中的步驟操作,即可將特定裝置指定為網路的群組擁有者。
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. } }
現在請返回廣播接收器的 onReceive()
方法,並修改監聽 WIFI_P2P_CONNECTION_CHANGED_ACTION
Intent 的區段。收到此意圖時,請呼叫 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 的裝置) 的網路,請按照「連線至對等互連裝置」一節中的步驟操作,但請建立新的 WifiP2pManager.ActionListener
,並使用 createGroup()
而非 connect()
。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()
擷取網路上的對等互連詳細資料,包括裝置名稱和連線狀態。