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.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.ByteString 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.A2DPGrpc.A2DPImplBase 41 import pandora.A2DPProto.* 42 43 @kotlinx.coroutines.ExperimentalCoroutinesApi 44 class A2dpSink(val context: Context) : A2DPImplBase(), Closeable { 45 private val TAG = "PandoraA2dpSink" 46 47 private val scope: CoroutineScope 48 private val flow: Flow<Intent> 49 50 private val bluetoothManager = context.getSystemService(BluetoothManager::class.java)!! 51 private val bluetoothAdapter = bluetoothManager.adapter 52 private val bluetoothA2dpSink = 53 getProfileProxy<BluetoothA2dpSink>(context, BluetoothProfile.A2DP_SINK) 54 55 init { 56 scope = CoroutineScope(Dispatchers.Default.limitedParallelism(1)) 57 val intentFilter = IntentFilter() 58 intentFilter.addAction(BluetoothA2dpSink.ACTION_CONNECTION_STATE_CHANGED) 59 60 flow = intentFlow(context, intentFilter, scope).shareIn(scope, SharingStarted.Eagerly) 61 } 62 closenull63 override fun close() { 64 bluetoothAdapter.closeProfileProxy(BluetoothProfile.A2DP_SINK, bluetoothA2dpSink) 65 scope.cancel() 66 } 67 waitSinknull68 override fun waitSink( 69 request: WaitSinkRequest, 70 responseObserver: StreamObserver<WaitSinkResponse> 71 ) { 72 grpcUnary<WaitSinkResponse>(scope, responseObserver) { 73 val device = request.connection.toBluetoothDevice(bluetoothAdapter) 74 Log.i(TAG, "waitSink: device=$device") 75 76 if (bluetoothA2dpSink.getConnectionState(device) != BluetoothProfile.STATE_CONNECTED) { 77 val state = 78 flow 79 .filter { 80 it.getAction() == BluetoothA2dpSink.ACTION_CONNECTION_STATE_CHANGED 81 } 82 .filter { it.getBluetoothDeviceExtra() == device } 83 .map { 84 it.getIntExtra(BluetoothProfile.EXTRA_STATE, BluetoothAdapter.ERROR) 85 } 86 .filter { 87 it == BluetoothProfile.STATE_CONNECTED || 88 it == BluetoothProfile.STATE_DISCONNECTED 89 } 90 .first() 91 92 if (state == BluetoothProfile.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) != BluetoothProfile.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( 118 device, 119 BluetoothProfile.CONNECTION_POLICY_FORBIDDEN 120 ) 121 a2dpConnectionStateChangedFlow 122 .filter { it == BluetoothProfile.STATE_DISCONNECTED } 123 .first() 124 125 CloseResponse.getDefaultInstance() 126 } 127 } 128 } 129