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.annotation.SuppressLint 20 import android.bluetooth.BluetoothDevice 21 import android.bluetooth.BluetoothHeadset 22 import android.bluetooth.BluetoothManager 23 import android.bluetooth.BluetoothProfile 24 import android.content.Context 25 import android.content.Intent 26 import android.content.IntentFilter 27 import android.net.Uri 28 import android.os.Bundle 29 import android.os.IBinder 30 import android.provider.CallLog 31 import android.telecom.Call 32 import android.telecom.CallAudioState 33 import android.telecom.InCallService 34 import android.telecom.TelecomManager 35 import android.telecom.VideoProfile 36 import com.google.protobuf.Empty 37 import io.grpc.stub.StreamObserver 38 import java.io.Closeable 39 import kotlinx.coroutines.CoroutineScope 40 import kotlinx.coroutines.Dispatchers 41 import kotlinx.coroutines.cancel 42 import kotlinx.coroutines.flow.Flow 43 import kotlinx.coroutines.flow.SharingStarted 44 import kotlinx.coroutines.flow.shareIn 45 import pandora.HFPGrpc.HFPImplBase 46 import pandora.HfpProto.* 47 48 private const val TAG = "PandoraHfp" 49 50 @kotlinx.coroutines.ExperimentalCoroutinesApi 51 class Hfp(val context: Context) : HFPImplBase(), Closeable { 52 private val scope: CoroutineScope = CoroutineScope(Dispatchers.Default.limitedParallelism(1)) 53 private val flow: Flow<Intent> 54 55 private val bluetoothManager = context.getSystemService(BluetoothManager::class.java)!! 56 private val telecomManager = context.getSystemService(TelecomManager::class.java)!! 57 private val bluetoothAdapter = bluetoothManager.adapter 58 59 private val bluetoothHfp = getProfileProxy<BluetoothHeadset>(context, BluetoothProfile.HEADSET) 60 61 companion object { 62 @SuppressLint("StaticFieldLeak") private lateinit var inCallService: InCallService 63 } 64 65 init { 66 67 val intentFilter = IntentFilter(BluetoothDevice.ACTION_BOND_STATE_CHANGED) 68 flow = intentFlow(context, intentFilter, scope).shareIn(scope, SharingStarted.Eagerly) 69 70 // kill any existing call 71 telecomManager.endCall() 72 73 shell("su root setprop persist.bluetooth.disableinbandringing false") 74 } 75 closenull76 override fun close() { 77 // kill any existing call 78 telecomManager.endCall() 79 80 bluetoothAdapter.closeProfileProxy(BluetoothProfile.HEADSET, bluetoothHfp) 81 scope.cancel() 82 } 83 84 class PandoraInCallService : InCallService() { onBindnull85 override fun onBind(intent: Intent?): IBinder? { 86 inCallService = this 87 return super.onBind(intent) 88 } 89 } 90 enableSlcnull91 override fun enableSlc(request: EnableSlcRequest, responseObserver: StreamObserver<Empty>) { 92 grpcUnary<Empty>(scope, responseObserver) { 93 val device = request.connection.toBluetoothDevice(bluetoothAdapter) 94 95 bluetoothHfp.setConnectionPolicy(device, BluetoothProfile.CONNECTION_POLICY_ALLOWED) 96 97 Empty.getDefaultInstance() 98 } 99 } 100 disableSlcnull101 override fun disableSlc(request: DisableSlcRequest, responseObserver: StreamObserver<Empty>) { 102 grpcUnary<Empty>(scope, responseObserver) { 103 val device = request.connection.toBluetoothDevice(bluetoothAdapter) 104 105 bluetoothHfp.setConnectionPolicy(device, BluetoothProfile.CONNECTION_POLICY_FORBIDDEN) 106 107 Empty.getDefaultInstance() 108 } 109 } 110 setBatteryLevelnull111 override fun setBatteryLevel( 112 request: SetBatteryLevelRequest, 113 responseObserver: StreamObserver<Empty>, 114 ) { 115 grpcUnary<Empty>(scope, responseObserver) { 116 val action = "android.intent.action.BATTERY_CHANGED" 117 shell("am broadcast -a $action --ei level ${request.batteryPercentage} --ei scale 100") 118 Empty.getDefaultInstance() 119 } 120 } 121 declineCallnull122 override fun declineCall( 123 request: DeclineCallRequest, 124 responseObserver: StreamObserver<DeclineCallResponse>, 125 ) { 126 grpcUnary(scope, responseObserver) { 127 telecomManager.endCall() 128 DeclineCallResponse.getDefaultInstance() 129 } 130 } 131 setAudioPathnull132 override fun setAudioPath( 133 request: SetAudioPathRequest, 134 responseObserver: StreamObserver<SetAudioPathResponse>, 135 ) { 136 grpcUnary(scope, responseObserver) { 137 when (request.audioPath!!) { 138 AudioPath.AUDIO_PATH_UNKNOWN, 139 AudioPath.UNRECOGNIZED, -> {} 140 AudioPath.AUDIO_PATH_HANDSFREE -> { 141 check(bluetoothHfp.getActiveDevice() != null) 142 inCallService.setAudioRoute(CallAudioState.ROUTE_BLUETOOTH) 143 } 144 AudioPath.AUDIO_PATH_SPEAKERS -> 145 inCallService.setAudioRoute(CallAudioState.ROUTE_SPEAKER) 146 } 147 SetAudioPathResponse.getDefaultInstance() 148 } 149 } 150 answerCallnull151 override fun answerCall( 152 request: AnswerCallRequest, 153 responseObserver: StreamObserver<AnswerCallResponse>, 154 ) { 155 grpcUnary(scope, responseObserver) { 156 telecomManager.acceptRingingCall() 157 AnswerCallResponse.getDefaultInstance() 158 } 159 } 160 swapActiveCallnull161 override fun swapActiveCall( 162 request: SwapActiveCallRequest, 163 responseObserver: StreamObserver<SwapActiveCallResponse>, 164 ) { 165 grpcUnary(scope, responseObserver) { 166 val callsToActivate = mutableListOf<Call>() 167 for (call in inCallService.calls) { 168 if (call.details.state == Call.STATE_ACTIVE) { 169 call.hold() 170 } else { 171 callsToActivate.add(call) 172 } 173 } 174 for (call in callsToActivate) { 175 call.answer(VideoProfile.STATE_AUDIO_ONLY) 176 } 177 inCallService.calls[0].hold() 178 inCallService.calls[1].unhold() 179 SwapActiveCallResponse.getDefaultInstance() 180 } 181 } 182 setInBandRingtonenull183 override fun setInBandRingtone( 184 request: SetInBandRingtoneRequest, 185 responseObserver: StreamObserver<SetInBandRingtoneResponse>, 186 ) { 187 grpcUnary(scope, responseObserver) { 188 shell( 189 "su root setprop persist.bluetooth.disableinbandringing " + 190 (!request.enabled).toString() 191 ) 192 SetInBandRingtoneResponse.getDefaultInstance() 193 } 194 } 195 makeCallnull196 override fun makeCall( 197 request: MakeCallRequest, 198 responseObserver: StreamObserver<MakeCallResponse> 199 ) { 200 grpcUnary(scope, responseObserver) { 201 telecomManager.placeCall(Uri.fromParts("tel", request.number, null), Bundle()) 202 MakeCallResponse.getDefaultInstance() 203 } 204 } 205 setVoiceRecognitionnull206 override fun setVoiceRecognition( 207 request: SetVoiceRecognitionRequest, 208 responseObserver: StreamObserver<SetVoiceRecognitionResponse> 209 ) { 210 grpcUnary(scope, responseObserver) { 211 if (request.enabled) { 212 bluetoothHfp.startVoiceRecognition( 213 request.connection.toBluetoothDevice(bluetoothAdapter) 214 ) 215 } else { 216 bluetoothHfp.stopVoiceRecognition( 217 request.connection.toBluetoothDevice(bluetoothAdapter) 218 ) 219 } 220 SetVoiceRecognitionResponse.getDefaultInstance() 221 } 222 } 223 clearCallHistorynull224 override fun clearCallHistory( 225 request: ClearCallHistoryRequest, 226 responseObserver: StreamObserver<ClearCallHistoryResponse> 227 ) { 228 grpcUnary(scope, responseObserver) { 229 context.contentResolver.delete(CallLog.Calls.CONTENT_URI, null, null) 230 ClearCallHistoryResponse.getDefaultInstance() 231 } 232 } 233 } 234