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