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.BluetoothA2dpSink 20 import android.bluetooth.BluetoothAdapter 21 import android.bluetooth.BluetoothManager 22 import android.bluetooth.BluetoothProfile 23 import android.bluetooth.BluetoothProfile.CONNECTION_POLICY_FORBIDDEN 24 import android.bluetooth.BluetoothProfile.STATE_CONNECTED 25 import android.bluetooth.BluetoothProfile.STATE_DISCONNECTED 26 import android.content.Context 27 import android.content.Intent 28 import android.content.IntentFilter 29 import android.media.* 30 import android.util.Log 31 import com.google.protobuf.ByteString 32 import io.grpc.stub.StreamObserver 33 import java.io.Closeable 34 import kotlinx.coroutines.CoroutineScope 35 import kotlinx.coroutines.Dispatchers 36 import kotlinx.coroutines.cancel 37 import kotlinx.coroutines.flow.Flow 38 import kotlinx.coroutines.flow.SharingStarted 39 import kotlinx.coroutines.flow.filter 40 import kotlinx.coroutines.flow.first 41 import kotlinx.coroutines.flow.map 42 import kotlinx.coroutines.flow.shareIn 43 import pandora.A2DPGrpc.A2DPImplBase 44 import pandora.A2DPProto.* 45 46 @kotlinx.coroutines.ExperimentalCoroutinesApi 47 class A2dpSink(val context: Context) : A2DPImplBase(), Closeable { 48 private val TAG = "PandoraA2dpSink" 49 50 private val scope: CoroutineScope 51 private val flow: Flow<Intent> 52 53 private val bluetoothManager = context.getSystemService(BluetoothManager::class.java)!! 54 private val bluetoothAdapter = bluetoothManager.adapter 55 private val bluetoothA2dpSink = 56 getProfileProxy<BluetoothA2dpSink>(context, BluetoothProfile.A2DP_SINK) 57 58 init { 59 scope = CoroutineScope(Dispatchers.Default.limitedParallelism(1)) 60 val intentFilter = IntentFilter() 61 intentFilter.addAction(BluetoothA2dpSink.ACTION_CONNECTION_STATE_CHANGED) 62 63 flow = intentFlow(context, intentFilter, scope).shareIn(scope, SharingStarted.Eagerly) 64 } 65 closenull66 override fun close() { 67 bluetoothAdapter.closeProfileProxy(BluetoothProfile.A2DP_SINK, bluetoothA2dpSink) 68 scope.cancel() 69 } 70 waitSinknull71 override fun waitSink( 72 request: WaitSinkRequest, 73 responseObserver: StreamObserver<WaitSinkResponse>, 74 ) { 75 grpcUnary<WaitSinkResponse>(scope, responseObserver) { 76 val device = request.connection.toBluetoothDevice(bluetoothAdapter) 77 Log.i(TAG, "waitSink: device=$device") 78 79 if (bluetoothA2dpSink.getConnectionState(device) != STATE_CONNECTED) { 80 val state = 81 flow 82 .filter { 83 it.getAction() == BluetoothA2dpSink.ACTION_CONNECTION_STATE_CHANGED 84 } 85 .filter { it.getBluetoothDeviceExtra() == device } 86 .map { 87 it.getIntExtra(BluetoothProfile.EXTRA_STATE, BluetoothAdapter.ERROR) 88 } 89 .filter { it == STATE_CONNECTED || it == STATE_DISCONNECTED } 90 .first() 91 92 if (state == STATE_DISCONNECTED) { 93 throw RuntimeException("waitStream failed, A2DP has been disconnected") 94 } 95 } 96 97 val sink = 98 Sink.newBuilder().setCookie(ByteString.copyFrom(device.getAddress(), "UTF-8")) 99 WaitSinkResponse.newBuilder().setSink(sink).build() 100 } 101 } 102 closenull103 override fun close(request: CloseRequest, responseObserver: StreamObserver<CloseResponse>) { 104 grpcUnary<CloseResponse>(scope, responseObserver) { 105 val device = bluetoothAdapter.getRemoteDevice(request.sink.cookie.toString("UTF-8")) 106 Log.i(TAG, "close: device=$device") 107 if (bluetoothA2dpSink.getConnectionState(device) != STATE_CONNECTED) { 108 throw RuntimeException("Device is not connected, cannot close") 109 } 110 111 val a2dpConnectionStateChangedFlow = 112 flow 113 .filter { it.getAction() == BluetoothA2dpSink.ACTION_CONNECTION_STATE_CHANGED } 114 .filter { it.getBluetoothDeviceExtra() == device } 115 .map { it.getIntExtra(BluetoothProfile.EXTRA_STATE, BluetoothAdapter.ERROR) } 116 117 bluetoothA2dpSink.setConnectionPolicy(device, CONNECTION_POLICY_FORBIDDEN) 118 a2dpConnectionStateChangedFlow.filter { it == STATE_DISCONNECTED }.first() 119 120 CloseResponse.getDefaultInstance() 121 } 122 } 123 } 124