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