Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 66c01a2a authored by Treehugger Robot's avatar Treehugger Robot Committed by Gerrit Code Review
Browse files

Merge "Avatar implement first HAP test" into main

parents f4ed978a c62a18f1
Loading
Loading
Loading
Loading
+63 −11
Original line number Diff line number Diff line
@@ -16,6 +16,7 @@

package com.android.pandora

import android.bluetooth.BluetoothAdapter
import android.bluetooth.BluetoothDevice
import android.bluetooth.BluetoothDevice.TRANSPORT_LE
import android.bluetooth.BluetoothHapClient
@@ -24,7 +25,9 @@ import android.bluetooth.BluetoothHapPresetInfo
import android.bluetooth.BluetoothLeAudio
import android.bluetooth.BluetoothManager
import android.bluetooth.BluetoothProfile
import android.bluetooth.BluetoothProfile.CONNECTION_POLICY_ALLOWED
import android.content.Context
import android.content.IntentFilter
import android.media.AudioManager
import android.media.AudioTrack
import android.util.Log
@@ -39,8 +42,12 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.cancel
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.shareIn
import pandora.HAPGrpc.HAPImplBase
import pandora.HapProto.*
import pandora.HostProto.Connection
@@ -61,12 +68,22 @@ class Hap(val context: Context) : HAPImplBase(), Closeable {
    private val bluetoothLeAudio =
        getProfileProxy<BluetoothLeAudio>(context, BluetoothProfile.LE_AUDIO)

    private val flow =
        intentFlow(
                context,
                IntentFilter().apply {
                    addAction(BluetoothHapClient.ACTION_HAP_CONNECTION_STATE_CHANGED)
                },
                scope,
            )
            .shareIn(scope, SharingStarted.Eagerly)

    private var audioTrack: AudioTrack? = null

    private class PresetInfoChanged(
        var connection: Connection,
        var presetInfoList: List<BluetoothHapPresetInfo>,
        var reason: Int
        var reason: Int,
    ) {}

    private val mPresetChanged = callbackFlow {
@@ -75,7 +92,7 @@ class Hap(val context: Context) : HAPImplBase(), Closeable {
                override fun onPresetSelected(
                    device: BluetoothDevice,
                    presetIndex: Int,
                    reason: Int
                    reason: Int,
                ) {
                    Log.i(TAG, "$device preset info changed")
                }
@@ -91,7 +108,7 @@ class Hap(val context: Context) : HAPImplBase(), Closeable {
                override fun onPresetInfoChanged(
                    device: BluetoothDevice,
                    presetInfoList: List<BluetoothHapPresetInfo>,
                    reason: Int
                    reason: Int,
                ) {
                    Log.i(TAG, "$device preset info changed")

@@ -120,9 +137,22 @@ class Hap(val context: Context) : HAPImplBase(), Closeable {
        scope.cancel()
    }

    override fun getFeatures(
        request: GetFeaturesRequest,
        responseObserver: StreamObserver<GetFeaturesResponse>,
    ) {
        grpcUnary<GetFeaturesResponse>(scope, responseObserver) {
            val device = request.connection.toBluetoothDevice(bluetoothAdapter)
            Log.i(TAG, "getFeatures(${device})")
            GetFeaturesResponse.newBuilder()
                .setFeatures(bluetoothHapClient.getFeatures(device))
                .build()
        }
    }

    override fun getPresetRecord(
        request: GetPresetRecordRequest,
        responseObserver: StreamObserver<GetPresetRecordResponse>
        responseObserver: StreamObserver<GetPresetRecordResponse>,
    ) {
        grpcUnary<GetPresetRecordResponse>(scope, responseObserver) {
            val device = request.connection.toBluetoothDevice(bluetoothAdapter)
@@ -150,7 +180,7 @@ class Hap(val context: Context) : HAPImplBase(), Closeable {

    override fun getAllPresetRecords(
        request: GetAllPresetRecordsRequest,
        responseObserver: StreamObserver<GetAllPresetRecordsResponse>
        responseObserver: StreamObserver<GetAllPresetRecordsResponse>,
    ) {
        grpcUnary<GetAllPresetRecordsResponse>(scope, responseObserver) {
            val device = request.connection.toBluetoothDevice(bluetoothAdapter)
@@ -177,7 +207,7 @@ class Hap(val context: Context) : HAPImplBase(), Closeable {

    override fun writePresetName(
        request: WritePresetNameRequest,
        responseObserver: StreamObserver<Empty>
        responseObserver: StreamObserver<Empty>,
    ) {
        grpcUnary<Empty>(scope, responseObserver) {
            val device = request.connection.toBluetoothDevice(bluetoothAdapter)
@@ -192,7 +222,7 @@ class Hap(val context: Context) : HAPImplBase(), Closeable {

    override fun setActivePreset(
        request: SetActivePresetRequest,
        responseObserver: StreamObserver<Empty>
        responseObserver: StreamObserver<Empty>,
    ) {
        grpcUnary<Empty>(scope, responseObserver) {
            val device = request.connection.toBluetoothDevice(bluetoothAdapter)
@@ -207,7 +237,7 @@ class Hap(val context: Context) : HAPImplBase(), Closeable {

    override fun setNextPreset(
        request: SetNextPresetRequest,
        responseObserver: StreamObserver<Empty>
        responseObserver: StreamObserver<Empty>,
    ) {
        grpcUnary<Empty>(scope, responseObserver) {
            val device = request.connection.toBluetoothDevice(bluetoothAdapter)
@@ -222,7 +252,7 @@ class Hap(val context: Context) : HAPImplBase(), Closeable {

    override fun setPreviousPreset(
        request: SetPreviousPresetRequest,
        responseObserver: StreamObserver<Empty>
        responseObserver: StreamObserver<Empty>,
    ) {
        grpcUnary<Empty>(scope, responseObserver) {
            val device = request.connection.toBluetoothDevice(bluetoothAdapter)
@@ -264,7 +294,7 @@ class Hap(val context: Context) : HAPImplBase(), Closeable {
                audioManager.setStreamVolume(
                    AudioManager.STREAM_MUSIC,
                    maxVolume,
                    AudioManager.FLAG_SHOW_UI
                    AudioManager.FLAG_SHOW_UI,
                )
            }
        }
@@ -298,7 +328,7 @@ class Hap(val context: Context) : HAPImplBase(), Closeable {

    override fun waitPresetChanged(
        request: Empty,
        responseObserver: StreamObserver<WaitPresetChangedResponse>
        responseObserver: StreamObserver<WaitPresetChangedResponse>,
    ) {
        grpcUnary<WaitPresetChangedResponse>(scope, responseObserver) {
            val presetChangedReceived = mPresetChanged.first()!!
@@ -322,4 +352,26 @@ class Hap(val context: Context) : HAPImplBase(), Closeable {
                .build()
        }
    }

    override fun waitPeripheral(
        request: WaitPeripheralRequest,
        responseObserver: StreamObserver<Empty>,
    ) {
        grpcUnary<Empty>(scope, responseObserver) {
            val device = request.connection.toBluetoothDevice(bluetoothAdapter)
            Log.i(TAG, "waitPeripheral(${device}")
            if (bluetoothHapClient.getConnectionState(device) != BluetoothProfile.STATE_CONNECTED) {
                Log.d(TAG, "Manual call to setConnectionPolicy")
                bluetoothHapClient.setConnectionPolicy(device, CONNECTION_POLICY_ALLOWED)
                Log.d(TAG, "now waiting for bluetoothHapClient profile connection")
                flow
                    .filter { it.getBluetoothDeviceExtra() == device }
                    .map { it.getIntExtra(BluetoothProfile.EXTRA_STATE, BluetoothAdapter.ERROR) }
                    .filter { it == BluetoothProfile.STATE_CONNECTED }
                    .first()
            }

            Empty.getDefaultInstance()
        }
    }
}
+1 −0
Original line number Diff line number Diff line
@@ -33,6 +33,7 @@
        <option name="dep-module" value="grpcio==1.51.1" />
        <option name="dep-module" value="cryptography==35" />
        <option name="dep-module" value="numpy" />
        <option name="dep-module" value="pytruth" />
    </target_preparer>
    <target_preparer class="com.android.tradefed.targetprep.DeviceSetup">
         <option name="set-property" key="persist.log.tag.bluetooth" value="VERBOSE"/>
+145 −0
Original line number Diff line number Diff line
# 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.

import asyncio

from avatar import BumblePandoraDevice, PandoraDevice, PandoraDevices, asynchronous
from bumble.gatt import GATT_HEARING_ACCESS_SERVICE, GATT_AUDIO_STREAM_CONTROL_SERVICE, GATT_PUBLISHED_AUDIO_CAPABILITIES_SERVICE
from bumble.profiles import hap
from bumble.profiles.hap import DynamicPresets, HearingAccessService, HearingAidFeatures, HearingAidType, IndependentPresets, PresetRecord, PresetSynchronizationSupport, WritablePresetsSupport

from pandora_experimental.gatt_grpc_aio import GATT
from pandora_experimental.hap_grpc_aio import HAP
from pandora._utils import AioStream
from pandora.security_pb2 import LE_LEVEL3
from pandora.host_pb2 import RANDOM, AdvertiseResponse, Connection, DataTypes, ScanningResponse
from mobly import base_test, signals
from truth.truth import AssertThat  # type: ignore
from typing import Tuple

COMPLETE_LOCAL_NAME: str = "Bumble"
HAP_UUID = GATT_HEARING_ACCESS_SERVICE.to_hex_str('-')
ASCS_UUID = GATT_AUDIO_STREAM_CONTROL_SERVICE.to_hex_str('-')
PACS_UUID = GATT_PUBLISHED_AUDIO_CAPABILITIES_SERVICE.to_hex_str('-')

long_name = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua."
foo_preset = PresetRecord(1, "foo preset")
bar_preset = PresetRecord(50, "bar preset")
longname_preset = PresetRecord(5, f'[{long_name[:38]}]')
unavailable_preset = PresetRecord(
    7, "unavailable preset",
    PresetRecord.Property(PresetRecord.Property.Writable.CANNOT_BE_WRITTEN,
                          PresetRecord.Property.IsAvailable.IS_UNAVAILABLE))


class HapTest(base_test.BaseTestClass):
    devices: PandoraDevices
    dut: PandoraDevice
    ref_left: BumblePandoraDevice
    hap_grpc: HAP
    has: HearingAccessService

    def setup_class(self):
        self.devices = PandoraDevices(self)
        dut, ref_left, *_ = self.devices

        if isinstance(dut, BumblePandoraDevice):
            raise signals.TestAbortClass('DUT Bumble does not support HAP')
        self.dut = dut
        if not isinstance(ref_left, BumblePandoraDevice):
            raise signals.TestAbortClass('Test require Bumble as reference device(s)')
        self.ref_left = ref_left

    def teardown_class(self):
        self.devices.stop_all()

    @asynchronous
    async def setup_test(self) -> None:
        await asyncio.gather(self.dut.reset(), self.ref_left.reset())
        self.hap_grpc = HAP(channel=self.dut.aio.channel)
        device_features = HearingAidFeatures(HearingAidType.MONAURAL_HEARING_AID,
                                             PresetSynchronizationSupport.PRESET_SYNCHRONIZATION_IS_NOT_SUPPORTED,
                                             IndependentPresets.IDENTICAL_PRESET_RECORD,
                                             DynamicPresets.PRESET_RECORDS_DOES_NOT_CHANGE,
                                             WritablePresetsSupport.WRITABLE_PRESET_RECORDS_SUPPORTED)
        self.has = HearingAccessService(self.ref_left.device, device_features,
                                        [foo_preset, bar_preset, longname_preset, unavailable_preset])
        self.dut_gatt = GATT(channel=self.dut.aio.channel)

        self.ref_left.device.add_service(self.has)  # type: ignore

    async def advertise_hap(self, device: PandoraDevice) -> AioStream[AdvertiseResponse]:
        return device.aio.host.Advertise(
            legacy=True,
            connectable=True,
            own_address_type=RANDOM,
            data=DataTypes(
                complete_local_name=COMPLETE_LOCAL_NAME,
                incomplete_service_class_uuids16=[HAP_UUID],
            ),
        )

    async def dut_scan_for_hap(self) -> ScanningResponse:
        """
        DUT starts to scan for the Ref device.
        :return: ScanningResponse for ASHA
        """
        dut_scan = self.dut.aio.host.Scan(RANDOM)  # type: ignore
        scan_response = await anext((x async for x in dut_scan if HAP_UUID in x.data.incomplete_service_class_uuids16))
        dut_scan.cancel()
        return scan_response

    async def dut_connect_to_ref(self, advertisement: AioStream[AdvertiseResponse],
                                 ref: ScanningResponse) -> Tuple[Connection, Connection]:
        """
        Helper method for Dut connects to Ref
        :return: a Tuple (DUT to REF connection, REF to DUT connection)
        """
        (dut_ref_res, ref_dut_res) = await asyncio.gather(
            self.dut.aio.host.ConnectLE(own_address_type=RANDOM, **ref.address_asdict()),
            anext(aiter(advertisement)),
        )
        AssertThat(dut_ref_res.result_variant()).IsEqualTo('connection')  # type: ignore
        dut_ref, ref_dut = dut_ref_res.connection, ref_dut_res.connection
        AssertThat(dut_ref).IsNotNone()  # type: ignore
        assert dut_ref
        advertisement.cancel()
        return dut_ref, ref_dut

    async def setupHapConnection(self):
        advertisement = await self.advertise_hap(self.ref_left)
        scan_response = await self.dut_scan_for_hap()
        dut_connection_to_ref, ref_connection_to_dut = await self.dut_connect_to_ref(advertisement, scan_response)

        await self.dut_gatt.ExchangeMTU(mtu=512, connection=dut_connection_to_ref)

        (secure, wait_security) = await asyncio.gather(
            self.dut.aio.security.Secure(connection=dut_connection_to_ref, le=LE_LEVEL3),
            self.ref_left.aio.security.WaitSecurity(connection=ref_connection_to_dut, le=LE_LEVEL3),
        )

        AssertThat(secure.result_variant()).IsEqualTo('success')  # type: ignore
        AssertThat(wait_security.result_variant()).IsEqualTo('success')  # type: ignore

        await self.hap_grpc.WaitPeripheral(connection=dut_connection_to_ref)  # type: ignore
        advertisement.cancel()

        return dut_connection_to_ref

    @asynchronous
    async def test_get_features(self) -> None:
        dut_connection_to_ref = await self.setupHapConnection()

        features = hap.HearingAidFeatures_from_bytes(
            (await self.hap_grpc.GetFeatures(connection=dut_connection_to_ref)).features)
        AssertThat(features).IsEqualTo(self.has.server_features)  # type: ignore
+2 −0
Original line number Diff line number Diff line
@@ -23,6 +23,7 @@ import avatar.cases.le_host_test
import avatar.cases.le_security_test
import avatar.cases.security_test
import gatt_test
import hap_test
import hfpclient_test
from pairing import _test_class_list as _pairing_test_class_list
import sdp_test
@@ -36,6 +37,7 @@ _TEST_CLASSES_LIST = [
    aics_test.AicsTest,
    sdp_test.SdpTest,
    gatt_test.GattTest,
    hap_test.HapTest,
    asha_test.AshaTest,
    hfpclient_test.HfpClientTest,
] + _pairing_test_class_list
+17 −0
Original line number Diff line number Diff line
@@ -18,6 +18,8 @@ option java_outer_classname = "HapProto";
import "google/protobuf/empty.proto";

service HAP {
  // get the Hearing aid features
  rpc GetFeatures(GetFeaturesRequest) returns (GetFeaturesResponse);
  // Set active preset by index
  rpc SetActivePreset(SetActivePresetRequest) returns (google.protobuf.Empty);
  // Set next preset
@@ -34,6 +36,16 @@ service HAP {
  rpc GetAllPresetRecords(GetAllPresetRecordsRequest) returns (GetAllPresetRecordsResponse);
  // Wait for Preset Changed event
  rpc WaitPresetChanged(google.protobuf.Empty) returns (WaitPresetChangedResponse);
  // Wait for HAP device to be connected.
  rpc WaitPeripheral(WaitPeripheralRequest) returns (google.protobuf.Empty);
}

message GetFeaturesRequest{
  Connection connection = 1;
}

message GetFeaturesResponse{
    int32 features = 1;
}

// Request of the `PlaybackAudio` method.
@@ -123,3 +135,8 @@ message WaitPresetChangedResponse {
  // Reason why the presets were changed
  uint32 reason = 3;
}

message WaitPeripheralRequest {
  Connection connection = 1;
}