1 /*
2  * Copyright (C) 2024 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.BluetoothManager
21 import android.bluetooth.BluetoothProfile
22 import android.bluetooth.BluetoothProfile.CONNECTION_POLICY_ALLOWED
23 import android.bluetooth.BluetoothVolumeControl
24 import android.content.Context
25 import android.content.IntentFilter
26 import android.util.Log
27 import com.google.protobuf.Empty
28 import io.grpc.stub.StreamObserver
29 import java.io.Closeable
30 import kotlinx.coroutines.CoroutineScope
31 import kotlinx.coroutines.Dispatchers
32 import kotlinx.coroutines.cancel
33 import kotlinx.coroutines.flow.SharingStarted
34 import kotlinx.coroutines.flow.filter
35 import kotlinx.coroutines.flow.first
36 import kotlinx.coroutines.flow.map
37 import kotlinx.coroutines.flow.shareIn
38 import pandora.vcp.VCPGrpc.VCPImplBase
39 import pandora.vcp.VcpProto.*
40 
41 @kotlinx.coroutines.ExperimentalCoroutinesApi
42 class Vcp(val context: Context) : VCPImplBase(), Closeable {
43     private val TAG = "PandoraVcp"
44 
45     private val scope: CoroutineScope = CoroutineScope(Dispatchers.Default.limitedParallelism(1))
46 
47     private val bluetoothManager = context.getSystemService(BluetoothManager::class.java)!!
48     private val bluetoothAdapter = bluetoothManager.adapter
49 
50     private val bluetoothVolumeControl =
51         getProfileProxy<BluetoothVolumeControl>(context, BluetoothProfile.VOLUME_CONTROL)
52 
53     private val flow =
54         intentFlow(
55                 context,
<lambda>null56                 IntentFilter().apply {
57                     addAction(BluetoothVolumeControl.ACTION_CONNECTION_STATE_CHANGED)
58                 },
59                 scope,
60             )
61             .shareIn(scope, SharingStarted.Eagerly)
62 
closenull63     override fun close() {
64         // Deinit the CoroutineScope
65         scope.cancel()
66     }
67 
setDeviceVolumenull68     override fun setDeviceVolume(
69         request: SetDeviceVolumeRequest,
70         responseObserver: StreamObserver<Empty>,
71     ) {
72         grpcUnary<Empty>(scope, responseObserver) {
73             val device = request.connection.toBluetoothDevice(bluetoothAdapter)
74 
75             Log.i(TAG, "setDeviceVolume(${device}, ${request.volume})")
76 
77             bluetoothVolumeControl.setDeviceVolume(device, request.volume, false)
78 
79             Empty.getDefaultInstance()
80         }
81     }
82 
setVolumeOffsetnull83     override fun setVolumeOffset(
84         request: SetVolumeOffsetRequest,
85         responseObserver: StreamObserver<Empty>,
86     ) {
87         grpcUnary<Empty>(scope, responseObserver) {
88             val device = request.connection.toBluetoothDevice(bluetoothAdapter)
89 
90             Log.i(TAG, "setVolumeOffset(${device}, ${request.offset})")
91 
92             bluetoothVolumeControl.setVolumeOffset(device, 1, request.offset)
93 
94             Empty.getDefaultInstance()
95         }
96     }
97 
waitConnectnull98     override fun waitConnect(request: WaitConnectRequest, responseObserver: StreamObserver<Empty>) {
99         grpcUnary<Empty>(scope, responseObserver) {
100             val device = request.connection.toBluetoothDevice(bluetoothAdapter)
101             Log.i(TAG, "waitPeripheral(${device}")
102             if (
103                 bluetoothVolumeControl.getConnectionState(device) !=
104                     BluetoothProfile.STATE_CONNECTED
105             ) {
106                 Log.d(TAG, "Manual call to setConnectionPolicy")
107                 bluetoothVolumeControl.setConnectionPolicy(device, CONNECTION_POLICY_ALLOWED)
108                 Log.d(TAG, "wait for bluetoothVolumeControl profile connection")
109                 flow
110                     .filter { it.getBluetoothDeviceExtra() == device }
111                     .map { it.getIntExtra(BluetoothProfile.EXTRA_STATE, BluetoothAdapter.ERROR) }
112                     .filter { it == BluetoothProfile.STATE_CONNECTED }
113                     .first()
114             }
115 
116             Empty.getDefaultInstance()
117         }
118     }
119 
setGainSettingnull120     override fun setGainSetting(
121         request: SetGainSettingRequest,
122         responseObserver: StreamObserver<Empty>,
123     ) {
124         grpcUnary<Empty>(scope, responseObserver) {
125             val device = request.connection.toBluetoothDevice(bluetoothAdapter)
126             Log.i(TAG, "setGainSetting(${device}, ${request.gainSetting})")
127 
128             bluetoothVolumeControl.getAudioInputControlServices(device).forEach {
129                 it.setGainSetting(request.gainSetting)
130             }
131 
132             Empty.getDefaultInstance()
133         }
134     }
135 
setMutenull136     override fun setMute(request: SetMuteRequest, responseObserver: StreamObserver<Empty>) {
137         grpcUnary<Empty>(scope, responseObserver) {
138             val device = request.connection.toBluetoothDevice(bluetoothAdapter)
139             Log.i(TAG, "setMute(${device}, ${request.mute})")
140 
141             bluetoothVolumeControl.getAudioInputControlServices(device).forEach {
142                 it.setMute(request.mute)
143             }
144 
145             Empty.getDefaultInstance()
146         }
147     }
148 
setGainModenull149     override fun setGainMode(request: SetGainModeRequest, responseObserver: StreamObserver<Empty>) {
150         grpcUnary<Empty>(scope, responseObserver) {
151             val device = request.connection.toBluetoothDevice(bluetoothAdapter)
152             Log.i(TAG, "setMute(${device}, ${request.gainMode})")
153 
154             bluetoothVolumeControl.getAudioInputControlServices(device).forEach {
155                 it.setGainMode(request.gainMode)
156             }
157 
158             Empty.getDefaultInstance()
159         }
160     }
161 }
162