Wi-Fi 偵測總覽

有了 Wi-Fi 感知功能,搭載 Android 8.0 (API 級別 26) 以上版本的裝置就能直接找到彼此並直接連線,完全不需進行任何其他類型的連線。Wi-Fi Aware 也稱為鄰近感知網路 (NAN)。

Wi-Fi Aware 網路的運作方式為使用鄰近裝置的叢集建立叢集,或者如果裝置是區域內的第一部裝置,則會建立新的叢集。這項叢集行為會套用至整部裝置,並由 Wi-Fi 感知系統服務管理;應用程式無法控管叢集行為。應用程式會使用 Wi-Fi Aware API 與 Wi-Fi Aware 系統服務進行通訊,該服務用於管理裝置上的 Wi-Fi Aware 硬體。

Wi-Fi Aware API 可讓應用程式執行以下作業:

  • 探索其他裝置:這個 API 是一種尋找鄰近裝置的機制,當一部裝置「發布」一或多個可搜尋的服務時,啟動程序就會開始。這樣一來,當裝置訂閱一或多項服務,並進入發布者的 Wi-Fi 範圍時,訂閱者就會收到已發現相符發布者的通知。訂閱者找到發布者後,訂閱者可以傳送簡短訊息,或與找到的裝置建立網路連線。裝置可以同時為發布者和訂閱者。

  • 建立網路連線:兩部裝置在彼此發現後,就能在沒有存取點的情況下建立雙向 Wi-Fi Aware 網路連線。

相較於藍牙連線,Wi-Fi Aware 網路連線支援更長的處理量,如果應用程式會在使用者之間共用大量資料 (例如相片分享應用程式),這些連線類型就非常實用。

Android 13 (API 級別 33) 改善項目

在搭載 Android 13 (API 級別 33) 以上版本的裝置上,如果裝置支援即時通訊模式,應用程式可以使用 PublishConfig.Builder.setInstantCommunicationModeEnabled()SubscribeConfig.Builder.setInstantCommunicationModeEnabled() 方法,啟用或停用發布者或訂閱者探索工作階段的即時通訊模式。即時通訊模式可以加快訊息交換、服務探索,以及發布者或訂閱者探索工作階段中設定的任何資料路徑。如要判斷裝置是否支援即時通訊模式,請使用 isInstantCommunicationModeSupported() 方法。

Android 12 (API 級別 31) 改善項目

Android 12 (API 級別 31) 為 Wi-Fi Aware 新增了幾項改善:

  • 在搭載 Android 12 (API 級別 31) 以上版本的裝置上,您可以使用 onServiceLost() 回呼,在應用程式因服務停止或移出範圍而遺失偵測到的服務時,收到快訊通知。
  • 已成功設定 Wi-Fi Aware 資料路徑。較舊版本則使用 L2 訊息功能提供啟動者的 MAC 位址,因而導致延遲。在搭載 Android 12 以上版本的裝置上,回應者 (伺服器) 可設為接受任何對等互連,也就是無須事先知道啟動器的 MAC 位址。這樣可以加快資料路徑出現的速度,並且只建立含有一個網路要求的多個點對點連結。
  • 在 Android 12 以上版本上執行的應用程式可以使用 WifiAwareManager.getAvailableAwareResources() 方法,取得目前可用資料路徑的數量、發布工作階段和訂閱工作階段。這可協助應用程式判斷是否有足夠資源來執行所需功能。

初始設定

如要設定應用程式使用 Wi-Fi Aware 探索和網路功能,請執行下列步驟:

  1. 在應用程式資訊清單中要求下列權限:

    <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
    <uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
    <uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
    <uses-permission android:name="android.permission.INTERNET" />
    <!-- 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: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" />
    
  2. 使用 PackageManager API 檢查裝置是否支援 Wi-Fi Aware,如下所示:

    Kotlin

    context.packageManager.hasSystemFeature(PackageManager.FEATURE_WIFI_AWARE)
    

    Java

    context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_WIFI_AWARE);
    
  3. 檢查目前是否提供 Wi-Fi Aware。裝置上可能會出現 Wi-Fi Aware,但使用者已停用 Wi-Fi 或定位功能,因此目前可能無法使用。視其硬體和韌體功能而定,如果使用 Wi-Fi Direct、SoftAP 或網路共用功能,部分裝置可能不支援 Wi-Fi 感知功能。如要確認目前是否提供 Wi-Fi Aware,請呼叫 isAvailable()

    Wi-Fi Aware 的可用性可能隨時變更。應用程式應註冊 BroadcastReceiver,以便接收 ACTION_WIFI_AWARE_STATE_CHANGED,每當有可用性變更時,系統就會傳送此內容。應用程式收到廣播意圖時,應捨棄所有現有的工作階段 (假設 Wi-Fi Aware 服務中斷),檢查目前的可用性狀態,並據此調整運作行為。舉例來說:

    Kotlin

    val wifiAwareManager = context.getSystemService(Context.WIFI_AWARE_SERVICE) as WifiAwareManager?
    val filter = IntentFilter(WifiAwareManager.ACTION_WIFI_AWARE_STATE_CHANGED)
    val myReceiver = object : BroadcastReceiver() {
    
        override fun onReceive(context: Context, intent: Intent) {
            // discard current sessions
            if (wifiAwareManager?.isAvailable) {
                ...
            } else {
                ...
            }
        }
    }
    context.registerReceiver(myReceiver, filter)
    

    Java

    WifiAwareManager wifiAwareManager = 
            (WifiAwareManager)context.getSystemService(Context.WIFI_AWARE_SERVICE)
    IntentFilter filter =
            new IntentFilter(WifiAwareManager.ACTION_WIFI_AWARE_STATE_CHANGED);
    BroadcastReceiver myReceiver = new BroadcastReceiver() {
        @Override
        public void onReceive(Context context, Intent intent) {
            // discard current sessions
            if (wifiAwareManager.isAvailable()) {
                ...
            } else {
                ...
            }
        }
    };
    context.registerReceiver(myReceiver, filter);
    

詳情請參閱廣播的相關說明。

取得課程

如要開始使用 Wi-Fi Aware,應用程式必須呼叫 attach() 來取得 WifiAwareSession。這個方法可執行以下作業:

  • 開啟 Wi-Fi Aware 硬體。
  • 加入或建立 Wi-Fi Aware 叢集。
  • 使用不重複的命名空間建立 Wi-Fi Aware 工作階段,做為其中建立的所有探索工作階段的容器。

如果應用程式成功連接,系統會執行 onAttached() 回呼。這個回呼會提供 WifiAwareSession 物件,您的應用程式應用於所有後續工作階段作業。應用程式可以使用工作階段發布服務訂閱服務

應用程式只能呼叫 attach() 一次。如果應用程式多次呼叫 attach(),應用程式會在每次呼叫時收到不同的工作階段,而每個工作階段都有自己的命名空間。這在複雜情境中非常實用,但在一般情況下應避免使用。

發布服務

如要讓服務可供搜尋,請呼叫 publish() 方法,該方法會採用下列參數:

  • PublishConfig 會指定服務名稱和其他設定屬性,例如比對篩選器。
  • DiscoverySessionCallback 會指定事件發生時 (例如訂閱者收到訊息時) 要執行的動作。

範例如下:

Kotlin

val config: PublishConfig = PublishConfig.Builder()
        .setServiceName(AWARE_FILE_SHARE_SERVICE_NAME)
        .build()
awareSession.publish(config, object : DiscoverySessionCallback() {

    override fun onPublishStarted(session: PublishDiscoverySession) {
        ...
    }

    override fun onMessageReceived(peerHandle: PeerHandle, message: ByteArray) {
        ...
    }
})

Java

PublishConfig config = new PublishConfig.Builder()
    .setServiceName(“Aware_File_Share_Service_Name”)
    .build();

awareSession.publish(config, new DiscoverySessionCallback() {
    @Override
    public void onPublishStarted(PublishDiscoverySession session) {
        ...
    }
    @Override
    public void onMessageReceived(PeerHandle peerHandle, byte[] message) {
        ...
    }
}, null);

如果發布成功,系統會呼叫 onPublishStarted() 回呼方法。

發布後,執行相符訂閱者應用程式的裝置移至發布裝置的 Wi-Fi 範圍時,訂閱者即可找到這項服務。當訂閱者找到發布者時,發布者不會收到通知;但如果訂閱者傳送訊息給發布者,則發布者會收到通知。發生這種情況時,系統會呼叫 onMessageReceived() 回呼方法。您可以使用這個方法中的 PeerHandle 引數來傳送訊息給訂閱者,或是向該訂閱者建立連線

如要停止發布服務,請呼叫 DiscoverySession.close()。探索工作階段與上層 WifiAwareSession 相關聯。如果父項工作階段已關閉,相關聯的探索工作階段也會關閉。雖然捨棄的物件也會關閉,但系統不保證範圍外的工作階段會在關閉時關閉,因此建議您明確呼叫 close() 方法。

訂閱服務

如要訂閱服務,請呼叫 subscribe() 方法,該方法會採用下列參數:

  • SubscribeConfig 會指定要訂閱的服務名稱以及其他設定屬性,例如比對篩選器。
  • DiscoverySessionCallback 會指定事件發生時 (例如發現發布商時) 要執行的動作。

範例如下:

Kotlin

val config: SubscribeConfig = SubscribeConfig.Builder()
        .setServiceName(AWARE_FILE_SHARE_SERVICE_NAME)
        .build()
awareSession.subscribe(config, object : DiscoverySessionCallback() {

    override fun onSubscribeStarted(session: SubscribeDiscoverySession) {
        ...
    }

    override fun onServiceDiscovered(
            peerHandle: PeerHandle,
            serviceSpecificInfo: ByteArray,
            matchFilter: List<ByteArray>
    ) {
        ...
    }
}, null)

Java

SubscribeConfig config = new SubscribeConfig.Builder()
    .setServiceName("Aware_File_Share_Service_Name")
    .build();

awareSession.subscribe(config, new DiscoverySessionCallback() {
    @Override
    public void onSubscribeStarted(SubscribeDiscoverySession session) {
        ...
    }

    @Override
    public void onServiceDiscovered(PeerHandle peerHandle,
            byte[] serviceSpecificInfo, List<byte[]> matchFilter) {
        ...
    }
}, null);

如果訂閱作業成功,系統會在應用程式中呼叫 onSubscribeStarted() 回呼。因為您可以在應用程式發現後,使用回呼中的 SubscribeDiscoverySession 引數與發布商通訊,因此建議您儲存此參照。您可以隨時在探索工作階段中呼叫 updateSubscribe() 來更新訂閱工作階段。

此時,您的訂閱會等待相符發布者進入 Wi-Fi 範圍。發生這種情況時,系統會執行 onServiceDiscovered() 回呼方法。您可以使用這個回呼中的 PeerHandle 引數來傳送訊息建立連線與發布者。

如要停止訂閱服務,請呼叫 DiscoverySession.close()。探索工作階段與上層 WifiAwareSession 相關聯。如果父項工作階段已關閉,相關聯的探索工作階段也會關閉。雖然捨棄的物件也會關閉,但系統不保證範圍外的工作階段會在關閉時關閉,因此建議您明確呼叫 close() 方法。

傳送訊息

如要傳送訊息至其他裝置,您需要下列物件:

如要傳送訊息,請呼叫 sendMessage()。此時可能會發生以下回呼:

  • 當對等點成功收到訊息後,系統會在「傳送」應用程式中呼叫 onMessageSendSucceeded() 回呼。
  • 當對等互連收到訊息時,系統會在接收應用程式中呼叫 onMessageReceived() 回呼。

雖然必須使用 PeerHandle 與同業通訊,但不應將其做為對等點的永久 ID。應用程式可使用較高層級的 ID,嵌入探索服務本身或後續訊息中。您可以使用 PublishConfigSubscribeConfigsetMatchFilter()setServiceSpecificInfo() 方法,在探索服務中嵌入 ID。setMatchFilter() 方法會影響探索作業,setServiceSpecificInfo() 方法則不會影響探索作業。

在訊息中嵌入 ID,意味著修改訊息位元組陣列加入 ID (例如作為前幾位元組)。

建立連線

Wi-Fi Aware 支援在兩部 Wi-Fi Aware 裝置之間使用用戶端伺服器網路。

如要設定用戶端與伺服器連線:

  1. 使用 Wi-Fi Aware 探索功能發布服務 (在伺服器上),並在用戶端訂閱服務

  2. 訂閱者找到發布者後,從訂閱者傳送訊息給發布者。

  3. 在發布者裝置上啟動 ServerSocket,然後設定或取得通訊埠:

    Kotlin

    val ss = ServerSocket(0)
    val port = ss.localPort
    

    Java

    ServerSocket ss = new ServerSocket(0);
    int port = ss.getLocalPort();
    
  4. 使用 ConnectivityManager 使用 WifiAwareNetworkSpecifier,向發布者要求 Wi-Fi Aware 網路,指定探索工作階段和訂閱者的 PeerHandle (從訂閱者傳送的訊息取得):

    Kotlin

    val networkSpecifier = WifiAwareNetworkSpecifier.Builder(discoverySession, peerHandle)
        .setPskPassphrase("somePassword")
        .setPort(port)
        .build()
    val myNetworkRequest = NetworkRequest.Builder()
        .addTransportType(NetworkCapabilities.TRANSPORT_WIFI_AWARE)
        .setNetworkSpecifier(networkSpecifier)
        .build()
    val callback = object : ConnectivityManager.NetworkCallback() {
        override fun onAvailable(network: Network) {
            ...
        }
    
        override fun onCapabilitiesChanged(network: Network, networkCapabilities: NetworkCapabilities) {
            ...
        }
    
        override fun onLost(network: Network) {
            ...
        }
    }
    
    connMgr.requestNetwork(myNetworkRequest, callback);
    

    Java

    NetworkSpecifier networkSpecifier = new WifiAwareNetworkSpecifier.Builder(discoverySession, peerHandle)
        .setPskPassphrase("somePassword")
        .setPort(port)
        .build();
    NetworkRequest myNetworkRequest = new NetworkRequest.Builder()
        .addTransportType(NetworkCapabilities.TRANSPORT_WIFI_AWARE)
        .setNetworkSpecifier(networkSpecifier)
        .build();
    ConnectivityManager.NetworkCallback callback = new ConnectivityManager.NetworkCallback() {
        @Override
        public void onAvailable(Network network) {
            ...
        }
    
        @Override
        public void onCapabilitiesChanged(Network network, NetworkCapabilities networkCapabilities) {
            ...
        }
    
        @Override
        public void onLost(Network network) {
            ...
        }
    };
    
    ConnectivityManager connMgr.requestNetwork(myNetworkRequest, callback);
    
  5. 發布者要求聯播網後,應傳送訊息給訂閱者。

  6. 訂閱者收到來自發布者的訊息後,請使用與發布者相同的方法,在訂閱者要求 Wi-Fi 感知網路。建立 NetworkSpecifier 時,請勿指定通訊埠。當網路連線可用、變更或遺失時,系統就會呼叫適當的回呼方法。

  7. 在訂閱者上呼叫 onAvailable() 方法後,即可使用 Network 物件,此物件可以開啟 Socket 與發布者的 ServerSocket 通訊,但您必須知道 ServerSocket 的 IPv6 位址和通訊埠。您可以從 onCapabilitiesChanged() 回呼中提供的 NetworkCapabilities 物件取得這些資訊:

    Kotlin

    val peerAwareInfo = networkCapabilities.transportInfo as WifiAwareNetworkInfo
    val peerIpv6 = peerAwareInfo.peerIpv6Addr
    val peerPort = peerAwareInfo.port
    ...
    val socket = network.getSocketFactory().createSocket(peerIpv6, peerPort)
    

    Java

    WifiAwareNetworkInfo peerAwareInfo = (WifiAwareNetworkInfo) networkCapabilities.getTransportInfo();
    Inet6Address peerIpv6 = peerAwareInfo.getPeerIpv6Addr();
    int peerPort = peerAwareInfo.getPort();
    ...
    Socket socket = network.getSocketFactory().createSocket(peerIpv6, peerPort);
    
  8. 網路連線完成後,請呼叫 unregisterNetworkCallback()

選擇同類應用程式及可辨識位置的探索方式

具備 Wi-Fi RTT 位置功能的裝置可以直接測量與同類應用程式的距離,並使用這項資訊限制 Wi-Fi Aware 服務探索。

Wi-Fi RTT API 允許透過 MAC 位址或 PeerHandle 之間的連線,直接傳輸至 Wi-Fi Aware 對等點。

您可以將 Wi-Fi Aware 探索功能限制為只能探索特定地理圍欄內的服務。舉例來說,您可以設定地理圍欄,供使用者發現裝置發布的 "Aware_File_Share_Service_Name" 服務範圍不超過 3 公尺 (指定為 3,000 公釐),且不超過 10 公尺 (指定為 10,000 公釐)。

如要啟用地理圍欄功能,發布者與訂閱者必須採取下列行動:

  • 發布者必須使用 setRangingEnabled(true) 啟用對已發布服務執行計量的功能。

    如果發布者未啟用測距功能,系統會忽略訂閱者指定的任何地理圍欄限制,並忽略距離進行一般探索。

  • 訂閱者必須使用 setMinDistanceMmsetMaxDistanceMm 的組合,來指定地理圍欄。

    對於任一值,未指定距離代表無限制。只指定最大距離表示最短距離 0。只指定最小距離表示沒有上限。

如果在地理圍欄內發現對等互連服務,就會觸發 onServiceDiscoveredWithinRange 回呼,提供與對等點的測量距離。接著,系統會視需要呼叫直接 Wi-Fi RTT API,以便在之後測量距離。