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