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