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.BluetoothA2dp
20 import android.bluetooth.BluetoothAdapter
21 import android.bluetooth.BluetoothManager
22 import android.bluetooth.BluetoothProfile
23 import android.content.Context
24 import android.content.Intent
25 import android.content.IntentFilter
26 import android.media.*
27 import android.util.Log
28 import com.google.protobuf.BoolValue
29 import com.google.protobuf.ByteString
30 import io.grpc.Status
31 import io.grpc.stub.StreamObserver
32 import java.io.Closeable
33 import java.io.PrintWriter
34 import java.io.StringWriter
35 import kotlinx.coroutines.CoroutineScope
36 import kotlinx.coroutines.Dispatchers
37 import kotlinx.coroutines.cancel
38 import kotlinx.coroutines.delay
39 import kotlinx.coroutines.flow.Flow
40 import kotlinx.coroutines.flow.SharingStarted
41 import kotlinx.coroutines.flow.filter
42 import kotlinx.coroutines.flow.first
43 import kotlinx.coroutines.flow.map
44 import kotlinx.coroutines.flow.shareIn
45 import pandora.A2DPGrpc.A2DPImplBase
46 import pandora.A2DPProto.*
47 
48 @kotlinx.coroutines.ExperimentalCoroutinesApi
49 class A2dp(val context: Context) : A2DPImplBase(), Closeable {
50     private val TAG = "PandoraA2dp"
51 
52     private val scope: CoroutineScope
53     private val flow: Flow<Intent>
54 
55     private val audioManager = context.getSystemService(AudioManager::class.java)!!
56 
57     private val bluetoothManager = context.getSystemService(BluetoothManager::class.java)!!
58     private val bluetoothAdapter = bluetoothManager.adapter
59     private val bluetoothA2dp = getProfileProxy<BluetoothA2dp>(context, BluetoothProfile.A2DP)
60 
61     private var audioTrack: AudioTrack? = null
62 
63     init {
64         scope = CoroutineScope(Dispatchers.Default.limitedParallelism(1))
65         val intentFilter = IntentFilter()
66         intentFilter.addAction(BluetoothA2dp.ACTION_PLAYING_STATE_CHANGED)
67         intentFilter.addAction(BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED)
68 
69         flow = intentFlow(context, intentFilter, scope).shareIn(scope, SharingStarted.Eagerly)
70     }
71 
closenull72     override fun close() {
73         bluetoothAdapter.closeProfileProxy(BluetoothProfile.A2DP, bluetoothA2dp)
74         scope.cancel()
75     }
76 
openSourcenull77     override fun openSource(
78         request: OpenSourceRequest,
79         responseObserver: StreamObserver<OpenSourceResponse>
80     ) {
81         grpcUnary<OpenSourceResponse>(scope, responseObserver) {
82             val device = request.connection.toBluetoothDevice(bluetoothAdapter)
83             Log.i(TAG, "openSource: device=$device")
84 
85             if (bluetoothA2dp.getConnectionState(device) != BluetoothA2dp.STATE_CONNECTED) {
86                 bluetoothA2dp.connect(device)
87                 val state =
88                     flow
89                         .filter { it.getAction() == BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED }
90                         .filter { it.getBluetoothDeviceExtra() == device }
91                         .map {
92                             it.getIntExtra(BluetoothProfile.EXTRA_STATE, BluetoothAdapter.ERROR)
93                         }
94                         .filter {
95                             it == BluetoothProfile.STATE_CONNECTED ||
96                                 it == BluetoothProfile.STATE_DISCONNECTED
97                         }
98                         .first()
99 
100                 if (state == BluetoothProfile.STATE_DISCONNECTED) {
101                     throw RuntimeException("openSource failed, A2DP has been disconnected")
102                 }
103             }
104 
105             // TODO: b/234891800, AVDTP start request sometimes never sent if playback starts too
106             // early.
107             delay(2000L)
108 
109             val source =
110                 Source.newBuilder().setCookie(ByteString.copyFrom(device.getAddress(), "UTF-8"))
111             OpenSourceResponse.newBuilder().setSource(source).build()
112         }
113     }
114 
waitSourcenull115     override fun waitSource(
116         request: WaitSourceRequest,
117         responseObserver: StreamObserver<WaitSourceResponse>
118     ) {
119         grpcUnary<WaitSourceResponse>(scope, responseObserver) {
120             val device = request.connection.toBluetoothDevice(bluetoothAdapter)
121             Log.i(TAG, "waitSource: device=$device")
122 
123             if (bluetoothA2dp.getConnectionState(device) != BluetoothA2dp.STATE_CONNECTED) {
124                 val state =
125                     flow
126                         .filter { it.getAction() == BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED }
127                         .filter { it.getBluetoothDeviceExtra() == device }
128                         .map {
129                             it.getIntExtra(BluetoothProfile.EXTRA_STATE, BluetoothAdapter.ERROR)
130                         }
131                         .filter {
132                             it == BluetoothProfile.STATE_CONNECTED ||
133                                 it == BluetoothProfile.STATE_DISCONNECTED
134                         }
135                         .first()
136 
137                 if (state == BluetoothProfile.STATE_DISCONNECTED) {
138                     throw RuntimeException("waitSource failed, A2DP has been disconnected")
139                 }
140             }
141 
142             // TODO: b/234891800, AVDTP start request sometimes never sent if playback starts too
143             // early.
144             delay(2000L)
145 
146             val source =
147                 Source.newBuilder().setCookie(ByteString.copyFrom(device.getAddress(), "UTF-8"))
148             WaitSourceResponse.newBuilder().setSource(source).build()
149         }
150     }
151 
startnull152     override fun start(request: StartRequest, responseObserver: StreamObserver<StartResponse>) {
153         grpcUnary<StartResponse>(scope, responseObserver) {
154             if (audioTrack == null) {
155                 audioTrack = buildAudioTrack()
156             }
157             val device = bluetoothAdapter.getRemoteDevice(request.source.cookie.toString("UTF-8"))
158             Log.i(TAG, "start: device=$device")
159 
160             if (bluetoothA2dp.getConnectionState(device) != BluetoothA2dp.STATE_CONNECTED) {
161                 throw RuntimeException("Device is not connected, cannot start")
162             }
163 
164             audioTrack!!.play()
165 
166             // If A2dp is not already playing, wait for it
167             if (!bluetoothA2dp.isA2dpPlaying(device)) {
168                 flow
169                     .filter { it.getAction() == BluetoothA2dp.ACTION_PLAYING_STATE_CHANGED }
170                     .filter { it.getBluetoothDeviceExtra() == device }
171                     .map { it.getIntExtra(BluetoothA2dp.EXTRA_STATE, BluetoothAdapter.ERROR) }
172                     .filter { it == BluetoothA2dp.STATE_PLAYING }
173                     .first()
174             }
175             StartResponse.getDefaultInstance()
176         }
177     }
178 
suspendnull179     override fun suspend(
180         request: SuspendRequest,
181         responseObserver: StreamObserver<SuspendResponse>
182     ) {
183         grpcUnary<SuspendResponse>(scope, responseObserver) {
184             val device = bluetoothAdapter.getRemoteDevice(request.source.cookie.toString("UTF-8"))
185             Log.i(TAG, "suspend: device=$device")
186 
187             if (bluetoothA2dp.getConnectionState(device) != BluetoothA2dp.STATE_CONNECTED) {
188                 throw RuntimeException("Device is not connected, cannot suspend")
189             }
190 
191             if (!bluetoothA2dp.isA2dpPlaying(device)) {
192                 throw RuntimeException("Device is already suspended, cannot suspend")
193             }
194 
195             val a2dpPlayingStateFlow =
196                 flow
197                     .filter { it.getAction() == BluetoothA2dp.ACTION_PLAYING_STATE_CHANGED }
198                     .filter { it.getBluetoothDeviceExtra() == device }
199                     .map { it.getIntExtra(BluetoothA2dp.EXTRA_STATE, BluetoothAdapter.ERROR) }
200 
201             audioTrack!!.pause()
202             a2dpPlayingStateFlow.filter { it == BluetoothA2dp.STATE_NOT_PLAYING }.first()
203             SuspendResponse.getDefaultInstance()
204         }
205     }
206 
isSuspendednull207     override fun isSuspended(
208         request: IsSuspendedRequest,
209         responseObserver: StreamObserver<BoolValue>
210     ) {
211         grpcUnary(scope, responseObserver) {
212             val device = bluetoothAdapter.getRemoteDevice(request.source.cookie.toString("UTF-8"))
213             Log.i(TAG, "isSuspended: device=$device")
214 
215             if (bluetoothA2dp.getConnectionState(device) != BluetoothA2dp.STATE_CONNECTED) {
216                 throw RuntimeException("Device is not connected, cannot get suspend state")
217             }
218 
219             val isSuspended = bluetoothA2dp.isA2dpPlaying(device)
220 
221             BoolValue.newBuilder().setValue(isSuspended).build()
222         }
223     }
224 
closenull225     override fun close(request: CloseRequest, responseObserver: StreamObserver<CloseResponse>) {
226         grpcUnary<CloseResponse>(scope, responseObserver) {
227             val device = bluetoothAdapter.getRemoteDevice(request.source.cookie.toString("UTF-8"))
228             Log.i(TAG, "close: device=$device")
229 
230             if (bluetoothA2dp.getConnectionState(device) != BluetoothA2dp.STATE_CONNECTED) {
231                 throw RuntimeException("Device is not connected, cannot close")
232             }
233 
234             val a2dpConnectionStateChangedFlow =
235                 flow
236                     .filter { it.getAction() == BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED }
237                     .filter { it.getBluetoothDeviceExtra() == device }
238                     .map { it.getIntExtra(BluetoothA2dp.EXTRA_STATE, BluetoothAdapter.ERROR) }
239 
240             bluetoothA2dp.disconnect(device)
241             a2dpConnectionStateChangedFlow.filter { it == BluetoothA2dp.STATE_DISCONNECTED }.first()
242 
243             CloseResponse.getDefaultInstance()
244         }
245     }
246 
playbackAudionull247     override fun playbackAudio(
248         responseObserver: StreamObserver<PlaybackAudioResponse>
249     ): StreamObserver<PlaybackAudioRequest> {
250         Log.i(TAG, "playbackAudio")
251 
252         if (audioTrack!!.getPlayState() != AudioTrack.PLAYSTATE_PLAYING) {
253             responseObserver.onError(
254                 Status.UNKNOWN.withDescription("AudioTrack is not started").asException()
255             )
256         }
257 
258         // Volume is maxed out to avoid any amplitude modification of the provided audio data,
259         // enabling the test runner to do comparisons between input and output audio signal.
260         // Any volume modification should be done before providing the audio data.
261         if (audioManager.isVolumeFixed) {
262             Log.w(TAG, "Volume is fixed, cannot max out the volume")
263         } else {
264             val maxVolume = audioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC)
265             if (audioManager.getStreamVolume(AudioManager.STREAM_MUSIC) < maxVolume) {
266                 audioManager.setStreamVolume(
267                     AudioManager.STREAM_MUSIC,
268                     maxVolume,
269                     AudioManager.FLAG_SHOW_UI
270                 )
271             }
272         }
273 
274         return object : StreamObserver<PlaybackAudioRequest> {
275             override fun onNext(request: PlaybackAudioRequest) {
276                 val data = request.data.toByteArray()
277                 val written = synchronized(audioTrack!!) { audioTrack!!.write(data, 0, data.size) }
278                 if (written != data.size) {
279                     responseObserver.onError(
280                         Status.UNKNOWN.withDescription("AudioTrack write failed").asException()
281                     )
282                 }
283             }
284             override fun onError(t: Throwable) {
285                 t.printStackTrace()
286                 val sw = StringWriter()
287                 t.printStackTrace(PrintWriter(sw))
288                 responseObserver.onError(
289                     Status.UNKNOWN.withCause(t).withDescription(sw.toString()).asException()
290                 )
291             }
292             override fun onCompleted() {
293                 responseObserver.onNext(PlaybackAudioResponse.getDefaultInstance())
294                 responseObserver.onCompleted()
295             }
296         }
297     }
298 
getAudioEncodingnull299     override fun getAudioEncoding(
300         request: GetAudioEncodingRequest,
301         responseObserver: StreamObserver<GetAudioEncodingResponse>
302     ) {
303         grpcUnary<GetAudioEncodingResponse>(scope, responseObserver) {
304             val device = bluetoothAdapter.getRemoteDevice(request.source.cookie.toString("UTF-8"))
305             Log.i(TAG, "getAudioEncoding: device=$device")
306 
307             if (bluetoothA2dp.getConnectionState(device) != BluetoothA2dp.STATE_CONNECTED) {
308                 throw RuntimeException("Device is not connected, cannot getAudioEncoding")
309             }
310 
311             // For now, we only support 44100 kHz sampling rate.
312             GetAudioEncodingResponse.newBuilder()
313                 .setEncoding(AudioEncoding.PCM_S16_LE_44K1_STEREO)
314                 .build()
315         }
316     }
317 }
318