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