1 /* 2 * Copyright (C) 2023 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.google.snippet.wifi.aware; 18 19 import static android.net.wifi.aware.AwarePairingConfig.PAIRING_BOOTSTRAPPING_OPPORTUNISTIC; 20 21 import static java.nio.charset.StandardCharsets.UTF_8; 22 23 import android.content.Context; 24 import android.net.wifi.aware.AwarePairingConfig; 25 import android.net.wifi.aware.Characteristics; 26 import android.net.wifi.aware.DiscoverySession; 27 import android.net.wifi.aware.PeerHandle; 28 import android.net.wifi.aware.PublishConfig; 29 import android.net.wifi.aware.SubscribeConfig; 30 import android.net.wifi.aware.WifiAwareManager; 31 import android.net.wifi.aware.WifiAwareSession; 32 import android.os.Build; 33 import android.os.Handler; 34 import android.os.HandlerThread; 35 import android.util.Log; 36 import android.util.Pair; 37 38 import androidx.test.platform.app.InstrumentationRegistry; 39 40 import com.android.compatibility.common.util.ApiLevelUtil; 41 42 import com.google.android.mobly.snippet.Snippet; 43 import com.google.android.mobly.snippet.rpc.Rpc; 44 import com.google.common.collect.ImmutableSet; 45 46 import java.util.ArrayList; 47 import java.util.Arrays; 48 import java.util.List; 49 import java.util.Set; 50 import java.util.concurrent.Executors; 51 import java.util.concurrent.atomic.AtomicReference; 52 import java.util.function.Consumer; 53 54 /** An example snippet class with a simple Rpc. */ 55 public class WifiAwareSnippet implements Snippet { 56 57 private Object mLock; 58 59 private static class WifiAwareSnippetException extends Exception { 60 private static final long SERIAL_VERSION_UID = 1; 61 WifiAwareSnippetException(String msg)62 WifiAwareSnippetException(String msg) { 63 super(msg); 64 } 65 WifiAwareSnippetException(String msg, Throwable err)66 WifiAwareSnippetException(String msg, Throwable err) { 67 super(msg, err); 68 } 69 } 70 71 private static final String TAG = "WifiAwareSnippet"; 72 73 private static final String SERVICE_NAME = "CtsVerifierTestService"; 74 private static final byte[] MATCH_FILTER_BYTES = "bytes used for matching".getBytes(UTF_8); 75 private static final byte[] PUB_SSI = "Extra bytes in the publisher discovery".getBytes(UTF_8); 76 private static final byte[] SUB_SSI = 77 "Arbitrary bytes for the subscribe discovery".getBytes(UTF_8); 78 private static final int LARGE_ENOUGH_DISTANCE = 100000; // 100 meters 79 private static final String PASSWORD = "Some super secret password"; 80 private static final String ALIAS_PUBLISH = "publisher"; 81 private static final String ALIAS_SUBSCRIBE = "subscriber"; 82 private static final int TEST_WAIT_DURATION_MS = 10000; 83 84 private final WifiAwareManager mWifiAwareManager; 85 86 private final Context mContext; 87 88 private final HandlerThread mHandlerThread; 89 90 private final Handler mHandler; 91 92 private WifiAwareSession mWifiAwareSession; 93 private DiscoverySession mDiscoverySession; 94 private CallbackUtils.DiscoveryCb mDiscoveryCb; 95 private PeerHandle mPeerHandle; 96 private final AwarePairingConfig mPairingConfig = new AwarePairingConfig.Builder() 97 .setPairingCacheEnabled(true) 98 .setPairingSetupEnabled(true) 99 .setPairingVerificationEnabled(true) 100 .setBootstrappingMethods(PAIRING_BOOTSTRAPPING_OPPORTUNISTIC) 101 .build(); 102 WifiAwareSnippet()103 public WifiAwareSnippet() { 104 mContext = InstrumentationRegistry.getInstrumentation().getTargetContext(); 105 mWifiAwareManager = mContext.getSystemService(WifiAwareManager.class); 106 mHandlerThread = new HandlerThread("Snippet-Aware"); 107 mHandlerThread.start(); 108 mHandler = new Handler(mHandlerThread.getLooper()); 109 } 110 111 @Rpc(description = "Execute attach.") attach()112 public void attach() throws InterruptedException, WifiAwareSnippetException { 113 CallbackUtils.AttachCb attachCb = new CallbackUtils.AttachCb(); 114 mWifiAwareManager.attach(attachCb, mHandler); 115 Pair<CallbackUtils.AttachCb.CallbackCode, WifiAwareSession> results = 116 attachCb.waitForAttach(); 117 if (results.first != CallbackUtils.AttachCb.CallbackCode.ON_ATTACHED) { 118 throw new WifiAwareSnippetException( 119 String.format("executeTest: attach " + results.first)); 120 } 121 mWifiAwareSession = results.second; 122 if (mWifiAwareSession == null) { 123 throw new WifiAwareSnippetException( 124 "executeTest: attach callback succeeded but null session returned!?"); 125 } 126 } 127 128 @Rpc(description = "Execute subscribe.") subscribe(Boolean isUnsolicited, Boolean isRangingRequired, Boolean isPairingRequired)129 public void subscribe(Boolean isUnsolicited, Boolean isRangingRequired, 130 Boolean isPairingRequired) throws InterruptedException, WifiAwareSnippetException { 131 mDiscoveryCb = new CallbackUtils.DiscoveryCb(); 132 133 List<byte[]> matchFilter = new ArrayList<>(); 134 matchFilter.add(MATCH_FILTER_BYTES); 135 SubscribeConfig.Builder builder = 136 new SubscribeConfig.Builder() 137 .setServiceName(SERVICE_NAME) 138 .setServiceSpecificInfo(SUB_SSI) 139 .setMatchFilter(matchFilter) 140 .setSubscribeType( 141 isUnsolicited 142 ? SubscribeConfig.SUBSCRIBE_TYPE_PASSIVE 143 : SubscribeConfig.SUBSCRIBE_TYPE_ACTIVE) 144 .setTerminateNotificationEnabled(true); 145 146 if (isRangingRequired) { 147 // set up a distance that will always trigger - i.e. that we're already in that range 148 builder.setMaxDistanceMm(LARGE_ENOUGH_DISTANCE); 149 } 150 if (isPairingRequired) { 151 builder.setPairingConfig(mPairingConfig); 152 } 153 SubscribeConfig subscribeConfig = builder.build(); 154 Log.d(TAG, "executeTestSubscriber: subscribeConfig=" + subscribeConfig); 155 mWifiAwareSession.subscribe(subscribeConfig, mDiscoveryCb, mHandler); 156 157 // wait for results - subscribe session 158 CallbackUtils.DiscoveryCb.CallbackData callbackData = 159 mDiscoveryCb.waitForCallbacks( 160 ImmutableSet.of( 161 CallbackUtils.DiscoveryCb.CallbackCode.ON_SUBSCRIBE_STARTED, 162 CallbackUtils.DiscoveryCb.CallbackCode.ON_SESSION_CONFIG_FAILED)); 163 if (callbackData.callbackCode 164 != CallbackUtils.DiscoveryCb.CallbackCode.ON_SUBSCRIBE_STARTED) { 165 throw new WifiAwareSnippetException( 166 String.format("executeTestSubscriber: subscribe %s", 167 callbackData.callbackCode)); 168 } 169 mDiscoverySession = callbackData.subscribeDiscoverySession; 170 if (mDiscoverySession == null) { 171 throw new WifiAwareSnippetException( 172 "executeTestSubscriber: subscribe succeeded but null session returned"); 173 } 174 Log.d(TAG, "executeTestSubscriber: subscribe succeeded"); 175 176 // 3. wait for discovery 177 callbackData = 178 mDiscoveryCb.waitForCallbacks(ImmutableSet.of(isRangingRequired ? CallbackUtils 179 .DiscoveryCb.CallbackCode.ON_SERVICE_DISCOVERED_WITH_RANGE 180 : CallbackUtils.DiscoveryCb.CallbackCode.ON_SERVICE_DISCOVERED)); 181 182 if (callbackData.callbackCode == CallbackUtils.DiscoveryCb.CallbackCode.TIMEOUT) { 183 throw new WifiAwareSnippetException( 184 "executeTestSubscriber: waiting for discovery TIMEOUT"); 185 } 186 mPeerHandle = callbackData.peerHandle; 187 if (!isRangingRequired) { 188 Log.d(TAG, "executeTestSubscriber: discovery"); 189 } else { 190 Log.d(TAG, "executeTestSubscriber: discovery with range=" 191 + callbackData.distanceMm); 192 } 193 194 if (!Arrays.equals(PUB_SSI, callbackData.serviceSpecificInfo)) { 195 throw new WifiAwareSnippetException( 196 "executeTestSubscriber: discovery but SSI mismatch: rx='" 197 + new String(callbackData.serviceSpecificInfo, UTF_8) 198 + "'"); 199 } 200 if (callbackData.matchFilter.size() != 1 201 || !Arrays.equals(MATCH_FILTER_BYTES, callbackData.matchFilter.get(0))) { 202 StringBuilder sb = new StringBuilder(); 203 sb.append("size=").append(callbackData.matchFilter.size()); 204 for (byte[] mf : callbackData.matchFilter) { 205 sb.append(", e='").append(new String(mf, UTF_8)).append("'"); 206 } 207 throw new WifiAwareSnippetException( 208 "executeTestSubscriber: discovery but matchFilter mismatch: " + sb); 209 } 210 if (mPeerHandle == null) { 211 throw new WifiAwareSnippetException( 212 "executeTestSubscriber: discovery but null peerHandle"); 213 } 214 } 215 216 @Rpc(description = "Send message.") sendMessage(int messageId, String message)217 public void sendMessage(int messageId, String message) 218 throws InterruptedException, WifiAwareSnippetException { 219 // 4. send message & wait for send status 220 mDiscoverySession.sendMessage(mPeerHandle, messageId, message.getBytes(UTF_8)); 221 CallbackUtils.DiscoveryCb.CallbackData callbackData = 222 mDiscoveryCb.waitForCallbacks( 223 ImmutableSet.of( 224 CallbackUtils.DiscoveryCb.CallbackCode.ON_MESSAGE_SEND_SUCCEEDED, 225 CallbackUtils.DiscoveryCb.CallbackCode.ON_MESSAGE_SEND_FAILED)); 226 227 if (callbackData.callbackCode 228 != CallbackUtils.DiscoveryCb.CallbackCode.ON_MESSAGE_SEND_SUCCEEDED) { 229 throw new WifiAwareSnippetException( 230 String.format("executeTestSubscriber: sendMessage %s", 231 callbackData.callbackCode)); 232 } 233 Log.d(TAG, "executeTestSubscriber: send message succeeded"); 234 if (callbackData.messageId != messageId) { 235 throw new WifiAwareSnippetException( 236 "executeTestSubscriber: send message message ID mismatch: " 237 + callbackData.messageId); 238 } 239 } 240 241 @Rpc(description = "Create publish session.") publish(Boolean isUnsolicited, Boolean isRangingRequired, Boolean isPairingRequired)242 public void publish(Boolean isUnsolicited, Boolean isRangingRequired, Boolean isPairingRequired) 243 throws WifiAwareSnippetException, InterruptedException { 244 mDiscoveryCb = new CallbackUtils.DiscoveryCb(); 245 246 // 2. publish 247 List<byte[]> matchFilter = new ArrayList<>(); 248 matchFilter.add(MATCH_FILTER_BYTES); 249 PublishConfig.Builder builder = 250 new PublishConfig.Builder() 251 .setServiceName(SERVICE_NAME) 252 .setServiceSpecificInfo(PUB_SSI) 253 .setMatchFilter(matchFilter) 254 .setPublishType( 255 isUnsolicited 256 ? PublishConfig.PUBLISH_TYPE_UNSOLICITED 257 : PublishConfig.PUBLISH_TYPE_SOLICITED) 258 .setTerminateNotificationEnabled(true) 259 .setRangingEnabled(isRangingRequired); 260 if (isPairingRequired) { 261 builder.setPairingConfig(mPairingConfig); 262 } 263 PublishConfig publishConfig = builder.build(); 264 Log.d(TAG, "executeTestPublisher: publishConfig=" + publishConfig); 265 mWifiAwareSession.publish(publishConfig, mDiscoveryCb, mHandler); 266 267 // wait for results - publish session 268 CallbackUtils.DiscoveryCb.CallbackData callbackData = 269 mDiscoveryCb.waitForCallbacks( 270 ImmutableSet.of( 271 CallbackUtils.DiscoveryCb.CallbackCode.ON_PUBLISH_STARTED, 272 CallbackUtils.DiscoveryCb.CallbackCode.ON_SESSION_CONFIG_FAILED)); 273 if (callbackData.callbackCode 274 != CallbackUtils.DiscoveryCb.CallbackCode.ON_PUBLISH_STARTED) { 275 throw new WifiAwareSnippetException( 276 String.format("executeTestPublisher: publish %s", callbackData.callbackCode)); 277 } 278 mDiscoverySession = callbackData.publishDiscoverySession; 279 if (mDiscoverySession == null) { 280 throw new WifiAwareSnippetException( 281 "executeTestPublisher: publish succeeded but null session returned"); 282 } 283 Log.d(TAG, "executeTestPublisher: publish succeeded"); 284 } 285 286 @Rpc(description = "Initiate pairing setup, should be on subscriber") initiatePairingSetup(Boolean withPassword, Boolean accept)287 public void initiatePairingSetup(Boolean withPassword, Boolean accept) 288 throws InterruptedException, WifiAwareSnippetException { 289 mDiscoverySession.initiateBootstrappingRequest(mPeerHandle, 290 PAIRING_BOOTSTRAPPING_OPPORTUNISTIC); 291 CallbackUtils.DiscoveryCb.CallbackData callbackData = 292 mDiscoveryCb.waitForCallbacks(Set.of( 293 CallbackUtils.DiscoveryCb.CallbackCode.ON_BOOTSTRAPPING_CONFIRMED)); 294 if (callbackData.callbackCode 295 != CallbackUtils.DiscoveryCb.CallbackCode.ON_BOOTSTRAPPING_CONFIRMED) { 296 throw new WifiAwareSnippetException( 297 String.format("initiatePairingSetup: bootstrapping confirm missing %s", 298 callbackData.callbackCode)); 299 } 300 if (!callbackData.bootstrappingAccept 301 || callbackData.bootstrappingMethod != PAIRING_BOOTSTRAPPING_OPPORTUNISTIC) { 302 throw new WifiAwareSnippetException("initiatePairingSetup: bootstrapping failed"); 303 } 304 mDiscoverySession.initiatePairingRequest(mPeerHandle, ALIAS_PUBLISH, 305 Characteristics.WIFI_AWARE_CIPHER_SUITE_NCS_PK_PASN_128, 306 withPassword ? PASSWORD : null); 307 callbackData = 308 mDiscoveryCb.waitForCallbacks(Set.of( 309 CallbackUtils.DiscoveryCb.CallbackCode.ON_PAIRING_SETUP_CONFIRMED)); 310 if (callbackData.callbackCode 311 != CallbackUtils.DiscoveryCb.CallbackCode.ON_PAIRING_SETUP_CONFIRMED) { 312 throw new WifiAwareSnippetException( 313 String.format("initiatePairingSetup: pairing confirm missing %s", 314 callbackData.callbackCode)); 315 } 316 if (!accept) { 317 if (callbackData.pairingAccept) { 318 throw new WifiAwareSnippetException("initiatePairingSetup: pairing should be " 319 + "rejected"); 320 } 321 return; 322 } 323 if (!callbackData.pairingAccept) { 324 throw new WifiAwareSnippetException("initiatePairingSetup: pairing reject"); 325 } 326 mWifiAwareManager.removePairedDevice(ALIAS_PUBLISH); 327 AtomicReference<List<String>> aliasList = new AtomicReference<>(); 328 Consumer<List<String>> consumer = value -> { 329 synchronized (mLock) { 330 aliasList.set(value); 331 mLock.notify(); 332 } 333 }; 334 mWifiAwareManager.getPairedDevices(Executors.newSingleThreadScheduledExecutor(), consumer); 335 synchronized (mLock) { 336 mLock.wait(TEST_WAIT_DURATION_MS); 337 } 338 if (aliasList.get().size() != 1 || !ALIAS_PUBLISH.equals(aliasList.get().get(0))) { 339 throw new WifiAwareSnippetException("initiatePairingSetup: pairing alias mismatch"); 340 } 341 mWifiAwareManager.removePairedDevice(ALIAS_SUBSCRIBE); 342 mWifiAwareManager.getPairedDevices(Executors.newSingleThreadScheduledExecutor(), consumer); 343 synchronized (mLock) { 344 mLock.wait(TEST_WAIT_DURATION_MS); 345 } 346 if (!aliasList.get().isEmpty()) { 347 throw new WifiAwareSnippetException( 348 "initiatePairingSetup: pairing alias is not empty after " 349 + "removal"); 350 } 351 } 352 353 @Rpc(description = "respond to a pairing request, should be on publisher") respondToPairingSetup(Boolean withPassword, Boolean accept)354 public void respondToPairingSetup(Boolean withPassword, Boolean accept) 355 throws InterruptedException, WifiAwareSnippetException { 356 CallbackUtils.DiscoveryCb.CallbackData callbackData = mDiscoveryCb.waitForCallbacks(Set.of( 357 CallbackUtils.DiscoveryCb.CallbackCode.ON_BOOTSTRAPPING_CONFIRMED)); 358 if (callbackData.callbackCode 359 != CallbackUtils.DiscoveryCb.CallbackCode.ON_BOOTSTRAPPING_CONFIRMED) { 360 throw new WifiAwareSnippetException( 361 String.format("respondToPairingSetup: bootstrapping confirm missing %s", 362 callbackData.callbackCode)); 363 } 364 if (!callbackData.bootstrappingAccept 365 || callbackData.bootstrappingMethod != PAIRING_BOOTSTRAPPING_OPPORTUNISTIC) { 366 throw new WifiAwareSnippetException("respondToPairingSetup: bootstrapping failed"); 367 } 368 callbackData = 369 mDiscoveryCb.waitForCallbacks(Set.of( 370 CallbackUtils.DiscoveryCb.CallbackCode.ON_PAIRING_REQUEST_RECEIVED)); 371 if (callbackData.callbackCode 372 != CallbackUtils.DiscoveryCb.CallbackCode.ON_PAIRING_REQUEST_RECEIVED) { 373 throw new WifiAwareSnippetException( 374 String.format("respondToPairingSetup: pairing request missing %s", 375 callbackData.callbackCode)); 376 } 377 mPeerHandle = callbackData.peerHandle; 378 if (mPeerHandle == null) { 379 throw new WifiAwareSnippetException("respondToPairingSetup: peerHandle null"); 380 } 381 if (accept) { 382 mDiscoverySession.acceptPairingRequest(callbackData.pairingRequestId, mPeerHandle, 383 ALIAS_SUBSCRIBE, Characteristics.WIFI_AWARE_CIPHER_SUITE_NCS_PK_PASN_128, 384 withPassword ? PASSWORD : null); 385 } else { 386 mDiscoverySession.rejectPairingRequest(callbackData.pairingRequestId, mPeerHandle); 387 return; 388 } 389 callbackData = 390 mDiscoveryCb.waitForCallbacks(Set.of( 391 CallbackUtils.DiscoveryCb.CallbackCode.ON_PAIRING_SETUP_CONFIRMED)); 392 if (callbackData.callbackCode 393 != CallbackUtils.DiscoveryCb.CallbackCode.ON_PAIRING_SETUP_CONFIRMED) { 394 throw new WifiAwareSnippetException( 395 String.format("respondToPairingSetup: pairing confirm missing %s", 396 callbackData.callbackCode)); 397 } 398 if (!callbackData.pairingAccept) { 399 throw new WifiAwareSnippetException("respondToPairingSetup: pairing reject"); 400 } 401 mWifiAwareManager.removePairedDevice(ALIAS_PUBLISH); 402 AtomicReference<List<String>> aliasList = new AtomicReference<>(); 403 Consumer<List<String>> consumer = value -> { 404 synchronized (mLock) { 405 aliasList.set(value); 406 mLock.notify(); 407 } 408 }; 409 mWifiAwareManager.getPairedDevices(Executors.newSingleThreadScheduledExecutor(), consumer); 410 synchronized (mLock) { 411 mLock.wait(TEST_WAIT_DURATION_MS); 412 } 413 if (aliasList.get().size() != 1 || !ALIAS_PUBLISH.equals(aliasList.get().get(0))) { 414 throw new WifiAwareSnippetException("respondToPairingSetup: pairing alias mismatch"); 415 } 416 mWifiAwareManager.removePairedDevice(ALIAS_SUBSCRIBE); 417 mWifiAwareManager.getPairedDevices(Executors.newSingleThreadScheduledExecutor(), consumer); 418 synchronized (mLock) { 419 mLock.wait(TEST_WAIT_DURATION_MS); 420 } 421 if (!aliasList.get().isEmpty()) { 422 throw new WifiAwareSnippetException( 423 "respondToPairingSetup: pairing alias is not empty after " 424 + "removal"); 425 } 426 } 427 428 @Rpc(description = "Check if Aware pairing supported") checkIfPairingSupported()429 public Boolean checkIfPairingSupported() 430 throws WifiAwareSnippetException, InterruptedException { 431 if (!ApiLevelUtil.isAfter(Build.VERSION_CODES.TIRAMISU)) { 432 return false; 433 } 434 return mWifiAwareManager.getCharacteristics().isAwarePairingSupported(); 435 } 436 437 @Rpc(description = "Receive message.") receiveMessage()438 public String receiveMessage() throws WifiAwareSnippetException, InterruptedException { 439 // 3. wait to receive message. 440 CallbackUtils.DiscoveryCb.CallbackData callbackData = 441 mDiscoveryCb.waitForCallbacks( 442 ImmutableSet.of( 443 CallbackUtils.DiscoveryCb.CallbackCode.ON_MESSAGE_RECEIVED)); 444 mPeerHandle = callbackData.peerHandle; 445 Log.d(TAG, "executeTestPublisher: received message"); 446 447 if (mPeerHandle == null) { 448 throw new WifiAwareSnippetException( 449 "executeTestPublisher: received message but peerHandle is null!?"); 450 } 451 return new String(callbackData.serviceSpecificInfo, UTF_8); 452 } 453 454 @Override shutdown()455 public void shutdown() { 456 if (mDiscoverySession != null) { 457 mDiscoverySession.close(); 458 mDiscoverySession = null; 459 } 460 if (mWifiAwareSession != null) { 461 mWifiAwareSession.close(); 462 mWifiAwareSession = null; 463 } 464 mWifiAwareManager.resetPairedDevices(); 465 } 466 } 467