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.BluetoothHearingAid
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.AudioDeviceCallback
27 import android.media.AudioDeviceInfo
28 import android.media.AudioManager
29 import android.media.AudioRouting
30 import android.media.AudioTrack
31 import android.os.Handler
32 import android.os.Looper
33 import android.util.Log
34 import io.grpc.Status
35 import io.grpc.stub.StreamObserver
36 import java.io.Closeable
37 import kotlinx.coroutines.CoroutineScope
38 import kotlinx.coroutines.Dispatchers
39 import kotlinx.coroutines.cancel
40 import kotlinx.coroutines.channels.awaitClose
41 import kotlinx.coroutines.channels.trySendBlocking
42 import kotlinx.coroutines.flow.Flow
43 import kotlinx.coroutines.flow.SharingStarted
44 import kotlinx.coroutines.flow.callbackFlow
45 import kotlinx.coroutines.flow.filter
46 import kotlinx.coroutines.flow.first
47 import kotlinx.coroutines.flow.map
48 import kotlinx.coroutines.flow.shareIn
49 import pandora.asha.AshaGrpc.AshaImplBase
50 import pandora.asha.AshaProto.*
51 
52 @kotlinx.coroutines.ExperimentalCoroutinesApi
53 class Asha(val context: Context) : AshaImplBase(), Closeable {
54     private val TAG = "PandoraAsha"
55     private val scope: CoroutineScope
56     private val flow: Flow<Intent>
57 
58     private val bluetoothManager =
59         context.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
60     private val bluetoothHearingAid =
61         getProfileProxy<BluetoothHearingAid>(context, BluetoothProfile.HEARING_AID)
62     private val bluetoothAdapter = bluetoothManager.adapter
63     private val audioManager = context.getSystemService(AudioManager::class.java)!!
64 
65     private var audioTrack: AudioTrack? = null
66 
67     init {
68         // Init the CoroutineScope
69         scope = CoroutineScope(Dispatchers.Default.limitedParallelism(1))
70         val intentFilter = IntentFilter()
71         intentFilter.addAction(BluetoothHearingAid.ACTION_CONNECTION_STATE_CHANGED)
72         flow = intentFlow(context, intentFilter, scope).shareIn(scope, SharingStarted.Eagerly)
73     }
74 
closenull75     override fun close() {
76         // Deinit the CoroutineScope
77         scope.cancel()
78     }
79 
waitPeripheralnull80     override fun waitPeripheral(
81         request: WaitPeripheralRequest,
82         responseObserver: StreamObserver<WaitPeripheralResponse>
83     ) {
84         grpcUnary<WaitPeripheralResponse>(scope, responseObserver) {
85             Log.i(TAG, "waitPeripheral")
86 
87             val device = request.connection.toBluetoothDevice(bluetoothAdapter)
88             Log.d(TAG, "connection address ${device.getAddress()}")
89 
90             if (
91                 bluetoothHearingAid.getConnectionState(device) != BluetoothProfile.STATE_CONNECTED
92             ) {
93                 Log.d(TAG, "wait for bluetoothHearingAid profile connection")
94                 flow
95                     .filter {
96                         it.getAction() == BluetoothHearingAid.ACTION_CONNECTION_STATE_CHANGED
97                     }
98                     .filter { it.getBluetoothDeviceExtra() == device }
99                     .map { it.getIntExtra(BluetoothProfile.EXTRA_STATE, BluetoothAdapter.ERROR) }
100                     .filter { it == BluetoothProfile.STATE_CONNECTED }
101                     .first()
102             }
103 
104             WaitPeripheralResponse.getDefaultInstance()
105         }
106     }
107 
startnull108     override fun start(request: StartRequest, responseObserver: StreamObserver<StartResponse>) {
109         grpcUnary<StartResponse>(scope, responseObserver) {
110             Log.i(TAG, "play")
111 
112             // wait until BluetoothHearingAid profile is connected
113             val device = request.connection.toBluetoothDevice(bluetoothAdapter)
114             Log.d(TAG, "connection address ${device.getAddress()}")
115 
116             if (
117                 bluetoothHearingAid.getConnectionState(device) != BluetoothProfile.STATE_CONNECTED
118             ) {
119                 throw RuntimeException("Hearing aid device is not connected, cannot start")
120             }
121 
122             // wait for hearing aid is added as an audio device
123             val audioDeviceAddedFlow = callbackFlow {
124                 val outputDevices = audioManager.getDevices(AudioManager.GET_DEVICES_OUTPUTS)
125                 for (outputDevice in outputDevices) {
126                     if (
127                         outputDevice.type == AudioDeviceInfo.TYPE_HEARING_AID &&
128                             outputDevice.address.equals(device.getAddress())
129                     ) {
130                         trySendBlocking(null)
131                     }
132                 }
133 
134                 val audioDeviceCallback =
135                     object : AudioDeviceCallback() {
136                         override fun onAudioDevicesAdded(addedDevices: Array<out AudioDeviceInfo>) {
137                             for (addedDevice in addedDevices) {
138                                 if (
139                                     addedDevice.type == AudioDeviceInfo.TYPE_HEARING_AID &&
140                                         addedDevice.address.equals(device.getAddress())
141                                 ) {
142                                     Log.d(
143                                         TAG,
144                                         "TYPE_HEARING_AID added with address: ${addedDevice.address}"
145                                     )
146                                     trySendBlocking(null)
147                                 }
148                             }
149                         }
150                     }
151 
152                 audioManager.registerAudioDeviceCallback(
153                     audioDeviceCallback,
154                     Handler(Looper.getMainLooper())
155                 )
156                 awaitClose { audioManager.unregisterAudioDeviceCallback(audioDeviceCallback) }
157             }
158             audioDeviceAddedFlow.first()
159 
160             if (audioTrack == null) {
161                 audioTrack = buildAudioTrack()
162                 Log.i(TAG, "buildAudioTrack")
163             }
164             audioTrack!!.play()
165 
166             // wait for hearing aid is selected as routed device
167             val audioRoutingFlow = callbackFlow {
168                 if (audioTrack!!.routedDevice.type == AudioDeviceInfo.TYPE_HEARING_AID) {
169                     Log.d(TAG, "already route to TYPE_HEARING_AID")
170                     trySendBlocking(null)
171                 }
172 
173                 val audioRoutingListener =
174                     object : AudioRouting.OnRoutingChangedListener {
175                         override fun onRoutingChanged(router: AudioRouting) {
176                             if (router.routedDevice.type == AudioDeviceInfo.TYPE_HEARING_AID) {
177                                 Log.d(TAG, "Route to TYPE_HEARING_AID")
178                                 trySendBlocking(null)
179                             } else {
180                                 val outputDevices =
181                                     audioManager.getDevices(AudioManager.GET_DEVICES_OUTPUTS)
182                                 for (outputDevice in outputDevices) {
183                                     Log.d(
184                                         TAG,
185                                         "available output device in listener:${outputDevice.type}"
186                                     )
187                                     if (outputDevice.type == AudioDeviceInfo.TYPE_HEARING_AID) {
188                                         val result = router.setPreferredDevice(outputDevice)
189                                         Log.d(TAG, "setPreferredDevice result:$result")
190                                         trySendBlocking(null)
191                                     }
192                                 }
193                             }
194                         }
195                     }
196 
197                 audioTrack!!.addOnRoutingChangedListener(
198                     audioRoutingListener,
199                     Handler(Looper.getMainLooper())
200                 )
201                 awaitClose { audioTrack!!.removeOnRoutingChangedListener(audioRoutingListener) }
202             }
203             audioRoutingFlow.first()
204 
205             val minVolume = audioManager.getStreamMinVolume(AudioManager.STREAM_MUSIC)
206             audioManager.setStreamVolume(
207                 AudioManager.STREAM_MUSIC,
208                 minVolume,
209                 AudioManager.FLAG_SHOW_UI
210             )
211 
212             StartResponse.getDefaultInstance()
213         }
214     }
215 
stopnull216     override fun stop(request: StopRequest, responseObserver: StreamObserver<StopResponse>) {
217         grpcUnary<StopResponse>(scope, responseObserver) {
218             Log.i(TAG, "stop")
219             audioTrack!!.pause()
220             audioTrack!!.flush()
221 
222             StopResponse.getDefaultInstance()
223         }
224     }
225 
playbackAudionull226     override fun playbackAudio(
227         responseObserver: StreamObserver<PlaybackAudioResponse>
228     ): StreamObserver<PlaybackAudioRequest> {
229         Log.i(TAG, "playbackAudio")
230         if (audioTrack!!.getPlayState() != AudioTrack.PLAYSTATE_PLAYING) {
231             responseObserver.onError(
232                 Status.UNKNOWN.withDescription("AudioTrack is not started").asException()
233             )
234         }
235 
236         // Volume is maxed out to avoid any amplitude modification of the provided audio data,
237         // enabling the test runner to do comparisons between input and output audio signal.
238         // Any volume modification should be done before providing the audio data.
239         if (audioManager.isVolumeFixed) {
240             Log.w(TAG, "Volume is fixed, cannot max out the volume")
241         } else {
242             val maxVolume = audioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC)
243             if (audioManager.getStreamVolume(AudioManager.STREAM_MUSIC) < maxVolume) {
244                 audioManager.setStreamVolume(
245                     AudioManager.STREAM_MUSIC,
246                     maxVolume,
247                     AudioManager.FLAG_SHOW_UI
248                 )
249             }
250         }
251 
252         return object : StreamObserver<PlaybackAudioRequest> {
253             override fun onNext(request: PlaybackAudioRequest) {
254                 val data = request.data.toByteArray()
255                 Log.d(TAG, "audio track writes data=$data")
256                 val written = synchronized(audioTrack!!) { audioTrack!!.write(data, 0, data.size) }
257                 if (written != data.size) {
258                     Log.e(TAG, "AudioTrack write failed")
259                     responseObserver.onError(
260                         Status.UNKNOWN.withDescription("AudioTrack write failed").asException()
261                     )
262                 }
263             }
264             override fun onError(t: Throwable?) {
265                 Log.e(TAG, t.toString())
266                 responseObserver.onError(t)
267             }
268             override fun onCompleted() {
269                 Log.i(TAG, "onCompleted")
270                 responseObserver.onNext(PlaybackAudioResponse.getDefaultInstance())
271                 responseObserver.onCompleted()
272             }
273         }
274     }
275 }
276