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