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