chromium/device/bluetooth/test/android/java/src/org/chromium/device/bluetooth/Fakes.java

// Copyright 2015 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

package org.chromium.device.bluetooth;

import android.Manifest;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.le.ScanFilter;
import android.bluetooth.le.ScanSettings;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.PackageManager;
import android.os.Handler;
import android.os.ParcelUuid;
import android.test.mock.MockContext;
import android.util.SparseArray;

import org.jni_zero.CalledByNative;
import org.jni_zero.JNINamespace;
import org.jni_zero.NativeMethods;

import org.chromium.base.Log;
import org.chromium.components.location.LocationUtils;
import org.chromium.device.bluetooth.test.TestRSSI;
import org.chromium.device.bluetooth.test.TestTxPower;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;

/**
 * Fake implementations of android.bluetooth.* classes for testing.
 *
 * Fakes are contained in a single file to simplify code. Only one C++ file may
 * access a Java file via JNI, and all of these classes are accessed by
 * bluetooth_test_android.cc. The alternative would be a C++ .h, .cc file for
 * each of these classes.
 */
@JNINamespace("device")
class Fakes {
    private static final String TAG = "Bluetooth";

    // Android uses Integer.MIN_VALUE to signal no Tx Power in advertisement
    // packet.
    // https://developer.android.com/reference/android/bluetooth/le/ScanRecord.html#getTxPowerLevel()
    private static final int NO_TX_POWER = Integer.MIN_VALUE;

    /**
     * Sets the factory for LocationUtils to return an instance whose
     * isSystemLocationSettingEnabled method returns |isEnabled|.
     */
    @CalledByNative
    public static void setLocationServicesState(final boolean isEnabled) {
        LocationUtils.setFactory(
                new LocationUtils.Factory() {
                    @Override
                    public LocationUtils create() {
                        return new LocationUtils() {
                            @Override
                            public boolean isSystemLocationSettingEnabled() {
                                return isEnabled;
                            }
                        };
                    }
                });
    }

    /**
     * Sets the factory for ThreadUtilsWrapper to always post a task to the UI thread
     * rather than running the task immediately. This simulates events arriving on a separate
     * thread on Android.
     * runOnUiThread uses FakesJni.get().postTaskFromJava. This allows java to post tasks to the
     * message loop that the test is using rather than to the Java message loop which
     * is not running during tests.
     */
    @CalledByNative
    public static void initFakeThreadUtilsWrapper(final long nativeBluetoothTestAndroid) {
        Wrappers.ThreadUtilsWrapper.setFactory(
                new Wrappers.ThreadUtilsWrapper.Factory() {
                    @Override
                    public Wrappers.ThreadUtilsWrapper create() {
                        return new Wrappers.ThreadUtilsWrapper() {
                            @Override
                            public void runOnUiThread(Runnable r) {
                                FakesJni.get().postTaskFromJava(nativeBluetoothTestAndroid, r);
                            }
                        };
                    }
                });
    }

    @CalledByNative
    public static void runRunnable(Runnable r) {
        r.run();
    }

    /** Fakes android.bluetooth.BluetoothAdapter. */
    static class FakeBluetoothAdapter extends Wrappers.BluetoothAdapterWrapper {
        private final FakeContext mFakeContext;
        private final FakeBluetoothLeScanner mFakeScanner;
        private boolean mPowered = true;
        final long mNativeBluetoothTestAndroid;

        /** Creates a FakeBluetoothAdapter. */
        @CalledByNative("FakeBluetoothAdapter")
        public static FakeBluetoothAdapter create(long nativeBluetoothTestAndroid) {
            Log.v(TAG, "FakeBluetoothAdapter created.");
            return new FakeBluetoothAdapter(nativeBluetoothTestAndroid);
        }

        private FakeBluetoothAdapter(long nativeBluetoothTestAndroid) {
            super(null, new FakeContext());
            mNativeBluetoothTestAndroid = nativeBluetoothTestAndroid;
            mFakeContext = (FakeContext) mContext;
            mFakeScanner = new FakeBluetoothLeScanner();
        }

        @CalledByNative("FakeBluetoothAdapter")
        public void setFakeContextLocationPermission(boolean enabled) {
            mFakeContext.setLocationPermission(enabled);
        }

        /** Creates and discovers a new device. */
        @CalledByNative("FakeBluetoothAdapter")
        public void simulateLowEnergyDevice(int deviceOrdinal) {
            if (mFakeScanner == null) {
                return;
            }

            switch (deviceOrdinal) {
                case 1:
                    {
                        ArrayList<ParcelUuid> uuids = new ArrayList<ParcelUuid>(2);
                        uuids.add(ParcelUuid.fromString("00001800-0000-1000-8000-00805f9b34fb"));
                        uuids.add(ParcelUuid.fromString("00001801-0000-1000-8000-00805f9b34fb"));

                        HashMap<ParcelUuid, byte[]> serviceData = new HashMap<>();
                        serviceData.put(
                                ParcelUuid.fromString("0000180d-0000-1000-8000-00805f9b34fb"),
                                new byte[] {1});

                        SparseArray<byte[]> manufacturerData = new SparseArray<>();
                        manufacturerData.put(0x00E0, new byte[] {0x01, 0x02, 0x03, 0x04});

                        mFakeScanner.mScanCallback.onScanResult(
                                ScanSettings.CALLBACK_TYPE_ALL_MATCHES,
                                new FakeScanResult(
                                        new FakeBluetoothDevice(
                                                this, "01:00:00:90:1E:BE", "FakeBluetoothDevice"),
                                        "FakeBluetoothDevice",
                                        TestRSSI.LOWEST,
                                        4,
                                        uuids,
                                        TestTxPower.LOWEST,
                                        serviceData,
                                        manufacturerData));
                        break;
                    }
                case 2:
                    {
                        ArrayList<ParcelUuid> uuids = new ArrayList<ParcelUuid>(2);
                        uuids.add(ParcelUuid.fromString("00001802-0000-1000-8000-00805f9b34fb"));
                        uuids.add(ParcelUuid.fromString("00001803-0000-1000-8000-00805f9b34fb"));

                        HashMap<ParcelUuid, byte[]> serviceData = new HashMap<>();
                        serviceData.put(
                                ParcelUuid.fromString("0000180d-0000-1000-8000-00805f9b34fb"),
                                new byte[] {});
                        serviceData.put(
                                ParcelUuid.fromString("00001802-0000-1000-8000-00805f9b34fb"),
                                new byte[] {0, 2});

                        SparseArray<byte[]> manufacturerData = new SparseArray<>();
                        manufacturerData.put(0x00E0, new byte[] {});

                        mFakeScanner.mScanCallback.onScanResult(
                                ScanSettings.CALLBACK_TYPE_ALL_MATCHES,
                                new FakeScanResult(
                                        new FakeBluetoothDevice(
                                                this, "01:00:00:90:1E:BE", "FakeBluetoothDevice"),
                                        "Local Device Name",
                                        TestRSSI.LOWER,
                                        5,
                                        uuids,
                                        TestTxPower.LOWER,
                                        serviceData,
                                        manufacturerData));
                        break;
                    }
                case 3:
                    {
                        ArrayList<ParcelUuid> uuids = null;
                        mFakeScanner.mScanCallback.onScanResult(
                                ScanSettings.CALLBACK_TYPE_ALL_MATCHES,
                                new FakeScanResult(
                                        new FakeBluetoothDevice(this, "01:00:00:90:1E:BE", ""),
                                        "Local Device Name",
                                        TestRSSI.LOW,
                                        -1,
                                        uuids,
                                        NO_TX_POWER,
                                        null,
                                        null));

                        break;
                    }
                case 4:
                    {
                        ArrayList<ParcelUuid> uuids = null;
                        mFakeScanner.mScanCallback.onScanResult(
                                ScanSettings.CALLBACK_TYPE_ALL_MATCHES,
                                new FakeScanResult(
                                        new FakeBluetoothDevice(this, "02:00:00:8B:74:63", ""),
                                        "Local Device Name",
                                        TestRSSI.MEDIUM,
                                        -1,
                                        uuids,
                                        NO_TX_POWER,
                                        null,
                                        null));

                        break;
                    }
                case 5:
                    {
                        ArrayList<ParcelUuid> uuids = null;
                        mFakeScanner.mScanCallback.onScanResult(
                                ScanSettings.CALLBACK_TYPE_ALL_MATCHES,
                                new FakeScanResult(
                                        new FakeBluetoothDevice(this, "01:00:00:90:1E:BE", null),
                                        "Local Device Name",
                                        TestRSSI.HIGH,
                                        -1,
                                        uuids,
                                        NO_TX_POWER,
                                        null,
                                        null));
                        break;
                    }
                case 6:
                    {
                        ArrayList<ParcelUuid> uuids = null;
                        mFakeScanner.mScanCallback.onScanResult(
                                ScanSettings.CALLBACK_TYPE_ALL_MATCHES,
                                new FakeScanResult(
                                        new FakeBluetoothDevice(this, "02:00:00:8B:74:63", null),
                                        "Local Device Name",
                                        TestRSSI.LOWEST,
                                        -1,
                                        uuids,
                                        NO_TX_POWER,
                                        null,
                                        null));
                        break;
                    }
                case 7:
                    {
                        ArrayList<ParcelUuid> uuids = new ArrayList<ParcelUuid>(2);
                        uuids.add(ParcelUuid.fromString("f1d0fff3-deaa-ecee-b42f-c9ba7ed623bb"));

                        HashMap<ParcelUuid, byte[]> serviceData = new HashMap<>();
                        serviceData.put(
                                ParcelUuid.fromString("f1d0fff3-deaa-ecee-b42f-c9ba7ed623bb"),
                                new byte[] {0, 20});

                        mFakeScanner.mScanCallback.onScanResult(
                                ScanSettings.CALLBACK_TYPE_ALL_MATCHES,
                                new FakeScanResult(
                                        new FakeBluetoothDevice(
                                                this, "01:00:00:90:1E:BE", "U2F FakeDevice"),
                                        "Local Device Name",
                                        TestRSSI.LOWEST,
                                        -1,
                                        uuids,
                                        NO_TX_POWER,
                                        serviceData,
                                        null));
                        break;
                    }
            }
        }

        @CalledByNative("FakeBluetoothAdapter")
        public void forceIllegalStateException() {
            if (mFakeScanner != null) {
                mFakeScanner.forceIllegalStateException();
            }
        }

        // -----------------------------------------------------------------------------------------
        // BluetoothAdapterWrapper overrides:

        @Override
        public boolean disable() {
            // android.bluetooth.BluetoothAdapter::disable() is an async call, so we simulate this
            // by posting a task to the UI thread.
            FakesJni.get()
                    .postTaskFromJava(
                            mNativeBluetoothTestAndroid,
                            new Runnable() {
                                @Override
                                public void run() {
                                    mPowered = false;
                                    FakesJni.get()
                                            .onFakeAdapterStateChanged(
                                                    mNativeBluetoothTestAndroid, false);
                                }
                            });
            return true;
        }

        @Override
        public boolean enable() {
            // android.bluetooth.BluetoothAdapter::enable() is an async call, so we simulate this by
            // posting a task to the UI thread.
            FakesJni.get()
                    .postTaskFromJava(
                            mNativeBluetoothTestAndroid,
                            new Runnable() {
                                @Override
                                public void run() {
                                    mPowered = true;
                                    FakesJni.get()
                                            .onFakeAdapterStateChanged(
                                                    mNativeBluetoothTestAndroid, true);
                                }
                            });
            return true;
        }

        @Override
        public String getAddress() {
            return "A1:B2:C3:D4:E5:F6";
        }

        @Override
        public Wrappers.BluetoothLeScannerWrapper getBluetoothLeScanner() {
            if (isEnabled()) {
                return mFakeScanner;
            }
            return null;
        }

        @Override
        public String getName() {
            return "FakeBluetoothAdapter";
        }

        @Override
        public int getScanMode() {
            return android.bluetooth.BluetoothAdapter.SCAN_MODE_NONE;
        }

        @Override
        public boolean isEnabled() {
            return mPowered;
        }

        @Override
        public boolean isDiscovering() {
            return false;
        }
    }

    /** Fakes android.content.Context by extending MockContext. */
    static class FakeContext extends MockContext {
        private boolean mLocationPermission;

        public FakeContext() {
            super();
            mLocationPermission = true;
        }

        public void setLocationPermission(boolean enabled) {
            mLocationPermission = enabled;
        }

        @Override
        public Intent registerReceiver(
                BroadcastReceiver receiver,
                IntentFilter filter,
                String permission,
                Handler scheduler) {
            return null;
        }

        @Override
        public Intent registerReceiver(
                BroadcastReceiver receiver,
                IntentFilter filter,
                String permission,
                Handler scheduler,
                int flags) {
            return null;
        }

        @Override
        public void unregisterReceiver(BroadcastReceiver receiver) {}

        @Override
        public int checkCallingOrSelfPermission(String permission) {
            if (permission.equals(Manifest.permission.ACCESS_FINE_LOCATION)
                    || permission.equals(Manifest.permission.ACCESS_COARSE_LOCATION)) {
                return mLocationPermission
                        ? PackageManager.PERMISSION_GRANTED
                        : PackageManager.PERMISSION_DENIED;
            }
            return PackageManager.PERMISSION_DENIED;
        }
    }

    /** Fakes android.bluetooth.le.BluetoothLeScanner. */
    static class FakeBluetoothLeScanner extends Wrappers.BluetoothLeScannerWrapper {
        public Wrappers.ScanCallbackWrapper mScanCallback;
        private boolean mThrowException;

        private FakeBluetoothLeScanner() {
            super(null);
        }

        @Override
        public void startScan(
                List<ScanFilter> filters,
                int scanSettingsScanMode,
                Wrappers.ScanCallbackWrapper callback) {
            if (mScanCallback != null) {
                throw new IllegalArgumentException(
                        "FakeBluetoothLeScanner does not support multiple scans.");
            }
            if (mThrowException) {
                throw new IllegalStateException("Adapter is off.");
            }
            mScanCallback = callback;
        }

        @Override
        public void stopScan(Wrappers.ScanCallbackWrapper callback) {
            if (mScanCallback != callback) {
                throw new IllegalArgumentException("No scan in progress.");
            }
            if (mThrowException) {
                throw new IllegalStateException("Adapter is off.");
            }
            mScanCallback = null;
        }

        void forceIllegalStateException() {
            mThrowException = true;
        }
    }

    /** Fakes android.bluetooth.le.ScanResult */
    static class FakeScanResult extends Wrappers.ScanResultWrapper {
        private final FakeBluetoothDevice mDevice;
        private final String mLocalName;
        private final int mRssi;
        private final int mTxPower;
        private final int mAdvertisementFlags;
        private final ArrayList<ParcelUuid> mUuids;
        private final Map<ParcelUuid, byte[]> mServiceData;
        private final SparseArray<byte[]> mManufacturerData;

        FakeScanResult(
                FakeBluetoothDevice device,
                String localName,
                int rssi,
                int advertisementFlags,
                ArrayList<ParcelUuid> uuids,
                int txPower,
                Map<ParcelUuid, byte[]> serviceData,
                SparseArray<byte[]> manufacturerData) {
            super(null);
            mDevice = device;
            mLocalName = localName;
            mRssi = rssi;
            mAdvertisementFlags = advertisementFlags;
            mUuids = uuids;
            mTxPower = txPower;
            mServiceData = serviceData;
            mManufacturerData = manufacturerData;
        }

        @Override
        public Wrappers.BluetoothDeviceWrapper getDevice() {
            return mDevice;
        }

        @Override
        public int getRssi() {
            return mRssi;
        }

        @Override
        public List<ParcelUuid> getScanRecord_getServiceUuids() {
            return mUuids;
        }

        @Override
        public int getScanRecord_getTxPowerLevel() {
            return mTxPower;
        }

        @Override
        public Map<ParcelUuid, byte[]> getScanRecord_getServiceData() {
            return mServiceData;
        }

        @Override
        public SparseArray<byte[]> getScanRecord_getManufacturerSpecificData() {
            return mManufacturerData;
        }

        @Override
        public String getScanRecord_getDeviceName() {
            return mLocalName;
        }

        @Override
        public int getScanRecord_getAdvertiseFlags() {
            return mAdvertisementFlags;
        }
    }

    /** Fakes android.bluetooth.BluetoothDevice. */
    static class FakeBluetoothDevice extends Wrappers.BluetoothDeviceWrapper {
        final FakeBluetoothAdapter mAdapter;
        private String mAddress;
        private String mName;
        final FakeBluetoothGatt mGatt;
        private Wrappers.BluetoothGattCallbackWrapper mGattCallback;

        static FakeBluetoothDevice sRememberedDevice;

        public FakeBluetoothDevice(FakeBluetoothAdapter adapter, String address, String name) {
            super(null);
            mAdapter = adapter;
            mAddress = address;
            mName = name;
            mGatt = new FakeBluetoothGatt(this);
        }

        // Implements BluetoothTestAndroid::RememberDeviceForSubsequentAction.
        @CalledByNative("FakeBluetoothDevice")
        private static void rememberDeviceForSubsequentAction(ChromeBluetoothDevice chromeDevice) {
            sRememberedDevice = (FakeBluetoothDevice) chromeDevice.mDevice;
        }

        // Create a call to onConnectionStateChange on the |chrome_device| using parameters
        // |status| & |connected|.
        @CalledByNative("FakeBluetoothDevice")
        private static void connectionStateChange(
                ChromeBluetoothDevice chromeDevice, int status, boolean connected) {
            FakeBluetoothDevice fakeDevice = (FakeBluetoothDevice) chromeDevice.mDevice;
            fakeDevice.mGattCallback.onConnectionStateChange(
                    status,
                    connected
                            ? android.bluetooth.BluetoothProfile.STATE_CONNECTED
                            : android.bluetooth.BluetoothProfile.STATE_DISCONNECTED);
        }

        // Create a call to onServicesDiscovered on the |chrome_device| using parameter
        // |status|.
        @CalledByNative("FakeBluetoothDevice")
        private static void servicesDiscovered(
                ChromeBluetoothDevice chromeDevice, int status, String uuidsSpaceDelimited) {
            if (chromeDevice == null && sRememberedDevice == null) {
                throw new IllegalArgumentException("rememberDevice wasn't called previously.");
            }

            FakeBluetoothDevice fakeDevice =
                    (chromeDevice == null)
                            ? sRememberedDevice
                            : (FakeBluetoothDevice) chromeDevice.mDevice;

            if (status == android.bluetooth.BluetoothGatt.GATT_SUCCESS) {
                fakeDevice.mGatt.mServices.clear();
                HashMap<String, Integer> uuidsToInstanceIdMap = new HashMap<String, Integer>();
                for (String uuid : uuidsSpaceDelimited.split(" ")) {
                    // String.split() can return empty strings. Ignore them.
                    if (uuid.isEmpty()) continue;
                    Integer previousId = uuidsToInstanceIdMap.get(uuid);
                    int instanceId = (previousId == null) ? 0 : previousId + 1;
                    uuidsToInstanceIdMap.put(uuid, instanceId);
                    fakeDevice.mGatt.mServices.add(
                            new FakeBluetoothGattService(
                                    fakeDevice, UUID.fromString(uuid), instanceId));
                }
            }

            fakeDevice.mGattCallback.onServicesDiscovered(status);
        }

        // -----------------------------------------------------------------------------------------
        // Wrappers.BluetoothDeviceWrapper overrides:

        @Override
        public Wrappers.BluetoothGattWrapper connectGatt(
                Context context,
                boolean autoConnect,
                Wrappers.BluetoothGattCallbackWrapper callback,
                int transport) {
            if (mGattCallback != null && mGattCallback != callback) {
                throw new IllegalArgumentException(
                        "BluetoothGattWrapper doesn't support calls to connectGatt() with "
                                + "multiple distinct callbacks.");
            }
            FakesJni.get()
                    .onFakeBluetoothDeviceConnectGattCalled(mAdapter.mNativeBluetoothTestAndroid);
            mGattCallback = callback;
            return mGatt;
        }

        @Override
        public String getAddress() {
            return mAddress;
        }

        @Override
        public int getBluetoothClass_getDeviceClass() {
            return Wrappers.DEVICE_CLASS_UNSPECIFIED;
        }

        @Override
        public int getBondState() {
            return BluetoothDevice.BOND_NONE;
        }

        @Override
        public String getName() {
            return mName;
        }
    }

    /** Fakes android.bluetooth.BluetoothGatt. */
    static class FakeBluetoothGatt extends Wrappers.BluetoothGattWrapper {
        final FakeBluetoothDevice mDevice;
        final ArrayList<Wrappers.BluetoothGattServiceWrapper> mServices;
        boolean mReadCharacteristicWillFailSynchronouslyOnce;
        boolean mSetCharacteristicNotificationWillFailSynchronouslyOnce;
        boolean mWriteCharacteristicWillFailSynchronouslyOnce;
        boolean mReadDescriptorWillFailSynchronouslyOnce;
        boolean mWriteDescriptorWillFailSynchronouslyOnce;

        public FakeBluetoothGatt(FakeBluetoothDevice device) {
            super(null, null);
            mDevice = device;
            mServices = new ArrayList<Wrappers.BluetoothGattServiceWrapper>();
        }

        @Override
        public void disconnect() {
            FakesJni.get()
                    .onFakeBluetoothGattDisconnect(mDevice.mAdapter.mNativeBluetoothTestAndroid);
        }

        @Override
        public void close() {
            FakesJni.get().onFakeBluetoothGattClose(mDevice.mAdapter.mNativeBluetoothTestAndroid);
        }

        @Override
        public boolean requestMtu(int mtu) {
            return false;
        }

        @Override
        public void discoverServices() {
            FakesJni.get()
                    .onFakeBluetoothGattDiscoverServices(
                            mDevice.mAdapter.mNativeBluetoothTestAndroid);
        }

        @Override
        public List<Wrappers.BluetoothGattServiceWrapper> getServices() {
            return mServices;
        }

        @Override
        boolean readCharacteristic(Wrappers.BluetoothGattCharacteristicWrapper characteristic) {
            if (mReadCharacteristicWillFailSynchronouslyOnce) {
                mReadCharacteristicWillFailSynchronouslyOnce = false;
                return false;
            }
            FakesJni.get()
                    .onFakeBluetoothGattReadCharacteristic(
                            mDevice.mAdapter.mNativeBluetoothTestAndroid);
            return true;
        }

        @Override
        boolean setCharacteristicNotification(
                Wrappers.BluetoothGattCharacteristicWrapper characteristic, boolean enable) {
            if (mSetCharacteristicNotificationWillFailSynchronouslyOnce) {
                mSetCharacteristicNotificationWillFailSynchronouslyOnce = false;
                return false;
            }
            FakesJni.get()
                    .onFakeBluetoothGattSetCharacteristicNotification(
                            mDevice.mAdapter.mNativeBluetoothTestAndroid);
            return true;
        }

        @Override
        boolean writeCharacteristic(Wrappers.BluetoothGattCharacteristicWrapper characteristic) {
            if (mWriteCharacteristicWillFailSynchronouslyOnce) {
                mWriteCharacteristicWillFailSynchronouslyOnce = false;
                return false;
            }
            FakesJni.get()
                    .onFakeBluetoothGattWriteCharacteristic(
                            mDevice.mAdapter.mNativeBluetoothTestAndroid,
                            characteristic.getValue());
            return true;
        }

        @Override
        boolean readDescriptor(Wrappers.BluetoothGattDescriptorWrapper descriptor) {
            if (mReadDescriptorWillFailSynchronouslyOnce) {
                mReadDescriptorWillFailSynchronouslyOnce = false;
                return false;
            }
            FakesJni.get()
                    .onFakeBluetoothGattReadDescriptor(
                            mDevice.mAdapter.mNativeBluetoothTestAndroid);
            return true;
        }

        @Override
        boolean writeDescriptor(Wrappers.BluetoothGattDescriptorWrapper descriptor) {
            if (mWriteDescriptorWillFailSynchronouslyOnce) {
                mWriteDescriptorWillFailSynchronouslyOnce = false;
                return false;
            }
            FakesJni.get()
                    .onFakeBluetoothGattWriteDescriptor(
                            mDevice.mAdapter.mNativeBluetoothTestAndroid, descriptor.getValue());
            return true;
        }
    }

    /** Fakes android.bluetooth.BluetoothGattService. */
    static class FakeBluetoothGattService extends Wrappers.BluetoothGattServiceWrapper {
        final FakeBluetoothDevice mDevice;
        final int mInstanceId;
        final UUID mUuid;
        final ArrayList<Wrappers.BluetoothGattCharacteristicWrapper> mCharacteristics;

        public FakeBluetoothGattService(FakeBluetoothDevice device, UUID uuid, int instanceId) {
            super(null, null);
            mDevice = device;
            mUuid = uuid;
            mInstanceId = instanceId;
            mCharacteristics = new ArrayList<Wrappers.BluetoothGattCharacteristicWrapper>();
        }

        // Create a characteristic and add it to this service.
        @CalledByNative("FakeBluetoothGattService")
        private static void addCharacteristic(
                ChromeBluetoothRemoteGattService chromeService, String uuidString, int properties) {
            FakeBluetoothGattService fakeService =
                    (FakeBluetoothGattService) chromeService.mService;
            UUID uuid = UUID.fromString(uuidString);

            int countOfDuplicateUUID = 0;
            for (Wrappers.BluetoothGattCharacteristicWrapper characteristic :
                    fakeService.mCharacteristics) {
                if (characteristic.getUuid().equals(uuid)) {
                    countOfDuplicateUUID++;
                }
            }
            fakeService.mCharacteristics.add(
                    new FakeBluetoothGattCharacteristic(
                            fakeService, /* instanceId= */ countOfDuplicateUUID, properties, uuid));
        }

        // -----------------------------------------------------------------------------------------
        // Wrappers.BluetoothGattServiceWrapper overrides:

        @Override
        public List<Wrappers.BluetoothGattCharacteristicWrapper> getCharacteristics() {
            return mCharacteristics;
        }

        @Override
        public int getInstanceId() {
            return mInstanceId;
        }

        @Override
        public UUID getUuid() {
            return mUuid;
        }
    }

    /** Fakes android.bluetooth.BluetoothGattCharacteristic. */
    static class FakeBluetoothGattCharacteristic
            extends Wrappers.BluetoothGattCharacteristicWrapper {
        final FakeBluetoothGattService mService;
        final int mInstanceId;
        final int mProperties;
        final UUID mUuid;
        byte[] mValue;
        int mWriteType;
        static FakeBluetoothGattCharacteristic sRememberedCharacteristic;
        final ArrayList<Wrappers.BluetoothGattDescriptorWrapper> mDescriptors;

        public FakeBluetoothGattCharacteristic(
                FakeBluetoothGattService service, int instanceId, int properties, UUID uuid) {
            super(null, null);
            mService = service;
            mInstanceId = instanceId;
            mProperties = properties;
            mUuid = uuid;
            mValue = new byte[0];
            mDescriptors = new ArrayList<Wrappers.BluetoothGattDescriptorWrapper>();
        }

        // Simulate a characteristic value notified as changed.
        @CalledByNative("FakeBluetoothGattCharacteristic")
        private static void valueChanged(
                ChromeBluetoothRemoteGattCharacteristic chromeCharacteristic, byte[] value) {
            if (chromeCharacteristic == null && sRememberedCharacteristic == null) {
                throw new IllegalArgumentException(
                        "rememberCharacteristic wasn't called previously.");
            }

            FakeBluetoothGattCharacteristic fakeCharacteristic =
                    (chromeCharacteristic == null)
                            ? sRememberedCharacteristic
                            : (FakeBluetoothGattCharacteristic)
                                    chromeCharacteristic.mCharacteristic;

            fakeCharacteristic.mValue = value;
            fakeCharacteristic.mService.mDevice.mGattCallback.onCharacteristicChanged(
                    fakeCharacteristic);
        }

        // Implements BluetoothTestAndroid::RememberCharacteristicForSubsequentAction.
        @CalledByNative("FakeBluetoothGattCharacteristic")
        private static void rememberCharacteristicForSubsequentAction(
                ChromeBluetoothRemoteGattCharacteristic chromeCharacteristic) {
            sRememberedCharacteristic =
                    (FakeBluetoothGattCharacteristic) chromeCharacteristic.mCharacteristic;
        }

        // Simulate a value being read from a characteristic.
        @CalledByNative("FakeBluetoothGattCharacteristic")
        private static void valueRead(
                ChromeBluetoothRemoteGattCharacteristic chromeCharacteristic,
                int status,
                byte[] value) {
            if (chromeCharacteristic == null && sRememberedCharacteristic == null) {
                throw new IllegalArgumentException(
                        "rememberCharacteristic wasn't called previously.");
            }

            FakeBluetoothGattCharacteristic fakeCharacteristic =
                    (chromeCharacteristic == null)
                            ? sRememberedCharacteristic
                            : (FakeBluetoothGattCharacteristic)
                                    chromeCharacteristic.mCharacteristic;

            fakeCharacteristic.mValue = value;
            fakeCharacteristic.mService.mDevice.mGattCallback.onCharacteristicRead(
                    fakeCharacteristic, status);
        }

        // Simulate a value being written to a characteristic.
        @CalledByNative("FakeBluetoothGattCharacteristic")
        private static void valueWrite(
                ChromeBluetoothRemoteGattCharacteristic chromeCharacteristic, int status) {
            if (chromeCharacteristic == null && sRememberedCharacteristic == null) {
                throw new IllegalArgumentException(
                        "rememberCharacteristic wasn't called previously.");
            }

            FakeBluetoothGattCharacteristic fakeCharacteristic =
                    (chromeCharacteristic == null)
                            ? sRememberedCharacteristic
                            : (FakeBluetoothGattCharacteristic)
                                    chromeCharacteristic.mCharacteristic;

            fakeCharacteristic.mService.mDevice.mGattCallback.onCharacteristicWrite(
                    fakeCharacteristic, status);
        }

        // Cause subsequent notification of a characteristic to fail synchronously.
        @CalledByNative("FakeBluetoothGattCharacteristic")
        private static void setCharacteristicNotificationWillFailSynchronouslyOnce(
                ChromeBluetoothRemoteGattCharacteristic chromeCharacteristic) {
            FakeBluetoothGattCharacteristic fakeCharacteristic =
                    (FakeBluetoothGattCharacteristic) chromeCharacteristic.mCharacteristic;

            fakeCharacteristic
                            .mService
                            .mDevice
                            .mGatt
                            .mSetCharacteristicNotificationWillFailSynchronouslyOnce =
                    true;
        }

        // Cause subsequent value read of a characteristic to fail synchronously.
        @CalledByNative("FakeBluetoothGattCharacteristic")
        private static void setReadCharacteristicWillFailSynchronouslyOnce(
                ChromeBluetoothRemoteGattCharacteristic chromeCharacteristic) {
            FakeBluetoothGattCharacteristic fakeCharacteristic =
                    (FakeBluetoothGattCharacteristic) chromeCharacteristic.mCharacteristic;

            fakeCharacteristic.mService.mDevice.mGatt.mReadCharacteristicWillFailSynchronouslyOnce =
                    true;
        }

        // Cause subsequent value write of a characteristic to fail synchronously.
        @CalledByNative("FakeBluetoothGattCharacteristic")
        private static void setWriteCharacteristicWillFailSynchronouslyOnce(
                ChromeBluetoothRemoteGattCharacteristic chromeCharacteristic) {
            FakeBluetoothGattCharacteristic fakeCharacteristic =
                    (FakeBluetoothGattCharacteristic) chromeCharacteristic.mCharacteristic;

            fakeCharacteristic
                            .mService
                            .mDevice
                            .mGatt
                            .mWriteCharacteristicWillFailSynchronouslyOnce =
                    true;
        }

        // Create a descriptor and add it to this characteristic.
        @CalledByNative("FakeBluetoothGattCharacteristic")
        private static void addDescriptor(
                ChromeBluetoothRemoteGattCharacteristic chromeCharacteristic, String uuidString) {
            FakeBluetoothGattCharacteristic fakeCharacteristic =
                    (FakeBluetoothGattCharacteristic) chromeCharacteristic.mCharacteristic;
            UUID uuid = UUID.fromString(uuidString);

            fakeCharacteristic.mDescriptors.add(
                    new FakeBluetoothGattDescriptor(fakeCharacteristic, uuid));
        }

        // -----------------------------------------------------------------------------------------
        // Wrappers.BluetoothGattCharacteristicWrapper overrides:

        @Override
        public List<Wrappers.BluetoothGattDescriptorWrapper> getDescriptors() {
            return mDescriptors;
        }

        @Override
        public int getInstanceId() {
            return mInstanceId;
        }

        @Override
        public int getProperties() {
            return mProperties;
        }

        @Override
        public UUID getUuid() {
            return mUuid;
        }

        @Override
        public byte[] getValue() {
            return mValue;
        }

        @Override
        public boolean setValue(byte[] value) {
            mValue = value;
            return true;
        }

        @Override
        public void setWriteType(int writeType) {
            mWriteType = writeType;
        }
    }

    /** Fakes android.bluetooth.BluetoothGattDescriptor. */
    static class FakeBluetoothGattDescriptor extends Wrappers.BluetoothGattDescriptorWrapper {
        final FakeBluetoothGattCharacteristic mCharacteristic;
        final UUID mUuid;
        byte[] mValue;
        static FakeBluetoothGattDescriptor sRememberedDescriptor;

        public FakeBluetoothGattDescriptor(
                FakeBluetoothGattCharacteristic characteristic, UUID uuid) {
            super(null, null);
            mCharacteristic = characteristic;
            mUuid = uuid;
            mValue = new byte[0];
        }

        // Implements BluetoothTestAndroid::RememberDescriptorForSubsequentAction.
        @CalledByNative("FakeBluetoothGattDescriptor")
        private static void rememberDescriptorForSubsequentAction(
                ChromeBluetoothRemoteGattDescriptor chromeDescriptor) {
            sRememberedDescriptor = (FakeBluetoothGattDescriptor) chromeDescriptor.mDescriptor;
        }

        // Simulate a value being read from a descriptor.
        @CalledByNative("FakeBluetoothGattDescriptor")
        private static void valueRead(
                ChromeBluetoothRemoteGattDescriptor chromeDescriptor, int status, byte[] value) {
            if (chromeDescriptor == null && sRememberedDescriptor == null) {
                throw new IllegalArgumentException("rememberDescriptor wasn't called previously.");
            }

            FakeBluetoothGattDescriptor fakeDescriptor =
                    (chromeDescriptor == null)
                            ? sRememberedDescriptor
                            : (FakeBluetoothGattDescriptor) chromeDescriptor.mDescriptor;

            fakeDescriptor.mValue = value;
            fakeDescriptor.mCharacteristic.mService.mDevice.mGattCallback.onDescriptorRead(
                    fakeDescriptor, status);
        }

        // Simulate a value being written to a descriptor.
        @CalledByNative("FakeBluetoothGattDescriptor")
        private static void valueWrite(
                ChromeBluetoothRemoteGattDescriptor chromeDescriptor, int status) {
            if (chromeDescriptor == null && sRememberedDescriptor == null) {
                throw new IllegalArgumentException("rememberDescriptor wasn't called previously.");
            }

            FakeBluetoothGattDescriptor fakeDescriptor =
                    (chromeDescriptor == null)
                            ? sRememberedDescriptor
                            : (FakeBluetoothGattDescriptor) chromeDescriptor.mDescriptor;

            fakeDescriptor.mCharacteristic.mService.mDevice.mGattCallback.onDescriptorWrite(
                    fakeDescriptor, status);
        }

        // Cause subsequent value read of a descriptor to fail synchronously.
        @CalledByNative("FakeBluetoothGattDescriptor")
        private static void setReadDescriptorWillFailSynchronouslyOnce(
                ChromeBluetoothRemoteGattDescriptor chromeDescriptor) {
            FakeBluetoothGattDescriptor fakeDescriptor =
                    (FakeBluetoothGattDescriptor) chromeDescriptor.mDescriptor;

            fakeDescriptor
                            .mCharacteristic
                            .mService
                            .mDevice
                            .mGatt
                            .mReadDescriptorWillFailSynchronouslyOnce =
                    true;
        }

        // Cause subsequent value write of a descriptor to fail synchronously.
        @CalledByNative("FakeBluetoothGattDescriptor")
        private static void setWriteDescriptorWillFailSynchronouslyOnce(
                ChromeBluetoothRemoteGattDescriptor chromeDescriptor) {
            FakeBluetoothGattDescriptor fakeDescriptor =
                    (FakeBluetoothGattDescriptor) chromeDescriptor.mDescriptor;

            fakeDescriptor
                            .mCharacteristic
                            .mService
                            .mDevice
                            .mGatt
                            .mWriteDescriptorWillFailSynchronouslyOnce =
                    true;
        }

        // -----------------------------------------------------------------------------------------
        // Wrappers.BluetoothGattDescriptorWrapper overrides:

        @Override
        public Wrappers.BluetoothGattCharacteristicWrapper getCharacteristic() {
            return mCharacteristic;
        }

        @Override
        public UUID getUuid() {
            return mUuid;
        }

        @Override
        public byte[] getValue() {
            return mValue;
        }

        @Override
        public boolean setValue(byte[] value) {
            mValue = value;
            return true;
        }
    }

    // ---------------------------------------------------------------------------------------------
    // BluetoothTestAndroid C++ methods declared for access from java:
    @NativeMethods
    interface Natives {

        // Bind to BluetoothTestAndroid::PostTaskFromJava.
        void postTaskFromJava(long nativeBluetoothTestAndroid, Runnable r);

        // Binds to BluetoothTestAndroid::OnFakeAdapterStateChanged.
        void onFakeAdapterStateChanged(long nativeBluetoothTestAndroid, boolean powered);

        // Binds to BluetoothTestAndroid::OnFakeBluetoothDeviceConnectGattCalled.
        void onFakeBluetoothDeviceConnectGattCalled(long nativeBluetoothTestAndroid);

        // Binds to BluetoothTestAndroid::OnFakeBluetoothGattDisconnect.
        void onFakeBluetoothGattDisconnect(long nativeBluetoothTestAndroid);

        // Binds to BluetoothTestAndroid::OnFakeBluetoothGattClose.
        void onFakeBluetoothGattClose(long nativeBluetoothTestAndroid);

        // Binds to BluetoothTestAndroid::OnFakeBluetoothGattDiscoverServices.
        void onFakeBluetoothGattDiscoverServices(long nativeBluetoothTestAndroid);

        // Binds to BluetoothTestAndroid::OnFakeBluetoothGattSetCharacteristicNotification.
        void onFakeBluetoothGattSetCharacteristicNotification(long nativeBluetoothTestAndroid);

        // Binds to BluetoothTestAndroid::OnFakeBluetoothGattReadCharacteristic.
        void onFakeBluetoothGattReadCharacteristic(long nativeBluetoothTestAndroid);

        // Binds to BluetoothTestAndroid::OnFakeBluetoothGattWriteCharacteristic.
        void onFakeBluetoothGattWriteCharacteristic(long nativeBluetoothTestAndroid, byte[] value);

        // Binds to BluetoothTestAndroid::OnFakeBluetoothGattReadDescriptor.
        void onFakeBluetoothGattReadDescriptor(long nativeBluetoothTestAndroid);

        // Binds to BluetoothTestAndroid::OnFakeBluetoothGattWriteDescriptor.
        void onFakeBluetoothGattWriteDescriptor(long nativeBluetoothTestAndroid, byte[] value);
    }
}