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

Commit 2692a768 authored by Treehugger Robot's avatar Treehugger Robot Committed by Automerger Merge Worker
Browse files

Merge "Avatar implement first HAP test" into main am: 66c01a2a am: 6bb0646d

parents 92bea633 6bb0646d
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;
}