1/* 2 * Copyright (C) 2015 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 */ 16package com.android.car.dialer.telecom; 17 18import android.bluetooth.BluetoothAdapter; 19import android.bluetooth.BluetoothDevice; 20import android.bluetooth.BluetoothHeadsetClient; 21import android.bluetooth.BluetoothHeadsetClientCall; 22import android.bluetooth.BluetoothProfile; 23import android.content.ComponentName; 24import android.content.Context; 25import android.content.Intent; 26import android.content.ServiceConnection; 27import android.database.Cursor; 28import android.net.Uri; 29import android.os.IBinder; 30import android.provider.CallLog; 31import android.telecom.Call; 32import android.telecom.CallAudioState; 33import android.telecom.CallAudioState.CallAudioRoute; 34import android.telecom.DisconnectCause; 35import android.telecom.GatewayInfo; 36import android.telecom.InCallService; 37import android.telecom.PhoneAccountHandle; 38import android.telecom.TelecomManager; 39import android.telephony.TelephonyManager; 40import android.text.TextUtils; 41import android.util.Log; 42 43import com.android.car.dialer.CallListener; 44import com.android.car.dialer.R; 45 46import java.lang.ref.WeakReference; 47import java.util.ArrayList; 48import java.util.Calendar; 49import java.util.Collections; 50import java.util.Comparator; 51import java.util.HashMap; 52import java.util.List; 53import java.util.Map; 54import java.util.concurrent.CopyOnWriteArrayList; 55 56/** 57 * The entry point for all interactions between UI and telecom. 58 */ 59public class UiCallManager { 60 private static String TAG = "Em.TelecomMgr"; 61 62 private static final String HFP_CLIENT_CONNECTION_SERVICE_CLASS_NAME 63 = "com.android.bluetooth.hfpclient.connserv.HfpClientConnectionService"; 64 // Rate limit how often you can place outgoing calls. 65 private static final long MIN_TIME_BETWEEN_CALLS_MS = 3000; 66 private static final List<Integer> sCallStateRank = new ArrayList<>(); 67 private static UiCallManager sUiCallManager; 68 69 // Used to assign id's to UiCall objects as they're created. 70 private static int nextCarPhoneCallId = 0; 71 72 static { 73 // States should be added from lowest rank to highest 74 sCallStateRank.add(Call.STATE_DISCONNECTED); 75 sCallStateRank.add(Call.STATE_DISCONNECTING); 76 sCallStateRank.add(Call.STATE_NEW); 77 sCallStateRank.add(Call.STATE_CONNECTING); 78 sCallStateRank.add(Call.STATE_SELECT_PHONE_ACCOUNT); 79 sCallStateRank.add(Call.STATE_HOLDING); 80 sCallStateRank.add(Call.STATE_ACTIVE); 81 sCallStateRank.add(Call.STATE_DIALING); 82 sCallStateRank.add(Call.STATE_RINGING); 83 } 84 85 private Context mContext; 86 private TelephonyManager mTelephonyManager; 87 private long mLastPlacedCallTimeMs; 88 89 private TelecomManager mTelecomManager; 90 private InCallServiceImpl mInCallService; 91 private BluetoothHeadsetClient mBluetoothHeadsetClient; 92 private final Map<UiCall, Call> mCallMapping = new HashMap<>(); 93 private final List<CallListener> mCallListeners = new CopyOnWriteArrayList<>(); 94 95 /** 96 * Initialized a globally accessible {@link UiCallManager} which can be retrieved by 97 * {@link #get}. If this function is called a second time before calling {@link #tearDown()}, 98 * an exception will be thrown. 99 * 100 * @param applicationContext Application context. 101 */ 102 public static UiCallManager init(Context applicationContext) { 103 if (sUiCallManager == null) { 104 sUiCallManager = new UiCallManager(applicationContext); 105 } else { 106 throw new IllegalStateException("UiCallManager has been initialized."); 107 } 108 return sUiCallManager; 109 } 110 111 /** 112 * Gets the global {@link UiCallManager} instance. Make sure 113 * {@link #init(Context)} is called before calling this method. 114 */ 115 public static UiCallManager get() { 116 if (sUiCallManager == null) { 117 throw new IllegalStateException( 118 "Call UiCallManager.init(Context) before calling this function"); 119 } 120 return sUiCallManager; 121 } 122 123 private UiCallManager(Context context) { 124 if (Log.isLoggable(TAG, Log.DEBUG)) { 125 Log.d(TAG, "SetUp"); 126 } 127 128 mContext = context; 129 mTelephonyManager = (TelephonyManager) mContext.getSystemService(Context.TELEPHONY_SERVICE); 130 131 mTelecomManager = (TelecomManager) context.getSystemService(Context.TELECOM_SERVICE); 132 Intent intent = new Intent(context, InCallServiceImpl.class); 133 intent.setAction(InCallServiceImpl.ACTION_LOCAL_BIND); 134 context.bindService(intent, mInCallServiceConnection, Context.BIND_AUTO_CREATE); 135 136 BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter(); 137 adapter.getProfileProxy(mContext, new BluetoothProfile.ServiceListener() { 138 @Override 139 public void onServiceConnected(int profile, BluetoothProfile proxy) { 140 if (profile == BluetoothProfile.HEADSET_CLIENT) { 141 mBluetoothHeadsetClient = (BluetoothHeadsetClient) proxy; 142 } 143 } 144 145 @Override 146 public void onServiceDisconnected(int profile) { 147 } 148 }, BluetoothProfile.HEADSET_CLIENT); 149 } 150 151 private final ServiceConnection mInCallServiceConnection = new ServiceConnection() { 152 153 @Override 154 public void onServiceConnected(ComponentName name, IBinder binder) { 155 if (Log.isLoggable(TAG, Log.DEBUG)) { 156 Log.d(TAG, "onServiceConnected: " + name + ", service: " + binder); 157 } 158 mInCallService = ((InCallServiceImpl.LocalBinder) binder).getService(); 159 mInCallService.registerCallback(mInCallServiceCallback); 160 161 // The InCallServiceImpl could be bound when we already have some active calls, let's 162 // notify UI about these calls. 163 for (Call telecomCall : mInCallService.getCalls()) { 164 UiCall uiCall = doTelecomCallAdded(telecomCall); 165 onStateChanged(uiCall, uiCall.getState()); 166 } 167 } 168 169 @Override 170 public void onServiceDisconnected(ComponentName name) { 171 if (Log.isLoggable(TAG, Log.DEBUG)) { 172 Log.d(TAG, "onServiceDisconnected: " + name); 173 } 174 mInCallService.unregisterCallback(mInCallServiceCallback); 175 } 176 177 private InCallServiceImpl.Callback mInCallServiceCallback = 178 new InCallServiceImpl.Callback() { 179 @Override 180 public void onTelecomCallAdded(Call telecomCall) { 181 doTelecomCallAdded(telecomCall); 182 } 183 184 @Override 185 public void onTelecomCallRemoved(Call telecomCall) { 186 doTelecomCallRemoved(telecomCall); 187 } 188 189 @Override 190 public void onCallAudioStateChanged(CallAudioState audioState) { 191 doCallAudioStateChanged(audioState); 192 } 193 }; 194 }; 195 196 /** 197 * Tears down the {@link UiCallManager}. Calling this function will null out the global 198 * accessible {@link UiCallManager} instance. Remember to re-initialize the 199 * {@link UiCallManager}. 200 */ 201 public void tearDown() { 202 if (mInCallService != null) { 203 mContext.unbindService(mInCallServiceConnection); 204 mInCallService = null; 205 } 206 mCallMapping.clear(); 207 // Clear out the mContext reference to avoid memory leak. 208 mContext = null; 209 sUiCallManager = null; 210 } 211 212 public void addListener(CallListener listener) { 213 if (Log.isLoggable(TAG, Log.DEBUG)) { 214 Log.d(TAG, "addListener: " + listener); 215 } 216 mCallListeners.add(listener); 217 } 218 219 public void removeListener(CallListener listener) { 220 if (Log.isLoggable(TAG, Log.DEBUG)) { 221 Log.d(TAG, "removeListener: " + listener); 222 } 223 mCallListeners.remove(listener); 224 } 225 226 protected void placeCall(String number) { 227 if (Log.isLoggable(TAG, Log.DEBUG)) { 228 Log.d(TAG, "placeCall: " + number); 229 } 230 Uri uri = Uri.fromParts("tel", number, null); 231 Log.d(TAG, "android.telecom.TelecomManager#placeCall: " + uri); 232 mTelecomManager.placeCall(uri, null); 233 } 234 235 public void answerCall(UiCall uiCall) { 236 if (Log.isLoggable(TAG, Log.DEBUG)) { 237 Log.d(TAG, "answerCall: " + uiCall); 238 } 239 240 Call telecomCall = mCallMapping.get(uiCall); 241 if (telecomCall != null) { 242 telecomCall.answer(0); 243 } 244 } 245 246 public void rejectCall(UiCall uiCall, boolean rejectWithMessage, String textMessage) { 247 if (Log.isLoggable(TAG, Log.DEBUG)) { 248 Log.d(TAG, "rejectCall: " + uiCall + ", rejectWithMessage: " + rejectWithMessage 249 + "textMessage: " + textMessage); 250 } 251 252 Call telecomCall = mCallMapping.get(uiCall); 253 if (telecomCall != null) { 254 telecomCall.reject(rejectWithMessage, textMessage); 255 } 256 } 257 258 public void disconnectCall(UiCall uiCall) { 259 if (Log.isLoggable(TAG, Log.DEBUG)) { 260 Log.d(TAG, "disconnectCall: " + uiCall); 261 } 262 263 Call telecomCall = mCallMapping.get(uiCall); 264 if (telecomCall != null) { 265 telecomCall.disconnect(); 266 } 267 } 268 269 public List<UiCall> getCalls() { 270 return new ArrayList<>(mCallMapping.keySet()); 271 } 272 273 public boolean getMuted() { 274 if (Log.isLoggable(TAG, Log.DEBUG)) { 275 Log.d(TAG, "getMuted"); 276 } 277 if (mInCallService == null) { 278 return false; 279 } 280 CallAudioState audioState = mInCallService.getCallAudioState(); 281 return audioState != null && audioState.isMuted(); 282 } 283 284 public void setMuted(boolean muted) { 285 if (Log.isLoggable(TAG, Log.DEBUG)) { 286 Log.d(TAG, "setMuted: " + muted); 287 } 288 if (mInCallService == null) { 289 return; 290 } 291 mInCallService.setMuted(muted); 292 } 293 294 public int getSupportedAudioRouteMask() { 295 if (Log.isLoggable(TAG, Log.DEBUG)) { 296 Log.d(TAG, "getSupportedAudioRouteMask"); 297 } 298 299 CallAudioState audioState = getCallAudioStateOrNull(); 300 return audioState != null ? audioState.getSupportedRouteMask() : 0; 301 } 302 303 public List<Integer> getSupportedAudioRoute() { 304 List<Integer> audioRouteList = new ArrayList<>(); 305 306 boolean isBluetoothPhoneCall = isBluetoothCall(); 307 if (isBluetoothPhoneCall) { 308 // if this is bluetooth phone call, we can only select audio route between vehicle 309 // and phone. 310 // Vehicle speaker route. 311 audioRouteList.add(CallAudioState.ROUTE_BLUETOOTH); 312 // Headset route. 313 audioRouteList.add(CallAudioState.ROUTE_EARPIECE); 314 } else { 315 // Most likely we are making phone call with on board SIM card. 316 int supportedAudioRouteMask = getSupportedAudioRouteMask(); 317 318 if ((supportedAudioRouteMask & CallAudioState.ROUTE_EARPIECE) != 0) { 319 audioRouteList.add(CallAudioState.ROUTE_EARPIECE); 320 } else if ((supportedAudioRouteMask & CallAudioState.ROUTE_BLUETOOTH) != 0) { 321 audioRouteList.add(CallAudioState.ROUTE_BLUETOOTH); 322 } else if ((supportedAudioRouteMask & CallAudioState.ROUTE_WIRED_HEADSET) != 0) { 323 audioRouteList.add(CallAudioState.ROUTE_WIRED_HEADSET); 324 } else if ((supportedAudioRouteMask & CallAudioState.ROUTE_SPEAKER) != 0) { 325 audioRouteList.add(CallAudioState.ROUTE_SPEAKER); 326 } 327 } 328 329 return audioRouteList; 330 } 331 332 public boolean isBluetoothCall() { 333 PhoneAccountHandle phoneAccountHandle = 334 mTelecomManager.getUserSelectedOutgoingPhoneAccount(); 335 if (phoneAccountHandle != null && phoneAccountHandle.getComponentName() != null) { 336 return HFP_CLIENT_CONNECTION_SERVICE_CLASS_NAME.equals( 337 phoneAccountHandle.getComponentName().getClassName()); 338 } else { 339 return false; 340 } 341 } 342 343 public int getAudioRoute() { 344 CallAudioState audioState = getCallAudioStateOrNull(); 345 int audioRoute = audioState != null ? audioState.getRoute() : 0; 346 if (Log.isLoggable(TAG, Log.DEBUG)) { 347 Log.d(TAG, "getAudioRoute " + audioRoute); 348 } 349 return audioRoute; 350 } 351 352 /** 353 * Re-route the audio out phone of the ongoing phone call. 354 */ 355 public void setAudioRoute(@CallAudioRoute int audioRoute) { 356 if (mBluetoothHeadsetClient != null && isBluetoothCall()) { 357 for (BluetoothDevice device : mBluetoothHeadsetClient.getConnectedDevices()) { 358 List<BluetoothHeadsetClientCall> currentCalls = 359 mBluetoothHeadsetClient.getCurrentCalls(device); 360 if (currentCalls != null && !currentCalls.isEmpty()) { 361 if (audioRoute == CallAudioState.ROUTE_BLUETOOTH) { 362 mBluetoothHeadsetClient.connectAudio(device); 363 } else if ((audioRoute & CallAudioState.ROUTE_WIRED_OR_EARPIECE) != 0) { 364 mBluetoothHeadsetClient.disconnectAudio(device); 365 } 366 } 367 } 368 } 369 // TODO: Implement routing audio if current call is not a bluetooth call. 370 } 371 372 public void holdCall(UiCall uiCall) { 373 if (Log.isLoggable(TAG, Log.DEBUG)) { 374 Log.d(TAG, "holdCall: " + uiCall); 375 } 376 377 Call telecomCall = mCallMapping.get(uiCall); 378 if (telecomCall != null) { 379 telecomCall.hold(); 380 } 381 } 382 383 public void unholdCall(UiCall uiCall) { 384 if (Log.isLoggable(TAG, Log.DEBUG)) { 385 Log.d(TAG, "unholdCall: " + uiCall); 386 } 387 388 Call telecomCall = mCallMapping.get(uiCall); 389 if (telecomCall != null) { 390 telecomCall.unhold(); 391 } 392 } 393 394 public void playDtmfTone(UiCall uiCall, char digit) { 395 if (Log.isLoggable(TAG, Log.DEBUG)) { 396 Log.d(TAG, "playDtmfTone: call: " + uiCall + ", digit: " + digit); 397 } 398 399 Call telecomCall = mCallMapping.get(uiCall); 400 if (telecomCall != null) { 401 telecomCall.playDtmfTone(digit); 402 } 403 } 404 405 public void stopDtmfTone(UiCall uiCall) { 406 if (Log.isLoggable(TAG, Log.DEBUG)) { 407 Log.d(TAG, "stopDtmfTone: call: " + uiCall); 408 } 409 410 Call telecomCall = mCallMapping.get(uiCall); 411 if (telecomCall != null) { 412 telecomCall.stopDtmfTone(); 413 } 414 } 415 416 public void postDialContinue(UiCall uiCall, boolean proceed) { 417 if (Log.isLoggable(TAG, Log.DEBUG)) { 418 Log.d(TAG, "postDialContinue: call: " + uiCall + ", proceed: " + proceed); 419 } 420 421 Call telecomCall = mCallMapping.get(uiCall); 422 if (telecomCall != null) { 423 telecomCall.postDialContinue(proceed); 424 } 425 } 426 427 public void conference(UiCall uiCall, UiCall otherUiCall) { 428 if (Log.isLoggable(TAG, Log.DEBUG)) { 429 Log.d(TAG, "conference: call: " + uiCall + ", otherCall: " + otherUiCall); 430 } 431 432 Call telecomCall = mCallMapping.get(uiCall); 433 Call otherTelecomCall = mCallMapping.get(otherUiCall); 434 if (telecomCall != null) { 435 telecomCall.conference(otherTelecomCall); 436 } 437 } 438 439 public void splitFromConference(UiCall uiCall) { 440 if (Log.isLoggable(TAG, Log.DEBUG)) { 441 Log.d(TAG, "splitFromConference: call: " + uiCall); 442 } 443 444 Call telecomCall = mCallMapping.get(uiCall); 445 if (telecomCall != null) { 446 telecomCall.splitFromConference(); 447 } 448 } 449 450 private UiCall doTelecomCallAdded(final Call telecomCall) { 451 Log.d(TAG, "doTelecomCallAdded: " + telecomCall); 452 453 UiCall uiCall = getOrCreateCallContainer(telecomCall); 454 telecomCall.registerCallback(new TelecomCallListener(this, uiCall)); 455 for (CallListener listener : mCallListeners) { 456 listener.onCallAdded(uiCall); 457 } 458 Log.d(TAG, "Call backs registered"); 459 460 if (telecomCall.getState() == Call.STATE_SELECT_PHONE_ACCOUNT) { 461 // TODO(b/26189994): need to show Phone Account picker to let user choose a phone 462 // account. It should be an account from TelecomManager#getCallCapablePhoneAccounts 463 // list. 464 Log.w(TAG, "Need to select phone account for the given call: " + telecomCall + ", " 465 + "but this feature is not implemented yet."); 466 telecomCall.disconnect(); 467 } 468 return uiCall; 469 } 470 471 private void doTelecomCallRemoved(Call telecomCall) { 472 UiCall uiCall = getOrCreateCallContainer(telecomCall); 473 474 mCallMapping.remove(uiCall); 475 476 for (CallListener listener : mCallListeners) { 477 listener.onCallRemoved(uiCall); 478 } 479 } 480 481 private void doCallAudioStateChanged(CallAudioState audioState) { 482 for (CallListener listener : mCallListeners) { 483 listener.onAudioStateChanged(audioState.isMuted(), audioState.getRoute(), 484 audioState.getSupportedRouteMask()); 485 } 486 } 487 488 private void onStateChanged(UiCall uiCall, int state) { 489 for (CallListener listener : mCallListeners) { 490 listener.onCallStateChanged(uiCall, state); 491 } 492 } 493 494 private void onCallUpdated(UiCall uiCall) { 495 for (CallListener listener : mCallListeners) { 496 listener.onCallUpdated(uiCall); 497 } 498 } 499 500 private UiCall getOrCreateCallContainer(Call telecomCall) { 501 for (Map.Entry<UiCall, Call> entry : mCallMapping.entrySet()) { 502 if (entry.getValue() == telecomCall) { 503 return entry.getKey(); 504 } 505 } 506 507 UiCall uiCall = new UiCall(nextCarPhoneCallId++); 508 updateCallContainerFromTelecom(uiCall, telecomCall); 509 mCallMapping.put(uiCall, telecomCall); 510 return uiCall; 511 } 512 513 private static void updateCallContainerFromTelecom(UiCall uiCall, Call telecomCall) { 514 if (Log.isLoggable(TAG, Log.DEBUG)) { 515 Log.d(TAG, "updateCallContainerFromTelecom: call: " + uiCall + ", telecomCall: " 516 + telecomCall); 517 } 518 519 uiCall.setState(telecomCall.getState()); 520 uiCall.setHasChildren(!telecomCall.getChildren().isEmpty()); 521 uiCall.setHasParent(telecomCall.getParent() != null); 522 523 Call.Details details = telecomCall.getDetails(); 524 if (details == null) { 525 return; 526 } 527 528 uiCall.setConnectTimeMillis(details.getConnectTimeMillis()); 529 530 DisconnectCause cause = details.getDisconnectCause(); 531 uiCall.setDisconnectCause(cause == null ? null : cause.getLabel()); 532 533 GatewayInfo gatewayInfo = details.getGatewayInfo(); 534 uiCall.setGatewayInfoOriginalAddress( 535 gatewayInfo == null ? null : gatewayInfo.getOriginalAddress()); 536 537 String number = ""; 538 if (gatewayInfo != null) { 539 number = gatewayInfo.getOriginalAddress().getSchemeSpecificPart(); 540 } else if (details.getHandle() != null) { 541 number = details.getHandle().getSchemeSpecificPart(); 542 } 543 uiCall.setNumber(number); 544 } 545 546 private CallAudioState getCallAudioStateOrNull() { 547 return mInCallService != null ? mInCallService.getCallAudioState() : null; 548 } 549 550 /** Returns a first call that matches at least one provided call state */ 551 public UiCall getCallWithState(int... callStates) { 552 if (Log.isLoggable(TAG, Log.DEBUG)) { 553 Log.d(TAG, "getCallWithState: " + callStates); 554 } 555 for (UiCall call : getCalls()) { 556 for (int callState : callStates) { 557 if (call.getState() == callState) { 558 return call; 559 } 560 } 561 } 562 return null; 563 } 564 565 public UiCall getPrimaryCall() { 566 if (Log.isLoggable(TAG, Log.DEBUG)) { 567 Log.d(TAG, "getPrimaryCall"); 568 } 569 List<UiCall> calls = getCalls(); 570 if (calls.isEmpty()) { 571 return null; 572 } 573 574 Collections.sort(calls, getCallComparator()); 575 UiCall uiCall = calls.get(0); 576 if (uiCall.hasParent()) { 577 return null; 578 } 579 return uiCall; 580 } 581 582 public UiCall getSecondaryCall() { 583 if (Log.isLoggable(TAG, Log.DEBUG)) { 584 Log.d(TAG, "getSecondaryCall"); 585 } 586 List<UiCall> calls = getCalls(); 587 if (calls.size() < 2) { 588 return null; 589 } 590 591 Collections.sort(calls, getCallComparator()); 592 UiCall uiCall = calls.get(1); 593 if (uiCall.hasParent()) { 594 return null; 595 } 596 return uiCall; 597 } 598 599 public static final int CAN_PLACE_CALL_RESULT_OK = 0; 600 public static final int CAN_PLACE_CALL_RESULT_NETWORK_UNAVAILABLE = 1; 601 public static final int CAN_PLACE_CALL_RESULT_HFP_UNAVAILABLE = 2; 602 public static final int CAN_PLACE_CALL_RESULT_AIRPLANE_MODE = 3; 603 604 public int getCanPlaceCallStatus(String number, boolean bluetoothRequired) { 605 // TODO(b/26191392): figure out the logic for projected and embedded modes 606 return CAN_PLACE_CALL_RESULT_OK; 607 } 608 609 public String getFailToPlaceCallMessage(int canPlaceCallResult) { 610 switch (canPlaceCallResult) { 611 case CAN_PLACE_CALL_RESULT_OK: 612 return ""; 613 case CAN_PLACE_CALL_RESULT_HFP_UNAVAILABLE: 614 return mContext.getString(R.string.error_no_hfp); 615 case CAN_PLACE_CALL_RESULT_AIRPLANE_MODE: 616 return mContext.getString(R.string.error_airplane_mode); 617 case CAN_PLACE_CALL_RESULT_NETWORK_UNAVAILABLE: 618 default: 619 return mContext.getString(R.string.error_network_not_available); 620 } 621 } 622 623 /** Places call only if there's no outgoing call right now */ 624 public void safePlaceCall(String number, boolean bluetoothRequired) { 625 if (Log.isLoggable(TAG, Log.DEBUG)) { 626 Log.d(TAG, "safePlaceCall: " + number); 627 } 628 629 int placeCallStatus = getCanPlaceCallStatus(number, bluetoothRequired); 630 if (placeCallStatus != CAN_PLACE_CALL_RESULT_OK) { 631 if (Log.isLoggable(TAG, Log.DEBUG)) { 632 Log.d(TAG, "Unable to place a call: " + placeCallStatus); 633 } 634 return; 635 } 636 637 UiCall outgoingCall = getCallWithState( 638 Call.STATE_CONNECTING, Call.STATE_NEW, Call.STATE_DIALING); 639 if (outgoingCall == null) { 640 long now = Calendar.getInstance().getTimeInMillis(); 641 if (now - mLastPlacedCallTimeMs > MIN_TIME_BETWEEN_CALLS_MS) { 642 placeCall(number); 643 mLastPlacedCallTimeMs = now; 644 } else { 645 if (Log.isLoggable(TAG, Log.INFO)) { 646 Log.i(TAG, "You have to wait " + MIN_TIME_BETWEEN_CALLS_MS 647 + "ms between making calls"); 648 } 649 } 650 } 651 } 652 653 public void callVoicemail() { 654 if (Log.isLoggable(TAG, Log.DEBUG)) { 655 Log.d(TAG, "callVoicemail"); 656 } 657 658 String voicemailNumber = TelecomUtils.getVoicemailNumber(mContext); 659 if (TextUtils.isEmpty(voicemailNumber)) { 660 Log.w(TAG, "Unable to get voicemail number."); 661 return; 662 } 663 safePlaceCall(voicemailNumber, false); 664 } 665 666 /** 667 * Returns the call types for the given number of items in the cursor. 668 * <p/> 669 * It uses the next {@code count} rows in the cursor to extract the types. 670 * <p/> 671 * Its position in the cursor is unchanged by this function. 672 */ 673 public int[] getCallTypes(Cursor cursor, int count) { 674 if (Log.isLoggable(TAG, Log.DEBUG)) { 675 Log.d(TAG, "getCallTypes: cursor: " + cursor + ", count: " + count); 676 } 677 678 int position = cursor.getPosition(); 679 int[] callTypes = new int[count]; 680 String voicemailNumber = mTelephonyManager.getVoiceMailNumber(); 681 int column; 682 for (int index = 0; index < count; ++index) { 683 column = cursor.getColumnIndex(CallLog.Calls.NUMBER); 684 String phoneNumber = cursor.getString(column); 685 if (phoneNumber != null && phoneNumber.equals(voicemailNumber)) { 686 callTypes[index] = PhoneLoader.VOICEMAIL_TYPE; 687 } else { 688 column = cursor.getColumnIndex(CallLog.Calls.TYPE); 689 callTypes[index] = cursor.getInt(column); 690 } 691 cursor.moveToNext(); 692 } 693 cursor.moveToPosition(position); 694 return callTypes; 695 } 696 697 private static Comparator<UiCall> getCallComparator() { 698 return new Comparator<UiCall>() { 699 @Override 700 public int compare(UiCall call, UiCall otherCall) { 701 if (call.hasParent() && !otherCall.hasParent()) { 702 return 1; 703 } else if (!call.hasParent() && otherCall.hasParent()) { 704 return -1; 705 } 706 int carCallRank = sCallStateRank.indexOf(call.getState()); 707 int otherCarCallRank = sCallStateRank.indexOf(otherCall.getState()); 708 709 return otherCarCallRank - carCallRank; 710 } 711 }; 712 } 713 714 private static class TelecomCallListener extends Call.Callback { 715 private final WeakReference<UiCallManager> mCarTelecomMangerRef; 716 private final WeakReference<UiCall> mCallContainerRef; 717 718 TelecomCallListener(UiCallManager carTelecomManager, UiCall uiCall) { 719 mCarTelecomMangerRef = new WeakReference<>(carTelecomManager); 720 mCallContainerRef = new WeakReference<>(uiCall); 721 } 722 723 @Override 724 public void onStateChanged(Call telecomCall, int state) { 725 if (Log.isLoggable(TAG, Log.DEBUG)) { 726 Log.d(TAG, "onStateChanged: " + state); 727 } 728 UiCallManager manager = mCarTelecomMangerRef.get(); 729 UiCall call = mCallContainerRef.get(); 730 if (manager != null && call != null) { 731 call.setState(state); 732 manager.onStateChanged(call, state); 733 } 734 } 735 736 @Override 737 public void onParentChanged(Call telecomCall, Call parent) { 738 doCallUpdated(telecomCall); 739 } 740 741 @Override 742 public void onCallDestroyed(Call telecomCall) { 743 if (Log.isLoggable(TAG, Log.DEBUG)) { 744 Log.d(TAG, "onCallDestroyed"); 745 } 746 } 747 748 @Override 749 public void onDetailsChanged(Call telecomCall, Call.Details details) { 750 doCallUpdated(telecomCall); 751 } 752 753 @Override 754 public void onVideoCallChanged(Call telecomCall, InCallService.VideoCall videoCall) { 755 doCallUpdated(telecomCall); 756 } 757 758 @Override 759 public void onCannedTextResponsesLoaded(Call telecomCall, 760 List<String> cannedTextResponses) { 761 doCallUpdated(telecomCall); 762 } 763 764 @Override 765 public void onChildrenChanged(Call telecomCall, List<Call> children) { 766 doCallUpdated(telecomCall); 767 } 768 769 private void doCallUpdated(Call telecomCall) { 770 UiCallManager manager = mCarTelecomMangerRef.get(); 771 UiCall uiCall = mCallContainerRef.get(); 772 if (manager != null && uiCall != null) { 773 updateCallContainerFromTelecom(uiCall, telecomCall); 774 manager.onCallUpdated(uiCall); 775 } 776 } 777 } 778} 779