Add AccountKeyGenerator, BluetoothClassicPairer, DeviceIntentReceiver,
SimpleBroadcastReceiver.

DeviceIntentReceiver and SimpleBroadcastReceiver will be tested by usage class.
BluetoothClassPairer can't be tested without a test framework. Has added a TODO.
// TODO(b/202524672): Add class unit test.

Test: unit test

Bug: 200231384
Change-Id: I97343ea932dd7a7f3563a990dbfd638fdf5025dc
diff --git a/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/AccountKeyGenerator.java b/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/AccountKeyGenerator.java
new file mode 100644
index 0000000..28a9c33
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/AccountKeyGenerator.java
@@ -0,0 +1,45 @@
+/*
+ * 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.generateKey;
+
+import com.android.server.nearby.common.bluetooth.fastpair.Constants.FastPairService.AccountKeyCharacteristic;
+
+import java.security.NoSuchAlgorithmException;
+
+/**
+ * This is to generate account key with fast-pair style.
+ */
+public final class AccountKeyGenerator {
+
+    // Generate a key where the first byte is always defined as the type, 0x04. This maintains 15
+    // bytes of entropy in the key while also allowing providers to verify that they have received
+    // a properly formatted key and decrypted it correctly, minimizing the risk of replay attacks.
+
+    /**
+     * Creates account key.
+     */
+    public static byte[] createAccountKey() throws NoSuchAlgorithmException {
+        byte[] accountKey = generateKey();
+        accountKey[0] = AccountKeyCharacteristic.TYPE;
+        return accountKey;
+    }
+
+    private AccountKeyGenerator() {
+    }
+}
diff --git a/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/BluetoothClassicPairer.java b/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/BluetoothClassicPairer.java
new file mode 100644
index 0000000..6c467d3
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/BluetoothClassicPairer.java
@@ -0,0 +1,180 @@
+/*
+ * 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 android.bluetooth.BluetoothDevice.BOND_BONDED;
+import static android.bluetooth.BluetoothDevice.BOND_BONDING;
+import static android.bluetooth.BluetoothDevice.BOND_NONE;
+import static android.bluetooth.BluetoothDevice.ERROR;
+import static android.bluetooth.BluetoothDevice.EXTRA_DEVICE;
+
+import static com.android.server.nearby.common.bluetooth.fastpair.BluetoothAddress.maskBluetoothAddress;
+
+import static java.util.concurrent.TimeUnit.SECONDS;
+
+import android.annotation.SuppressLint;
+import android.bluetooth.BluetoothDevice;
+import android.content.Context;
+import android.content.Intent;
+import android.util.Log;
+
+import androidx.annotation.WorkerThread;
+
+import com.google.common.base.Strings;
+
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeoutException;
+
+/**
+ * Pairs to Bluetooth classic devices with passkey confirmation.
+ */
+// TODO(b/202524672): Add class unit test.
+public class BluetoothClassicPairer {
+
+    private static final String TAG = BluetoothClassicPairer.class.getSimpleName();
+    /**
+     * Hidden, see {@link BluetoothDevice}.
+     */
+    private static final String EXTRA_REASON = "android.bluetooth.device.extra.REASON";
+
+    private final Context mContext;
+    private final BluetoothDevice mDevice;
+    private final Preferences mPreferences;
+    private final PasskeyConfirmationHandler mPasskeyConfirmationHandler;
+
+    public BluetoothClassicPairer(
+            Context context,
+            BluetoothDevice device,
+            Preferences preferences,
+            PasskeyConfirmationHandler passkeyConfirmationHandler) {
+        this.mContext = context;
+        this.mDevice = device;
+        this.mPreferences = preferences;
+        this.mPasskeyConfirmationHandler = passkeyConfirmationHandler;
+    }
+
+    /**
+     * Pairs with the device. Throws a {@link PairingException} if any error occurs.
+     */
+    @WorkerThread
+    public void pair() throws PairingException {
+        Log.i(TAG, "BluetoothClassicPairer, createBond with " + maskBluetoothAddress(mDevice)
+                + ", type=" + mDevice.getType());
+        try (BondedReceiver bondedReceiver = new BondedReceiver()) {
+            if (mDevice.createBond()) {
+                bondedReceiver.await(mPreferences.getCreateBondTimeoutSeconds(), SECONDS);
+            } else {
+                throw new PairingException(
+                        "BluetoothClassicPairer, createBond got immediate error");
+            }
+        } catch (TimeoutException | InterruptedException | ExecutionException e) {
+            throw new PairingException("BluetoothClassicPairer, createBond failed", e);
+        }
+    }
+
+    protected boolean isPaired() {
+        return mDevice.getBondState() == BOND_BONDED;
+    }
+
+    /**
+     * Receiver that closes after bonding has completed.
+     */
+    private class BondedReceiver extends DeviceIntentReceiver {
+
+        private BondedReceiver() {
+            super(
+                    mContext,
+                    mPreferences,
+                    mDevice,
+                    BluetoothDevice.ACTION_PAIRING_REQUEST,
+                    BluetoothDevice.ACTION_BOND_STATE_CHANGED);
+        }
+
+        /**
+         * Called with ACTION_PAIRING_REQUEST and ACTION_BOND_STATE_CHANGED about the interesting
+         * device (see {@link DeviceIntentReceiver}).
+         *
+         * <p>The ACTION_PAIRING_REQUEST intent provides the passkey which will be sent to the
+         * {@link PasskeyConfirmationHandler} for showing the UI, and the ACTION_BOND_STATE_CHANGED
+         * will provide the result of the bonding.
+         */
+        @Override
+        protected void onReceiveDeviceIntent(Intent intent) {
+            String intentAction = intent.getAction();
+            BluetoothDevice remoteDevice = intent.getParcelableExtra(EXTRA_DEVICE);
+            if (Strings.isNullOrEmpty(intentAction)
+                    || remoteDevice == null
+                    || !remoteDevice.getAddress().equals(mDevice.getAddress())) {
+                Log.w(TAG,
+                        "BluetoothClassicPairer, receives " + intentAction
+                                + " from unexpected device " + maskBluetoothAddress(remoteDevice));
+                return;
+            }
+            switch (intentAction) {
+                case BluetoothDevice.ACTION_PAIRING_REQUEST:
+                    handlePairingRequest(
+                            remoteDevice,
+                            intent.getIntExtra(BluetoothDevice.EXTRA_PAIRING_VARIANT, ERROR),
+                            intent.getIntExtra(BluetoothDevice.EXTRA_PAIRING_KEY, ERROR));
+                    break;
+                case BluetoothDevice.ACTION_BOND_STATE_CHANGED:
+                    handleBondStateChanged(
+                            intent.getIntExtra(BluetoothDevice.EXTRA_BOND_STATE, ERROR),
+                            intent.getIntExtra(EXTRA_REASON, ERROR));
+                    break;
+                default:
+                    break;
+            }
+        }
+
+        private void handlePairingRequest(BluetoothDevice device, int variant, int passkey) {
+            Log.i(TAG,
+                    "BluetoothClassicPairer, pairing request, " + device + ", " + variant + ", "
+                            + passkey);
+            // Prevent Bluetooth Settings from getting the pairing request and showing its own UI.
+            abortBroadcast();
+            mPasskeyConfirmationHandler.onPasskeyConfirmation(device, passkey);
+        }
+
+        private void handleBondStateChanged(int bondState, int reason) {
+            Log.i(TAG,
+                    "BluetoothClassicPairer, bond state changed to " + bondState + ", reason="
+                            + reason);
+            switch (bondState) {
+                case BOND_BONDING:
+                    // Don't close!
+                    return;
+                case BOND_BONDED:
+                    close();
+                    return;
+                case BOND_NONE:
+                default:
+                    closeWithError(
+                            new PairingException(
+                                    "BluetoothClassicPairer, createBond failed, reason:" + reason));
+            }
+        }
+    }
+
+    // Applies UsesPermission annotation will create circular dependency.
+    @SuppressLint("MissingPermission")
+    static void setPairingConfirmation(BluetoothDevice device, boolean confirm) {
+        Log.i(TAG, "BluetoothClassicPairer: setPairingConfirmation " + maskBluetoothAddress(device)
+                + ", confirm: " + confirm);
+        device.setPairingConfirmation(confirm);
+    }
+}
diff --git a/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/DeviceIntentReceiver.java b/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/DeviceIntentReceiver.java
new file mode 100644
index 0000000..5bcf10a
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/DeviceIntentReceiver.java
@@ -0,0 +1,75 @@
+/*
+ * 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.BluetoothAddress.maskBluetoothAddress;
+
+import android.bluetooth.BluetoothDevice;
+import android.content.Context;
+import android.content.Intent;
+import android.util.Log;
+
+/**
+ * Like {@link SimpleBroadcastReceiver}, but for intents about a certain {@link BluetoothDevice}.
+ */
+abstract class DeviceIntentReceiver extends SimpleBroadcastReceiver {
+
+    private static final String TAG = DeviceIntentReceiver.class.getSimpleName();
+
+    private final BluetoothDevice mDevice;
+
+    static DeviceIntentReceiver oneShotReceiver(
+            Context context, Preferences preferences, BluetoothDevice device, String... actions) {
+        return new DeviceIntentReceiver(context, preferences, device, actions) {
+            @Override
+            protected void onReceiveDeviceIntent(Intent intent) throws Exception {
+                close();
+            }
+        };
+    }
+
+    /**
+     * @param context The context to use to register / unregister the receiver.
+     * @param device The interesting device. We ignore intents about other devices.
+     * @param actions The actions to include in our intent filter.
+     */
+    protected DeviceIntentReceiver(
+            Context context, Preferences preferences, BluetoothDevice device, String... actions) {
+        super(context, preferences, actions);
+        this.mDevice = device;
+    }
+
+    /**
+     * Called with intents about the interesting device (see {@link #DeviceIntentReceiver}). Any
+     * exception thrown by this method will be delivered via {@link #await}.
+     */
+    protected abstract void onReceiveDeviceIntent(Intent intent) throws Exception;
+
+    // incompatible types in argument.
+    @Override
+    protected void onReceive(Intent intent) throws Exception {
+        BluetoothDevice intentDevice = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
+        if (mDevice == null || mDevice.equals(intentDevice)) {
+            onReceiveDeviceIntent(intent);
+        } else {
+            Log.v(TAG,
+                    "Ignoring intent for device=" + maskBluetoothAddress(intentDevice)
+                            + "(expected "
+                            + maskBluetoothAddress(mDevice) + ")");
+        }
+    }
+}
diff --git a/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/SimpleBroadcastReceiver.java b/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/SimpleBroadcastReceiver.java
new file mode 100644
index 0000000..7f525a7
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/SimpleBroadcastReceiver.java
@@ -0,0 +1,148 @@
+/*
+ * 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 android.annotation.TargetApi;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.os.Build.VERSION_CODES;
+import android.os.Handler;
+import android.util.Log;
+
+import androidx.annotation.Nullable;
+
+import com.google.common.util.concurrent.SettableFuture;
+
+import java.util.Arrays;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+/**
+ * Like {@link BroadcastReceiver}, but:
+ *
+ * <ul>
+ *   <li>Simpler to create and register, with a list of actions.
+ *   <li>Implements AutoCloseable. If used as a resource in try-with-resources (available on
+ *       KitKat+), unregisters itself automatically.
+ *   <li>Lets you block waiting for your state transition with {@link #await}.
+ * </ul>
+ */
+// AutoCloseable only available on KitKat+.
+@TargetApi(VERSION_CODES.KITKAT)
+public abstract class SimpleBroadcastReceiver extends BroadcastReceiver implements AutoCloseable {
+
+    private static final String TAG = SimpleBroadcastReceiver.class.getSimpleName();
+
+    /**
+     * Creates a one shot receiver.
+     */
+    public static SimpleBroadcastReceiver oneShotReceiver(
+            Context context, Preferences preferences, String... actions) {
+        return new SimpleBroadcastReceiver(context, preferences, actions) {
+            @Override
+            protected void onReceive(Intent intent) {
+                close();
+            }
+        };
+    }
+
+    private final Context mContext;
+    private final SettableFuture<Void> mIsClosedFuture = SettableFuture.create();
+    private long mAwaitExtendSecond;
+
+    // Nullness checker complains about 'this' being @UnderInitialization
+    @SuppressWarnings("nullness")
+    public SimpleBroadcastReceiver(
+            Context context, Preferences preferences, @Nullable Handler handler,
+            String... actions) {
+        Log.v(TAG, this + " listening for actions " + Arrays.toString(actions));
+        this.mContext = context;
+        IntentFilter intentFilter = new IntentFilter();
+        if (preferences.getIncreaseIntentFilterPriority()) {
+            intentFilter.setPriority(IntentFilter.SYSTEM_HIGH_PRIORITY);
+        }
+        for (String action : actions) {
+            intentFilter.addAction(action);
+        }
+        context.registerReceiver(this, intentFilter, /* broadcastPermission= */ null, handler);
+    }
+
+    public SimpleBroadcastReceiver(Context context, Preferences preferences, String... actions) {
+        this(context, preferences, /* handler= */ null, actions);
+    }
+
+    /**
+     * Any exception thrown by this method will be delivered via {@link #await}.
+     */
+    protected abstract void onReceive(Intent intent) throws Exception;
+
+    @Override
+    public void onReceive(Context context, Intent intent) {
+        Log.v(TAG, "Got intent with action= " + intent.getAction());
+        try {
+            onReceive(intent);
+        } catch (Exception e) {
+            closeWithError(e);
+        }
+    }
+
+    @Override
+    public void close() {
+        closeWithError(null);
+    }
+
+    void closeWithError(@Nullable Exception e) {
+        try {
+            mContext.unregisterReceiver(this);
+        } catch (IllegalArgumentException ignored) {
+            // Ignore. Happens if you unregister twice.
+        }
+        if (e == null) {
+            mIsClosedFuture.set(null);
+        } else {
+            mIsClosedFuture.setException(e);
+        }
+    }
+
+    /**
+     * Extends the awaiting time.
+     */
+    public void extendAwaitSecond(int awaitExtendSecond) {
+        this.mAwaitExtendSecond = awaitExtendSecond;
+    }
+
+    /**
+     * Blocks until this receiver has closed (i.e. the state transition that this receiver is
+     * interested in has completed). Throws an exception on any error.
+     */
+    public void await(long timeout, TimeUnit timeUnit)
+            throws InterruptedException, ExecutionException, TimeoutException {
+        Log.v(TAG, this + " waiting on future for " + timeout + " " + timeUnit);
+        try {
+            mIsClosedFuture.get(timeout, timeUnit);
+        } catch (TimeoutException e) {
+            if (mAwaitExtendSecond <= 0) {
+                throw e;
+            }
+            Log.i(TAG, "Extend timeout for " + mAwaitExtendSecond + " seconds");
+            mIsClosedFuture.get(mAwaitExtendSecond, TimeUnit.SECONDS);
+        }
+    }
+}
diff --git a/nearby/tests/src/com/android/server/nearby/common/bluetooth/fastpair/AccountKeyGeneratorTest.java b/nearby/tests/src/com/android/server/nearby/common/bluetooth/fastpair/AccountKeyGeneratorTest.java
new file mode 100644
index 0000000..5084e7e
--- /dev/null
+++ b/nearby/tests/src/com/android/server/nearby/common/bluetooth/fastpair/AccountKeyGeneratorTest.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 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 com.android.server.nearby.common.bluetooth.fastpair.Constants.FastPairService.AccountKeyCharacteristic;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.platform.test.annotations.Presubmit;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.security.NoSuchAlgorithmException;
+
+/**
+ * Unit tests for {@link AccountKeyGenerator}.
+ */
+@Presubmit
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class AccountKeyGeneratorTest {
+    @Test
+    public void createAccountKey() throws NoSuchAlgorithmException {
+        byte[] accountKey = AccountKeyGenerator.createAccountKey();
+
+        assertThat(accountKey).hasLength(16);
+        assertThat(accountKey[0]).isEqualTo(AccountKeyCharacteristic.TYPE);
+    }
+}