1/* 2 * Copyright 2017 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 17package com.android.bluetooth.hfp; 18 19import android.bluetooth.BluetoothDevice; 20import android.bluetooth.BluetoothHeadset; 21import android.bluetooth.IBluetoothHeadsetPhone; 22import android.content.ActivityNotFoundException; 23import android.content.ComponentName; 24import android.content.Context; 25import android.content.Intent; 26import android.content.ServiceConnection; 27import android.media.AudioManager; 28import android.os.IBinder; 29import android.os.PowerManager; 30import android.os.RemoteException; 31import android.util.Log; 32 33import com.android.internal.annotations.VisibleForTesting; 34 35/** 36 * Defines system calls that is used by state machine/service to either send or receive 37 * messages from the Android System. 38 */ 39@VisibleForTesting 40public class HeadsetSystemInterface { 41 private static final String TAG = HeadsetSystemInterface.class.getSimpleName(); 42 private static final boolean DBG = false; 43 44 private final HeadsetService mHeadsetService; 45 private final AudioManager mAudioManager; 46 private final HeadsetPhoneState mHeadsetPhoneState; 47 private PowerManager.WakeLock mVoiceRecognitionWakeLock; 48 private volatile IBluetoothHeadsetPhone mPhoneProxy; 49 private final ServiceConnection mPhoneProxyConnection = new ServiceConnection() { 50 @Override 51 public void onServiceConnected(ComponentName className, IBinder service) { 52 if (DBG) { 53 Log.d(TAG, "Proxy object connected"); 54 } 55 synchronized (HeadsetSystemInterface.this) { 56 mPhoneProxy = IBluetoothHeadsetPhone.Stub.asInterface(service); 57 } 58 } 59 60 @Override 61 public void onServiceDisconnected(ComponentName className) { 62 if (DBG) { 63 Log.d(TAG, "Proxy object disconnected"); 64 } 65 synchronized (HeadsetSystemInterface.this) { 66 mPhoneProxy = null; 67 } 68 } 69 }; 70 71 HeadsetSystemInterface(HeadsetService headsetService) { 72 if (headsetService == null) { 73 Log.wtfStack(TAG, "HeadsetService parameter is null"); 74 } 75 mHeadsetService = headsetService; 76 mAudioManager = (AudioManager) mHeadsetService.getSystemService(Context.AUDIO_SERVICE); 77 PowerManager powerManager = 78 (PowerManager) mHeadsetService.getSystemService(Context.POWER_SERVICE); 79 mVoiceRecognitionWakeLock = 80 powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, TAG + ":VoiceRecognition"); 81 mVoiceRecognitionWakeLock.setReferenceCounted(false); 82 mHeadsetPhoneState = new HeadsetPhoneState(mHeadsetService); 83 } 84 85 /** 86 * Initialize this system interface 87 */ 88 public synchronized void init() { 89 // Bind to Telecom phone proxy service 90 Intent intent = new Intent(IBluetoothHeadsetPhone.class.getName()); 91 intent.setComponent(intent.resolveSystemService(mHeadsetService.getPackageManager(), 0)); 92 if (intent.getComponent() == null || !mHeadsetService.bindService(intent, 93 mPhoneProxyConnection, 0)) { 94 // Crash the stack if cannot bind to Telecom 95 Log.wtfStack(TAG, "Could not bind to IBluetoothHeadsetPhone Service, intent=" + intent); 96 } 97 } 98 99 /** 100 * Stop this system interface 101 */ 102 public synchronized void stop() { 103 if (mPhoneProxy != null) { 104 if (DBG) { 105 Log.d(TAG, "Unbinding phone proxy"); 106 } 107 mPhoneProxy = null; 108 // Synchronization should make sure unbind can be successful 109 mHeadsetService.unbindService(mPhoneProxyConnection); 110 } 111 mHeadsetPhoneState.cleanup(); 112 } 113 114 /** 115 * Get audio manager. Most audio manager oprations are pass through and therefore are not 116 * individually managed by this class 117 * 118 * @return audio manager for setting audio parameters 119 */ 120 @VisibleForTesting 121 public AudioManager getAudioManager() { 122 return mAudioManager; 123 } 124 125 /** 126 * Get wake lock for voice recognition 127 * 128 * @return wake lock for voice recognition 129 */ 130 @VisibleForTesting 131 public PowerManager.WakeLock getVoiceRecognitionWakeLock() { 132 return mVoiceRecognitionWakeLock; 133 } 134 135 /** 136 * Get HeadsetPhoneState instance to interact with Telephony service 137 * 138 * @return HeadsetPhoneState interface to interact with Telephony service 139 */ 140 @VisibleForTesting 141 public HeadsetPhoneState getHeadsetPhoneState() { 142 return mHeadsetPhoneState; 143 } 144 145 /** 146 * Answer the current incoming call in Telecom service 147 * 148 * @param device the Bluetooth device used for answering this call 149 */ 150 @VisibleForTesting 151 public void answerCall(BluetoothDevice device) { 152 if (device == null) { 153 Log.w(TAG, "answerCall device is null"); 154 return; 155 } 156 157 if (mPhoneProxy != null) { 158 try { 159 mHeadsetService.setActiveDevice(device); 160 mPhoneProxy.answerCall(); 161 } catch (RemoteException e) { 162 Log.e(TAG, Log.getStackTraceString(new Throwable())); 163 } 164 } else { 165 Log.e(TAG, "Handsfree phone proxy null for answering call"); 166 } 167 } 168 169 /** 170 * Hangup the current call, could either be Telecom call or virtual call 171 * 172 * @param device the Bluetooth device used for hanging up this call 173 */ 174 @VisibleForTesting 175 public void hangupCall(BluetoothDevice device) { 176 if (device == null) { 177 Log.w(TAG, "hangupCall device is null"); 178 return; 179 } 180 // Close the virtual call if active. Virtual call should be 181 // terminated for CHUP callback event 182 if (mHeadsetService.isVirtualCallStarted()) { 183 mHeadsetService.stopScoUsingVirtualVoiceCall(); 184 } else { 185 if (mPhoneProxy != null) { 186 try { 187 mPhoneProxy.hangupCall(); 188 } catch (RemoteException e) { 189 Log.e(TAG, Log.getStackTraceString(new Throwable())); 190 } 191 } else { 192 Log.e(TAG, "Handsfree phone proxy null for hanging up call"); 193 } 194 } 195 } 196 197 /** 198 * Instructs Telecom to play the specified DTMF tone for the current foreground call 199 * 200 * @param dtmf dtmf code 201 * @param device the Bluetooth device that sent this code 202 */ 203 @VisibleForTesting 204 public boolean sendDtmf(int dtmf, BluetoothDevice device) { 205 if (device == null) { 206 Log.w(TAG, "sendDtmf device is null"); 207 return false; 208 } 209 if (mPhoneProxy != null) { 210 try { 211 return mPhoneProxy.sendDtmf(dtmf); 212 } catch (RemoteException e) { 213 Log.e(TAG, Log.getStackTraceString(new Throwable())); 214 } 215 } else { 216 Log.e(TAG, "Handsfree phone proxy null for sending DTMF"); 217 } 218 return false; 219 } 220 221 /** 222 * Instructs Telecom hold an incoming call 223 * 224 * @param chld index of the call to hold 225 */ 226 @VisibleForTesting 227 public boolean processChld(int chld) { 228 if (mPhoneProxy != null) { 229 try { 230 return mPhoneProxy.processChld(chld); 231 } catch (RemoteException e) { 232 Log.e(TAG, Log.getStackTraceString(new Throwable())); 233 } 234 } else { 235 Log.e(TAG, "Handsfree phone proxy null for sending DTMF"); 236 } 237 return false; 238 } 239 240 /** 241 * Get the the alphabetic name of current registered operator. 242 * 243 * @return null on error, empty string if not available 244 */ 245 @VisibleForTesting 246 public String getNetworkOperator() { 247 final IBluetoothHeadsetPhone phoneProxy = mPhoneProxy; 248 if (phoneProxy == null) { 249 Log.e(TAG, "getNetworkOperator() failed: mPhoneProxy is null"); 250 return null; 251 } 252 try { 253 // Should never return null 254 return mPhoneProxy.getNetworkOperator(); 255 } catch (RemoteException exception) { 256 Log.e(TAG, "getNetworkOperator() failed: " + exception.getMessage()); 257 exception.printStackTrace(); 258 return null; 259 } 260 } 261 262 /** 263 * Get the phone number of this device 264 * 265 * @return null if unavailable 266 */ 267 @VisibleForTesting 268 public String getSubscriberNumber() { 269 final IBluetoothHeadsetPhone phoneProxy = mPhoneProxy; 270 if (phoneProxy == null) { 271 Log.e(TAG, "getSubscriberNumber() failed: mPhoneProxy is null"); 272 return null; 273 } 274 try { 275 return mPhoneProxy.getSubscriberNumber(); 276 } catch (RemoteException exception) { 277 Log.e(TAG, "getSubscriberNumber() failed: " + exception.getMessage()); 278 exception.printStackTrace(); 279 return null; 280 } 281 } 282 283 284 /** 285 * Ask the Telecomm service to list current list of calls through CLCC response 286 * {@link BluetoothHeadset#clccResponse(int, int, int, int, boolean, String, int)} 287 * 288 * @return 289 */ 290 @VisibleForTesting 291 public boolean listCurrentCalls() { 292 final IBluetoothHeadsetPhone phoneProxy = mPhoneProxy; 293 if (phoneProxy == null) { 294 Log.e(TAG, "listCurrentCalls() failed: mPhoneProxy is null"); 295 return false; 296 } 297 try { 298 return mPhoneProxy.listCurrentCalls(); 299 } catch (RemoteException exception) { 300 Log.e(TAG, "listCurrentCalls() failed: " + exception.getMessage()); 301 exception.printStackTrace(); 302 return false; 303 } 304 } 305 306 /** 307 * Request Telecom service to send an update of the current call state to the headset service 308 * through {@link BluetoothHeadset#phoneStateChanged(int, int, int, String, int)} 309 */ 310 @VisibleForTesting 311 public void queryPhoneState() { 312 final IBluetoothHeadsetPhone phoneProxy = mPhoneProxy; 313 if (phoneProxy != null) { 314 try { 315 mPhoneProxy.queryPhoneState(); 316 } catch (RemoteException e) { 317 Log.e(TAG, Log.getStackTraceString(new Throwable())); 318 } 319 } else { 320 Log.e(TAG, "Handsfree phone proxy null for query phone state"); 321 } 322 } 323 324 /** 325 * Check if we are currently in a phone call 326 * 327 * @return True iff we are in a phone call 328 */ 329 @VisibleForTesting 330 public boolean isInCall() { 331 return ((mHeadsetPhoneState.getNumActiveCall() > 0) || (mHeadsetPhoneState.getNumHeldCall() 332 > 0) || ((mHeadsetPhoneState.getCallState() != HeadsetHalConstants.CALL_STATE_IDLE) 333 && (mHeadsetPhoneState.getCallState() != HeadsetHalConstants.CALL_STATE_INCOMING))); 334 } 335 336 /** 337 * Check if there is currently an incoming call 338 * 339 * @return True iff there is an incoming call 340 */ 341 @VisibleForTesting 342 public boolean isRinging() { 343 return mHeadsetPhoneState.getCallState() == HeadsetHalConstants.CALL_STATE_INCOMING; 344 } 345 346 /** 347 * Check if call status is idle 348 * 349 * @return true if call state is neither ringing nor in call 350 */ 351 @VisibleForTesting 352 public boolean isCallIdle() { 353 return !isInCall() && !isRinging(); 354 } 355 356 /** 357 * Activate voice recognition on Android system 358 * 359 * @return true if activation succeeds, caller should wait for 360 * {@link BluetoothHeadset#startVoiceRecognition(BluetoothDevice)} callback that will then 361 * trigger {@link HeadsetService#startVoiceRecognition(BluetoothDevice)}, false if failed to 362 * activate 363 */ 364 @VisibleForTesting 365 public boolean activateVoiceRecognition() { 366 Intent intent = new Intent(Intent.ACTION_VOICE_COMMAND); 367 intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 368 try { 369 mHeadsetService.startActivity(intent); 370 } catch (ActivityNotFoundException e) { 371 Log.e(TAG, "activateVoiceRecognition, failed due to activity not found for " + intent); 372 return false; 373 } 374 return true; 375 } 376 377 /** 378 * Deactivate voice recognition on Android system 379 * 380 * @return true if activation succeeds, caller should wait for 381 * {@link BluetoothHeadset#stopVoiceRecognition(BluetoothDevice)} callback that will then 382 * trigger {@link HeadsetService#stopVoiceRecognition(BluetoothDevice)}, false if failed to 383 * activate 384 */ 385 @VisibleForTesting 386 public boolean deactivateVoiceRecognition() { 387 // TODO: need a method to deactivate voice recognition on Android 388 return true; 389 } 390 391} 392