• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2022 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.pandora
18 
19 import android.bluetooth.BluetoothAdapter
20 import android.bluetooth.BluetoothLeAudio
21 import android.bluetooth.BluetoothManager
22 import android.bluetooth.BluetoothProfile
23 import android.bluetooth.BluetoothProfile.STATE_CONNECTED
24 import android.bluetooth.BluetoothProfile.STATE_DISCONNECTED
25 import android.content.Context
26 import android.content.Intent
27 import android.content.IntentFilter
28 import android.media.AudioTrack
29 import android.media.AudioManager
30 import android.util.Log
31 import com.google.protobuf.Empty
32 import io.grpc.Status
33 import io.grpc.stub.StreamObserver
34 import java.io.Closeable
35 import kotlinx.coroutines.CoroutineScope
36 import kotlinx.coroutines.Dispatchers
37 import kotlinx.coroutines.cancel
38 import kotlinx.coroutines.flow.Flow
39 import kotlinx.coroutines.flow.SharingStarted
40 import kotlinx.coroutines.flow.filter
41 import kotlinx.coroutines.flow.first
42 import kotlinx.coroutines.flow.map
43 import kotlinx.coroutines.flow.shareIn
44 import pandora.LeAudioGrpc.LeAudioImplBase
45 import pandora.LeAudioProto.*
46 import java.io.PrintWriter
47 import java.io.StringWriter
48 
49 @kotlinx.coroutines.ExperimentalCoroutinesApi
50 class LeAudio(val context: Context) : LeAudioImplBase(), Closeable {
51 
52     private val TAG = "PandoraLeAudio"
53 
54     private val scope: CoroutineScope
55     private val flow: Flow<Intent>
56 
57     private val audioManager = context.getSystemService(AudioManager::class.java)!!
58 
59     private val bluetoothManager = context.getSystemService(BluetoothManager::class.java)!!
60     private val bluetoothAdapter = bluetoothManager.adapter
61     private val bluetoothLeAudio =
62         getProfileProxy<BluetoothLeAudio>(context, BluetoothProfile.LE_AUDIO)
63 
64     private var audioTrack: AudioTrack? = null
65 
66     init {
67         scope = CoroutineScope(Dispatchers.Default)
68         val intentFilter = IntentFilter()
69         intentFilter.addAction(BluetoothLeAudio.ACTION_LE_AUDIO_CONNECTION_STATE_CHANGED)
70 
71         flow = intentFlow(context, intentFilter, scope).shareIn(scope, SharingStarted.Eagerly)
72     }
73 
closenull74     override fun close() {
75         bluetoothAdapter.closeProfileProxy(BluetoothProfile.LE_AUDIO, bluetoothLeAudio)
76         scope.cancel()
77     }
78 
opennull79     override fun open(request: OpenRequest, responseObserver: StreamObserver<Empty>) {
80         grpcUnary<Empty>(scope, responseObserver) {
81             val device = request.connection.toBluetoothDevice(bluetoothAdapter)
82             Log.i(TAG, "open: device=$device")
83 
84             if (bluetoothLeAudio.getConnectionState(device) != STATE_CONNECTED) {
85                 bluetoothLeAudio.connect(device)
86                 val state =
87                     flow
88                         .filter {
89                             it.getAction() ==
90                                 BluetoothLeAudio.ACTION_LE_AUDIO_CONNECTION_STATE_CHANGED
91                         }
92                         .filter { it.getBluetoothDeviceExtra() == device }
93                         .map {
94                             it.getIntExtra(BluetoothProfile.EXTRA_STATE, BluetoothAdapter.ERROR)
95                         }
96                         .filter { it == STATE_CONNECTED || it == STATE_DISCONNECTED }
97                         .first()
98 
99                 if (state == STATE_DISCONNECTED) {
100                     throw RuntimeException("open failed, LE_AUDIO has been disconnected")
101                 }
102             }
103 
104             Empty.getDefaultInstance()
105         }
106     }
107 
leAudioStartnull108     override fun leAudioStart(request: LeAudioStartRequest, responseObserver: StreamObserver<Empty>) {
109         grpcUnary<Empty>(scope, responseObserver) {
110             if (audioTrack == null) {
111                 audioTrack = buildAudioTrack()
112             }
113             val device = request.connection.toBluetoothDevice(bluetoothAdapter)
114             Log.i(TAG, "start: device=$device")
115 
116             if (bluetoothLeAudio.getConnectionState(device) != BluetoothLeAudio.STATE_CONNECTED) {
117                 throw RuntimeException("Device is not connected, cannot start")
118             }
119 
120             // Configure the selected device as active device if it is not
121             // already.
122             bluetoothLeAudio.setActiveDevice(device)
123 
124             // Play an audio track.
125             audioTrack!!.play()
126 
127             Empty.getDefaultInstance()
128         }
129     }
130 
leAudioStopnull131     override fun leAudioStop(request: LeAudioStopRequest, responseObserver: StreamObserver<Empty>) {
132         grpcUnary<Empty>(scope, responseObserver) {
133             checkNotNull(audioTrack) { "No track to pause!" }
134 
135             // Play an audio track.
136             audioTrack!!.pause()
137 
138             Empty.getDefaultInstance()
139         }
140     }
141 
leAudioPlaybackAudionull142     override fun leAudioPlaybackAudio(
143         responseObserver: StreamObserver<LeAudioPlaybackAudioResponse>
144     ): StreamObserver<LeAudioPlaybackAudioRequest> {
145         Log.i(TAG, "leAudioPlaybackAudio")
146 
147         if (audioTrack!!.getPlayState() != AudioTrack.PLAYSTATE_PLAYING) {
148             responseObserver.onError(
149                 Status.UNKNOWN.withDescription("AudioTrack is not started").asException()
150             )
151         }
152 
153         // Volume is maxed out to avoid any amplitude modification of the provided audio data,
154         // enabling the test runner to do comparisons between input and output audio signal.
155         // Any volume modification should be done before providing the audio data.
156         if (audioManager.isVolumeFixed) {
157             Log.w(TAG, "Volume is fixed, cannot max out the volume")
158         } else {
159             val maxVolume = audioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC)
160             if (audioManager.getStreamVolume(AudioManager.STREAM_MUSIC) < maxVolume) {
161                 audioManager.setStreamVolume(
162                     AudioManager.STREAM_MUSIC,
163                     maxVolume,
164                     AudioManager.FLAG_SHOW_UI,
165                 )
166             }
167         }
168 
169         return object : StreamObserver<LeAudioPlaybackAudioRequest> {
170             override fun onNext(request: LeAudioPlaybackAudioRequest) {
171                 val data = request.data.toByteArray()
172                 val written = synchronized(audioTrack!!) { audioTrack!!.write(data, 0, data.size) }
173                 if (written != data.size) {
174                     responseObserver.onError(
175                         Status.UNKNOWN.withDescription("AudioTrack write failed").asException()
176                     )
177                 }
178             }
179 
180             override fun onError(t: Throwable) {
181                 t.printStackTrace()
182                 val sw = StringWriter()
183                 t.printStackTrace(PrintWriter(sw))
184                 responseObserver.onError(
185                     Status.UNKNOWN.withCause(t).withDescription(sw.toString()).asException()
186                 )
187             }
188 
189             override fun onCompleted() {
190                 responseObserver.onNext(LeAudioPlaybackAudioResponse.getDefaultInstance())
191                 responseObserver.onCompleted()
192             }
193         }
194     }
195 }
196