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