Wi-Fi Aware 概述

Wi-Fi Aware 功能使运行 Android 8.0(API 级别 26)及更高版本的设备能够相互发现和直接连接,而无需它们之间进行任何其他类型的连接。Wi-Fi Aware 也称为邻居感知网络 (NAN)。

Wi-Fi Aware 网络通过与附近设备形成集群,或者如果设备是区域中的第一个设备则创建新集群来工作。此集群行为适用于整个设备,并由 Wi-Fi Aware 系统服务管理;应用程序无法控制集群行为。应用程序使用 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 Aware。要检查 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() 方法,该方法采用以下参数

以下是一个示例

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()。然后可能会发生以下回调

尽管 PeerHandle 是与对等节点通信所必需的,但您不应将其视为对等节点的永久标识符。应用程序可以使用更高级别的标识符,这些标识符可以嵌入到发现服务本身或后续消息中。您可以使用 setMatchFilter()setServiceSpecificInfo() 方法将标识符嵌入到发现服务中,这些方法属于 PublishConfigSubscribeConfigsetMatchFilter() 方法会影响发现,而 setServiceSpecificInfo() 方法不会影响发现。

在消息中嵌入标识符意味着修改消息字节数组以包含标识符(例如,作为前两个字节)。

创建连接

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 请求发布者上的 Wi-Fi Aware 网络,使用 WifiAwareNetworkSpecifier,指定发现会话和订阅者的 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 Aware 网络。在创建 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 发现可以被限制为仅发现特定地理围栏内的服务。例如,您可以设置一个地理围栏,允许发现发布 "Aware_File_Share_Service_Name" 服务的设备,该设备距离不小于 3 米(指定为 3,000 毫米)且不超过 10 米(指定为 10,000 毫米)。

要启用地理围栏,发布者和订阅者都必须采取行动

  • 发布者必须使用 setRangingEnabled(true) 在已发布的服务上启用测距。

    如果发布者没有启用测距,则订阅者指定的任何地理围栏约束都将被忽略,并将执行正常的发现,忽略距离。

  • 订阅者必须使用 setMinDistanceMmsetMaxDistanceMm 的组合指定地理围栏。

    对于任一值,未指定的距离意味着没有限制。仅指定最大距离意味着最小距离为 0。仅指定最小距离意味着没有最大距离。

当在地理围栏内发现对等服务时,将触发 onServiceDiscoveredWithinRange 回调,它将提供到对等节点的测量距离。然后可以根据需要调用直接 Wi-Fi RTT API 来测量以后时间的距离。