• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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