diff options
author | 2024-08-02 17:27:51 +0000 | |
---|---|---|
committer | 2024-08-02 17:27:51 +0000 | |
commit | 2110c70f2e8b2c49ba7fdffa64b8afcf20b06c2f (patch) | |
tree | bd11a1d8e22be01cd9f09cc310f74870ef0aaffd | |
parent | cb5774b5b057de01a6b1aad919ac272d0caa0d19 (diff) | |
parent | 43845b5f375df72bf4a559b033e6ada15a029ede (diff) |
Merge changes Idabb8939,Ibbd8e9a1,If246e311,I788c1f6b,I76858fd8 into main
* changes:
Bumble Java HID Test cases
Bumble Java HID Test cases
Bumble test infra changes
Bumble Java HID Test cases
Bumble Java HID Test cases
5 files changed, 813 insertions, 67 deletions
diff --git a/framework/tests/bumble/AndroidTest.xml b/framework/tests/bumble/AndroidTest.xml index 1b31b827e2..4855fec134 100644 --- a/framework/tests/bumble/AndroidTest.xml +++ b/framework/tests/bumble/AndroidTest.xml @@ -42,6 +42,7 @@ <option name="test-tag" value="BumbleBluetoothTests" /> <test class="com.android.tradefed.testtype.AndroidJUnitTest" > <option name="package" value="android.bluetooth" /> + <option name="hidden-api-checks" value="false" /> </test> <!-- Only run if the Bluetooth Mainline module is installed. --> diff --git a/framework/tests/bumble/src/android/bluetooth/hid/HidHostDualModeTest.java b/framework/tests/bumble/src/android/bluetooth/hid/HidHostDualModeTest.java new file mode 100644 index 0000000000..9cdd8f9c29 --- /dev/null +++ b/framework/tests/bumble/src/android/bluetooth/hid/HidHostDualModeTest.java @@ -0,0 +1,410 @@ +/* + * Copyright (C) 2024 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 android.bluetooth; + +import static com.google.common.truth.Truth.assertThat; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.os.ParcelUuid; +import android.platform.test.annotations.RequiresFlagsEnabled; +import android.platform.test.flag.junit.CheckFlagsRule; +import android.platform.test.flag.junit.DeviceFlagsValueProvider; +import android.util.Log; + +import androidx.test.core.app.ApplicationProvider; +import androidx.test.ext.junit.runners.AndroidJUnit4; + +import com.android.bluetooth.flags.Flags; +import com.android.compatibility.common.util.AdoptShellPermissionsRule; + +import com.google.common.util.concurrent.SettableFuture; + +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +import pandora.HIDGrpc; +import pandora.HostProto.AdvertiseRequest; +import pandora.HostProto.OwnAddressType; + +/** Test cases for {@link Hid Host}. */ +@RunWith(AndroidJUnit4.class) +public class HidHostDualModeTest { + private static final String TAG = "HidHostDualModeTest"; + private SettableFuture<Integer> mFutureConnectionIntent, + mFutureBondIntent, + mFutureHandShakeIntent, + mFutureReportIntent, + mFutureProtocolModeIntent, + mFutureTransportIntent; + private SettableFuture<Boolean> mFutureHogpServiceIntent; + private BluetoothDevice mDevice; + private BluetoothHidHost mHidService; + private BluetoothHeadset mHfpService; + private BluetoothA2dp mA2dpService; + private final Context mContext = ApplicationProvider.getApplicationContext(); + private final BluetoothManager mManager = mContext.getSystemService(BluetoothManager.class); + private final BluetoothAdapter mAdapter = mManager.getAdapter(); + private HIDGrpc.HIDBlockingStub mHidBlockingStub; + private byte mReportId; + private static final int KEYBD_RPT_ID = 1; + private static final int KEYBD_RPT_SIZE = 9; + private static final int MOUSE_RPT_ID = 2; + private static final int MOUSE_RPT_SIZE = 4; + + @Rule(order = 0) + public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule(); + + @Rule(order = 1) + public final AdoptShellPermissionsRule mPermissionRule = new AdoptShellPermissionsRule(); + + @Rule(order = 2) + public final PandoraDevice mBumble = new PandoraDevice(); + + private BroadcastReceiver mHidStateReceiver = + new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + switch (intent.getAction()) { + case BluetoothHidHost.ACTION_CONNECTION_STATE_CHANGED: + int state = + intent.getIntExtra( + BluetoothProfile.EXTRA_STATE, BluetoothAdapter.ERROR); + int transport = + intent.getIntExtra( + BluetoothDevice.EXTRA_TRANSPORT, + BluetoothDevice.TRANSPORT_AUTO); + Log.i( + TAG, + "Connection state change: " + + state + + "transport: " + + transport); + if (state == BluetoothProfile.STATE_CONNECTED + || state == BluetoothProfile.STATE_DISCONNECTED) { + if (mFutureConnectionIntent != null) { + mFutureConnectionIntent.set(state); + } + if (state == BluetoothProfile.STATE_CONNECTED + && mFutureTransportIntent != null) { + mFutureTransportIntent.set(transport); + } + } + break; + case BluetoothDevice.ACTION_PAIRING_REQUEST: + mBumble.getRemoteDevice().setPairingConfirmation(true); + break; + case BluetoothDevice.ACTION_BOND_STATE_CHANGED: + int bondState = + intent.getIntExtra( + BluetoothDevice.EXTRA_BOND_STATE, + BluetoothDevice.ERROR); + Log.i(TAG, "Bond state change:" + bondState); + if (bondState == BluetoothDevice.BOND_BONDED + || bondState == BluetoothDevice.BOND_NONE) { + if (mFutureBondIntent != null) { + mFutureBondIntent.set(bondState); + } + } + break; + case BluetoothDevice.ACTION_UUID: + ParcelUuid[] parcelUuids = + intent.getParcelableArrayExtra( + BluetoothDevice.EXTRA_UUID, ParcelUuid.class); + for (int i = 0; i < parcelUuids.length; i++) { + Log.d(TAG, "UUIDs : index=" + i + " uuid=" + parcelUuids[i]); + if (parcelUuids[i].equals(BluetoothUuid.HOGP)) { + if (mFutureHogpServiceIntent != null) { + mFutureHogpServiceIntent.set(true); + } + } + } + break; + case BluetoothHidHost.ACTION_PROTOCOL_MODE_CHANGED: + int protocolMode = + intent.getIntExtra( + BluetoothHidHost.EXTRA_PROTOCOL_MODE, + BluetoothHidHost.PROTOCOL_UNSUPPORTED_MODE); + Log.i(TAG, "Protocol mode:" + protocolMode); + if (mFutureProtocolModeIntent != null) { + mFutureProtocolModeIntent.set(protocolMode); + } + break; + case BluetoothHidHost.ACTION_HANDSHAKE: + int handShake = + intent.getIntExtra( + BluetoothHidHost.EXTRA_STATUS, + BluetoothHidDevice.ERROR_RSP_UNKNOWN); + Log.i(TAG, "Handshake status:" + handShake); + if (mFutureHandShakeIntent != null) { + mFutureHandShakeIntent.set(handShake); + } + break; + case BluetoothHidHost.ACTION_REPORT: + byte[] report = intent.getByteArrayExtra(BluetoothHidHost.EXTRA_REPORT); + int reportSize = + intent.getIntExtra( + BluetoothHidHost.EXTRA_REPORT_BUFFER_SIZE, 0); + mReportId = report[0]; + if (mFutureReportIntent != null) { + mFutureReportIntent.set((reportSize - 1)); + } + break; + default: + break; + } + } + }; + + // These callbacks run on the main thread. + private final BluetoothProfile.ServiceListener mBluetoothProfileServiceListener = + new BluetoothProfile.ServiceListener() { + + @Override + public void onServiceConnected(int profile, BluetoothProfile proxy) { + switch (profile) { + case BluetoothProfile.HEADSET: + mHfpService = (BluetoothHeadset) proxy; + break; + case BluetoothProfile.A2DP: + mA2dpService = (BluetoothA2dp) proxy; + break; + case BluetoothProfile.HID_HOST: + mHidService = (BluetoothHidHost) proxy; + break; + default: + break; + } + } + + @Override + public void onServiceDisconnected(int profile) {} + }; + + @Before + public void setUp() throws Exception { + final IntentFilter filter = new IntentFilter(); + filter.addAction(BluetoothHidHost.ACTION_CONNECTION_STATE_CHANGED); + filter.addAction(BluetoothDevice.ACTION_BOND_STATE_CHANGED); + filter.addAction(BluetoothDevice.ACTION_PAIRING_REQUEST); + filter.addAction(BluetoothDevice.ACTION_UUID); + filter.addAction(BluetoothHidHost.ACTION_PROTOCOL_MODE_CHANGED); + filter.addAction(BluetoothHidHost.ACTION_HANDSHAKE); + filter.addAction(BluetoothHidHost.ACTION_REPORT); + mContext.registerReceiver(mHidStateReceiver, filter); + mAdapter.getProfileProxy( + mContext, mBluetoothProfileServiceListener, BluetoothProfile.HID_HOST); + mAdapter.getProfileProxy(mContext, mBluetoothProfileServiceListener, BluetoothProfile.A2DP); + mAdapter.getProfileProxy( + mContext, mBluetoothProfileServiceListener, BluetoothProfile.HEADSET); + mHidBlockingStub = mBumble.hidBlocking(); + AdvertiseRequest request = + AdvertiseRequest.newBuilder() + .setLegacy(true) + .setConnectable(true) + .setOwnAddressType(OwnAddressType.RANDOM) + .build(); + mBumble.hostBlocking().advertise(request); + + mFutureConnectionIntent = SettableFuture.create(); + + mDevice = mBumble.getRemoteDevice(); + assertThat(mDevice.createBond()).isTrue(); + assertThat(mFutureConnectionIntent.get()).isEqualTo(BluetoothProfile.STATE_CONNECTED); + if (mA2dpService != null) { + assertThat( + mA2dpService.setConnectionPolicy( + mDevice, BluetoothProfile.CONNECTION_POLICY_FORBIDDEN)) + .isTrue(); + } + if (mHfpService != null) { + assertThat( + mHfpService.setConnectionPolicy( + mDevice, BluetoothProfile.CONNECTION_POLICY_FORBIDDEN)) + .isTrue(); + } + mFutureHogpServiceIntent = SettableFuture.create(); + assertThat(mFutureHogpServiceIntent.get()).isTrue(); + assertThat(mHidService.getPreferredTransport(mDevice)) + .isEqualTo(BluetoothDevice.TRANSPORT_BREDR); + // LE transport + mFutureTransportIntent = SettableFuture.create(); + mHidService.setPreferredTransport(mDevice, BluetoothDevice.TRANSPORT_LE); + // Verifies BREDR transport Disconnected + mFutureConnectionIntent = SettableFuture.create(); + assertThat(mFutureConnectionIntent.get()).isEqualTo(BluetoothProfile.STATE_DISCONNECTED); + + assertThat(mFutureTransportIntent.get()).isEqualTo(BluetoothDevice.TRANSPORT_LE); + assertThat(mHidService.getPreferredTransport(mDevice)) + .isEqualTo(BluetoothDevice.TRANSPORT_LE); + } + + @After + public void tearDown() throws Exception { + if (mDevice.getBondState() == BluetoothDevice.BOND_BONDED) { + mFutureBondIntent = SettableFuture.create(); + mDevice.removeBond(); + assertThat(mFutureBondIntent.get()).isEqualTo(BluetoothDevice.BOND_NONE); + } + mContext.unregisterReceiver(mHidStateReceiver); + } + + /** + * Test HID Preferred transport selection Test case + * + * <ol> + * <li>1. Android to creates bonding and HID connected with default transport. + * <li>2. Android switch the transport to LE and Verifies the transport + * <li>3. Android switch the transport to BR/EDR and Verifies the transport + * </ol> + */ + @Test + @RequiresFlagsEnabled({ + Flags.FLAG_ALLOW_SWITCHING_HID_AND_HOGP, + Flags.FLAG_SAVE_INITIAL_HID_CONNECTION_POLICY + }) + public void setPreferredTransportTest() throws Exception { + + // BREDR transport + mFutureTransportIntent = SettableFuture.create(); + mHidService.setPreferredTransport(mDevice, BluetoothDevice.TRANSPORT_BREDR); + // Verifies LE transport Disconnected + mFutureConnectionIntent = SettableFuture.create(); + assertThat(mFutureConnectionIntent.get()).isEqualTo(BluetoothProfile.STATE_DISCONNECTED); + + assertThat(mFutureTransportIntent.get()).isEqualTo(BluetoothDevice.TRANSPORT_BREDR); + assertThat(mHidService.getPreferredTransport(mDevice)) + .isEqualTo(BluetoothDevice.TRANSPORT_BREDR); + } + + /** + * Test Get Report + * + * <ol> + * <li>1. Android creates bonding and connect the HID Device + * <li>2. Android get report and verifies the report + * </ol> + */ + @Test + @RequiresFlagsEnabled({ + Flags.FLAG_ALLOW_SWITCHING_HID_AND_HOGP, + Flags.FLAG_SAVE_INITIAL_HID_CONNECTION_POLICY + }) + public void hogpGetReportTest() throws Exception { + + // Keyboard report + byte id = KEYBD_RPT_ID; + mHidService.getReport(mDevice, BluetoothHidHost.REPORT_TYPE_INPUT, id, (int) 0); + mFutureReportIntent = SettableFuture.create(); + assertThat(mFutureReportIntent.get()).isEqualTo(KEYBD_RPT_SIZE); + assertThat(mReportId).isEqualTo(KEYBD_RPT_ID); + + // Mouse report + id = MOUSE_RPT_ID; + mHidService.getReport(mDevice, BluetoothHidHost.REPORT_TYPE_INPUT, id, (int) 0); + mFutureReportIntent = SettableFuture.create(); + assertThat(mFutureReportIntent.get()).isEqualTo(MOUSE_RPT_SIZE); + assertThat(mReportId).isEqualTo(MOUSE_RPT_ID); + } + + /** + * Test Get Protocol mode + * + * <ol> + * <li>1. Android creates bonding and connect the HID Device + * <li>2. Android Gets the Protocol mode and verifies the mode + * </ol> + */ + @Test + @RequiresFlagsEnabled({ + Flags.FLAG_ALLOW_SWITCHING_HID_AND_HOGP, + Flags.FLAG_SAVE_INITIAL_HID_CONNECTION_POLICY + }) + public void hogpGetProtocolModeTest() throws Exception { + mHidService.getProtocolMode(mDevice); + mFutureProtocolModeIntent = SettableFuture.create(); + assertThat(mFutureProtocolModeIntent.get()) + .isEqualTo(BluetoothHidHost.PROTOCOL_REPORT_MODE); + } + + /** + * Test Set Protocol mode + * + * <ol> + * <li>1. Android creates bonding and connect the HID Device + * <li>2. Android Sets the Protocol mode and verifies the mode + * </ol> + */ + @Test + @RequiresFlagsEnabled({ + Flags.FLAG_ALLOW_SWITCHING_HID_AND_HOGP, + Flags.FLAG_SAVE_INITIAL_HID_CONNECTION_POLICY + }) + public void hogpSetProtocolModeTest() throws Exception { + mHidService.setProtocolMode(mDevice, BluetoothHidHost.PROTOCOL_BOOT_MODE); + mFutureHandShakeIntent = SettableFuture.create(); + assertThat(mFutureHandShakeIntent.get()).isEqualTo(BluetoothHidDevice.ERROR_RSP_SUCCESS); + } + + /** + * Test Set Report + * + * <ol> + * <li>1. Android creates bonding and connect the HID Device + * <li>2. Android Set report and verifies the report + * </ol> + */ + @Test + @RequiresFlagsEnabled({ + Flags.FLAG_ALLOW_SWITCHING_HID_AND_HOGP, + Flags.FLAG_SAVE_INITIAL_HID_CONNECTION_POLICY + }) + public void hogpSetReportTest() throws Exception { + // Keyboard report + mHidService.setReport(mDevice, BluetoothHidHost.REPORT_TYPE_INPUT, "010203040506070809"); + mFutureHandShakeIntent = SettableFuture.create(); + assertThat(mFutureHandShakeIntent.get()).isEqualTo(BluetoothHidDevice.ERROR_RSP_SUCCESS); + // Mouse report + mHidService.setReport(mDevice, BluetoothHidHost.REPORT_TYPE_INPUT, "02030405"); + mFutureHandShakeIntent = SettableFuture.create(); + assertThat(mFutureHandShakeIntent.get()).isEqualTo(BluetoothHidDevice.ERROR_RSP_SUCCESS); + } + + /** + * Test Virtual Unplug from Hid Host + * + * <ol> + * <li>1. Android creates bonding and connect the HID Device + * <li>2. Android Virtual Unplug and verifies Bonding + * </ol> + */ + @Test + @RequiresFlagsEnabled({ + Flags.FLAG_ALLOW_SWITCHING_HID_AND_HOGP, + Flags.FLAG_SAVE_INITIAL_HID_CONNECTION_POLICY + }) + public void hogpVirtualUnplugFromHidHostTest() throws Exception { + mHidService.virtualUnplug(mDevice); + mFutureBondIntent = SettableFuture.create(); + assertThat(mFutureBondIntent.get()).isEqualTo(BluetoothDevice.BOND_NONE); + } +} diff --git a/framework/tests/bumble/src/android/bluetooth/hid/HidHostTest.java b/framework/tests/bumble/src/android/bluetooth/hid/HidHostTest.java index 4166d4b3e2..2a2a20ef18 100644 --- a/framework/tests/bumble/src/android/bluetooth/hid/HidHostTest.java +++ b/framework/tests/bumble/src/android/bluetooth/hid/HidHostTest.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2023 The Android Open Source Project + * Copyright (C) 2024 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. @@ -44,7 +44,11 @@ import org.junit.Test; import org.junit.runner.RunWith; import pandora.HIDGrpc; +import pandora.HidProto.ProtocolModeEvent; +import pandora.HidProto.ReportEvent; +import java.time.Duration; +import java.util.Iterator; import java.util.concurrent.CompletableFuture; import java.util.concurrent.TimeUnit; @@ -55,7 +59,11 @@ public class HidHostTest { private static final String TAG = "HidHostTest"; private SettableFuture<Integer> mFutureConnectionIntent, mFutureAdapterStateIntent, - mFutureBondIntent; + mFutureBondIntent, + mFutureHandShakeIntent, + mFutureProtocolModeIntent, + mFutureVirtualUnplugIntent, + mFutureReportIntent; private SettableFuture<Boolean> mAclConnectionIntent; private BluetoothDevice mDevice; private BluetoothHidHost mHidService; @@ -65,8 +73,16 @@ public class HidHostTest { private final BluetoothManager mManager = mContext.getSystemService(BluetoothManager.class); private final BluetoothAdapter mAdapter = mManager.getAdapter(); private HIDGrpc.HIDBlockingStub mHidBlockingStub; + private byte mReportId; + private static final int KEYBD_RPT_ID = 1; + private static final int KEYBD_RPT_SIZE = 9; + private static final int MOUSE_RPT_ID = 2; + private static final int MOUSE_RPT_SIZE = 4; + private static final int INVALID_RPT_ID = 3; private static final int CONNECTION_TIMEOUT_MS = 2_000; + private static final Duration PROTO_MODE_TIMEOUT = Duration.ofSeconds(10); + @Rule(order = 0) public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule(); @@ -76,51 +92,98 @@ public class HidHostTest { @Rule(order = 2) public final PandoraDevice mBumble = new PandoraDevice(); - private BroadcastReceiver mConnectionStateReceiver = + private BroadcastReceiver mHidStateReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { - if (BluetoothHidHost.ACTION_CONNECTION_STATE_CHANGED.equals( - intent.getAction())) { - int state = - intent.getIntExtra( - BluetoothProfile.EXTRA_STATE, BluetoothAdapter.ERROR); - Log.i(TAG, "Connection state change:" + state); - if (state == BluetoothProfile.STATE_CONNECTED - || state == BluetoothProfile.STATE_DISCONNECTED) { - if (mFutureConnectionIntent != null) { - mFutureConnectionIntent.set(state); + switch (intent.getAction()) { + case BluetoothHidHost.ACTION_CONNECTION_STATE_CHANGED: + int state = + intent.getIntExtra( + BluetoothProfile.EXTRA_STATE, BluetoothAdapter.ERROR); + Log.i(TAG, "Connection state change:" + state); + if (state == BluetoothProfile.STATE_CONNECTED + || state == BluetoothProfile.STATE_DISCONNECTED) { + if (mFutureConnectionIntent != null) { + mFutureConnectionIntent.set(state); + } + } + break; + case BluetoothDevice.ACTION_PAIRING_REQUEST: + mBumble.getRemoteDevice().setPairingConfirmation(true); + break; + case BluetoothAdapter.ACTION_STATE_CHANGED: + int adapterState = + intent.getIntExtra( + BluetoothAdapter.EXTRA_STATE, BluetoothAdapter.ERROR); + Log.i(TAG, "Adapter state change:" + adapterState); + if (adapterState == BluetoothAdapter.STATE_ON + || adapterState == BluetoothAdapter.STATE_OFF) { + if (mFutureAdapterStateIntent != null) { + mFutureAdapterStateIntent.set(adapterState); + } } - } - } else if (BluetoothDevice.ACTION_PAIRING_REQUEST.equals(intent.getAction())) { - mBumble.getRemoteDevice().setPairingConfirmation(true); - } else if (BluetoothAdapter.ACTION_STATE_CHANGED.equals(intent.getAction())) { - int adapterState = - intent.getIntExtra( - BluetoothAdapter.EXTRA_STATE, BluetoothAdapter.ERROR); - Log.i(TAG, "Adapter state change:" + adapterState); - if (adapterState == BluetoothAdapter.STATE_ON - || adapterState == BluetoothAdapter.STATE_OFF) { - if (mFutureAdapterStateIntent != null) { - mFutureAdapterStateIntent.set(adapterState); + break; + case BluetoothDevice.ACTION_BOND_STATE_CHANGED: + int bondState = + intent.getIntExtra( + BluetoothDevice.EXTRA_BOND_STATE, + BluetoothDevice.ERROR); + Log.i(TAG, "Bond state change:" + bondState); + if (bondState == BluetoothDevice.BOND_BONDED + || bondState == BluetoothDevice.BOND_NONE) { + if (mFutureBondIntent != null) { + mFutureBondIntent.set(bondState); + } } - } - } else if (BluetoothDevice.ACTION_BOND_STATE_CHANGED.equals( - intent.getAction())) { - int bondState = - intent.getIntExtra( - BluetoothDevice.EXTRA_BOND_STATE, BluetoothDevice.ERROR); - Log.i(TAG, "Bond state change:" + bondState); - if (bondState == BluetoothDevice.BOND_BONDED - || bondState == BluetoothDevice.BOND_NONE) { - if (mFutureBondIntent != null) { - mFutureBondIntent.set(bondState); + break; + case BluetoothHidHost.ACTION_PROTOCOL_MODE_CHANGED: + int protocolMode = + intent.getIntExtra( + BluetoothHidHost.EXTRA_PROTOCOL_MODE, + BluetoothHidHost.PROTOCOL_UNSUPPORTED_MODE); + Log.i(TAG, "Protocol mode:" + protocolMode); + if (mFutureProtocolModeIntent != null) { + mFutureProtocolModeIntent.set(protocolMode); } - } - } else if (BluetoothDevice.ACTION_ACL_DISCONNECTED.equals(intent.getAction())) { - if (mAclConnectionIntent != null) { - mAclConnectionIntent.set(true); - } + break; + case BluetoothHidHost.ACTION_HANDSHAKE: + int handShake = + intent.getIntExtra( + BluetoothHidHost.EXTRA_STATUS, + BluetoothHidDevice.ERROR_RSP_UNKNOWN); + Log.i(TAG, "Handshake status:" + handShake); + if (mFutureHandShakeIntent != null) { + mFutureHandShakeIntent.set(handShake); + } + break; + case BluetoothHidHost.ACTION_VIRTUAL_UNPLUG_STATUS: + int virtualUnplug = + intent.getIntExtra( + BluetoothHidHost.EXTRA_VIRTUAL_UNPLUG_STATUS, + BluetoothHidHost.VIRTUAL_UNPLUG_STATUS_FAIL); + Log.i(TAG, "Virtual Unplug status:" + virtualUnplug); + if (mFutureVirtualUnplugIntent != null) { + mFutureVirtualUnplugIntent.set(virtualUnplug); + } + break; + case BluetoothHidHost.ACTION_REPORT: + byte[] report = intent.getByteArrayExtra(BluetoothHidHost.EXTRA_REPORT); + int reportSize = + intent.getIntExtra( + BluetoothHidHost.EXTRA_REPORT_BUFFER_SIZE, 0); + mReportId = report[0]; + if (mFutureReportIntent != null) { + mFutureReportIntent.set((reportSize - 1)); + } + break; + case BluetoothDevice.ACTION_ACL_DISCONNECTED: + if (mAclConnectionIntent != null) { + mAclConnectionIntent.set(true); + } + break; + default: + break; } } }; @@ -152,17 +215,18 @@ public class HidHostTest { @Before public void setUp() throws Exception { - mContext.registerReceiver( - mConnectionStateReceiver, - new IntentFilter(BluetoothHidHost.ACTION_CONNECTION_STATE_CHANGED)); - mContext.registerReceiver( - mConnectionStateReceiver, new IntentFilter(BluetoothDevice.ACTION_PAIRING_REQUEST)); - mContext.registerReceiver( - mConnectionStateReceiver, - new IntentFilter(BluetoothDevice.ACTION_BOND_STATE_CHANGED)); - mContext.registerReceiver( - mConnectionStateReceiver, - new IntentFilter(BluetoothDevice.ACTION_ACL_DISCONNECTED)); + final IntentFilter filter = new IntentFilter(); + filter.addAction(BluetoothHidHost.ACTION_CONNECTION_STATE_CHANGED); + filter.addAction(BluetoothDevice.ACTION_PAIRING_REQUEST); + filter.addAction(BluetoothDevice.ACTION_BOND_STATE_CHANGED); + filter.addAction(BluetoothHidHost.ACTION_PROTOCOL_MODE_CHANGED); + filter.addAction(BluetoothHidHost.ACTION_HANDSHAKE); + filter.addAction(BluetoothHidHost.ACTION_VIRTUAL_UNPLUG_STATUS); + filter.addAction(BluetoothHidHost.ACTION_REPORT); + filter.addAction(BluetoothAdapter.ACTION_STATE_CHANGED); + filter.addAction(BluetoothDevice.ACTION_ACL_DISCONNECTED); + + mContext.registerReceiver(mHidStateReceiver, filter); mAdapter.getProfileProxy( mContext, mBluetoothProfileServiceListener, BluetoothProfile.HID_HOST); mAdapter.getProfileProxy(mContext, mBluetoothProfileServiceListener, BluetoothProfile.A2DP); @@ -191,6 +255,7 @@ public class HidHostTest { @After public void tearDown() throws Exception { + if (mDevice.getBondState() == BluetoothDevice.BOND_BONDED) { mFutureBondIntent = SettableFuture.create(); mDevice.removeBond(); @@ -202,7 +267,8 @@ public class HidHostTest { mDevice.disconnect(); assertThat(mAclConnectionIntent.get()).isTrue(); } - mContext.unregisterReceiver(mConnectionStateReceiver); + + mContext.unregisterReceiver(mHidStateReceiver); } /** @@ -368,12 +434,183 @@ public class HidHostTest { assertThat(mFutureConnectionIntent.get()).isEqualTo(BluetoothProfile.STATE_DISCONNECTED); + mFutureBondIntent = SettableFuture.create(); mDevice.removeBond(); + assertThat(mFutureBondIntent.get()).isEqualTo(BluetoothDevice.BOND_NONE); - mFutureConnectionIntent = SettableFuture.create(); - mHidBlockingStub.connectHost(Empty.getDefaultInstance()); - assertThat(mHidService.getConnectionState(mDevice)) - .isEqualTo(BluetoothProfile.STATE_DISCONNECTED); + reconnectionFromRemoteAndVerifyDisconnectedState(); + } + + /** + * Test Virtual Unplug from Hid Host + * + * <ol> + * <li>1. Android creates bonding and connect the HID Device + * <li>2. Android Virtual Unplug and verifies Bonding + * </ol> + */ + @Test + public void hidVirtualUnplugFromHidHostTest() throws Exception { + mHidService.virtualUnplug(mDevice); + mFutureBondIntent = SettableFuture.create(); + assertThat(mFutureBondIntent.get()).isEqualTo(BluetoothDevice.BOND_NONE); + } + + /** + * Test Virtual Unplug from Hid Device + * + * <ol> + * <li>1. Android creates bonding and connect the HID Device + * <li>2. Bumble Virtual Unplug and Android verifies Bonding + * </ol> + */ + @Test + public void hidVirtualUnplugFromHidDeviceTest() throws Exception { + mHidBlockingStub.virtualCableUnplugHost(Empty.getDefaultInstance()); + mFutureVirtualUnplugIntent = SettableFuture.create(); + assertThat(mFutureVirtualUnplugIntent.get()) + .isEqualTo(BluetoothHidHost.VIRTUAL_UNPLUG_STATUS_SUCCESS); + } + + /** + * Test Get Protocol mode + * + * <ol> + * <li>1. Android creates bonding and connect the HID Device + * <li>2. Android Gets the Protocol mode and verifies the mode + * </ol> + */ + @Test + public void hidGetProtocolModeTest() throws Exception { + mHidService.getProtocolMode(mDevice); + mFutureProtocolModeIntent = SettableFuture.create(); + assertThat(mFutureProtocolModeIntent.get()) + .isEqualTo(BluetoothHidHost.PROTOCOL_REPORT_MODE); + } + + /** + * Test Set Protocol mode + * + * <ol> + * <li>1. Android creates bonding and connect the HID Device + * <li>2. Android Sets the Protocol mode and verifies the mode + * </ol> + */ + @Test + @Ignore("b/349351673: sets wrong protocol mode value") + public void hidSetProtocolModeTest() throws Exception { + Iterator<ProtocolModeEvent> mHidProtoModeEventObserver = + mHidBlockingStub + .withDeadlineAfter(PROTO_MODE_TIMEOUT.toMillis(), TimeUnit.MILLISECONDS) + .onSetProtocolMode(Empty.getDefaultInstance()); + mHidService.setProtocolMode(mDevice, BluetoothHidHost.PROTOCOL_BOOT_MODE); + mFutureHandShakeIntent = SettableFuture.create(); + assertThat(mFutureHandShakeIntent.get()) + .isEqualTo(BluetoothHidDevice.ERROR_RSP_UNSUPPORTED_REQ); + if (mHidProtoModeEventObserver.hasNext()) { + ProtocolModeEvent hidProtoModeEvent = mHidProtoModeEventObserver.next(); + Log.i(TAG, "Protocol mode:" + hidProtoModeEvent.getProtocolMode()); + assertThat(hidProtoModeEvent.getProtocolModeValue()) + .isEqualTo(BluetoothHidHost.PROTOCOL_BOOT_MODE); + } + } + + /** + * Test Get Report + * + * <ol> + * <li>1. Android creates bonding and connect the HID Device + * <li>2. Android get report and verifies the report + * </ol> + */ + @Test + public void hidGetReportTest() throws Exception { + // Keyboard report + byte id = KEYBD_RPT_ID; + mHidService.getReport(mDevice, BluetoothHidHost.REPORT_TYPE_INPUT, id, (int) 0); + mFutureReportIntent = SettableFuture.create(); + assertThat(mFutureReportIntent.get()).isEqualTo(KEYBD_RPT_SIZE); + assertThat(mReportId).isEqualTo(KEYBD_RPT_ID); + + // Mouse report + id = MOUSE_RPT_ID; + mHidService.getReport(mDevice, BluetoothHidHost.REPORT_TYPE_INPUT, id, (int) 0); + mFutureReportIntent = SettableFuture.create(); + assertThat(mFutureReportIntent.get()).isEqualTo(MOUSE_RPT_SIZE); + assertThat(mReportId).isEqualTo(MOUSE_RPT_ID); + + // Invalid report + id = INVALID_RPT_ID; + mHidService.getReport(mDevice, BluetoothHidHost.REPORT_TYPE_INPUT, id, (int) 0); + mFutureHandShakeIntent = SettableFuture.create(); + assertThat(mFutureHandShakeIntent.get()) + .isEqualTo(BluetoothHidDevice.ERROR_RSP_INVALID_RPT_ID); + } + + /** + * Test Set Report + * + * <ol> + * <li>1. Android creates bonding and connect the HID Device + * <li>2. Android Set report and verifies the report + * </ol> + */ + @Test + public void hidSetReportTest() throws Exception { + Iterator<ReportEvent> mHidReportEventObserver = + mHidBlockingStub + .withDeadlineAfter(PROTO_MODE_TIMEOUT.toMillis(), TimeUnit.MILLISECONDS) + .onSetReport(Empty.getDefaultInstance()); + // Keyboard report + String kbReportData = "010203040506070809"; + mHidService.setReport(mDevice, BluetoothHidHost.REPORT_TYPE_INPUT, kbReportData); + mFutureHandShakeIntent = SettableFuture.create(); + assertThat(mFutureHandShakeIntent.get()).isEqualTo(BluetoothHidDevice.ERROR_RSP_SUCCESS); + if (mHidReportEventObserver.hasNext()) { + ReportEvent hidReportEvent = mHidReportEventObserver.next(); + assertThat(hidReportEvent.getReportTypeValue()) + .isEqualTo(BluetoothHidHost.REPORT_TYPE_INPUT); + assertThat(hidReportEvent.getReportIdValue()).isEqualTo(KEYBD_RPT_ID); + assertThat(hidReportEvent.getReportData()).isEqualTo(kbReportData.substring(2)); + } + // Keyboard report - Invalid param + mHidService.setReport( + mDevice, BluetoothHidHost.REPORT_TYPE_INPUT, kbReportData.substring(0, 10)); + mFutureHandShakeIntent = SettableFuture.create(); + assertThat(mFutureHandShakeIntent.get()) + .isEqualTo(BluetoothHidDevice.ERROR_RSP_INVALID_PARAM); + if (mHidReportEventObserver.hasNext()) { + ReportEvent hidReportEvent = mHidReportEventObserver.next(); + assertThat(hidReportEvent.getReportTypeValue()) + .isEqualTo(BluetoothHidHost.REPORT_TYPE_INPUT); + assertThat(hidReportEvent.getReportIdValue()).isEqualTo(KEYBD_RPT_ID); + assertThat(hidReportEvent.getReportData()).isEqualTo(kbReportData.substring(2, 10)); + } + // Mouse report + String mouseReportData = "02030405"; + mHidService.setReport(mDevice, BluetoothHidHost.REPORT_TYPE_INPUT, mouseReportData); + mFutureHandShakeIntent = SettableFuture.create(); + assertThat(mFutureHandShakeIntent.get()).isEqualTo(BluetoothHidDevice.ERROR_RSP_SUCCESS); + if (mHidReportEventObserver.hasNext()) { + ReportEvent hidReportEvent = mHidReportEventObserver.next(); + assertThat(hidReportEvent.getReportTypeValue()) + .isEqualTo(BluetoothHidHost.REPORT_TYPE_INPUT); + assertThat(hidReportEvent.getReportIdValue()).isEqualTo(MOUSE_RPT_ID); + assertThat(hidReportEvent.getReportData()).isEqualTo(mouseReportData.substring(2)); + } + // Invalid report id + String inValidReportData = "0304"; + mHidService.setReport(mDevice, BluetoothHidHost.REPORT_TYPE_INPUT, inValidReportData); + mFutureHandShakeIntent = SettableFuture.create(); + assertThat(mFutureHandShakeIntent.get()) + .isEqualTo(BluetoothHidDevice.ERROR_RSP_INVALID_RPT_ID); + if (mHidReportEventObserver.hasNext()) { + ReportEvent hidReportEvent = mHidReportEventObserver.next(); + assertThat(hidReportEvent.getReportTypeValue()) + .isEqualTo(BluetoothHidHost.REPORT_TYPE_INPUT); + assertThat(hidReportEvent.getReportIdValue()).isEqualTo(INVALID_RPT_ID); + assertThat(hidReportEvent.getReportData()).isEqualTo(inValidReportData.substring(2)); + } } private void reconnectionFromRemoteAndVerifyDisconnectedState() throws Exception { @@ -385,9 +622,6 @@ public class HidHostTest { } private void bluetoothRestart() throws Exception { - mContext.registerReceiver( - mConnectionStateReceiver, new IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED)); - mAdapter.disable(); mFutureAdapterStateIntent = SettableFuture.create(); assertThat(mFutureAdapterStateIntent.get()).isEqualTo(BluetoothAdapter.STATE_OFF); diff --git a/pandora/interfaces/pandora_experimental/hid.proto b/pandora/interfaces/pandora_experimental/hid.proto index 7b1e7d2c8a..709d1577ff 100644 --- a/pandora/interfaces/pandora_experimental/hid.proto +++ b/pandora/interfaces/pandora_experimental/hid.proto @@ -15,8 +15,11 @@ service HID { rpc VirtualCableUnplugHost(google.protobuf.Empty) returns (google.protobuf.Empty); // Send a SET_REPORT command, acting as a HID host, to a connected HID device rpc SendHostReport(SendHostReportRequest) returns (SendHostReportResponse); + // receive Protocol Mode Event + rpc OnSetProtocolMode(google.protobuf.Empty) returns (stream ProtocolModeEvent); + // receive Report Event + rpc OnSetReport(google.protobuf.Empty) returns (stream ReportEvent); } - // Enum values match those in BluetoothHidHost.java enum HidReportType { HID_REPORT_TYPE_UNSPECIFIED = 0; @@ -24,6 +27,17 @@ enum HidReportType { HID_REPORT_TYPE_OUTPUT = 2; HID_REPORT_TYPE_FEATURE = 3; } +// Enum values match those in BluetoothHidHost.java +enum ProtocolMode { + PROTOCOL_REPORT_MODE = 0; + PROTOCOL_BOOT_MODE = 1; + PROTOCOL_UNSUPPORTED_MODE = 255; +} +enum HidReportId { + HID_KEYBD_RPT_ID = 0; + HID_MOUSE_RPT_ID = 1; + HID_INVALID_RPT_ID = 3; +} message SendHostReportRequest { bytes address = 1; @@ -34,3 +48,13 @@ message SendHostReportRequest { message SendHostReportResponse { } + +message ProtocolModeEvent { + ProtocolMode protocol_mode = 1; +} + +message ReportEvent { + HidReportType report_type = 1; + HidReportId report_id = 2; + string report_data = 3; +} diff --git a/pandora/server/bumble_experimental/hid.py b/pandora/server/bumble_experimental/hid.py index 497f5edd4a..6da25ec6e1 100644 --- a/pandora/server/bumble_experimental/hid.py +++ b/pandora/server/bumble_experimental/hid.py @@ -11,6 +11,14 @@ from google.protobuf import empty_pb2 # pytype: disable=pyi-error from pandora_experimental.hid_grpc_aio import HIDServicer from bumble.pandora import utils +from pandora_experimental.hid_pb2 import ( + ProtocolModeEvent, + ReportEvent, + PROTOCOL_REPORT_MODE, + PROTOCOL_BOOT_MODE, + PROTOCOL_UNSUPPORTED_MODE, +) + from bumble.core import ( BT_BR_EDR_TRANSPORT, BT_L2CAP_PROTOCOL_ID, @@ -489,20 +497,30 @@ def sdp_records(): # ----------------------------------------------------------------------------- def hogp_device(device): - global input_report_characteristic # Create an 'input report' characteristic to send keyboard reports to the host - input_report_characteristic = Characteristic( + input_report_kb_characteristic = Characteristic( GATT_REPORT_CHARACTERISTIC, Characteristic.Properties.READ | Characteristic.Properties.WRITE | Characteristic.Properties.NOTIFY, Characteristic.READABLE | Characteristic.WRITEABLE, - bytes([0, 0, 0, 0, 0, 0, 0, 0]), + bytes([0, 0, 0, 0, 0, 0, 0, 0, 0]), [Descriptor( GATT_REPORT_REFERENCE_DESCRIPTOR, Descriptor.READABLE, bytes([0x01, HID_INPUT_REPORT]), )], ) - + # Create an 'input report' characteristic to send mouse reports to the host + input_report_mouse_characteristic = Characteristic( + GATT_REPORT_CHARACTERISTIC, + Characteristic.Properties.READ | Characteristic.Properties.WRITE | Characteristic.Properties.NOTIFY, + Characteristic.READABLE | Characteristic.WRITEABLE, + bytes([0, 0, 0, 0]), + [Descriptor( + GATT_REPORT_REFERENCE_DESCRIPTOR, + Descriptor.READABLE, + bytes([0x02, HID_INPUT_REPORT]), + )], + ) # Create an 'output report' characteristic to receive keyboard reports from the host output_report_characteristic = Characteristic( GATT_REPORT_CHARACTERISTIC, @@ -558,7 +576,8 @@ def hogp_device(device): Characteristic.READABLE, HID_KEYBOARD_REPORT_MAP, ), - input_report_characteristic, + input_report_kb_characteristic, + input_report_mouse_characteristic, output_report_characteristic, ], ), @@ -630,6 +649,12 @@ def on_set_report_cb(report_id: int, report_type: int, report_size: int, data: b logging.info("SET_REPORT report_id: " + str(report_id) + "report_type: " + str(report_type) + "report_size " + str(report_size) + "data:" + str(data)) + report = ReportEvent() + report.report_type = report_type + report.report_id = report_id + report.report_data = str(data.hex()) + hid_report_queue.put_nowait(report) + if report_type == Message.ReportType.FEATURE_REPORT: retValue.status = hid_device.GetSetReturn.ERR_INVALID_PARAMETER elif report_type == Message.ReportType.INPUT_REPORT: @@ -657,7 +682,15 @@ def on_get_protocol_cb(): def on_set_protocol_cb(protocol: int): retValue = hid_device.GetSetStatus() # We do not support SET_PROTOCOL. - logging.info(f"SET_PROTOCOL report_id: {protocol}") + logging.info(f"SET_PROTOCOL mode: {protocol}") + mode = ProtocolModeEvent() + if protocol == PROTOCOL_REPORT_MODE: + mode.protocol_mode = PROTOCOL_REPORT_MODE + elif protocol == PROTOCOL_BOOT_MODE: + mode.protocol_mode = PROTOCOL_BOOT_MODE + else: + mode.protocol_mode = PROTOCOL_UNSUPPORTED_MODE + hid_protoMode_queue.put_nowait(mode) retValue.status = hid_device.GetSetReturn.ERR_UNSUPPORTED_REQUEST return retValue @@ -667,6 +700,9 @@ def on_virtual_cable_unplug_cb(): asyncio.create_task(handle_virtual_cable_unplug()) +hid_protoMode_queue = None + + # This class implements the Hid Pandora interface. class HIDService(HIDServicer): @@ -676,6 +712,7 @@ class HIDService(HIDServicer): super().__init__() self.device = device self.device.sdp_service_records.update(sdp_records()) + self.event_queue: Optional[asyncio.Queue[ProtocolModeEvent]] = None hogp_device(self.device) logging.info(f'Hid device register: ') global hid_device @@ -742,3 +779,43 @@ class HIDService(HIDServicer): logging.exception(f'Device does not exist') raise e return empty_pb2.Empty() + + @utils.rpc + async def OnSetProtocolMode(self, request: empty_pb2.Empty, + context: grpc.ServicerContext) -> AsyncGenerator[ProtocolModeEvent, None]: + logging.info(f'OnSetProtocolMode') + + if self.event_queue is not None: + raise RuntimeError('already streaming OnSetProtocolMode events') + + self.event_queue = asyncio.Queue() + global hid_protoMode_queue + hid_protoMode_queue = self.event_queue + + try: + while event := await hid_protoMode_queue.get(): + yield event + + finally: + self.event_queue = None + hid_protoMode_queue = None + + @utils.rpc + async def OnSetReport(self, request: empty_pb2.Empty, + context: grpc.ServicerContext) -> AsyncGenerator[ReportEvent, None]: + logging.info(f'OnSetReport') + + if self.event_queue is not None: + raise RuntimeError('already streaming OnSetReport events') + + self.event_queue = asyncio.Queue() + global hid_report_queue + hid_report_queue = self.event_queue + + try: + while event := await hid_report_queue.get(): + yield event + + finally: + self.event_queue = None + hid_report_queue = None |