Add HandshakeHandler.

Will submit after
https://googleplex-android-review.googlesource.com/c/platform/packages/modules/Nearby/+/16032717,
where GattConnectionManager is added.

Test: skip test due to missing testing framework. Will be added later
per http://b/202524672.

Bug: 200231384
Change-Id: I8e5d197875385599a270fd6ba7ce8971d343ae64
diff --git a/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/HandshakeHandler.java b/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/HandshakeHandler.java
new file mode 100644
index 0000000..caeeb30
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/HandshakeHandler.java
@@ -0,0 +1,560 @@
+/*
+ * Copyright 2021 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.android.server.nearby.common.bluetooth.fastpair;
+
+import static com.android.server.nearby.common.bluetooth.fastpair.AesEcbSingleBlockEncryption.AES_BLOCK_LENGTH;
+import static com.android.server.nearby.common.bluetooth.fastpair.AesEcbSingleBlockEncryption.decrypt;
+import static com.android.server.nearby.common.bluetooth.fastpair.AesEcbSingleBlockEncryption.encrypt;
+import static com.android.server.nearby.common.bluetooth.fastpair.BluetoothAddress.maskBluetoothAddress;
+import static com.android.server.nearby.common.bluetooth.fastpair.Constants.BLUETOOTH_ADDRESS_LENGTH;
+import static com.android.server.nearby.common.bluetooth.fastpair.Constants.FastPairService.KeyBasedPairingCharacteristic.ActionOverBleFlag.ADDITIONAL_DATA_CHARACTERISTIC;
+import static com.android.server.nearby.common.bluetooth.fastpair.Constants.FastPairService.KeyBasedPairingCharacteristic.ActionOverBleFlag.DEVICE_ACTION;
+import static com.android.server.nearby.common.bluetooth.fastpair.Constants.FastPairService.KeyBasedPairingCharacteristic.Request.ADDITIONAL_DATA_TYPE_INDEX;
+import static com.android.server.nearby.common.bluetooth.fastpair.Constants.FastPairService.KeyBasedPairingCharacteristic.Request.EVENT_ADDITIONAL_DATA_INDEX;
+import static com.android.server.nearby.common.bluetooth.fastpair.Constants.FastPairService.KeyBasedPairingCharacteristic.Request.EVENT_ADDITIONAL_DATA_LENGTH_INDEX;
+import static com.android.server.nearby.common.bluetooth.fastpair.Constants.FastPairService.KeyBasedPairingCharacteristic.Request.EVENT_CODE_INDEX;
+import static com.android.server.nearby.common.bluetooth.fastpair.Constants.FastPairService.KeyBasedPairingCharacteristic.Request.EVENT_GROUP_INDEX;
+import static com.android.server.nearby.common.bluetooth.fastpair.Constants.FastPairService.KeyBasedPairingCharacteristic.Request.FLAGS_INDEX;
+import static com.android.server.nearby.common.bluetooth.fastpair.Constants.FastPairService.KeyBasedPairingCharacteristic.Request.SEEKER_PUBLIC_ADDRESS_INDEX;
+import static com.android.server.nearby.common.bluetooth.fastpair.Constants.FastPairService.KeyBasedPairingCharacteristic.Request.TYPE_ACTION_OVER_BLE;
+import static com.android.server.nearby.common.bluetooth.fastpair.Constants.FastPairService.KeyBasedPairingCharacteristic.Request.TYPE_INDEX;
+import static com.android.server.nearby.common.bluetooth.fastpair.Constants.FastPairService.KeyBasedPairingCharacteristic.Request.TYPE_KEY_BASED_PAIRING_REQUEST;
+import static com.android.server.nearby.common.bluetooth.fastpair.Constants.FastPairService.KeyBasedPairingCharacteristic.Request.VERIFICATION_DATA_INDEX;
+import static com.android.server.nearby.common.bluetooth.fastpair.Constants.FastPairService.KeyBasedPairingCharacteristic.Request.VERIFICATION_DATA_LENGTH;
+import static com.android.server.nearby.common.bluetooth.fastpair.FastPairDualConnection.logRetrySuccessEvent;
+import static com.android.server.nearby.common.bluetooth.fastpair.GattConnectionManager.isNoRetryError;
+
+import static com.google.common.base.Verify.verifyNotNull;
+import static com.google.common.io.BaseEncoding.base16;
+import static com.google.common.primitives.Bytes.concat;
+
+import static java.util.concurrent.TimeUnit.SECONDS;
+
+import android.os.SystemClock;
+import android.util.Log;
+
+import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
+import androidx.core.util.Consumer;
+
+import com.android.server.nearby.common.bluetooth.BluetoothException;
+import com.android.server.nearby.common.bluetooth.BluetoothTimeoutException;
+import com.android.server.nearby.common.bluetooth.fastpair.Constants.FastPairService;
+import com.android.server.nearby.common.bluetooth.fastpair.Constants.FastPairService.AdditionalDataCharacteristic.AdditionalDataType;
+import com.android.server.nearby.common.bluetooth.fastpair.Constants.FastPairService.KeyBasedPairingCharacteristic;
+import com.android.server.nearby.common.bluetooth.fastpair.Constants.FastPairService.KeyBasedPairingCharacteristic.ActionOverBleFlag;
+import com.android.server.nearby.common.bluetooth.fastpair.Constants.FastPairService.KeyBasedPairingCharacteristic.KeyBasedPairingRequestFlag;
+import com.android.server.nearby.common.bluetooth.fastpair.Constants.FastPairService.KeyBasedPairingCharacteristic.Request;
+import com.android.server.nearby.common.bluetooth.fastpair.FastPairConnection.SharedSecret;
+import com.android.server.nearby.common.bluetooth.gatt.BluetoothGattConnection;
+import com.android.server.nearby.common.bluetooth.gatt.BluetoothGattConnection.ChangeObserver;
+import com.android.server.nearby.common.bluetooth.util.BluetoothOperationExecutor.BluetoothOperationTimeoutException;
+import com.android.server.nearby.proto.FastPairEnums.FastPairEvent.ErrorCode;
+import com.android.server.nearby.proto.NearbyEventCodes.NearbyEvent.EventCode;
+
+import java.security.GeneralSecurityException;
+import java.security.SecureRandom;
+import java.util.Arrays;
+import java.util.UUID;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeoutException;
+
+/**
+ * Handles the handshake step of Fast Pair, the Provider's public address and the shared secret will
+ * be disclosed during this step. It is the first step of all key-based operations, e.g. key-based
+ * pairing and action over BLE.
+ *
+ * @see <a href="https://developers.google.com/nearby/fast-pair/spec#procedure">
+ *     Fastpair Spec Procedure</a>
+ */
+public class HandshakeHandler {
+
+    private static final String TAG = HandshakeHandler.class.getSimpleName();
+    private final GattConnectionManager mGattConnectionManager;
+    private final String mProviderBleAddress;
+    private final Preferences mPreferences;
+    private final EventLoggerWrapper mEventLogger;
+    @Nullable
+    private final FastPairConnection.FastPairSignalChecker mFastPairSignalChecker;
+
+    /**
+     * Keeps the keys used during handshaking, generated by {@link #createKey(byte[])}.
+     */
+    private static final class Keys {
+
+        private final byte[] mSharedSecret;
+        private final byte[] mPublicKey;
+
+        private Keys(byte[] sharedSecret, byte[] publicKey) {
+            this.mSharedSecret = sharedSecret;
+            this.mPublicKey = publicKey;
+        }
+    }
+
+    public HandshakeHandler(
+            GattConnectionManager gattConnectionManager,
+            String bleAddress,
+            Preferences preferences,
+            EventLoggerWrapper eventLogger,
+            @Nullable FastPairConnection.FastPairSignalChecker fastPairSignalChecker) {
+        this.mGattConnectionManager = gattConnectionManager;
+        this.mProviderBleAddress = bleAddress;
+        this.mPreferences = preferences;
+        this.mEventLogger = eventLogger;
+        this.mFastPairSignalChecker = fastPairSignalChecker;
+    }
+
+    /**
+     * Performs a handshake to authenticate and get the remote device's public address. Returns the
+     * AES-128 key as the shared secret for this pairing session.
+     */
+    public SharedSecret doHandshake(byte[] key, HandshakeMessage message)
+            throws GeneralSecurityException, InterruptedException, ExecutionException,
+            TimeoutException, BluetoothException, PairingException {
+        Keys keys = createKey(key);
+        Log.i(TAG,
+                "Handshake " + maskBluetoothAddress(mProviderBleAddress) + ", flags "
+                        + message.mFlags);
+        byte[] handshakeResponse =
+                processGattCommunication(
+                        createPacket(keys, message),
+                        SECONDS.toMillis(mPreferences.getGattOperationTimeoutSeconds()));
+        String providerPublicAddress = decodeResponse(keys.mSharedSecret, handshakeResponse);
+
+        return SharedSecret.create(keys.mSharedSecret, providerPublicAddress);
+    }
+
+    /**
+     * Performs a handshake to authenticate and get the remote device's public address. Returns the
+     * AES-128 key as the shared secret for this pairing session. Will retry and also performs
+     * FastPair signal check if fails.
+     */
+    public SharedSecret doHandshakeWithRetryAndSignalLostCheck(
+            byte[] key, HandshakeMessage message, @Nullable Consumer<ErrorCode> rescueFromError)
+            throws GeneralSecurityException, InterruptedException, ExecutionException,
+            TimeoutException, BluetoothException, PairingException {
+        Keys keys = createKey(key);
+        Log.i(TAG,
+                "Handshake " + maskBluetoothAddress(mProviderBleAddress) + ", flags "
+                        + message.mFlags);
+        int retryCount = 0;
+        byte[] handshakeResponse = null;
+        long startTime = SystemClock.elapsedRealtime();
+        BluetoothException lastException = null;
+        do {
+            try {
+                mEventLogger.setCurrentEvent(EventCode.SECRET_HANDSHAKE_GATT_COMMUNICATION);
+                handshakeResponse =
+                        processGattCommunication(
+                                createPacket(keys, message),
+                                getTimeoutMs(SystemClock.elapsedRealtime() - startTime));
+                mEventLogger.logCurrentEventSucceeded();
+                if (lastException != null) {
+                    logRetrySuccessEvent(EventCode.RECOVER_BY_RETRY_HANDSHAKE, lastException,
+                            mEventLogger);
+                }
+            } catch (BluetoothException e) {
+                lastException = e;
+                long spentTime = SystemClock.elapsedRealtime() - startTime;
+                Log.w(TAG, "Secret handshake failed, address="
+                        + maskBluetoothAddress(mProviderBleAddress)
+                        + ", spent time=" + spentTime + "ms, retryCount=" + retryCount);
+                mEventLogger.logCurrentEventFailed(e);
+
+                if (!mPreferences.getRetryGattConnectionAndSecretHandshake()) {
+                    throw e;
+                }
+
+                if (spentTime > mPreferences.getSecretHandshakeLongTimeoutRetryMaxSpentTimeMs()) {
+                    Log.w(TAG, "Spent too long time for handshake, timeInMs=" + spentTime);
+                    throw e;
+                }
+                if (isNoRetryError(mPreferences, e)) {
+                    throw e;
+                }
+
+                if (mFastPairSignalChecker != null) {
+                    FastPairDualConnection
+                            .checkFastPairSignal(mFastPairSignalChecker, mProviderBleAddress, e);
+                }
+                retryCount++;
+                if (retryCount > mPreferences.getSecretHandshakeRetryAttempts()
+                        || ((e instanceof BluetoothOperationTimeoutException)
+                        && !mPreferences.getRetrySecretHandshakeTimeout())) {
+                    throw new HandshakeException("Fail on handshake!", e);
+                }
+                if (rescueFromError != null) {
+                    rescueFromError.accept(
+                            (e instanceof BluetoothTimeoutException
+                                    || e instanceof BluetoothOperationTimeoutException)
+                                    ? ErrorCode.SUCCESS_RETRY_SECRET_HANDSHAKE_TIMEOUT
+                                    : ErrorCode.SUCCESS_RETRY_SECRET_HANDSHAKE_ERROR);
+                }
+            }
+        } while (mPreferences.getRetryGattConnectionAndSecretHandshake()
+                && handshakeResponse == null);
+        if (retryCount > 0) {
+            Log.i(TAG, "Secret handshake failed but restored by retry, retry count=" + retryCount);
+        }
+        String providerPublicAddress =
+                decodeResponse(keys.mSharedSecret, verifyNotNull(handshakeResponse));
+
+        return SharedSecret.create(keys.mSharedSecret, providerPublicAddress);
+    }
+
+    @VisibleForTesting
+    long getTimeoutMs(long spentTime) {
+        if (!mPreferences.getRetryGattConnectionAndSecretHandshake()) {
+            return SECONDS.toMillis(mPreferences.getGattOperationTimeoutSeconds());
+        } else {
+            return spentTime < mPreferences.getSecretHandshakeShortTimeoutRetryMaxSpentTimeMs()
+                    ? mPreferences.getSecretHandshakeShortTimeoutMs()
+                    : mPreferences.getSecretHandshakeLongTimeoutMs();
+        }
+    }
+
+    /**
+     * If the given key is an ecc-256 public key (currently, we are using secp256r1), the shared
+     * secret is generated by ECDH; if the input key is AES-128 key (should be the account key),
+     * then it is the shared secret.
+     */
+    private Keys createKey(byte[] key) throws GeneralSecurityException {
+        if (key.length == EllipticCurveDiffieHellmanExchange.PUBLIC_KEY_LENGTH) {
+            EllipticCurveDiffieHellmanExchange exchange = EllipticCurveDiffieHellmanExchange
+                    .create();
+            byte[] publicKey = exchange.getPublicKey();
+            if (publicKey != null) {
+                Log.i(TAG, "Handshake " + maskBluetoothAddress(mProviderBleAddress)
+                        + ", generates key by ECDH.");
+            } else {
+                throw new GeneralSecurityException("Failed to do ECDH.");
+            }
+            return new Keys(exchange.generateSecret(key), publicKey);
+        } else if (key.length == AesEcbSingleBlockEncryption.KEY_LENGTH) {
+            Log.i(TAG, "Handshake " + maskBluetoothAddress(mProviderBleAddress)
+                    + ", using the given secret.");
+            return new Keys(key, new byte[0]);
+        } else {
+            throw new GeneralSecurityException("Key length is not correct: " + key.length);
+        }
+    }
+
+    private static byte[] createPacket(Keys keys, HandshakeMessage message)
+            throws GeneralSecurityException {
+        byte[] encryptedMessage = encrypt(keys.mSharedSecret, message.getBytes());
+        return concat(encryptedMessage, keys.mPublicKey);
+    }
+
+    private byte[] processGattCommunication(byte[] packet, long gattOperationTimeoutMS)
+            throws BluetoothException, InterruptedException, ExecutionException, TimeoutException {
+        BluetoothGattConnection gattConnection = mGattConnectionManager.getConnection();
+        gattConnection.setOperationTimeout(gattOperationTimeoutMS);
+        UUID characteristicUuid = KeyBasedPairingCharacteristic.getId(gattConnection);
+        ChangeObserver changeObserver =
+                gattConnection.enableNotification(FastPairService.ID, characteristicUuid);
+
+        Log.i(TAG,
+                "Writing handshake packet to address=" + maskBluetoothAddress(mProviderBleAddress));
+        gattConnection.writeCharacteristic(FastPairService.ID, characteristicUuid, packet);
+        Log.i(TAG, "Waiting handshake packet from address=" + maskBluetoothAddress(
+                mProviderBleAddress));
+        return changeObserver.waitForUpdate(gattOperationTimeoutMS);
+    }
+
+    private String decodeResponse(byte[] sharedSecret, byte[] response)
+            throws PairingException, GeneralSecurityException {
+        if (response.length != AES_BLOCK_LENGTH) {
+            throw new PairingException(
+                    "Handshake failed because of incorrect response: " + base16().encode(response));
+        }
+        // 1 byte type, 6 bytes public address, remainder random salt.
+        byte[] decryptedResponse = decrypt(sharedSecret, response);
+        if (decryptedResponse[0] != KeyBasedPairingCharacteristic.Response.TYPE) {
+            throw new PairingException(
+                    "Handshake response type incorrect: " + decryptedResponse[0]);
+        }
+        String address = BluetoothAddress.encode(Arrays.copyOfRange(decryptedResponse, 1, 7));
+        Log.i(TAG, "Handshake success with public " + maskBluetoothAddress(address) + ", ble "
+                + maskBluetoothAddress(mProviderBleAddress));
+        return address;
+    }
+
+    /**
+     * The base class for handshake message that contains the common data: message type, flags and
+     * verification data.
+     */
+    abstract static class HandshakeMessage {
+
+        final byte mType;
+        final byte mFlags;
+        private final byte[] mVerificationData;
+
+        HandshakeMessage(Builder<?> builder) {
+            this.mType = builder.mType;
+            this.mVerificationData = builder.mVerificationData;
+            this.mFlags = builder.mFlags;
+        }
+
+        abstract static class Builder<T extends Builder<T>> {
+
+            byte mType;
+            byte mFlags;
+            private byte[] mVerificationData;
+
+            abstract T getThis();
+
+            T setVerificationData(byte[] verificationData) {
+                if (verificationData.length != BLUETOOTH_ADDRESS_LENGTH) {
+                    throw new IllegalArgumentException(
+                            "Incorrect verification data length: " + verificationData.length + ".");
+                }
+                this.mVerificationData = verificationData;
+                return getThis();
+            }
+        }
+
+        /**
+         * Constructs the base handshake message according to the format of Fast Pair spec.
+         */
+        byte[] constructBaseBytes() {
+            byte[] rawMessage = new byte[Request.SIZE];
+            new SecureRandom().nextBytes(rawMessage);
+            rawMessage[TYPE_INDEX] = mType;
+            rawMessage[FLAGS_INDEX] = mFlags;
+
+            System.arraycopy(
+                    mVerificationData,
+                    /* srcPos= */ 0,
+                    rawMessage,
+                    VERIFICATION_DATA_INDEX,
+                    VERIFICATION_DATA_LENGTH);
+            return rawMessage;
+        }
+
+        /**
+         * Returns the raw handshake message.
+         */
+        abstract byte[] getBytes();
+    }
+
+    /**
+     * Extends {@link HandshakeMessage} and contains the required data for key-based pairing
+     * request.
+     */
+    public static class KeyBasedPairingRequest extends HandshakeMessage {
+
+        @Nullable
+        private final byte[] mSeekerPublicAddress;
+
+        private KeyBasedPairingRequest(Builder builder) {
+            super(builder);
+            this.mSeekerPublicAddress = builder.mSeekerPublicAddress;
+        }
+
+        @Override
+        byte[] getBytes() {
+            byte[] rawMessage = constructBaseBytes();
+            if (mSeekerPublicAddress != null) {
+                System.arraycopy(
+                        mSeekerPublicAddress,
+                        /* srcPos= */ 0,
+                        rawMessage,
+                        SEEKER_PUBLIC_ADDRESS_INDEX,
+                        BLUETOOTH_ADDRESS_LENGTH);
+            }
+            Log.i(TAG,
+                    "Handshake Message: type (" + rawMessage[TYPE_INDEX] + "), flag ("
+                            + rawMessage[FLAGS_INDEX] + ").");
+            return rawMessage;
+        }
+
+        /**
+         * Builder class for key-based pairing request.
+         */
+        public static class Builder extends HandshakeMessage.Builder<Builder> {
+
+            @Nullable
+            private byte[] mSeekerPublicAddress;
+
+            /**
+             * Adds flags without changing other flags.
+             */
+            public Builder addFlag(KeyBasedPairingRequestFlag flag) {
+                this.mFlags |= flag.getValue();
+                return this;
+            }
+
+            /**
+             * Set seeker's public address.
+             */
+            public Builder setSeekerPublicAddress(byte[] seekerPublicAddress) {
+                this.mSeekerPublicAddress = seekerPublicAddress;
+                return this;
+            }
+
+            /**
+             * Buulds KeyBasedPairigRequest.
+             */
+            public KeyBasedPairingRequest build() {
+                mType = TYPE_KEY_BASED_PAIRING_REQUEST;
+                return new KeyBasedPairingRequest(this);
+            }
+
+            @Override
+            Builder getThis() {
+                return this;
+            }
+        }
+    }
+
+    /**
+     * Extends {@link HandshakeMessage} and contains the required data for action over BLE request.
+     */
+    public static class ActionOverBle extends HandshakeMessage {
+
+        private final byte mEventGroup;
+        private final byte mEventCode;
+        @Nullable
+        private final byte[] mEventData;
+        private final byte mAdditionalDataType;
+
+        private ActionOverBle(Builder builder) {
+            super(builder);
+            this.mEventGroup = builder.mEventGroup;
+            this.mEventCode = builder.mEventCode;
+            this.mEventData = builder.mEventData;
+            this.mAdditionalDataType = builder.mAdditionalDataType;
+        }
+
+        @Override
+        byte[] getBytes() {
+            byte[] rawMessage = constructBaseBytes();
+            StringBuilder stringBuilder =
+                    new StringBuilder(
+                            String.format(
+                                    "type (%02X), flag (%02X)", rawMessage[TYPE_INDEX],
+                                    rawMessage[FLAGS_INDEX]));
+            if ((mFlags & DEVICE_ACTION.getValue()) != 0) {
+                rawMessage[EVENT_GROUP_INDEX] = mEventGroup;
+                rawMessage[EVENT_CODE_INDEX] = mEventCode;
+
+                if (mEventData != null) {
+                    rawMessage[EVENT_ADDITIONAL_DATA_LENGTH_INDEX] = (byte) mEventData.length;
+                    System.arraycopy(
+                            mEventData,
+                            /* srcPos= */ 0,
+                            rawMessage,
+                            EVENT_ADDITIONAL_DATA_INDEX,
+                            mEventData.length);
+                } else {
+                    rawMessage[EVENT_ADDITIONAL_DATA_LENGTH_INDEX] = (byte) 0;
+                }
+                stringBuilder.append(
+                        String.format(
+                                ", group(%02X), code(%02X), length(%02X)",
+                                rawMessage[EVENT_GROUP_INDEX],
+                                rawMessage[EVENT_CODE_INDEX],
+                                rawMessage[EVENT_ADDITIONAL_DATA_LENGTH_INDEX]));
+            }
+            if ((mFlags & ADDITIONAL_DATA_CHARACTERISTIC.getValue()) != 0) {
+                rawMessage[ADDITIONAL_DATA_TYPE_INDEX] = mAdditionalDataType;
+                stringBuilder.append(
+                        String.format(", data id(%02X)", rawMessage[ADDITIONAL_DATA_TYPE_INDEX]));
+            }
+            Log.i(TAG, "Handshake Message: " + stringBuilder);
+            return rawMessage;
+        }
+
+        /**
+         * Builder class for action over BLE request.
+         */
+        public static class Builder extends HandshakeMessage.Builder<Builder> {
+
+            private byte mEventGroup;
+            private byte mEventCode;
+            @Nullable
+            private byte[] mEventData;
+            private byte mAdditionalDataType;
+
+            // Adds a flag to this handshake message. This can be called repeatedly for adding
+            // different preference.
+
+            /**
+             * Adds flag without changing other flags.
+             */
+            public Builder addFlag(ActionOverBleFlag flag) {
+                this.mFlags |= flag.getValue();
+                return this;
+            }
+
+            /**
+             * Set event group and event code.
+             */
+            public Builder setEvent(int eventGroup, int eventCode) {
+                this.mFlags |= DEVICE_ACTION.getValue();
+                this.mEventGroup = (byte) (eventGroup & 0xFF);
+                this.mEventCode = (byte) (eventCode & 0xFF);
+                return this;
+            }
+
+            /**
+             * Set event additional data.
+             */
+            public Builder setEventAdditionalData(byte[] data) {
+                this.mEventData = data;
+                return this;
+            }
+
+            /**
+             * Set event additional data type.
+             */
+            public Builder setAdditionalDataType(AdditionalDataType additionalDataType) {
+                this.mFlags |= ADDITIONAL_DATA_CHARACTERISTIC.getValue();
+                this.mAdditionalDataType = additionalDataType.getValue();
+                return this;
+            }
+
+            @Override
+            Builder getThis() {
+                return this;
+            }
+
+            ActionOverBle build() {
+                mType = TYPE_ACTION_OVER_BLE;
+                return new ActionOverBle(this);
+            }
+        }
+    }
+
+    /**
+     * Exception for handshake failure.
+     */
+    public static class HandshakeException extends PairingException {
+
+        private final BluetoothException mOriginalException;
+
+        @VisibleForTesting
+        HandshakeException(String format, BluetoothException e) {
+            super(format);
+            mOriginalException = e;
+        }
+
+        public BluetoothException getOriginalException() {
+            return mOriginalException;
+        }
+    }
+}