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