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