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