/* * Copyright (C) 2023 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.snippet.wifi.aware; import static java.util.concurrent.TimeUnit.SECONDS; import android.net.ConnectivityManager; import android.net.Network; import android.net.NetworkCapabilities; import android.net.wifi.aware.AttachCallback; import android.net.wifi.aware.AwarePairingConfig; import android.net.wifi.aware.DiscoverySessionCallback; import android.net.wifi.aware.IdentityChangedListener; import android.net.wifi.aware.PeerHandle; import android.net.wifi.aware.PublishDiscoverySession; import android.net.wifi.aware.ServiceDiscoveryInfo; import android.net.wifi.aware.SubscribeDiscoverySession; import android.net.wifi.aware.WifiAwareSession; import android.net.wifi.rtt.RangingResult; import android.net.wifi.rtt.RangingResultCallback; import android.os.Build; import android.util.Log; import android.util.Pair; import com.android.compatibility.common.util.ApiLevelUtil; import com.google.common.collect.ImmutableSet; import java.util.ArrayDeque; import java.util.List; import java.util.Set; import java.util.concurrent.CountDownLatch; /** Blocking callbacks for Wi-Fi Aware and Connectivity Manager. */ public final class CallbackUtils { private static final String TAG = "CallbackUtils"; public static final int CALLBACK_TIMEOUT_SEC = 15; /** * Utility AttachCallback - provides mechanism to block execution with the waitForAttach method. */ public static class AttachCb extends AttachCallback { /** Callback codes. */ public enum CallbackCode { TIMEOUT, ON_ATTACHED, ON_ATTACH_FAILED } private final CountDownLatch mBlocker = new CountDownLatch(1); private CallbackCode mCallbackCode = CallbackCode.TIMEOUT; private WifiAwareSession mWifiAwareSession = null; @Override public void onAttached(WifiAwareSession session) { mCallbackCode = CallbackCode.ON_ATTACHED; mWifiAwareSession = session; mBlocker.countDown(); } @Override public void onAttachFailed() { mCallbackCode = CallbackCode.ON_ATTACH_FAILED; mBlocker.countDown(); } /** * Wait (blocks) for any AttachCallback callback or timeout. * * @return A pair of values: the callback constant (or TIMEOUT) and the WifiAwareSession * created * when attach successful - null otherwise (attach failure or timeout). */ public Pair waitForAttach() throws InterruptedException { if (mBlocker.await(CALLBACK_TIMEOUT_SEC, SECONDS)) { return new Pair<>(mCallbackCode, mWifiAwareSession); } return new Pair<>(CallbackCode.TIMEOUT, null); } } /** * Utility IdentityChangedListener - provides mechanism to block execution with the * waitForIdentity method. Single shot listener - only listens for the first triggered callback. */ public static class IdentityListenerSingleShot extends IdentityChangedListener { private final CountDownLatch mBlocker = new CountDownLatch(1); private byte[] mMac = null; @Override public void onIdentityChanged(byte[] mac) { if (this.mMac != null) { return; } this.mMac = mac; mBlocker.countDown(); } /** * Wait (blocks) for the onIdentityChanged callback or a timeout. * * @return The MAC address returned by the onIdentityChanged() callback, or null on timeout. */ public byte[] waitForMac() throws InterruptedException { if (mBlocker.await(CALLBACK_TIMEOUT_SEC, SECONDS)) { return mMac; } return null; } } /** * Utility NetworkCallback - provides mechanism for blocking/serializing access with the * waitForNetwork method. */ public static class NetworkCb extends ConnectivityManager.NetworkCallback { private final CountDownLatch mBlocker = new CountDownLatch(1); private Network mNetwork = null; private NetworkCapabilities mNetworkCapabilities = null; @Override public void onUnavailable() { mNetworkCapabilities = null; mBlocker.countDown(); } @Override public void onCapabilitiesChanged(Network network, NetworkCapabilities networkCapabilities) { this.mNetwork = network; this.mNetworkCapabilities = networkCapabilities; mBlocker.countDown(); } /** * Wait (blocks) for Capabilities Changed callback - or timesout. * * @return Network + NetworkCapabilities (pair) if occurred, null otherwise. */ public Pair waitForNetworkCapabilities() throws InterruptedException { if (mBlocker.await(CALLBACK_TIMEOUT_SEC, SECONDS)) { return Pair.create(mNetwork, mNetworkCapabilities); } return null; } } /** * Utility DiscoverySessionCallback - provides mechanism to block/serialize Aware discovery * operations using the waitForCallbacks() method. */ public static class DiscoveryCb extends DiscoverySessionCallback { /** Callback codes. */ public enum CallbackCode { TIMEOUT, ON_PUBLISH_STARTED, ON_SUBSCRIBE_STARTED, ON_SESSION_CONFIG_UPDATED, ON_SESSION_CONFIG_FAILED, ON_SESSION_TERMINATED, ON_SERVICE_DISCOVERED, ON_MESSAGE_SEND_SUCCEEDED, ON_MESSAGE_SEND_FAILED, ON_MESSAGE_RECEIVED, ON_SERVICE_DISCOVERED_WITH_RANGE, ON_PAIRING_REQUEST_RECEIVED, ON_PAIRING_SETUP_CONFIRMED, ON_BOOTSTRAPPING_CONFIRMED, ON_PAIRING_VERIFICATION_CONFIRMED, } ; /** * Data container for all parameters which can be returned by any DiscoverySessionCallback * callback. */ public static class CallbackData { public CallbackData(CallbackCode callbackCode) { this.callbackCode = callbackCode; } public CallbackCode callbackCode; public PublishDiscoverySession publishDiscoverySession; public SubscribeDiscoverySession subscribeDiscoverySession; public PeerHandle peerHandle; public byte[] serviceSpecificInfo; public List matchFilter; public int messageId; public int distanceMm; public int pairingRequestId; public boolean pairingAccept; public boolean bootstrappingAccept; public String pairingAlias; public int bootstrappingMethod; public String pairedAlias; public AwarePairingConfig pairingConfig; } private CountDownLatch mBlocker = null; private Set mWaitForCallbackCodes = ImmutableSet.of(); private final Object mLock = new Object(); private final ArrayDeque mCallbackQueue = new ArrayDeque<>(); private void processCallback(CallbackData callbackData) { synchronized (mLock) { mCallbackQueue.addLast(callbackData); if (mBlocker != null && mWaitForCallbackCodes.contains(callbackData.callbackCode)) { mBlocker.countDown(); } } } private CallbackData getAndRemoveFirst(Set callbackCodes) { synchronized (mLock) { for (CallbackData cbd : mCallbackQueue) { if (callbackCodes.contains(cbd.callbackCode)) { mCallbackQueue.remove(cbd); return cbd; } } } return null; } private CallbackData waitForCallbacks(Set callbackCodes, boolean timeout) throws InterruptedException { synchronized (mLock) { CallbackData cbd = getAndRemoveFirst(callbackCodes); if (cbd != null) { return cbd; } mWaitForCallbackCodes = callbackCodes; mBlocker = new CountDownLatch(1); } boolean finishedNormally = true; if (timeout) { finishedNormally = mBlocker.await(CALLBACK_TIMEOUT_SEC, SECONDS); } else { mBlocker.await(); } if (finishedNormally) { CallbackData cbd = getAndRemoveFirst(callbackCodes); if (cbd != null) { return cbd; } Log.wtf( TAG, "DiscoveryCb.waitForCallback: callbackCodes=" + callbackCodes + ": did not time-out but doesn't have any of the requested " + "callbacks in " + "the stack!?"); // falling-through to TIMEOUT } return new CallbackData(CallbackCode.TIMEOUT); } /** * Wait for the specified callbacks - a bitmask of any of the ON_* constants. Returns the * CallbackData structure whose CallbackData.callback specifies the callback which was * triggered. The callback may be TIMEOUT. * *

Note: other callbacks happening while while waiting for the specified callback(s) will * be * queued. */ public CallbackData waitForCallbacks(Set callbackCodes) throws InterruptedException { return waitForCallbacks(callbackCodes, true); } /** * Wait for the specified callbacks - a bitmask of any of the ON_* constants. Returns the * CallbackData structure whose CallbackData.callback specifies the callback which was * triggered. * *

This call will not timeout - it can be interrupted though (which results in a thrown * exception). * *

Note: other callbacks happening while while waiting for the specified callback(s) will * be * queued. */ public CallbackData waitForCallbacksNoTimeout(Set callbackCodes) throws InterruptedException { return waitForCallbacks(callbackCodes, false); } @Override public void onPublishStarted(PublishDiscoverySession session) { CallbackData callbackData = new CallbackData(CallbackCode.ON_PUBLISH_STARTED); callbackData.publishDiscoverySession = session; processCallback(callbackData); } @Override public void onSubscribeStarted(SubscribeDiscoverySession session) { CallbackData callbackData = new CallbackData(CallbackCode.ON_SUBSCRIBE_STARTED); callbackData.subscribeDiscoverySession = session; processCallback(callbackData); } @Override public void onSessionConfigUpdated() { CallbackData callbackData = new CallbackData(CallbackCode.ON_SESSION_CONFIG_UPDATED); processCallback(callbackData); } @Override public void onSessionConfigFailed() { CallbackData callbackData = new CallbackData(CallbackCode.ON_SESSION_CONFIG_FAILED); processCallback(callbackData); } @Override public void onSessionTerminated() { CallbackData callbackData = new CallbackData(CallbackCode.ON_SESSION_TERMINATED); processCallback(callbackData); } @Override public void onServiceDiscovered( PeerHandle peerHandle, byte[] serviceSpecificInfo, List matchFilter) { CallbackData callbackData = new CallbackData(CallbackCode.ON_SERVICE_DISCOVERED); callbackData.peerHandle = peerHandle; callbackData.serviceSpecificInfo = serviceSpecificInfo; callbackData.matchFilter = matchFilter; processCallback(callbackData); } @Override public void onServiceDiscovered(ServiceDiscoveryInfo info) { CallbackData callbackData = new CallbackData(CallbackCode.ON_SERVICE_DISCOVERED); callbackData.peerHandle = info.getPeerHandle(); callbackData.serviceSpecificInfo = info.getServiceSpecificInfo(); callbackData.matchFilter = info.getMatchFilters(); if (ApiLevelUtil.isAfter(Build.VERSION_CODES.TIRAMISU)) { callbackData.pairedAlias = info.getPairedAlias(); callbackData.pairingConfig = info.getPairingConfig(); } processCallback(callbackData); } @Override public void onServiceDiscoveredWithinRange( PeerHandle peerHandle, byte[] serviceSpecificInfo, List matchFilter, int distanceMm) { CallbackData callbackData = new CallbackData( CallbackCode.ON_SERVICE_DISCOVERED_WITH_RANGE); callbackData.peerHandle = peerHandle; callbackData.serviceSpecificInfo = serviceSpecificInfo; callbackData.matchFilter = matchFilter; callbackData.distanceMm = distanceMm; processCallback(callbackData); } @Override public void onMessageSendSucceeded(int messageId) { CallbackData callbackData = new CallbackData(CallbackCode.ON_MESSAGE_SEND_SUCCEEDED); callbackData.messageId = messageId; processCallback(callbackData); } @Override public void onMessageSendFailed(int messageId) { CallbackData callbackData = new CallbackData(CallbackCode.ON_MESSAGE_SEND_FAILED); callbackData.messageId = messageId; processCallback(callbackData); } @Override public void onMessageReceived(PeerHandle peerHandle, byte[] message) { CallbackData callbackData = new CallbackData(CallbackCode.ON_MESSAGE_RECEIVED); callbackData.peerHandle = peerHandle; callbackData.serviceSpecificInfo = message; processCallback(callbackData); } @Override public void onPairingSetupRequestReceived(PeerHandle peerHandle, int requestId) { CallbackData callbackData = new CallbackData(CallbackCode.ON_PAIRING_REQUEST_RECEIVED); callbackData.peerHandle = peerHandle; callbackData.pairingRequestId = requestId; processCallback(callbackData); } @Override public void onPairingSetupSucceeded(PeerHandle peerHandle, String alias) { CallbackData callbackData = new CallbackData(CallbackCode.ON_PAIRING_SETUP_CONFIRMED); callbackData.peerHandle = peerHandle; callbackData.pairingAccept = true; callbackData.pairingAlias = alias; processCallback(callbackData); } @Override public void onPairingSetupFailed(PeerHandle peerHandle) { CallbackData callbackData = new CallbackData(CallbackCode.ON_PAIRING_SETUP_CONFIRMED); callbackData.peerHandle = peerHandle; callbackData.pairingAccept = false; processCallback(callbackData); } @Override public void onPairingVerificationSucceed(PeerHandle peerHandle, String alias) { CallbackData callbackData = new CallbackData( CallbackCode.ON_PAIRING_VERIFICATION_CONFIRMED); callbackData.peerHandle = peerHandle; callbackData.pairingAccept = true; callbackData.pairingAlias = alias; processCallback(callbackData); } @Override public void onPairingVerificationFailed(PeerHandle peerHandle) { CallbackData callbackData = new CallbackData( CallbackCode.ON_PAIRING_VERIFICATION_CONFIRMED); callbackData.peerHandle = peerHandle; callbackData.pairingAccept = false; processCallback(callbackData); } @Override public void onBootstrappingSucceeded(PeerHandle peerHandle, int method) { CallbackData callbackData = new CallbackData(CallbackCode.ON_BOOTSTRAPPING_CONFIRMED); callbackData.peerHandle = peerHandle; callbackData.bootstrappingAccept = true; callbackData.bootstrappingMethod = method; processCallback(callbackData); } @Override public void onBootstrappingFailed(PeerHandle peerHandle) { CallbackData callbackData = new CallbackData(CallbackCode.ON_BOOTSTRAPPING_CONFIRMED); callbackData.peerHandle = peerHandle; callbackData.bootstrappingAccept = false; processCallback(callbackData); } } /** * Utility RangingResultCallback - provides mechanism for blocking/serializing access with the * waitForRangingResults method. */ public static class RangingCb extends RangingResultCallback { public static final int TIMEOUT = -1; public static final int ON_FAILURE = 0; public static final int ON_RESULTS = 1; private final CountDownLatch mBlocker = new CountDownLatch(1); private int mStatus = TIMEOUT; private List mResults = null; /** * Wait (blocks) for Ranging results callbacks - or times-out. * * @return Pair of status & Ranging results if succeeded, null otherwise. */ public Pair> waitForRangingResults() throws InterruptedException { if (mBlocker.await(CALLBACK_TIMEOUT_SEC, SECONDS)) { return new Pair<>(mStatus, mResults); } return new Pair<>(TIMEOUT, null); } @Override public void onRangingFailure(int code) { mStatus = ON_FAILURE; mBlocker.countDown(); } @Override public void onRangingResults(List results) { mStatus = ON_RESULTS; this.mResults = results; mBlocker.countDown(); } } private CallbackUtils() { } }