Santos Cordon | 68d1a6b | 2014-09-19 12:25:58 -0700 | [diff] [blame] | 1 | /* |
| 2 | * Copyright (C) 2014 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.server.telecom; |
| 18 | |
| 19 | import android.app.Service; |
| 20 | import android.bluetooth.BluetoothAdapter; |
| 21 | import android.bluetooth.BluetoothHeadset; |
| 22 | import android.bluetooth.BluetoothProfile; |
| 23 | import android.bluetooth.IBluetoothHeadsetPhone; |
| 24 | import android.content.BroadcastReceiver; |
| 25 | import android.content.Context; |
| 26 | import android.content.Intent; |
| 27 | import android.content.IntentFilter; |
| 28 | import android.net.Uri; |
| 29 | import android.os.Handler; |
| 30 | import android.os.IBinder; |
| 31 | import android.os.Message; |
| 32 | import android.os.RemoteException; |
Santos Cordon | 33fff49 | 2014-09-25 14:57:01 -0700 | [diff] [blame] | 33 | import android.telecom.CallState; |
Santos Cordon | 68d1a6b | 2014-09-19 12:25:58 -0700 | [diff] [blame] | 34 | import android.telecom.PhoneAccount; |
Santos Cordon | 88a4a60 | 2014-09-29 19:32:21 -0700 | [diff] [blame^] | 35 | import android.telecom.PhoneCapabilities; |
Santos Cordon | 68d1a6b | 2014-09-19 12:25:58 -0700 | [diff] [blame] | 36 | import android.telephony.PhoneNumberUtils; |
| 37 | import android.telephony.TelephonyManager; |
| 38 | import android.text.TextUtils; |
| 39 | |
| 40 | import com.android.server.telecom.CallsManager.CallsManagerListener; |
| 41 | |
Santos Cordon | 33fff49 | 2014-09-25 14:57:01 -0700 | [diff] [blame] | 42 | import java.util.Collection; |
| 43 | import java.util.HashMap; |
Santos Cordon | 68d1a6b | 2014-09-19 12:25:58 -0700 | [diff] [blame] | 44 | import java.util.List; |
Santos Cordon | 33fff49 | 2014-09-25 14:57:01 -0700 | [diff] [blame] | 45 | import java.util.Map; |
Santos Cordon | 68d1a6b | 2014-09-19 12:25:58 -0700 | [diff] [blame] | 46 | |
| 47 | /** |
| 48 | * Bluetooth headset manager for Telecom. This class shares the call state with the bluetooth device |
| 49 | * and accepts call-related commands to perform on behalf of the BT device. |
| 50 | */ |
| 51 | public final class BluetoothPhoneService extends Service { |
| 52 | /** |
| 53 | * Request object for performing synchronous requests to the main thread. |
| 54 | */ |
| 55 | private static class MainThreadRequest { |
| 56 | Object result; |
| 57 | int param; |
| 58 | |
| 59 | MainThreadRequest(int param) { |
| 60 | this.param = param; |
| 61 | } |
| 62 | |
| 63 | void setResult(Object value) { |
| 64 | result = value; |
| 65 | synchronized (this) { |
| 66 | notifyAll(); |
| 67 | } |
| 68 | } |
| 69 | } |
| 70 | |
| 71 | private static final String TAG = "BluetoothPhoneService"; |
| 72 | |
| 73 | private static final int MSG_ANSWER_CALL = 1; |
| 74 | private static final int MSG_HANGUP_CALL = 2; |
| 75 | private static final int MSG_SEND_DTMF = 3; |
| 76 | private static final int MSG_PROCESS_CHLD = 4; |
| 77 | private static final int MSG_GET_NETWORK_OPERATOR = 5; |
| 78 | private static final int MSG_LIST_CURRENT_CALLS = 6; |
| 79 | private static final int MSG_QUERY_PHONE_STATE = 7; |
| 80 | private static final int MSG_GET_SUBSCRIBER_NUMBER = 8; |
| 81 | |
| 82 | // match up with bthf_call_state_t of bt_hf.h |
| 83 | private static final int CALL_STATE_ACTIVE = 0; |
| 84 | private static final int CALL_STATE_HELD = 1; |
| 85 | private static final int CALL_STATE_DIALING = 2; |
| 86 | private static final int CALL_STATE_ALERTING = 3; |
| 87 | private static final int CALL_STATE_INCOMING = 4; |
| 88 | private static final int CALL_STATE_WAITING = 5; |
| 89 | private static final int CALL_STATE_IDLE = 6; |
| 90 | |
| 91 | // match up with bthf_call_state_t of bt_hf.h |
| 92 | // Terminate all held or set UDUB("busy") to a waiting call |
| 93 | private static final int CHLD_TYPE_RELEASEHELD = 0; |
| 94 | // Terminate all active calls and accepts a waiting/held call |
| 95 | private static final int CHLD_TYPE_RELEASEACTIVE_ACCEPTHELD = 1; |
| 96 | // Hold all active calls and accepts a waiting/held call |
| 97 | private static final int CHLD_TYPE_HOLDACTIVE_ACCEPTHELD = 2; |
| 98 | // Add all held calls to a conference |
| 99 | private static final int CHLD_TYPE_ADDHELDTOCONF = 3; |
| 100 | |
Santos Cordon | a0b4612 | 2014-09-29 14:20:21 -0700 | [diff] [blame] | 101 | private int mNumActiveCalls = 0; |
| 102 | private int mNumHeldCalls = 0; |
| 103 | private int mBluetoothCallState = CALL_STATE_IDLE; |
| 104 | private String mRingingAddress = null; |
| 105 | private int mRingingAddressType = 0; |
| 106 | |
Santos Cordon | 68d1a6b | 2014-09-19 12:25:58 -0700 | [diff] [blame] | 107 | /** |
| 108 | * Binder implementation of IBluetoothHeadsetPhone. Implements the command interface that the |
| 109 | * bluetooth headset code uses to control call. |
| 110 | */ |
| 111 | private final IBluetoothHeadsetPhone.Stub mBinder = new IBluetoothHeadsetPhone.Stub() { |
| 112 | @Override |
| 113 | public boolean answerCall() throws RemoteException { |
| 114 | enforceModifyPermission(); |
| 115 | Log.i(TAG, "BT - answering call"); |
| 116 | return sendSynchronousRequest(MSG_ANSWER_CALL); |
| 117 | } |
| 118 | |
| 119 | @Override |
| 120 | public boolean hangupCall() throws RemoteException { |
| 121 | enforceModifyPermission(); |
| 122 | Log.i(TAG, "BT - hanging up call"); |
| 123 | return sendSynchronousRequest(MSG_HANGUP_CALL); |
| 124 | } |
| 125 | |
| 126 | @Override |
| 127 | public boolean sendDtmf(int dtmf) throws RemoteException { |
| 128 | enforceModifyPermission(); |
| 129 | Log.i(TAG, "BT - sendDtmf %c", Log.DEBUG ? dtmf : '.'); |
| 130 | return sendSynchronousRequest(MSG_SEND_DTMF, dtmf); |
| 131 | } |
| 132 | |
| 133 | @Override |
| 134 | public String getNetworkOperator() throws RemoteException { |
| 135 | Log.i(TAG, "getNetworkOperator"); |
| 136 | enforceModifyPermission(); |
| 137 | return sendSynchronousRequest(MSG_GET_NETWORK_OPERATOR); |
| 138 | } |
| 139 | |
| 140 | @Override |
| 141 | public String getSubscriberNumber() throws RemoteException { |
| 142 | Log.i(TAG, "getSubscriberNumber"); |
| 143 | enforceModifyPermission(); |
| 144 | return sendSynchronousRequest(MSG_GET_SUBSCRIBER_NUMBER); |
| 145 | } |
| 146 | |
| 147 | @Override |
| 148 | public boolean listCurrentCalls() throws RemoteException { |
Santos Cordon | 88a4a60 | 2014-09-29 19:32:21 -0700 | [diff] [blame^] | 149 | // only log if it is after we recently updated the headset state or else it can clog |
| 150 | // the android log since this can be queried every second. |
| 151 | boolean logQuery = mHeadsetUpdatedRecently; |
| 152 | mHeadsetUpdatedRecently = false; |
| 153 | |
| 154 | if (logQuery) { |
| 155 | Log.i(TAG, "listcurrentCalls"); |
| 156 | } |
Santos Cordon | 68d1a6b | 2014-09-19 12:25:58 -0700 | [diff] [blame] | 157 | enforceModifyPermission(); |
Santos Cordon | 88a4a60 | 2014-09-29 19:32:21 -0700 | [diff] [blame^] | 158 | return sendSynchronousRequest(MSG_LIST_CURRENT_CALLS, logQuery ? 1 : 0); |
Santos Cordon | 68d1a6b | 2014-09-19 12:25:58 -0700 | [diff] [blame] | 159 | } |
| 160 | |
| 161 | @Override |
| 162 | public boolean queryPhoneState() throws RemoteException { |
| 163 | Log.i(TAG, "queryPhoneState"); |
| 164 | enforceModifyPermission(); |
| 165 | return sendSynchronousRequest(MSG_QUERY_PHONE_STATE); |
| 166 | } |
| 167 | |
| 168 | @Override |
| 169 | public boolean processChld(int chld) throws RemoteException { |
| 170 | Log.i(TAG, "processChld %d", chld); |
| 171 | enforceModifyPermission(); |
| 172 | return sendSynchronousRequest(MSG_PROCESS_CHLD, chld); |
| 173 | } |
| 174 | |
| 175 | @Override |
| 176 | public void updateBtHandsfreeAfterRadioTechnologyChange() throws RemoteException { |
| 177 | Log.d(TAG, "RAT change"); |
| 178 | // deprecated |
| 179 | } |
| 180 | |
| 181 | @Override |
| 182 | public void cdmaSetSecondCallState(boolean state) throws RemoteException { |
| 183 | Log.d(TAG, "cdma 1"); |
| 184 | // deprecated |
| 185 | } |
| 186 | |
| 187 | @Override |
| 188 | public void cdmaSwapSecondCallState() throws RemoteException { |
| 189 | Log.d(TAG, "cdma 2"); |
| 190 | // deprecated |
| 191 | } |
| 192 | }; |
| 193 | |
| 194 | /** |
| 195 | * Main-thread handler for BT commands. Since telecom logic runs on a single thread, commands |
| 196 | * that are sent to it from the headset need to be moved over to the main thread before |
| 197 | * executing. This handler exists for that reason. |
| 198 | */ |
| 199 | private final Handler mHandler = new Handler() { |
| 200 | @Override |
| 201 | public void handleMessage(Message msg) { |
| 202 | MainThreadRequest request = msg.obj instanceof MainThreadRequest ? |
| 203 | (MainThreadRequest) msg.obj : null; |
| 204 | CallsManager callsManager = getCallsManager(); |
| 205 | Call call = null; |
| 206 | |
| 207 | Log.d(TAG, "handleMessage(%d) w/ param %s", |
| 208 | msg.what, request == null ? null : request.param); |
| 209 | |
| 210 | switch (msg.what) { |
| 211 | case MSG_ANSWER_CALL: |
| 212 | try { |
| 213 | call = callsManager.getRingingCall(); |
| 214 | if (call != null) { |
| 215 | getCallsManager().answerCall(call, 0); |
| 216 | } |
| 217 | } finally { |
| 218 | request.setResult(call != null); |
| 219 | } |
| 220 | break; |
| 221 | |
| 222 | case MSG_HANGUP_CALL: |
| 223 | try { |
| 224 | call = callsManager.getForegroundCall(); |
| 225 | if (call != null) { |
| 226 | callsManager.disconnectCall(call); |
| 227 | } |
| 228 | } finally { |
| 229 | request.setResult(call != null); |
| 230 | } |
| 231 | break; |
| 232 | |
| 233 | case MSG_SEND_DTMF: |
| 234 | try { |
| 235 | call = callsManager.getForegroundCall(); |
| 236 | if (call != null) { |
| 237 | // TODO: Consider making this a queue instead of starting/stopping |
| 238 | // in quick succession. |
| 239 | callsManager.playDtmfTone(call, (char) request.param); |
| 240 | callsManager.stopDtmfTone(call); |
| 241 | } |
| 242 | } finally { |
| 243 | request.setResult(call != null); |
| 244 | } |
| 245 | break; |
| 246 | |
| 247 | case MSG_PROCESS_CHLD: |
| 248 | Boolean result = false; |
| 249 | try { |
| 250 | result = processChld(request.param); |
| 251 | } finally { |
| 252 | request.setResult(result); |
| 253 | } |
| 254 | break; |
| 255 | |
| 256 | case MSG_GET_SUBSCRIBER_NUMBER: |
| 257 | String address = null; |
| 258 | try { |
| 259 | PhoneAccount account = getBestPhoneAccount(); |
| 260 | if (account != null) { |
| 261 | Uri addressUri = account.getAddress(); |
| 262 | if (addressUri != null) { |
| 263 | address = addressUri.getSchemeSpecificPart(); |
| 264 | } |
| 265 | } |
| 266 | |
| 267 | if (TextUtils.isEmpty(address)) { |
| 268 | address = TelephonyManager.from(BluetoothPhoneService.this) |
| 269 | .getLine1Number(); |
| 270 | } |
| 271 | } finally { |
| 272 | request.setResult(address); |
| 273 | } |
| 274 | break; |
| 275 | |
| 276 | case MSG_GET_NETWORK_OPERATOR: |
| 277 | String label = null; |
| 278 | try { |
| 279 | PhoneAccount account = getBestPhoneAccount(); |
| 280 | if (account != null) { |
| 281 | label = account.getLabel().toString(); |
| 282 | } else { |
| 283 | // Finally, just get the network name from telephony. |
| 284 | label = TelephonyManager.from(BluetoothPhoneService.this) |
| 285 | .getNetworkOperatorName(); |
| 286 | } |
| 287 | } finally { |
| 288 | request.setResult(label); |
| 289 | } |
| 290 | break; |
| 291 | |
| 292 | case MSG_LIST_CURRENT_CALLS: |
Santos Cordon | 33fff49 | 2014-09-25 14:57:01 -0700 | [diff] [blame] | 293 | try { |
Santos Cordon | 88a4a60 | 2014-09-29 19:32:21 -0700 | [diff] [blame^] | 294 | sendListOfCalls(request.param == 1); |
Santos Cordon | 33fff49 | 2014-09-25 14:57:01 -0700 | [diff] [blame] | 295 | } finally { |
| 296 | request.setResult(true); |
| 297 | } |
Santos Cordon | 68d1a6b | 2014-09-19 12:25:58 -0700 | [diff] [blame] | 298 | break; |
| 299 | |
| 300 | case MSG_QUERY_PHONE_STATE: |
| 301 | try { |
| 302 | updateHeadsetWithCallState(); |
| 303 | } finally { |
| 304 | if (request != null) { |
| 305 | request.setResult(true); |
| 306 | } |
| 307 | } |
| 308 | break; |
| 309 | } |
| 310 | } |
| 311 | }; |
| 312 | |
| 313 | /** |
| 314 | * Listens to call changes from the CallsManager and calls into methods to update the bluetooth |
| 315 | * headset with the new states. |
| 316 | */ |
| 317 | private CallsManagerListener mCallsManagerListener = new CallsManagerListenerBase() { |
| 318 | @Override |
| 319 | public void onCallAdded(Call call) { |
| 320 | updateHeadsetWithCallState(); |
| 321 | } |
| 322 | |
| 323 | @Override |
| 324 | public void onCallRemoved(Call call) { |
Santos Cordon | 33fff49 | 2014-09-25 14:57:01 -0700 | [diff] [blame] | 325 | mClccIndexMap.remove(call); |
Santos Cordon | 68d1a6b | 2014-09-19 12:25:58 -0700 | [diff] [blame] | 326 | updateHeadsetWithCallState(); |
| 327 | } |
| 328 | |
| 329 | @Override |
| 330 | public void onCallStateChanged(Call call, int oldState, int newState) { |
| 331 | updateHeadsetWithCallState(); |
| 332 | } |
| 333 | |
| 334 | @Override |
| 335 | public void onForegroundCallChanged(Call oldForegroundCall, Call newForegroundCall) { |
| 336 | updateHeadsetWithCallState(); |
| 337 | } |
| 338 | |
| 339 | @Override |
| 340 | public void onIsConferencedChanged(Call call) { |
| 341 | updateHeadsetWithCallState(); |
| 342 | } |
| 343 | }; |
| 344 | |
| 345 | /** |
| 346 | * Listens to connections and disconnections of bluetooth headsets. We need to save the current |
| 347 | * bluetooth headset so that we know where to send call updates. |
| 348 | */ |
| 349 | private BluetoothProfile.ServiceListener mProfileListener = |
| 350 | new BluetoothProfile.ServiceListener() { |
| 351 | @Override |
| 352 | public void onServiceConnected(int profile, BluetoothProfile proxy) { |
| 353 | mBluetoothHeadset = (BluetoothHeadset) proxy; |
| 354 | } |
| 355 | |
| 356 | @Override |
| 357 | public void onServiceDisconnected(int profile) { |
| 358 | mBluetoothHeadset = null; |
| 359 | } |
| 360 | }; |
| 361 | |
| 362 | /** |
| 363 | * Receives events for global state changes of the bluetooth adapter. |
| 364 | */ |
| 365 | private final BroadcastReceiver mBluetoothAdapterReceiver = new BroadcastReceiver() { |
| 366 | @Override |
| 367 | public void onReceive(Context context, Intent intent) { |
| 368 | int state = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, BluetoothAdapter.ERROR); |
| 369 | Log.d(TAG, "Bluetooth Adapter state: %d", state); |
| 370 | if (state == BluetoothAdapter.STATE_ON) { |
| 371 | mHandler.sendEmptyMessage(MSG_QUERY_PHONE_STATE); |
| 372 | } |
| 373 | } |
| 374 | }; |
| 375 | |
| 376 | private BluetoothAdapter mBluetoothAdapter; |
| 377 | private BluetoothHeadset mBluetoothHeadset; |
| 378 | |
Santos Cordon | 33fff49 | 2014-09-25 14:57:01 -0700 | [diff] [blame] | 379 | // A map from Calls to indexes used to identify calls for CLCC (C* List Current Calls). |
| 380 | private Map<Call, Integer> mClccIndexMap = new HashMap<>(); |
| 381 | |
Santos Cordon | 88a4a60 | 2014-09-29 19:32:21 -0700 | [diff] [blame^] | 382 | private boolean mHeadsetUpdatedRecently = false; |
| 383 | |
Santos Cordon | 68d1a6b | 2014-09-19 12:25:58 -0700 | [diff] [blame] | 384 | public BluetoothPhoneService() { |
| 385 | Log.v(TAG, "Constructor"); |
| 386 | } |
| 387 | |
| 388 | public static final void start(Context context) { |
| 389 | if (BluetoothAdapter.getDefaultAdapter() != null) { |
| 390 | context.startService(new Intent(context, BluetoothPhoneService.class)); |
| 391 | } |
| 392 | } |
| 393 | |
| 394 | @Override |
| 395 | public IBinder onBind(Intent intent) { |
| 396 | Log.d(TAG, "Binding service"); |
| 397 | return mBinder; |
| 398 | } |
| 399 | |
| 400 | @Override |
| 401 | public void onCreate() { |
| 402 | Log.d(TAG, "onCreate"); |
| 403 | |
| 404 | mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter(); |
| 405 | if (mBluetoothAdapter == null) { |
| 406 | Log.d(TAG, "BluetoothPhoneService shutting down, no BT Adapter found."); |
| 407 | return; |
| 408 | } |
| 409 | mBluetoothAdapter.getProfileProxy(this, mProfileListener, BluetoothProfile.HEADSET); |
| 410 | |
| 411 | IntentFilter intentFilter = new IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED); |
| 412 | registerReceiver(mBluetoothAdapterReceiver, intentFilter); |
| 413 | |
| 414 | CallsManager.getInstance().addListener(mCallsManagerListener); |
| 415 | updateHeadsetWithCallState(); |
| 416 | } |
| 417 | |
| 418 | @Override |
| 419 | public void onDestroy() { |
| 420 | Log.d(TAG, "onDestroy"); |
| 421 | CallsManager.getInstance().removeListener(mCallsManagerListener); |
| 422 | super.onDestroy(); |
| 423 | } |
| 424 | |
| 425 | private boolean processChld(int chld) { |
| 426 | CallsManager callsManager = CallsManager.getInstance(); |
| 427 | Call activeCall = callsManager.getActiveCall(); |
| 428 | Call ringingCall = callsManager.getRingingCall(); |
| 429 | Call heldCall = callsManager.getHeldCall(); |
| 430 | |
Santos Cordon | 88a4a60 | 2014-09-29 19:32:21 -0700 | [diff] [blame^] | 431 | Log.v(TAG, "Active: %s\nRinging: %s\nHeld: %s", activeCall, ringingCall, heldCall); |
| 432 | |
Santos Cordon | 68d1a6b | 2014-09-19 12:25:58 -0700 | [diff] [blame] | 433 | if (chld == CHLD_TYPE_RELEASEHELD) { |
| 434 | if (ringingCall != null) { |
| 435 | callsManager.rejectCall(ringingCall, false, null); |
| 436 | return true; |
| 437 | } else if (heldCall != null) { |
| 438 | callsManager.disconnectCall(heldCall); |
| 439 | return true; |
| 440 | } |
| 441 | } else if (chld == CHLD_TYPE_RELEASEACTIVE_ACCEPTHELD) { |
| 442 | if (activeCall != null) { |
| 443 | callsManager.disconnectCall(activeCall); |
| 444 | if (ringingCall != null) { |
| 445 | callsManager.answerCall(ringingCall, 0); |
| 446 | } else if (heldCall != null) { |
| 447 | callsManager.unholdCall(heldCall); |
| 448 | } |
| 449 | return true; |
| 450 | } |
| 451 | } else if (chld == CHLD_TYPE_HOLDACTIVE_ACCEPTHELD) { |
Santos Cordon | 88a4a60 | 2014-09-29 19:32:21 -0700 | [diff] [blame^] | 452 | if (activeCall != null && activeCall.can(PhoneCapabilities.SWAP_CONFERENCE)) { |
| 453 | activeCall.swapConference(); |
| 454 | return true; |
| 455 | } else if (ringingCall != null) { |
Santos Cordon | 68d1a6b | 2014-09-19 12:25:58 -0700 | [diff] [blame] | 456 | callsManager.answerCall(ringingCall, 0); |
| 457 | return true; |
| 458 | } else if (heldCall != null) { |
| 459 | // CallsManager will hold any active calls when unhold() is called on a |
| 460 | // currently-held call. |
| 461 | callsManager.unholdCall(heldCall); |
| 462 | return true; |
Santos Cordon | 88a4a60 | 2014-09-29 19:32:21 -0700 | [diff] [blame^] | 463 | } else if (activeCall != null && activeCall.can(PhoneCapabilities.HOLD)) { |
Santos Cordon | 68d1a6b | 2014-09-19 12:25:58 -0700 | [diff] [blame] | 464 | callsManager.holdCall(activeCall); |
| 465 | return true; |
| 466 | } |
| 467 | } else if (chld == CHLD_TYPE_ADDHELDTOCONF) { |
| 468 | if (activeCall != null) { |
Santos Cordon | 88a4a60 | 2014-09-29 19:32:21 -0700 | [diff] [blame^] | 469 | if (activeCall != null && activeCall.can(PhoneCapabilities.MERGE_CONFERENCE)) { |
| 470 | activeCall.mergeConference(); |
Santos Cordon | 68d1a6b | 2014-09-19 12:25:58 -0700 | [diff] [blame] | 471 | return true; |
Santos Cordon | 88a4a60 | 2014-09-29 19:32:21 -0700 | [diff] [blame^] | 472 | } else { |
| 473 | List<Call> conferenceable = activeCall.getConferenceableCalls(); |
| 474 | if (!conferenceable.isEmpty()) { |
| 475 | callsManager.conference(activeCall, conferenceable.get(0)); |
| 476 | return true; |
| 477 | } |
Santos Cordon | 68d1a6b | 2014-09-19 12:25:58 -0700 | [diff] [blame] | 478 | } |
| 479 | } |
| 480 | } |
| 481 | return false; |
| 482 | } |
| 483 | |
| 484 | private void enforceModifyPermission() { |
| 485 | enforceCallingOrSelfPermission(android.Manifest.permission.MODIFY_PHONE_STATE, null); |
| 486 | } |
| 487 | |
| 488 | private <T> T sendSynchronousRequest(int message) { |
| 489 | return sendSynchronousRequest(message, 0); |
| 490 | } |
| 491 | |
| 492 | private <T> T sendSynchronousRequest(int message, int param) { |
| 493 | MainThreadRequest request = new MainThreadRequest(param); |
| 494 | mHandler.obtainMessage(message, request).sendToTarget(); |
| 495 | synchronized (request) { |
| 496 | while (request.result == null) { |
| 497 | try { |
| 498 | request.wait(); |
| 499 | } catch (InterruptedException e) { |
| 500 | // Do nothing, go back and wait until the request is complete. |
| 501 | } |
| 502 | } |
| 503 | } |
| 504 | if (request.result != null) { |
| 505 | @SuppressWarnings("unchecked") |
| 506 | T retval = (T) request.result; |
| 507 | return retval; |
| 508 | } |
| 509 | return null; |
| 510 | } |
| 511 | |
Santos Cordon | 88a4a60 | 2014-09-29 19:32:21 -0700 | [diff] [blame^] | 512 | private void sendListOfCalls(boolean shouldLog) { |
Santos Cordon | 33fff49 | 2014-09-25 14:57:01 -0700 | [diff] [blame] | 513 | Collection<Call> mCalls = getCallsManager().getCalls(); |
| 514 | for (Call call : mCalls) { |
| 515 | // We don't send the parent conference call to the bluetooth device. |
| 516 | if (!call.isConference()) { |
Santos Cordon | 88a4a60 | 2014-09-29 19:32:21 -0700 | [diff] [blame^] | 517 | sendClccForCall(call, shouldLog); |
Santos Cordon | 33fff49 | 2014-09-25 14:57:01 -0700 | [diff] [blame] | 518 | } |
| 519 | } |
| 520 | sendClccEndMarker(); |
| 521 | } |
| 522 | |
| 523 | /** |
| 524 | * Sends a single clcc (C* List Current Calls) event for the specified call. |
| 525 | */ |
Santos Cordon | 88a4a60 | 2014-09-29 19:32:21 -0700 | [diff] [blame^] | 526 | private void sendClccForCall(Call call, boolean shouldLog) { |
Santos Cordon | 33fff49 | 2014-09-25 14:57:01 -0700 | [diff] [blame] | 527 | boolean isForeground = getCallsManager().getForegroundCall() == call; |
| 528 | int state = convertCallState(call.getState(), isForeground); |
Santos Cordon | 88a4a60 | 2014-09-29 19:32:21 -0700 | [diff] [blame^] | 529 | boolean isPartOfConference = false; |
Nancy Chen | 05a9e40 | 2014-09-26 14:14:32 -0700 | [diff] [blame] | 530 | |
| 531 | if (state == CALL_STATE_IDLE) { |
| 532 | return; |
| 533 | } |
| 534 | |
Santos Cordon | 88a4a60 | 2014-09-29 19:32:21 -0700 | [diff] [blame^] | 535 | Call conferenceCall = call.getParentCall(); |
| 536 | if (conferenceCall != null) { |
| 537 | isPartOfConference = true; |
| 538 | |
| 539 | // Run some alternative states for Conference-level merge/swap support. |
| 540 | // Basically, if call supports swapping or merging at the conference-level, then we need |
| 541 | // to expose the calls as having distinct states (ACTIVE vs HOLD) or the functionality |
| 542 | // won't show up on the bluetooth device. |
| 543 | |
| 544 | // Before doing any special logic, ensure that we are dealing with an ACTIVE call and |
| 545 | // that the conference itself has a notion of the current "active" child call. |
| 546 | Call activeChild = conferenceCall.getConferenceLevelActiveCall(); |
| 547 | if (state == CALL_STATE_ACTIVE && activeChild != null) { |
| 548 | // Reevaluate state if we can MERGE or if we can SWAP without previously having |
| 549 | // MERGED. |
| 550 | boolean shouldReevaluateState = |
| 551 | conferenceCall.can(PhoneCapabilities.MERGE_CONFERENCE) || |
| 552 | (conferenceCall.can(PhoneCapabilities.SWAP_CONFERENCE) && |
| 553 | !conferenceCall.wasConferencePreviouslyMerged()); |
| 554 | |
| 555 | if (shouldReevaluateState) { |
| 556 | isPartOfConference = false; |
| 557 | if (call == activeChild) { |
| 558 | state = CALL_STATE_ACTIVE; |
| 559 | } else { |
| 560 | // At this point we know there is an "active" child and we know that it is |
| 561 | // not this call, so set it to HELD instead. |
| 562 | state = CALL_STATE_HELD; |
| 563 | } |
| 564 | } |
| 565 | } |
| 566 | } |
| 567 | |
Nancy Chen | 05a9e40 | 2014-09-26 14:14:32 -0700 | [diff] [blame] | 568 | int index = getIndexForCall(call); |
| 569 | int direction = call.isIncoming() ? 1 : 0; |
Yorke Lee | fb70e1e | 2014-09-29 14:22:53 -0700 | [diff] [blame] | 570 | final Uri addressUri; |
| 571 | if (call.getGatewayInfo() != null) { |
| 572 | addressUri = call.getGatewayInfo().getOriginalAddress(); |
| 573 | } else { |
| 574 | addressUri = call.getHandle(); |
| 575 | } |
Santos Cordon | 33fff49 | 2014-09-25 14:57:01 -0700 | [diff] [blame] | 576 | String address = addressUri == null ? null : addressUri.getSchemeSpecificPart(); |
| 577 | int addressType = address == null ? -1 : PhoneNumberUtils.toaFromString(address); |
| 578 | |
Santos Cordon | 88a4a60 | 2014-09-29 19:32:21 -0700 | [diff] [blame^] | 579 | if (shouldLog) { |
| 580 | Log.i(this, "sending clcc for call %d, %d, %d, %b, %s, %d", |
| 581 | index, direction, state, isPartOfConference, Log.piiHandle(address), |
| 582 | addressType); |
| 583 | } |
Santos Cordon | 33fff49 | 2014-09-25 14:57:01 -0700 | [diff] [blame] | 584 | mBluetoothHeadset.clccResponse( |
| 585 | index, direction, state, 0, isPartOfConference, address, addressType); |
| 586 | } |
| 587 | |
| 588 | private void sendClccEndMarker() { |
| 589 | // End marker is recognized with an index value of 0. All other parameters are ignored. |
| 590 | mBluetoothHeadset.clccResponse(0 /* index */, 0, 0, 0, false, null, 0); |
| 591 | } |
| 592 | |
| 593 | /** |
| 594 | * Returns the caches index for the specified call. If no such index exists, then an index is |
| 595 | * given (smallest number starting from 1 that isn't already taken). |
| 596 | */ |
| 597 | private int getIndexForCall(Call call) { |
| 598 | if (mClccIndexMap.containsKey(call)) { |
| 599 | return mClccIndexMap.get(call); |
| 600 | } |
| 601 | |
| 602 | int i = 1; // Indexes for bluetooth clcc are 1-based. |
| 603 | while (mClccIndexMap.containsValue(i)) { |
| 604 | i++; |
| 605 | } |
| 606 | |
| 607 | // NOTE: Indexes are removed in {@link #onCallRemoved}. |
| 608 | mClccIndexMap.put(call, i); |
| 609 | return i; |
| 610 | } |
| 611 | |
Santos Cordon | 68d1a6b | 2014-09-19 12:25:58 -0700 | [diff] [blame] | 612 | private void updateHeadsetWithCallState() { |
| 613 | CallsManager callsManager = getCallsManager(); |
| 614 | Call activeCall = callsManager.getActiveCall(); |
| 615 | Call ringingCall = callsManager.getRingingCall(); |
| 616 | Call heldCall = callsManager.getHeldCall(); |
| 617 | |
| 618 | int bluetoothCallState = getBluetoothCallStateForUpdate(); |
| 619 | |
| 620 | String ringingAddress = null; |
| 621 | int ringingAddressType = 128; |
| 622 | if (ringingCall != null) { |
| 623 | ringingAddress = ringingCall.getHandle().getSchemeSpecificPart(); |
| 624 | if (ringingAddress != null) { |
| 625 | ringingAddressType = PhoneNumberUtils.toaFromString(ringingAddress); |
| 626 | } |
| 627 | } |
| 628 | if (ringingAddress == null) { |
| 629 | ringingAddress = ""; |
| 630 | } |
| 631 | |
Santos Cordon | a0b4612 | 2014-09-29 14:20:21 -0700 | [diff] [blame] | 632 | int numActiveCalls = activeCall == null ? 0 : 1; |
| 633 | int numHeldCalls = heldCall == null ? 0 : 1; |
Santos Cordon | 68d1a6b | 2014-09-19 12:25:58 -0700 | [diff] [blame] | 634 | |
Santos Cordon | 88a4a60 | 2014-09-29 19:32:21 -0700 | [diff] [blame^] | 635 | // For conference calls which support swapping the active call within the conference |
| 636 | // (namely CDMA calls) we need to expose that as a held call in order for the BT device |
| 637 | // to show "swap" and "merge" functionality. |
| 638 | if (activeCall != null && activeCall.isConference()) { |
| 639 | if (activeCall.can(PhoneCapabilities.SWAP_CONFERENCE)) { |
| 640 | // Indicate that BT device should show SWAP command by indicating that there is a |
| 641 | // call on hold, but only if the conference wasn't previously merged. |
| 642 | numHeldCalls = activeCall.wasConferencePreviouslyMerged() ? 0 : 1; |
| 643 | } else if (activeCall.can(PhoneCapabilities.MERGE_CONFERENCE)) { |
| 644 | numHeldCalls = 1; // Merge is available, so expose via numHeldCalls. |
| 645 | } |
| 646 | } |
| 647 | |
Santos Cordon | a0b4612 | 2014-09-29 14:20:21 -0700 | [diff] [blame] | 648 | if (mBluetoothHeadset != null && |
| 649 | (numActiveCalls != mNumActiveCalls || |
| 650 | numHeldCalls != mNumHeldCalls || |
| 651 | bluetoothCallState != mBluetoothCallState || |
| 652 | !TextUtils.equals(ringingAddress, mRingingAddress) || |
| 653 | ringingAddressType != mRingingAddressType)) { |
Santos Cordon | 68d1a6b | 2014-09-19 12:25:58 -0700 | [diff] [blame] | 654 | |
Santos Cordon | a0b4612 | 2014-09-29 14:20:21 -0700 | [diff] [blame] | 655 | // If the call is transitioning into the alerting state, send DIALING first. |
| 656 | // Some devices expect to see a DIALING state prior to seeing an ALERTING state |
| 657 | // so we need to send it first. |
| 658 | boolean sendDialingFirst = mBluetoothCallState != bluetoothCallState && |
| 659 | bluetoothCallState == CALL_STATE_ALERTING; |
| 660 | |
| 661 | mNumActiveCalls = numActiveCalls; |
| 662 | mNumHeldCalls = numHeldCalls; |
| 663 | mBluetoothCallState = bluetoothCallState; |
| 664 | mRingingAddress = ringingAddress; |
| 665 | mRingingAddressType = ringingAddressType; |
| 666 | |
| 667 | if (sendDialingFirst) { |
| 668 | Log.i(TAG, "Sending dialing state"); |
| 669 | mBluetoothHeadset.phoneStateChanged( |
| 670 | mNumActiveCalls, |
| 671 | mNumHeldCalls, |
| 672 | CALL_STATE_DIALING, |
| 673 | mRingingAddress, |
| 674 | mRingingAddressType); |
| 675 | } |
| 676 | |
| 677 | Log.i(TAG, "updateHeadsetWithCallState " + |
| 678 | "numActive %s, " + |
| 679 | "numHeld %s, " + |
| 680 | "callState %s, " + |
| 681 | "ringing number %s, " + |
| 682 | "ringing type %s", |
| 683 | mNumActiveCalls, |
| 684 | mNumHeldCalls, |
| 685 | mBluetoothCallState, |
| 686 | Log.pii(mRingingAddress), |
| 687 | mRingingAddressType); |
| 688 | |
Santos Cordon | 68d1a6b | 2014-09-19 12:25:58 -0700 | [diff] [blame] | 689 | mBluetoothHeadset.phoneStateChanged( |
Santos Cordon | a0b4612 | 2014-09-29 14:20:21 -0700 | [diff] [blame] | 690 | mNumActiveCalls, |
| 691 | mNumHeldCalls, |
| 692 | mBluetoothCallState, |
| 693 | mRingingAddress, |
| 694 | mRingingAddressType); |
Santos Cordon | 88a4a60 | 2014-09-29 19:32:21 -0700 | [diff] [blame^] | 695 | |
| 696 | mHeadsetUpdatedRecently = true; |
Santos Cordon | 68d1a6b | 2014-09-19 12:25:58 -0700 | [diff] [blame] | 697 | } |
| 698 | } |
| 699 | |
| 700 | private int getBluetoothCallStateForUpdate() { |
| 701 | CallsManager callsManager = getCallsManager(); |
| 702 | Call ringingCall = callsManager.getRingingCall(); |
Nancy Chen | 05a9e40 | 2014-09-26 14:14:32 -0700 | [diff] [blame] | 703 | Call dialingCall = callsManager.getDialingCall(); |
Santos Cordon | 68d1a6b | 2014-09-19 12:25:58 -0700 | [diff] [blame] | 704 | |
| 705 | // |
| 706 | // !! WARNING !! |
| 707 | // You will note that CALL_STATE_WAITING, CALL_STATE_HELD, and CALL_STATE_ACTIVE are not |
| 708 | // used in this version of the call state mappings. This is on purpose. |
| 709 | // phone_state_change() in btif_hf.c is not written to handle these states. Only with the |
| 710 | // listCalls*() method are WAITING and ACTIVE used. |
| 711 | // Using the unsupported states here caused problems with inconsistent state in some |
| 712 | // bluetooth devices (like not getting out of ringing state after answering a call). |
| 713 | // |
| 714 | int bluetoothCallState = CALL_STATE_IDLE; |
| 715 | if (ringingCall != null) { |
| 716 | bluetoothCallState = CALL_STATE_INCOMING; |
Nancy Chen | 05a9e40 | 2014-09-26 14:14:32 -0700 | [diff] [blame] | 717 | } else if (dialingCall != null) { |
Santos Cordon | 68d1a6b | 2014-09-19 12:25:58 -0700 | [diff] [blame] | 718 | bluetoothCallState = CALL_STATE_ALERTING; |
| 719 | } |
| 720 | return bluetoothCallState; |
| 721 | } |
| 722 | |
Santos Cordon | 33fff49 | 2014-09-25 14:57:01 -0700 | [diff] [blame] | 723 | private int convertCallState(int callState, boolean isForegroundCall) { |
| 724 | switch (callState) { |
| 725 | case CallState.NEW: |
| 726 | case CallState.ABORTED: |
| 727 | case CallState.DISCONNECTED: |
Nancy Chen | 05a9e40 | 2014-09-26 14:14:32 -0700 | [diff] [blame] | 728 | case CallState.CONNECTING: |
| 729 | case CallState.PRE_DIAL_WAIT: |
Santos Cordon | 33fff49 | 2014-09-25 14:57:01 -0700 | [diff] [blame] | 730 | return CALL_STATE_IDLE; |
| 731 | |
| 732 | case CallState.ACTIVE: |
| 733 | return CALL_STATE_ACTIVE; |
| 734 | |
Santos Cordon | 33fff49 | 2014-09-25 14:57:01 -0700 | [diff] [blame] | 735 | case CallState.DIALING: |
| 736 | // Yes, this is correctly returning ALERTING. |
| 737 | // "Dialing" for BT means that we have sent information to the service provider |
| 738 | // to place the call but there is no confirmation that the call is going through. |
| 739 | // When there finally is confirmation, the ringback is played which is referred to |
| 740 | // as an "alert" tone, thus, ALERTING. |
| 741 | // TODO: We should consider using the ALERTING terms in Telecom because that |
| 742 | // seems to be more industry-standard. |
| 743 | return CALL_STATE_ALERTING; |
| 744 | |
| 745 | case CallState.ON_HOLD: |
| 746 | return CALL_STATE_HELD; |
| 747 | |
| 748 | case CallState.RINGING: |
| 749 | if (isForegroundCall) { |
| 750 | return CALL_STATE_INCOMING; |
| 751 | } else { |
| 752 | return CALL_STATE_WAITING; |
| 753 | } |
| 754 | } |
| 755 | return CALL_STATE_IDLE; |
| 756 | } |
| 757 | |
Santos Cordon | 68d1a6b | 2014-09-19 12:25:58 -0700 | [diff] [blame] | 758 | private CallsManager getCallsManager() { |
| 759 | return CallsManager.getInstance(); |
| 760 | } |
| 761 | |
| 762 | /** |
| 763 | * Returns the best phone account to use for the given state of all calls. |
| 764 | * First, tries to return the phone account for the foreground call, second the default |
| 765 | * phone account for PhoneAccount.SCHEME_TEL. |
| 766 | */ |
| 767 | private PhoneAccount getBestPhoneAccount() { |
| 768 | TelecomApp app = (TelecomApp) getApplication(); |
| 769 | PhoneAccountRegistrar registry = app.getPhoneAccountRegistrar(); |
| 770 | Call call = getCallsManager().getForegroundCall(); |
| 771 | |
| 772 | PhoneAccount account = null; |
| 773 | if (call != null) { |
| 774 | // First try to get the network name of the foreground call. |
| 775 | account = registry.getPhoneAccount(call.getTargetPhoneAccount()); |
| 776 | } |
| 777 | |
| 778 | if (account == null) { |
| 779 | // Second, Try to get the label for the default Phone Account. |
| 780 | account = registry.getPhoneAccount( |
| 781 | registry.getDefaultOutgoingPhoneAccount(PhoneAccount.SCHEME_TEL)); |
| 782 | } |
| 783 | return account; |
| 784 | } |
| 785 | } |