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