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.BluetoothLeAudio 21 import android.bluetooth.BluetoothManager 22 import android.bluetooth.BluetoothProfile 23 import android.bluetooth.BluetoothProfile.STATE_CONNECTED 24 import android.bluetooth.BluetoothProfile.STATE_DISCONNECTED 25 import android.content.Context 26 import android.content.Intent 27 import android.content.IntentFilter 28 import android.media.AudioTrack 29 import android.media.AudioManager 30 import android.util.Log 31 import com.google.protobuf.Empty 32 import io.grpc.Status 33 import io.grpc.stub.StreamObserver 34 import java.io.Closeable 35 import kotlinx.coroutines.CoroutineScope 36 import kotlinx.coroutines.Dispatchers 37 import kotlinx.coroutines.cancel 38 import kotlinx.coroutines.flow.Flow 39 import kotlinx.coroutines.flow.SharingStarted 40 import kotlinx.coroutines.flow.filter 41 import kotlinx.coroutines.flow.first 42 import kotlinx.coroutines.flow.map 43 import kotlinx.coroutines.flow.shareIn 44 import pandora.LeAudioGrpc.LeAudioImplBase 45 import pandora.LeAudioProto.* 46 import java.io.PrintWriter 47 import java.io.StringWriter 48 49 @kotlinx.coroutines.ExperimentalCoroutinesApi 50 class LeAudio(val context: Context) : LeAudioImplBase(), Closeable { 51 52 private val TAG = "PandoraLeAudio" 53 54 private val scope: CoroutineScope 55 private val flow: Flow<Intent> 56 57 private val audioManager = context.getSystemService(AudioManager::class.java)!! 58 59 private val bluetoothManager = context.getSystemService(BluetoothManager::class.java)!! 60 private val bluetoothAdapter = bluetoothManager.adapter 61 private val bluetoothLeAudio = 62 getProfileProxy<BluetoothLeAudio>(context, BluetoothProfile.LE_AUDIO) 63 64 private var audioTrack: AudioTrack? = null 65 66 init { 67 scope = CoroutineScope(Dispatchers.Default) 68 val intentFilter = IntentFilter() 69 intentFilter.addAction(BluetoothLeAudio.ACTION_LE_AUDIO_CONNECTION_STATE_CHANGED) 70 71 flow = intentFlow(context, intentFilter, scope).shareIn(scope, SharingStarted.Eagerly) 72 } 73 closenull74 override fun close() { 75 bluetoothAdapter.closeProfileProxy(BluetoothProfile.LE_AUDIO, bluetoothLeAudio) 76 scope.cancel() 77 } 78 opennull79 override fun open(request: OpenRequest, responseObserver: StreamObserver<Empty>) { 80 grpcUnary<Empty>(scope, responseObserver) { 81 val device = request.connection.toBluetoothDevice(bluetoothAdapter) 82 Log.i(TAG, "open: device=$device") 83 84 if (bluetoothLeAudio.getConnectionState(device) != STATE_CONNECTED) { 85 bluetoothLeAudio.connect(device) 86 val state = 87 flow 88 .filter { 89 it.getAction() == 90 BluetoothLeAudio.ACTION_LE_AUDIO_CONNECTION_STATE_CHANGED 91 } 92 .filter { it.getBluetoothDeviceExtra() == device } 93 .map { 94 it.getIntExtra(BluetoothProfile.EXTRA_STATE, BluetoothAdapter.ERROR) 95 } 96 .filter { it == STATE_CONNECTED || it == STATE_DISCONNECTED } 97 .first() 98 99 if (state == STATE_DISCONNECTED) { 100 throw RuntimeException("open failed, LE_AUDIO has been disconnected") 101 } 102 } 103 104 Empty.getDefaultInstance() 105 } 106 } 107 leAudioStartnull108 override fun leAudioStart(request: LeAudioStartRequest, responseObserver: StreamObserver<Empty>) { 109 grpcUnary<Empty>(scope, responseObserver) { 110 if (audioTrack == null) { 111 audioTrack = buildAudioTrack() 112 } 113 val device = request.connection.toBluetoothDevice(bluetoothAdapter) 114 Log.i(TAG, "start: device=$device") 115 116 if (bluetoothLeAudio.getConnectionState(device) != BluetoothLeAudio.STATE_CONNECTED) { 117 throw RuntimeException("Device is not connected, cannot start") 118 } 119 120 // Configure the selected device as active device if it is not 121 // already. 122 bluetoothLeAudio.setActiveDevice(device) 123 124 // Play an audio track. 125 audioTrack!!.play() 126 127 Empty.getDefaultInstance() 128 } 129 } 130 leAudioStopnull131 override fun leAudioStop(request: LeAudioStopRequest, responseObserver: StreamObserver<Empty>) { 132 grpcUnary<Empty>(scope, responseObserver) { 133 checkNotNull(audioTrack) { "No track to pause!" } 134 135 // Play an audio track. 136 audioTrack!!.pause() 137 138 Empty.getDefaultInstance() 139 } 140 } 141 leAudioPlaybackAudionull142 override fun leAudioPlaybackAudio( 143 responseObserver: StreamObserver<LeAudioPlaybackAudioResponse> 144 ): StreamObserver<LeAudioPlaybackAudioRequest> { 145 Log.i(TAG, "leAudioPlaybackAudio") 146 147 if (audioTrack!!.getPlayState() != AudioTrack.PLAYSTATE_PLAYING) { 148 responseObserver.onError( 149 Status.UNKNOWN.withDescription("AudioTrack is not started").asException() 150 ) 151 } 152 153 // Volume is maxed out to avoid any amplitude modification of the provided audio data, 154 // enabling the test runner to do comparisons between input and output audio signal. 155 // Any volume modification should be done before providing the audio data. 156 if (audioManager.isVolumeFixed) { 157 Log.w(TAG, "Volume is fixed, cannot max out the volume") 158 } else { 159 val maxVolume = audioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC) 160 if (audioManager.getStreamVolume(AudioManager.STREAM_MUSIC) < maxVolume) { 161 audioManager.setStreamVolume( 162 AudioManager.STREAM_MUSIC, 163 maxVolume, 164 AudioManager.FLAG_SHOW_UI, 165 ) 166 } 167 } 168 169 return object : StreamObserver<LeAudioPlaybackAudioRequest> { 170 override fun onNext(request: LeAudioPlaybackAudioRequest) { 171 val data = request.data.toByteArray() 172 val written = synchronized(audioTrack!!) { audioTrack!!.write(data, 0, data.size) } 173 if (written != data.size) { 174 responseObserver.onError( 175 Status.UNKNOWN.withDescription("AudioTrack write failed").asException() 176 ) 177 } 178 } 179 180 override fun onError(t: Throwable) { 181 t.printStackTrace() 182 val sw = StringWriter() 183 t.printStackTrace(PrintWriter(sw)) 184 responseObserver.onError( 185 Status.UNKNOWN.withCause(t).withDescription(sw.toString()).asException() 186 ) 187 } 188 189 override fun onCompleted() { 190 responseObserver.onNext(LeAudioPlaybackAudioResponse.getDefaultInstance()) 191 responseObserver.onCompleted() 192 } 193 } 194 } 195 } 196