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