1 /* 2 * Copyright (C) 2013 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.incallui; 18 19 import android.os.Handler; 20 import android.os.Message; 21 import android.os.Trace; 22 import android.telecom.DisconnectCause; 23 import android.telecom.PhoneAccount; 24 25 import com.android.contacts.common.testing.NeededForTesting; 26 import com.google.common.base.Preconditions; 27 import com.google.common.collect.Maps; 28 29 import java.util.Collections; 30 import java.util.HashMap; 31 import java.util.Iterator; 32 import java.util.List; 33 import java.util.Set; 34 import java.util.concurrent.ConcurrentHashMap; 35 import java.util.concurrent.CopyOnWriteArrayList; 36 37 /** 38 * Maintains the list of active calls and notifies interested classes of changes to the call list 39 * as they are received from the telephony stack. Primary listener of changes to this class is 40 * InCallPresenter. 41 */ 42 public class CallList { 43 44 private static final int DISCONNECTED_CALL_SHORT_TIMEOUT_MS = 200; 45 private static final int DISCONNECTED_CALL_MEDIUM_TIMEOUT_MS = 2000; 46 private static final int DISCONNECTED_CALL_LONG_TIMEOUT_MS = 5000; 47 48 private static final int EVENT_DISCONNECTED_TIMEOUT = 1; 49 50 private static CallList sInstance = new CallList(); 51 52 private final HashMap<String, Call> mCallById = new HashMap<>(); 53 private final HashMap<android.telecom.Call, Call> mCallByTelecommCall = new HashMap<>(); 54 private final HashMap<String, List<String>> mCallTextReponsesMap = Maps.newHashMap(); 55 /** 56 * ConcurrentHashMap constructor params: 8 is initial table size, 0.9f is 57 * load factor before resizing, 1 means we only expect a single thread to 58 * access the map so make only a single shard 59 */ 60 private final Set<Listener> mListeners = Collections.newSetFromMap( 61 new ConcurrentHashMap<Listener, Boolean>(8, 0.9f, 1)); 62 private final HashMap<String, List<CallUpdateListener>> mCallUpdateListenerMap = Maps 63 .newHashMap(); 64 private final Set<Call> mPendingDisconnectCalls = Collections.newSetFromMap( 65 new ConcurrentHashMap<Call, Boolean>(8, 0.9f, 1)); 66 67 /** 68 * Static singleton accessor method. 69 */ getInstance()70 public static CallList getInstance() { 71 return sInstance; 72 } 73 74 /** 75 * USED ONLY FOR TESTING 76 * Testing-only constructor. Instance should only be acquired through getInstance(). 77 */ 78 @NeededForTesting CallList()79 CallList() { 80 } 81 onCallAdded(android.telecom.Call telecommCall)82 public void onCallAdded(android.telecom.Call telecommCall) { 83 Trace.beginSection("onCallAdded"); 84 Call call = new Call(telecommCall); 85 Log.d(this, "onCallAdded: callState=" + call.getState()); 86 if (call.getState() == Call.State.INCOMING || 87 call.getState() == Call.State.CALL_WAITING) { 88 onIncoming(call, call.getCannedSmsResponses()); 89 } else { 90 onUpdate(call); 91 } 92 Trace.endSection(); 93 } 94 onCallRemoved(android.telecom.Call telecommCall)95 public void onCallRemoved(android.telecom.Call telecommCall) { 96 if (mCallByTelecommCall.containsKey(telecommCall)) { 97 Call call = mCallByTelecommCall.get(telecommCall); 98 if (updateCallInMap(call)) { 99 Log.w(this, "Removing call not previously disconnected " + call.getId()); 100 } 101 updateCallTextMap(call, null); 102 } 103 } 104 105 /** 106 * Called when a single call disconnects. 107 */ onDisconnect(Call call)108 public void onDisconnect(Call call) { 109 if (updateCallInMap(call)) { 110 Log.i(this, "onDisconnect: " + call); 111 // notify those listening for changes on this specific change 112 notifyCallUpdateListeners(call); 113 // notify those listening for all disconnects 114 notifyListenersOfDisconnect(call); 115 } 116 } 117 118 /** 119 * Called when a single call has changed. 120 */ onIncoming(Call call, List<String> textMessages)121 public void onIncoming(Call call, List<String> textMessages) { 122 if (updateCallInMap(call)) { 123 Log.i(this, "onIncoming - " + call); 124 } 125 updateCallTextMap(call, textMessages); 126 127 for (Listener listener : mListeners) { 128 listener.onIncomingCall(call); 129 } 130 } 131 onUpgradeToVideo(Call call)132 public void onUpgradeToVideo(Call call){ 133 Log.d(this, "onUpgradeToVideo call=" + call); 134 for (Listener listener : mListeners) { 135 listener.onUpgradeToVideo(call); 136 } 137 } 138 /** 139 * Called when a single call has changed. 140 */ onUpdate(Call call)141 public void onUpdate(Call call) { 142 Trace.beginSection("onUpdate"); 143 onUpdateCall(call); 144 notifyGenericListeners(); 145 Trace.endSection(); 146 } 147 148 /** 149 * Called when a single call has changed session modification state. 150 * 151 * @param call The call. 152 * @param sessionModificationState The new session modification state. 153 */ onSessionModificationStateChange(Call call, int sessionModificationState)154 public void onSessionModificationStateChange(Call call, int sessionModificationState) { 155 final List<CallUpdateListener> listeners = mCallUpdateListenerMap.get(call.getId()); 156 if (listeners != null) { 157 for (CallUpdateListener listener : listeners) { 158 listener.onSessionModificationStateChange(sessionModificationState); 159 } 160 } 161 } 162 163 /** 164 * Called when the last forwarded number changes for a call. With IMS, the last forwarded 165 * number changes due to a supplemental service notification, so it is not pressent at the 166 * start of the call. 167 * 168 * @param call The call. 169 */ onLastForwardedNumberChange(Call call)170 public void onLastForwardedNumberChange(Call call) { 171 final List<CallUpdateListener> listeners = mCallUpdateListenerMap.get(call.getId()); 172 if (listeners != null) { 173 for (CallUpdateListener listener : listeners) { 174 listener.onLastForwardedNumberChange(); 175 } 176 } 177 } 178 179 /** 180 * Called when the child number changes for a call. The child number can be received after a 181 * call is initially set up, so we need to be able to inform listeners of the change. 182 * 183 * @param call The call. 184 */ onChildNumberChange(Call call)185 public void onChildNumberChange(Call call) { 186 final List<CallUpdateListener> listeners = mCallUpdateListenerMap.get(call.getId()); 187 if (listeners != null) { 188 for (CallUpdateListener listener : listeners) { 189 listener.onChildNumberChange(); 190 } 191 } 192 } 193 notifyCallUpdateListeners(Call call)194 public void notifyCallUpdateListeners(Call call) { 195 final List<CallUpdateListener> listeners = mCallUpdateListenerMap.get(call.getId()); 196 if (listeners != null) { 197 for (CallUpdateListener listener : listeners) { 198 listener.onCallChanged(call); 199 } 200 } 201 } 202 203 /** 204 * Add a call update listener for a call id. 205 * 206 * @param callId The call id to get updates for. 207 * @param listener The listener to add. 208 */ addCallUpdateListener(String callId, CallUpdateListener listener)209 public void addCallUpdateListener(String callId, CallUpdateListener listener) { 210 List<CallUpdateListener> listeners = mCallUpdateListenerMap.get(callId); 211 if (listeners == null) { 212 listeners = new CopyOnWriteArrayList<CallUpdateListener>(); 213 mCallUpdateListenerMap.put(callId, listeners); 214 } 215 listeners.add(listener); 216 } 217 218 /** 219 * Remove a call update listener for a call id. 220 * 221 * @param callId The call id to remove the listener for. 222 * @param listener The listener to remove. 223 */ removeCallUpdateListener(String callId, CallUpdateListener listener)224 public void removeCallUpdateListener(String callId, CallUpdateListener listener) { 225 List<CallUpdateListener> listeners = mCallUpdateListenerMap.get(callId); 226 if (listeners != null) { 227 listeners.remove(listener); 228 } 229 } 230 addListener(Listener listener)231 public void addListener(Listener listener) { 232 Preconditions.checkNotNull(listener); 233 234 mListeners.add(listener); 235 236 // Let the listener know about the active calls immediately. 237 listener.onCallListChange(this); 238 } 239 removeListener(Listener listener)240 public void removeListener(Listener listener) { 241 if (listener != null) { 242 mListeners.remove(listener); 243 } 244 } 245 246 /** 247 * TODO: Change so that this function is not needed. Instead of assuming there is an active 248 * call, the code should rely on the status of a specific Call and allow the presenters to 249 * update the Call object when the active call changes. 250 */ getIncomingOrActive()251 public Call getIncomingOrActive() { 252 Call retval = getIncomingCall(); 253 if (retval == null) { 254 retval = getActiveCall(); 255 } 256 return retval; 257 } 258 getOutgoingOrActive()259 public Call getOutgoingOrActive() { 260 Call retval = getOutgoingCall(); 261 if (retval == null) { 262 retval = getActiveCall(); 263 } 264 return retval; 265 } 266 267 /** 268 * A call that is waiting for {@link PhoneAccount} selection 269 */ getWaitingForAccountCall()270 public Call getWaitingForAccountCall() { 271 return getFirstCallWithState(Call.State.SELECT_PHONE_ACCOUNT); 272 } 273 getPendingOutgoingCall()274 public Call getPendingOutgoingCall() { 275 return getFirstCallWithState(Call.State.CONNECTING); 276 } 277 getOutgoingCall()278 public Call getOutgoingCall() { 279 Call call = getFirstCallWithState(Call.State.DIALING); 280 if (call == null) { 281 call = getFirstCallWithState(Call.State.REDIALING); 282 } 283 return call; 284 } 285 getActiveCall()286 public Call getActiveCall() { 287 return getFirstCallWithState(Call.State.ACTIVE); 288 } 289 getBackgroundCall()290 public Call getBackgroundCall() { 291 return getFirstCallWithState(Call.State.ONHOLD); 292 } 293 getDisconnectedCall()294 public Call getDisconnectedCall() { 295 return getFirstCallWithState(Call.State.DISCONNECTED); 296 } 297 getDisconnectingCall()298 public Call getDisconnectingCall() { 299 return getFirstCallWithState(Call.State.DISCONNECTING); 300 } 301 getSecondBackgroundCall()302 public Call getSecondBackgroundCall() { 303 return getCallWithState(Call.State.ONHOLD, 1); 304 } 305 getActiveOrBackgroundCall()306 public Call getActiveOrBackgroundCall() { 307 Call call = getActiveCall(); 308 if (call == null) { 309 call = getBackgroundCall(); 310 } 311 return call; 312 } 313 getIncomingCall()314 public Call getIncomingCall() { 315 Call call = getFirstCallWithState(Call.State.INCOMING); 316 if (call == null) { 317 call = getFirstCallWithState(Call.State.CALL_WAITING); 318 } 319 320 return call; 321 } 322 getFirstCall()323 public Call getFirstCall() { 324 Call result = getIncomingCall(); 325 if (result == null) { 326 result = getPendingOutgoingCall(); 327 } 328 if (result == null) { 329 result = getOutgoingCall(); 330 } 331 if (result == null) { 332 result = getFirstCallWithState(Call.State.ACTIVE); 333 } 334 if (result == null) { 335 result = getDisconnectingCall(); 336 } 337 if (result == null) { 338 result = getDisconnectedCall(); 339 } 340 return result; 341 } 342 hasLiveCall()343 public boolean hasLiveCall() { 344 Call call = getFirstCall(); 345 if (call == null) { 346 return false; 347 } 348 return call != getDisconnectingCall() && call != getDisconnectedCall(); 349 } 350 351 /** 352 * Returns the first call found in the call map with the specified call modification state. 353 * @param state The session modification state to search for. 354 * @return The first call with the specified state. 355 */ getVideoUpgradeRequestCall()356 public Call getVideoUpgradeRequestCall() { 357 for(Call call : mCallById.values()) { 358 if (call.getSessionModificationState() == 359 Call.SessionModificationState.RECEIVED_UPGRADE_TO_VIDEO_REQUEST) { 360 return call; 361 } 362 } 363 return null; 364 } 365 getCallById(String callId)366 public Call getCallById(String callId) { 367 return mCallById.get(callId); 368 } 369 getCallByTelecommCall(android.telecom.Call telecommCall)370 public Call getCallByTelecommCall(android.telecom.Call telecommCall) { 371 return mCallByTelecommCall.get(telecommCall); 372 } 373 getTextResponses(String callId)374 public List<String> getTextResponses(String callId) { 375 return mCallTextReponsesMap.get(callId); 376 } 377 378 /** 379 * Returns first call found in the call map with the specified state. 380 */ getFirstCallWithState(int state)381 public Call getFirstCallWithState(int state) { 382 return getCallWithState(state, 0); 383 } 384 385 /** 386 * Returns the [position]th call found in the call map with the specified state. 387 * TODO: Improve this logic to sort by call time. 388 */ getCallWithState(int state, int positionToFind)389 public Call getCallWithState(int state, int positionToFind) { 390 Call retval = null; 391 int position = 0; 392 for (Call call : mCallById.values()) { 393 if (call.getState() == state) { 394 if (position >= positionToFind) { 395 retval = call; 396 break; 397 } else { 398 position++; 399 } 400 } 401 } 402 403 return retval; 404 } 405 406 /** 407 * This is called when the service disconnects, either expectedly or unexpectedly. 408 * For the expected case, it's because we have no calls left. For the unexpected case, 409 * it is likely a crash of phone and we need to clean up our calls manually. Without phone, 410 * there can be no active calls, so this is relatively safe thing to do. 411 */ clearOnDisconnect()412 public void clearOnDisconnect() { 413 for (Call call : mCallById.values()) { 414 final int state = call.getState(); 415 if (state != Call.State.IDLE && 416 state != Call.State.INVALID && 417 state != Call.State.DISCONNECTED) { 418 419 call.setState(Call.State.DISCONNECTED); 420 call.setDisconnectCause(new DisconnectCause(DisconnectCause.UNKNOWN)); 421 updateCallInMap(call); 422 } 423 } 424 notifyGenericListeners(); 425 } 426 427 /** 428 * Called when the user has dismissed an error dialog. This indicates acknowledgement of 429 * the disconnect cause, and that any pending disconnects should immediately occur. 430 */ onErrorDialogDismissed()431 public void onErrorDialogDismissed() { 432 final Iterator<Call> iterator = mPendingDisconnectCalls.iterator(); 433 while (iterator.hasNext()) { 434 Call call = iterator.next(); 435 iterator.remove(); 436 finishDisconnectedCall(call); 437 } 438 } 439 440 /** 441 * Processes an update for a single call. 442 * 443 * @param call The call to update. 444 */ onUpdateCall(Call call)445 private void onUpdateCall(Call call) { 446 Log.d(this, "\t" + call); 447 if (updateCallInMap(call)) { 448 Log.i(this, "onUpdate - " + call); 449 } 450 updateCallTextMap(call, call.getCannedSmsResponses()); 451 notifyCallUpdateListeners(call); 452 } 453 454 /** 455 * Sends a generic notification to all listeners that something has changed. 456 * It is up to the listeners to call back to determine what changed. 457 */ notifyGenericListeners()458 private void notifyGenericListeners() { 459 for (Listener listener : mListeners) { 460 listener.onCallListChange(this); 461 } 462 } 463 notifyListenersOfDisconnect(Call call)464 private void notifyListenersOfDisconnect(Call call) { 465 for (Listener listener : mListeners) { 466 listener.onDisconnect(call); 467 } 468 } 469 470 /** 471 * Updates the call entry in the local map. 472 * @return false if no call previously existed and no call was added, otherwise true. 473 */ updateCallInMap(Call call)474 private boolean updateCallInMap(Call call) { 475 Preconditions.checkNotNull(call); 476 477 boolean updated = false; 478 479 if (call.getState() == Call.State.DISCONNECTED) { 480 // update existing (but do not add!!) disconnected calls 481 if (mCallById.containsKey(call.getId())) { 482 // For disconnected calls, we want to keep them alive for a few seconds so that the 483 // UI has a chance to display anything it needs when a call is disconnected. 484 485 // Set up a timer to destroy the call after X seconds. 486 final Message msg = mHandler.obtainMessage(EVENT_DISCONNECTED_TIMEOUT, call); 487 mHandler.sendMessageDelayed(msg, getDelayForDisconnect(call)); 488 mPendingDisconnectCalls.add(call); 489 490 mCallById.put(call.getId(), call); 491 mCallByTelecommCall.put(call.getTelecommCall(), call); 492 updated = true; 493 } 494 } else if (!isCallDead(call)) { 495 mCallById.put(call.getId(), call); 496 mCallByTelecommCall.put(call.getTelecommCall(), call); 497 updated = true; 498 } else if (mCallById.containsKey(call.getId())) { 499 mCallById.remove(call.getId()); 500 mCallByTelecommCall.remove(call.getTelecommCall()); 501 updated = true; 502 } 503 504 return updated; 505 } 506 getDelayForDisconnect(Call call)507 private int getDelayForDisconnect(Call call) { 508 Preconditions.checkState(call.getState() == Call.State.DISCONNECTED); 509 510 511 final int cause = call.getDisconnectCause().getCode(); 512 final int delay; 513 switch (cause) { 514 case DisconnectCause.LOCAL: 515 delay = DISCONNECTED_CALL_SHORT_TIMEOUT_MS; 516 break; 517 case DisconnectCause.REMOTE: 518 case DisconnectCause.ERROR: 519 delay = DISCONNECTED_CALL_MEDIUM_TIMEOUT_MS; 520 break; 521 case DisconnectCause.REJECTED: 522 case DisconnectCause.MISSED: 523 case DisconnectCause.CANCELED: 524 // no delay for missed/rejected incoming calls and canceled outgoing calls. 525 delay = 0; 526 break; 527 default: 528 delay = DISCONNECTED_CALL_LONG_TIMEOUT_MS; 529 break; 530 } 531 532 return delay; 533 } 534 updateCallTextMap(Call call, List<String> textResponses)535 private void updateCallTextMap(Call call, List<String> textResponses) { 536 Preconditions.checkNotNull(call); 537 538 if (!isCallDead(call)) { 539 if (textResponses != null) { 540 mCallTextReponsesMap.put(call.getId(), textResponses); 541 } 542 } else if (mCallById.containsKey(call.getId())) { 543 mCallTextReponsesMap.remove(call.getId()); 544 } 545 } 546 isCallDead(Call call)547 private boolean isCallDead(Call call) { 548 final int state = call.getState(); 549 return Call.State.IDLE == state || Call.State.INVALID == state; 550 } 551 552 /** 553 * Sets up a call for deletion and notifies listeners of change. 554 */ finishDisconnectedCall(Call call)555 private void finishDisconnectedCall(Call call) { 556 if (mPendingDisconnectCalls.contains(call)) { 557 mPendingDisconnectCalls.remove(call); 558 } 559 call.setState(Call.State.IDLE); 560 updateCallInMap(call); 561 notifyGenericListeners(); 562 } 563 564 /** 565 * Notifies all video calls of a change in device orientation. 566 * 567 * @param rotation The new rotation angle (in degrees). 568 */ notifyCallsOfDeviceRotation(int rotation)569 public void notifyCallsOfDeviceRotation(int rotation) { 570 for (Call call : mCallById.values()) { 571 // First, ensure a VideoCall is set on the call so that the change can be sent to the 572 // provider (a VideoCall can be present for a call that does not currently have video, 573 // but can be upgraded to video). 574 // Second, ensure that the call videoState has video enabled (there is no need to set 575 // device orientation on a voice call which has not yet been upgraded to video). 576 if (call.getVideoCall() != null && CallUtils.isVideoCall(call)) { 577 call.getVideoCall().setDeviceOrientation(rotation); 578 } 579 } 580 } 581 582 /** 583 * Handles the timeout for destroying disconnected calls. 584 */ 585 private Handler mHandler = new Handler() { 586 @Override 587 public void handleMessage(Message msg) { 588 switch (msg.what) { 589 case EVENT_DISCONNECTED_TIMEOUT: 590 Log.d(this, "EVENT_DISCONNECTED_TIMEOUT ", msg.obj); 591 finishDisconnectedCall((Call) msg.obj); 592 break; 593 default: 594 Log.wtf(this, "Message not expected: " + msg.what); 595 break; 596 } 597 } 598 }; 599 600 /** 601 * Listener interface for any class that wants to be notified of changes 602 * to the call list. 603 */ 604 public interface Listener { 605 /** 606 * Called when a new incoming call comes in. 607 * This is the only method that gets called for incoming calls. Listeners 608 * that want to perform an action on incoming call should respond in this method 609 * because {@link #onCallListChange} does not automatically get called for 610 * incoming calls. 611 */ onIncomingCall(Call call)612 public void onIncomingCall(Call call); 613 /** 614 * Called when a new modify call request comes in 615 * This is the only method that gets called for modify requests. 616 */ onUpgradeToVideo(Call call)617 public void onUpgradeToVideo(Call call); 618 /** 619 * Called anytime there are changes to the call list. The change can be switching call 620 * states, updating information, etc. This method will NOT be called for new incoming 621 * calls and for calls that switch to disconnected state. Listeners must add actions 622 * to those method implementations if they want to deal with those actions. 623 */ onCallListChange(CallList callList)624 public void onCallListChange(CallList callList); 625 626 /** 627 * Called when a call switches to the disconnected state. This is the only method 628 * that will get called upon disconnection. 629 */ onDisconnect(Call call)630 public void onDisconnect(Call call); 631 632 633 } 634 635 public interface CallUpdateListener { 636 // TODO: refactor and limit arg to be call state. Caller info is not needed. onCallChanged(Call call)637 public void onCallChanged(Call call); 638 639 /** 640 * Notifies of a change to the session modification state for a call. 641 * 642 * @param sessionModificationState The new session modification state. 643 */ onSessionModificationStateChange(int sessionModificationState)644 public void onSessionModificationStateChange(int sessionModificationState); 645 646 /** 647 * Notifies of a change to the last forwarded number for a call. 648 */ onLastForwardedNumberChange()649 public void onLastForwardedNumberChange(); 650 651 /** 652 * Notifies of a change to the child number for a call. 653 */ onChildNumberChange()654 public void onChildNumberChange(); 655 } 656 } 657