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.content.Context 24 import android.content.Intent 25 import android.content.IntentFilter 26 import android.media.* 27 import android.util.Log 28 import com.google.protobuf.Empty 29 import io.grpc.stub.StreamObserver 30 import java.io.Closeable 31 import kotlinx.coroutines.CoroutineScope 32 import kotlinx.coroutines.Dispatchers 33 import kotlinx.coroutines.cancel 34 import kotlinx.coroutines.flow.Flow 35 import kotlinx.coroutines.flow.SharingStarted 36 import kotlinx.coroutines.flow.filter 37 import kotlinx.coroutines.flow.first 38 import kotlinx.coroutines.flow.map 39 import kotlinx.coroutines.flow.shareIn 40 import pandora.LeAudioGrpc.LeAudioImplBase 41 import pandora.LeAudioProto.* 42 43 @kotlinx.coroutines.ExperimentalCoroutinesApi 44 class LeAudio(val context: Context) : LeAudioImplBase(), Closeable { 45 46 private val TAG = "PandoraLeAudio" 47 48 private val scope: CoroutineScope 49 private val flow: Flow<Intent> 50 51 private val audioManager = context.getSystemService(AudioManager::class.java)!! 52 53 private val bluetoothManager = context.getSystemService(BluetoothManager::class.java)!! 54 private val bluetoothAdapter = bluetoothManager.adapter 55 private val bluetoothLeAudio = 56 getProfileProxy<BluetoothLeAudio>(context, BluetoothProfile.LE_AUDIO) 57 58 init { 59 scope = CoroutineScope(Dispatchers.Default) 60 val intentFilter = IntentFilter() 61 intentFilter.addAction(BluetoothLeAudio.ACTION_LE_AUDIO_CONNECTION_STATE_CHANGED) 62 63 flow = intentFlow(context, intentFilter, scope).shareIn(scope, SharingStarted.Eagerly) 64 } 65 closenull66 override fun close() { 67 bluetoothAdapter.closeProfileProxy(BluetoothProfile.LE_AUDIO, bluetoothLeAudio) 68 scope.cancel() 69 } 70 opennull71 override fun open(request: OpenRequest, responseObserver: StreamObserver<Empty>) { 72 grpcUnary<Empty>(scope, responseObserver) { 73 val device = request.connection.toBluetoothDevice(bluetoothAdapter) 74 Log.i(TAG, "open: device=$device") 75 76 if (bluetoothLeAudio.getConnectionState(device) != BluetoothProfile.STATE_CONNECTED) { 77 bluetoothLeAudio.connect(device) 78 val state = 79 flow 80 .filter { 81 it.getAction() == 82 BluetoothLeAudio.ACTION_LE_AUDIO_CONNECTION_STATE_CHANGED 83 } 84 .filter { it.getBluetoothDeviceExtra() == device } 85 .map { 86 it.getIntExtra(BluetoothProfile.EXTRA_STATE, BluetoothAdapter.ERROR) 87 } 88 .filter { 89 it == BluetoothProfile.STATE_CONNECTED || 90 it == BluetoothProfile.STATE_DISCONNECTED 91 } 92 .first() 93 94 if (state == BluetoothProfile.STATE_DISCONNECTED) { 95 throw RuntimeException("open failed, LE_AUDIO has been disconnected") 96 } 97 } 98 99 Empty.getDefaultInstance() 100 } 101 } 102 } 103