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