1 /* <lambda>null2 * Copyright (C) 2024 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.BluetoothDevice 21 import android.bluetooth.BluetoothDevice.TRANSPORT_LE 22 import android.bluetooth.BluetoothHapClient 23 import android.bluetooth.BluetoothHapClient.Callback 24 import android.bluetooth.BluetoothHapPresetInfo 25 import android.bluetooth.BluetoothLeAudio 26 import android.bluetooth.BluetoothManager 27 import android.bluetooth.BluetoothProfile 28 import android.bluetooth.BluetoothProfile.CONNECTION_POLICY_ALLOWED 29 import android.content.Context 30 import android.content.IntentFilter 31 import android.media.AudioManager 32 import android.media.AudioTrack 33 import android.util.Log 34 import com.google.protobuf.Empty 35 import io.grpc.Status 36 import io.grpc.stub.StreamObserver 37 import java.io.Closeable 38 import java.io.PrintWriter 39 import java.io.StringWriter 40 import java.util.concurrent.Executors 41 import kotlinx.coroutines.CoroutineScope 42 import kotlinx.coroutines.Dispatchers 43 import kotlinx.coroutines.cancel 44 import kotlinx.coroutines.channels.awaitClose 45 import kotlinx.coroutines.flow.SharingStarted 46 import kotlinx.coroutines.flow.callbackFlow 47 import kotlinx.coroutines.flow.filter 48 import kotlinx.coroutines.flow.first 49 import kotlinx.coroutines.flow.map 50 import kotlinx.coroutines.flow.shareIn 51 import pandora.HAPGrpc.HAPImplBase 52 import pandora.HapProto.* 53 import pandora.HostProto.Connection 54 55 @kotlinx.coroutines.ExperimentalCoroutinesApi 56 class Hap(val context: Context) : HAPImplBase(), Closeable { 57 private val TAG = "PandoraHap" 58 59 private val scope: CoroutineScope = CoroutineScope(Dispatchers.Default.limitedParallelism(1)) 60 61 private val bluetoothManager = context.getSystemService(BluetoothManager::class.java)!! 62 private val bluetoothAdapter = bluetoothManager.adapter 63 private val audioManager = context.getSystemService(AudioManager::class.java)!! 64 65 private val bluetoothHapClient = 66 getProfileProxy<BluetoothHapClient>(context, BluetoothProfile.HAP_CLIENT) 67 68 private val bluetoothLeAudio = 69 getProfileProxy<BluetoothLeAudio>(context, BluetoothProfile.LE_AUDIO) 70 71 private val flow = 72 intentFlow( 73 context, 74 IntentFilter().apply { 75 addAction(BluetoothHapClient.ACTION_HAP_CONNECTION_STATE_CHANGED) 76 }, 77 scope, 78 ) 79 .shareIn(scope, SharingStarted.Eagerly) 80 81 private var audioTrack: AudioTrack? = null 82 83 private class PresetInfoChanged( 84 var connection: Connection, 85 var presetInfoList: List<BluetoothHapPresetInfo>, 86 var reason: Int, 87 ) {} 88 89 private val mPresetChanged = callbackFlow { 90 val callback = 91 object : BluetoothHapClient.Callback { 92 override fun onPresetSelected( 93 device: BluetoothDevice, 94 presetIndex: Int, 95 reason: Int, 96 ) { 97 Log.i(TAG, "$device preset info changed") 98 } 99 100 override fun onPresetSelectionFailed(device: BluetoothDevice, reason: Int) { 101 trySend(null) 102 } 103 104 override fun onPresetSelectionForGroupFailed(hapGroupId: Int, reason: Int) { 105 trySend(null) 106 } 107 108 override fun onPresetInfoChanged( 109 device: BluetoothDevice, 110 presetInfoList: List<BluetoothHapPresetInfo>, 111 reason: Int, 112 ) { 113 Log.i(TAG, "$device preset info changed") 114 115 var infoChanged = 116 PresetInfoChanged(device.toConnection(TRANSPORT_LE), presetInfoList, reason) 117 118 trySend(infoChanged) 119 } 120 121 override fun onSetPresetNameFailed(device: BluetoothDevice, reason: Int) { 122 trySend(null) 123 } 124 125 override fun onSetPresetNameForGroupFailed(hapGroupId: Int, reason: Int) { 126 trySend(null) 127 } 128 } 129 130 bluetoothHapClient.registerCallback(Executors.newSingleThreadExecutor(), callback) 131 132 awaitClose { bluetoothHapClient.unregisterCallback(callback) } 133 } 134 135 override fun close() { 136 // Deinit the CoroutineScope 137 scope.cancel() 138 } 139 140 override fun getFeatures( 141 request: GetFeaturesRequest, 142 responseObserver: StreamObserver<GetFeaturesResponse>, 143 ) { 144 grpcUnary<GetFeaturesResponse>(scope, responseObserver) { 145 val device = request.connection.toBluetoothDevice(bluetoothAdapter) 146 Log.i(TAG, "getFeatures(${device})") 147 GetFeaturesResponse.newBuilder() 148 .setFeatures(bluetoothHapClient.getFeatures(device)) 149 .build() 150 } 151 } 152 153 override fun getPresetRecord( 154 request: GetPresetRecordRequest, 155 responseObserver: StreamObserver<GetPresetRecordResponse>, 156 ) { 157 grpcUnary<GetPresetRecordResponse>(scope, responseObserver) { 158 val device = request.connection.toBluetoothDevice(bluetoothAdapter) 159 Log.i(TAG, "getPresetRecord($device, ${request.index})") 160 161 val presetInfo: BluetoothHapPresetInfo? = 162 bluetoothHapClient.getPresetInfo(device, request.index) 163 164 if (presetInfo != null) { 165 GetPresetRecordResponse.newBuilder() 166 .setPresetRecord( 167 PresetRecord.newBuilder() 168 .setIndex(presetInfo.getIndex()) 169 .setName(presetInfo.getName()) 170 .setIsWritable(presetInfo.isWritable()) 171 .setIsAvailable(presetInfo.isAvailable()) 172 ) 173 .build() 174 } else { 175 GetPresetRecordResponse.getDefaultInstance() 176 } 177 } 178 } 179 180 override fun getAllPresetRecords( 181 request: GetAllPresetRecordsRequest, 182 responseObserver: StreamObserver<GetAllPresetRecordsResponse>, 183 ) { 184 grpcUnary<GetAllPresetRecordsResponse>(scope, responseObserver) { 185 val device = request.connection.toBluetoothDevice(bluetoothAdapter) 186 Log.i(TAG, "getAllPresetRecords(${device})") 187 188 GetAllPresetRecordsResponse.newBuilder() 189 .addAllPresetRecordList( 190 bluetoothHapClient 191 .getAllPresetInfo(device) 192 .stream() 193 .map { it: BluetoothHapPresetInfo -> 194 PresetRecord.newBuilder() 195 .setIndex(it.getIndex()) 196 .setName(it.getName()) 197 .setIsWritable(it.isWritable()) 198 .setIsAvailable(it.isAvailable()) 199 .build() 200 } 201 .toList() 202 ) 203 .build() 204 } 205 } 206 207 override fun writePresetName( 208 request: WritePresetNameRequest, 209 responseObserver: StreamObserver<Empty>, 210 ) { 211 grpcUnary<Empty>(scope, responseObserver) { 212 val device = request.connection.toBluetoothDevice(bluetoothAdapter) 213 Log.i(TAG, "writePresetName($device, ${request.index}, ${request.name})") 214 215 bluetoothHapClient.setPresetName(device, request.index, request.name) 216 217 Empty.getDefaultInstance() 218 } 219 } 220 221 override fun setActivePreset( 222 request: SetActivePresetRequest, 223 responseObserver: StreamObserver<Empty>, 224 ) { 225 grpcUnary<Empty>(scope, responseObserver) { 226 val device = request.connection.toBluetoothDevice(bluetoothAdapter) 227 Log.i(TAG, "SetActivePreset($device, ${request.index})") 228 229 bluetoothHapClient.selectPreset(device, request.index) 230 231 Empty.getDefaultInstance() 232 } 233 } 234 235 override fun getActivePresetRecord( 236 request: GetActivePresetRecordRequest, 237 responseObserver: StreamObserver<GetActivePresetRecordResponse>, 238 ) { 239 grpcUnary<GetActivePresetRecordResponse>(scope, responseObserver) { 240 val device = request.connection.toBluetoothDevice(bluetoothAdapter) 241 Log.i(TAG, "GetActivePresetRecord($device)") 242 243 val presetInfo: BluetoothHapPresetInfo? = bluetoothHapClient.getActivePresetInfo(device) 244 245 if (presetInfo != null) { 246 GetActivePresetRecordResponse.newBuilder() 247 .setPresetRecord( 248 PresetRecord.newBuilder() 249 .setIndex(presetInfo.getIndex()) 250 .setName(presetInfo.getName()) 251 .setIsWritable(presetInfo.isWritable()) 252 .setIsAvailable(presetInfo.isAvailable()) 253 ) 254 .build() 255 } else { 256 GetActivePresetRecordResponse.getDefaultInstance() 257 } 258 } 259 } 260 261 override fun setNextPreset( 262 request: SetNextPresetRequest, 263 responseObserver: StreamObserver<Empty>, 264 ) { 265 grpcUnary<Empty>(scope, responseObserver) { 266 val device = request.connection.toBluetoothDevice(bluetoothAdapter) 267 Log.i(TAG, "setNextPreset($device)") 268 269 bluetoothHapClient.switchToNextPreset(device) 270 271 Empty.getDefaultInstance() 272 } 273 } 274 275 override fun setPreviousPreset( 276 request: SetPreviousPresetRequest, 277 responseObserver: StreamObserver<Empty>, 278 ) { 279 grpcUnary<Empty>(scope, responseObserver) { 280 val device = request.connection.toBluetoothDevice(bluetoothAdapter) 281 Log.i(TAG, "setPreviousPreset($device)") 282 283 bluetoothHapClient.switchToPreviousPreset(device) 284 285 Empty.getDefaultInstance() 286 } 287 } 288 289 override fun haPlaybackAudio( 290 responseObserver: StreamObserver<Empty> 291 ): StreamObserver<HaPlaybackAudioRequest> { 292 Log.i(TAG, "haPlaybackAudio") 293 294 if (audioTrack == null) { 295 audioTrack = buildAudioTrack() 296 } 297 298 // Play an audio track. 299 audioTrack!!.play() 300 301 if (audioTrack!!.getPlayState() != AudioTrack.PLAYSTATE_PLAYING) { 302 responseObserver.onError( 303 Status.UNKNOWN.withDescription("AudioTrack is not started").asException() 304 ) 305 } 306 307 // Volume is maxed out to avoid any amplitude modification of the provided audio data, 308 // enabling the test runner to do comparisons between input and output audio signal. 309 // Any volume modification should be done before providing the audio data. 310 if (audioManager.isVolumeFixed) { 311 Log.w(TAG, "Volume is fixed, cannot max out the volume") 312 } else { 313 val maxVolume = audioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC) 314 if (audioManager.getStreamVolume(AudioManager.STREAM_MUSIC) < maxVolume) { 315 audioManager.setStreamVolume( 316 AudioManager.STREAM_MUSIC, 317 maxVolume, 318 AudioManager.FLAG_SHOW_UI, 319 ) 320 } 321 } 322 323 return object : StreamObserver<HaPlaybackAudioRequest> { 324 override fun onNext(request: HaPlaybackAudioRequest) { 325 val data = request.data.toByteArray() 326 val written = synchronized(audioTrack!!) { audioTrack!!.write(data, 0, data.size) } 327 if (written != data.size) { 328 responseObserver.onError( 329 Status.UNKNOWN.withDescription("AudioTrack write failed").asException() 330 ) 331 } 332 } 333 334 override fun onError(t: Throwable) { 335 t.printStackTrace() 336 val sw = StringWriter() 337 t.printStackTrace(PrintWriter(sw)) 338 responseObserver.onError( 339 Status.UNKNOWN.withCause(t).withDescription(sw.toString()).asException() 340 ) 341 } 342 343 override fun onCompleted() { 344 responseObserver.onNext(Empty.getDefaultInstance()) 345 responseObserver.onCompleted() 346 } 347 } 348 } 349 350 override fun waitPresetChanged( 351 request: Empty, 352 responseObserver: StreamObserver<WaitPresetChangedResponse>, 353 ) { 354 grpcUnary<WaitPresetChangedResponse>(scope, responseObserver) { 355 val presetChangedReceived = mPresetChanged.first()!! 356 val presetRecordList = arrayListOf<PresetRecord>() 357 358 for (presetRecord in presetChangedReceived.presetInfoList) { 359 presetRecordList.add( 360 PresetRecord.newBuilder() 361 .setIndex(presetRecord.getIndex()) 362 .setName(presetRecord.getName()) 363 .setIsWritable(presetRecord.isWritable()) 364 .setIsAvailable(presetRecord.isAvailable()) 365 .build() 366 ) 367 } 368 369 WaitPresetChangedResponse.newBuilder() 370 .setConnection(presetChangedReceived.connection) 371 .addAllPresetRecordList(presetRecordList) 372 .setReason(presetChangedReceived.reason) 373 .build() 374 } 375 } 376 377 override fun waitPeripheral( 378 request: WaitPeripheralRequest, 379 responseObserver: StreamObserver<Empty>, 380 ) { 381 grpcUnary<Empty>(scope, responseObserver) { 382 val device = request.connection.toBluetoothDevice(bluetoothAdapter) 383 Log.i(TAG, "waitPeripheral(${device}") 384 if (bluetoothHapClient.getConnectionState(device) != BluetoothProfile.STATE_CONNECTED) { 385 Log.d(TAG, "Manual call to setConnectionPolicy") 386 bluetoothHapClient.setConnectionPolicy(device, CONNECTION_POLICY_ALLOWED) 387 Log.d(TAG, "now waiting for bluetoothHapClient profile connection") 388 flow 389 .filter { it.getBluetoothDeviceExtra() == device } 390 .map { it.getIntExtra(BluetoothProfile.EXTRA_STATE, BluetoothAdapter.ERROR) } 391 .filter { it == BluetoothProfile.STATE_CONNECTED } 392 .first() 393 } 394 395 Empty.getDefaultInstance() 396 } 397 } 398 } 399