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

Commit 7417d952 authored by Charlie Boutier's avatar Charlie Boutier Committed by Thomas Girardier
Browse files

[Pandora] - Add A2dpSink implementation

Bug: 245578454
Test: Test: atest pts-bot
Ignore-AOSP-First: Cherry-pick from aosp
Merged-In: I4ea7cffad5edeeec4d187e7b6d2455a725e8ce96
Change-Id: I4ea7cffad5edeeec4d187e7b6d2455a725e8ce96
parent 1ed8a8db
Loading
Loading
Loading
Loading
+25 −27
Original line number Diff line number Diff line
@@ -11,7 +11,6 @@
# 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.

"""A2DP proxy module."""

import time
@@ -49,15 +48,12 @@ class A2DPProxy(ProfileProxy):

        def convert_frame(data):
            return PlaybackAudioRequest(data=data, source=self.source)
        self.audio = AudioSignal(
            lambda frames: self.a2dp.PlaybackAudio(map(convert_frame, frames)),
            AUDIO_SIGNAL_AMPLITUDE,
            AUDIO_SIGNAL_SAMPLING_RATE
        )

        self.audio = AudioSignal(lambda frames: self.a2dp.PlaybackAudio(map(convert_frame, frames)),
                                 AUDIO_SIGNAL_AMPLITUDE, AUDIO_SIGNAL_SAMPLING_RATE)

    @assert_description
    def TSC_AVDTP_mmi_iut_accept_connect(
            self, test: str, pts_addr: bytes, **kwargs):
    def TSC_AVDTP_mmi_iut_accept_connect(self, test: str, pts_addr: bytes, **kwargs):
        """
        If necessary, take action to accept the AVDTP Signaling Channel
        Connection initiated by the tester.
@@ -71,27 +67,34 @@ class A2DPProxy(ProfileProxy):
        """

        if "SRC" in test:
            self.connection = self.host.WaitConnection(
                address=pts_addr).connection
            self.connection = self.host.WaitConnection(address=pts_addr).connection
            try:
                if "INT" in test:
                    self.source = self.a2dp.OpenSource(
                        connection=self.connection).source
                    self.source = self.a2dp.OpenSource(connection=self.connection).source
                else:
                    self.source = self.a2dp.WaitSource(
                        connection=self.connection).source
                    self.source = self.a2dp.WaitSource(connection=self.connection).source
            except RpcError:
                pass
        else:
            self.connection = self.host.WaitConnection(
                address=pts_addr).connection
            self.connection = self.host.WaitConnection(address=pts_addr).connection
            try:
                self.sink = self.a2dp.WaitSink(
                    connection=self.connection).sink
                self.sink = self.a2dp.WaitSink(connection=self.connection).sink
            except RpcError:
                pass
        return "OK"

    @assert_description
    def TSC_AVDTP_mmi_iut_accept_disconnect(self, **kwargs):
        """
        If necessary, take action to accept the AVDTP Signaling Channnel
        Disconnection initiated by the tester.

        Note: If an AVCTP signaling
        channel was established it will also be disconnected.
        """

        return "OK"

    @assert_description
    def TSC_AVDTP_mmi_iut_initiate_discover(self, **kwargs):
        """
@@ -152,8 +155,7 @@ class A2DPProxy(ProfileProxy):
        return "OK"

    @assert_description
    def TSC_AVDTP_mmi_iut_initiate_out_of_range(
            self, pts_addr: bytes, **kwargs):
    def TSC_AVDTP_mmi_iut_initiate_out_of_range(self, pts_addr: bytes, **kwargs):
        """
        Move the IUT out of range to create a link loss scenario.

@@ -162,8 +164,7 @@ class A2DPProxy(ProfileProxy):
         """

        if self.connection is None:
            self.connection = self.host.GetConnection(
                address=pts_addr).connection
            self.connection = self.host.GetConnection(address=pts_addr).connection
        self.host.Disconnect(connection=self.connection)
        self.connection = None
        self.sink = None
@@ -181,11 +182,8 @@ class A2DPProxy(ProfileProxy):

        if test == "AVDTP/SRC/ACP/SIG/SMG/BI-29-C":
            time.sleep(2)  # TODO: Remove, AVRCP SegFault
        if test in ("A2DP/SRC/CC/BV-09-I",
                    "A2DP/SRC/SET/BV-04-I",
                    "AVDTP/SRC/ACP/SIG/SMG/BV-18-C",
                    "AVDTP/SRC/ACP/SIG/SMG/BV-20-C",
                    "AVDTP/SRC/ACP/SIG/SMG/BV-22-C"):
        if test in ("A2DP/SRC/CC/BV-09-I", "A2DP/SRC/SET/BV-04-I", "AVDTP/SRC/ACP/SIG/SMG/BV-18-C",
                    "AVDTP/SRC/ACP/SIG/SMG/BV-20-C", "AVDTP/SRC/ACP/SIG/SMG/BV-22-C"):
            time.sleep(1)  # TODO: Remove, AVRCP SegFault
        if test == "A2DP/SRC/SUS/BV-01-I":
            # Stream is not suspended when we receive the interaction
+3 −3
Original line number Diff line number Diff line
@@ -8,6 +8,7 @@
    "A2DP/SRC/SET/BV-02-I",
    "A2DP/SRC/SET/BV-04-I",
    "A2DP/SNK/AS/BV-01-I",
    "A2DP/SNK/AS/BV-02-I",
    "A2DP/SNK/CC/BV-01-I",
    "A2DP/SNK/CC/BV-02-I",
    "A2DP/SNK/CC/BV-05-I",
@@ -15,6 +16,7 @@
    "A2DP/SNK/CC/BV-07-I",
    "A2DP/SNK/CC/BV-08-I",
    "A2DP/SNK/REL/BV-01-I",
    "A2DP/SNK/REL/BV-02-I",
    "A2DP/SNK/SET/BV-01-I",
    "A2DP/SNK/SET/BV-02-I",
    "A2DP/SNK/SET/BV-03-I",
@@ -277,8 +279,6 @@
    "A2DP/SRC/SUS/BV-01-I",
    "A2DP/SRC/SUS/BV-02-I",
    "A2DP/SRC/SYN/BV-02-I",
    "A2DP/SNK/AS/BV-02-I",
    "A2DP/SNK/REL/BV-02-I",
    "A2DP/SNK/SDP/BV-02-I",
    "A2DP/SNK/SET/BV-04-I",
    "A2DP/SNK/SET/BV-05-I",
+128 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2022 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.pandora

import android.bluetooth.BluetoothA2dpSink
import android.bluetooth.BluetoothAdapter
import android.bluetooth.BluetoothDevice
import android.bluetooth.BluetoothManager
import android.bluetooth.BluetoothProfile
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.media.*
import android.util.Log
import io.grpc.Status
import io.grpc.stub.StreamObserver
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.shareIn
import pandora.A2DPGrpc.A2DPImplBase
import pandora.A2dpProto.*

@kotlinx.coroutines.ExperimentalCoroutinesApi
class A2dpSink(val context: Context) : A2DPImplBase() {
  private val TAG = "PandoraA2dpSink"

  private val scope: CoroutineScope
  private val flow: Flow<Intent>

  private val bluetoothManager = context.getSystemService(BluetoothManager::class.java)!!
  private val bluetoothAdapter = bluetoothManager.adapter
  private val bluetoothA2dpSink =
    getProfileProxy<BluetoothA2dpSink>(context, BluetoothProfile.A2DP_SINK)

  init {
    scope = CoroutineScope(Dispatchers.Default)
    val intentFilter = IntentFilter()
    intentFilter.addAction(BluetoothA2dpSink.ACTION_CONNECTION_STATE_CHANGED)

    flow = intentFlow(context, intentFilter).shareIn(scope, SharingStarted.Eagerly)
  }

  fun deinit() {
    bluetoothAdapter.closeProfileProxy(BluetoothProfile.A2DP_SINK, bluetoothA2dpSink)
    scope.cancel()
  }

  override fun waitSink(
    request: WaitSinkRequest,
    responseObserver: StreamObserver<WaitSinkResponse>
  ) {
    grpcUnary<WaitSinkResponse>(scope, responseObserver) {
      val device = request.connection.toBluetoothDevice(bluetoothAdapter)
      Log.i(TAG, "waitSink: device=$device")

      if (device.getBondState() != BluetoothDevice.BOND_BONDED) {
        Log.e(TAG, "Device is not bonded, cannot wait for stream")
        throw Status.UNKNOWN.asException()
      }

      if (bluetoothA2dpSink.getConnectionState(device) != BluetoothProfile.STATE_CONNECTED) {
        val state =
          flow
            .filter { it.getAction() == BluetoothA2dpSink.ACTION_CONNECTION_STATE_CHANGED }
            .filter { it.getBluetoothDeviceExtra() == device }
            .map { it.getIntExtra(BluetoothProfile.EXTRA_STATE, BluetoothAdapter.ERROR) }
            .filter {
              it == BluetoothProfile.STATE_CONNECTED || it == BluetoothProfile.STATE_DISCONNECTED
            }
            .first()

        if (state == BluetoothProfile.STATE_DISCONNECTED) {
          Log.e(TAG, "waitStream failed, A2DP has been disconnected")
          throw Status.UNKNOWN.asException()
        }
      }

      val sink = Sink.newBuilder().setConnection(request.connection).build()
      WaitSinkResponse.newBuilder().setSink(sink).build()
    }
  }

  override fun close(request: CloseRequest, responseObserver: StreamObserver<CloseResponse>) {
    grpcUnary<CloseResponse>(scope, responseObserver) {
      val device =
        if (request.hasSink()) {
          request.sink.connection.toBluetoothDevice(bluetoothAdapter)
        } else {
          Log.e(TAG, "Sink device required")
          throw Status.UNKNOWN.asException()
        }
      Log.i(TAG, "close: device=$device")
      if (bluetoothA2dpSink.getConnectionState(device) != BluetoothProfile.STATE_CONNECTED) {
        Log.e(TAG, "Device is not connected, cannot close")
        throw Status.UNKNOWN.asException()
      }

      val a2dpConnectionStateChangedFlow =
        flow
          .filter { it.getAction() == BluetoothA2dpSink.ACTION_CONNECTION_STATE_CHANGED }
          .filter { it.getBluetoothDeviceExtra() == device }
          .map { it.getIntExtra(BluetoothProfile.EXTRA_STATE, BluetoothAdapter.ERROR) }
      bluetoothA2dpSink.setConnectionPolicy(device, BluetoothProfile.CONNECTION_POLICY_FORBIDDEN)
      a2dpConnectionStateChangedFlow.filter { it == BluetoothProfile.STATE_DISCONNECTED }.first()
      CloseResponse.getDefaultInstance()
    }
  }
}
+20 −6
Original line number Diff line number Diff line
@@ -16,6 +16,8 @@

package com.android.pandora

import android.bluetooth.BluetoothManager
import android.bluetooth.BluetoothProfile
import android.content.Context
import android.util.Log
import io.grpc.Server as GrpcServer
@@ -28,7 +30,8 @@ class Server(context: Context) {
  private val GRPC_PORT = 8999

  private var host: Host
  private var a2dp: A2dp
  private var a2dp: A2dp? = null
  private var a2dpSink: A2dpSink? = null
  private var avrcp: Avrcp
  private var gatt: Gatt
  private var hfp: Hfp
@@ -39,24 +42,34 @@ class Server(context: Context) {

  init {
    host = Host(context, this)
    a2dp = A2dp(context)
    avrcp = Avrcp(context)
    gatt = Gatt(context)
    hfp = Hfp(context)
    hid = Hid(context)
    l2cap = L2cap(context)
    security = Security(context)
    grpcServer =

    val grpcServerBuilder =
      NettyServerBuilder.forPort(GRPC_PORT)
        .addService(host)
        .addService(a2dp)
        .addService(avrcp)
        .addService(gatt)
        .addService(hfp)
        .addService(hid)
        .addService(l2cap)
        .addService(security)
        .build()

    val bluetoothAdapter = context.getSystemService(BluetoothManager::class.java)!!.adapter
    val is_a2dp_source = bluetoothAdapter.getSupportedProfiles().contains(BluetoothProfile.A2DP)
    if (is_a2dp_source) {
      a2dp = A2dp(context)
      grpcServerBuilder.addService(a2dp!!)
    } else {
      a2dpSink = A2dpSink(context)
      grpcServerBuilder.addService(a2dpSink!!)
    }

    grpcServer = grpcServerBuilder.build()

    Log.d(TAG, "Starting Pandora Server")
    grpcServer.start()
@@ -69,7 +82,8 @@ class Server(context: Context) {

  fun deinit() {
    host.deinit()
    a2dp.deinit()
    a2dp?.deinit()
    a2dpSink?.deinit()
    avrcp.deinit()
    gatt.deinit()
    hfp.deinit()
+1 −1
Original line number Diff line number Diff line
@@ -230,7 +230,7 @@ fun <T> getProfileProxy(context: Context, profile: Int): T {
  if (proxy == null) {
    Log.w(TAG, "profile proxy $profile is null")
  }
  return proxy as T
  return proxy!! as T
}

fun Intent.getBluetoothDeviceExtra(): BluetoothDevice =