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