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 /** 215 * A call that is waiting for {@link PhoneAccount} selection 216 */ 217 public Call getWaitingForAccountCall() { 218 return getFirstCallWithState(Call.State.PRE_DIAL_WAIT); 219 } 220 221 public Call getPendingOutgoingCall() { 222 return getFirstCallWithState(Call.State.CONNECTING); 223 } 224 225 public Call getOutgoingCall() { 226 Call call = getFirstCallWithState(Call.State.DIALING); 227 if (call == null) { 228 call = getFirstCallWithState(Call.State.REDIALING); 229 } 230 return call; 231 } 232 233 public Call getActiveCall() { 234 return getFirstCallWithState(Call.State.ACTIVE); 235 } 236 237 public Call getBackgroundCall() { 238 return getFirstCallWithState(Call.State.ONHOLD); 239 } 240 241 public Call getDisconnectedCall() { 242 return getFirstCallWithState(Call.State.DISCONNECTED); 243 } 244 245 public Call getDisconnectingCall() { 246 return getFirstCallWithState(Call.State.DISCONNECTING); 247 } 248 249 public Call getSecondBackgroundCall() { 250 return getCallWithState(Call.State.ONHOLD, 1); 251 } 252 253 public Call getActiveOrBackgroundCall() { 254 Call call = getActiveCall(); 255 if (call == null) { 256 call = getBackgroundCall(); 257 } 258 return call; 259 } 260 261 public Call getIncomingCall() { 262 Call call = getFirstCallWithState(Call.State.INCOMING); 263 if (call == null) { 264 call = getFirstCallWithState(Call.State.CALL_WAITING); 265 } 266 267 return call; 268 } 269 270 public Call getFirstCall() { 271 Call result = getIncomingCall(); 272 if (result == null) { 273 result = getPendingOutgoingCall(); 274 } 275 if (result == null) { 276 result = getOutgoingCall(); 277 } 278 if (result == null) { 279 result = getFirstCallWithState(Call.State.ACTIVE); 280 } 281 if (result == null) { 282 result = getDisconnectingCall(); 283 } 284 if (result == null) { 285 result = getDisconnectedCall(); 286 } 287 return result; 288 } 289 290 public boolean hasLiveCall() { 291 Call call = getFirstCall(); 292 if (call == null) { 293 return false; 294 } 295 return call != getDisconnectingCall() && call != getDisconnectedCall(); 296 } 297 298 /** 299 * Returns the first call found in the call map with the specified call modification state. 300 * @param state The session modification state to search for. 301 * @return The first call with the specified state. 302 */ 303 public Call getVideoUpgradeRequestCall() { 304 for(Call call : mCallById.values()) { 305 if (call.getSessionModificationState() == 306 Call.SessionModificationState.RECEIVED_UPGRADE_TO_VIDEO_REQUEST) { 307 return call; 308 } 309 } 310 return null; 311 } 312 313 public Call getCallById(String callId) { 314 return mCallById.get(callId); 315 } 316 317 public Call getCallByTelecommCall(android.telecom.Call telecommCall) { 318 return mCallByTelecommCall.get(telecommCall); 319 } 320 321 public List<String> getTextResponses(String callId) { 322 return mCallTextReponsesMap.get(callId); 323 } 324 325 /** 326 * Returns first call found in the call map with the specified state. 327 */ 328 public Call getFirstCallWithState(int state) { 329 return getCallWithState(state, 0); 330 } 331 332 /** 333 * Returns the [position]th call found in the call map with the specified state. 334 * TODO: Improve this logic to sort by call time. 335 */ 336 public Call getCallWithState(int state, int positionToFind) { 337 Call retval = null; 338 int position = 0; 339 for (Call call : mCallById.values()) { 340 if (call.getState() == state) { 341 if (position >= positionToFind) { 342 retval = call; 343 break; 344 } else { 345 position++; 346 } 347 } 348 } 349 350 return retval; 351 } 352 353 /** 354 * This is called when the service disconnects, either expectedly or unexpectedly. 355 * For the expected case, it's because we have no calls left. For the unexpected case, 356 * it is likely a crash of phone and we need to clean up our calls manually. Without phone, 357 * there can be no active calls, so this is relatively safe thing to do. 358 */ 359 public void clearOnDisconnect() { 360 for (Call call : mCallById.values()) { 361 final int state = call.getState(); 362 if (state != Call.State.IDLE && 363 state != Call.State.INVALID && 364 state != Call.State.DISCONNECTED) { 365 366 call.setState(Call.State.DISCONNECTED); 367 call.setDisconnectCause(new DisconnectCause(DisconnectCause.UNKNOWN)); 368 updateCallInMap(call); 369 } 370 } 371 notifyGenericListeners(); 372 } 373 374 /** 375 * Processes an update for a single call. 376 * 377 * @param call The call to update. 378 */ 379 private void onUpdateCall(Call call) { 380 Log.d(this, "\t" + call); 381 if (updateCallInMap(call)) { 382 Log.i(this, "onUpdate - " + call); 383 } 384 updateCallTextMap(call, call.getCannedSmsResponses()); 385 notifyCallUpdateListeners(call); 386 } 387 388 /** 389 * Sends a generic notification to all listeners that something has changed. 390 * It is up to the listeners to call back to determine what changed. 391 */ 392 private void notifyGenericListeners() { 393 for (Listener listener : mListeners) { 394 listener.onCallListChange(this); 395 } 396 } 397 398 private void notifyListenersOfDisconnect(Call call) { 399 for (Listener listener : mListeners) { 400 listener.onDisconnect(call); 401 } 402 } 403 404 /** 405 * Updates the call entry in the local map. 406 * @return false if no call previously existed and no call was added, otherwise true. 407 */ 408 private boolean updateCallInMap(Call call) { 409 Preconditions.checkNotNull(call); 410 411 boolean updated = false; 412 413 if (call.getState() == Call.State.DISCONNECTED) { 414 // update existing (but do not add!!) disconnected calls 415 if (mCallById.containsKey(call.getId())) { 416 417 // For disconnected calls, we want to keep them alive for a few seconds so that the 418 // UI has a chance to display anything it needs when a call is disconnected. 419 420 // Set up a timer to destroy the call after X seconds. 421 final Message msg = mHandler.obtainMessage(EVENT_DISCONNECTED_TIMEOUT, call); 422 mHandler.sendMessageDelayed(msg, getDelayForDisconnect(call)); 423 424 mCallById.put(call.getId(), call); 425 mCallByTelecommCall.put(call.getTelecommCall(), call); 426 updated = true; 427 } 428 } else if (!isCallDead(call)) { 429 mCallById.put(call.getId(), call); 430 mCallByTelecommCall.put(call.getTelecommCall(), call); 431 updated = true; 432 } else if (mCallById.containsKey(call.getId())) { 433 mCallById.remove(call.getId()); 434 mCallByTelecommCall.remove(call.getTelecommCall()); 435 updated = true; 436 } 437 438 return updated; 439 } 440 441 private int getDelayForDisconnect(Call call) { 442 Preconditions.checkState(call.getState() == Call.State.DISCONNECTED); 443 444 445 final int cause = call.getDisconnectCause().getCode(); 446 final int delay; 447 switch (cause) { 448 case DisconnectCause.LOCAL: 449 delay = DISCONNECTED_CALL_SHORT_TIMEOUT_MS; 450 break; 451 case DisconnectCause.REMOTE: 452 delay = DISCONNECTED_CALL_MEDIUM_TIMEOUT_MS; 453 break; 454 case DisconnectCause.REJECTED: 455 case DisconnectCause.MISSED: 456 case DisconnectCause.CANCELED: 457 // no delay for missed/rejected incoming calls and canceled outgoing calls. 458 delay = 0; 459 break; 460 default: 461 delay = DISCONNECTED_CALL_LONG_TIMEOUT_MS; 462 break; 463 } 464 465 return delay; 466 } 467 468 private void updateCallTextMap(Call call, List<String> textResponses) { 469 Preconditions.checkNotNull(call); 470 471 if (!isCallDead(call)) { 472 if (textResponses != null) { 473 mCallTextReponsesMap.put(call.getId(), textResponses); 474 } 475 } else if (mCallById.containsKey(call.getId())) { 476 mCallTextReponsesMap.remove(call.getId()); 477 } 478 } 479 480 private boolean isCallDead(Call call) { 481 final int state = call.getState(); 482 return Call.State.IDLE == state || Call.State.INVALID == state; 483 } 484 485 /** 486 * Sets up a call for deletion and notifies listeners of change. 487 */ 488 private void finishDisconnectedCall(Call call) { 489 call.setState(Call.State.IDLE); 490 updateCallInMap(call); 491 notifyGenericListeners(); 492 } 493 494 /** 495 * Notifies all video calls of a change in device orientation. 496 * 497 * @param rotation The new rotation angle (in degrees). 498 */ 499 public void notifyCallsOfDeviceRotation(int rotation) { 500 for (Call call : mCallById.values()) { 501 if (call.getVideoCall() != null) { 502 call.getVideoCall().setDeviceOrientation(rotation); 503 } 504 } 505 } 506 507 /** 508 * Handles the timeout for destroying disconnected calls. 509 */ 510 private Handler mHandler = new Handler() { 511 @Override 512 public void handleMessage(Message msg) { 513 switch (msg.what) { 514 case EVENT_DISCONNECTED_TIMEOUT: 515 Log.d(this, "EVENT_DISCONNECTED_TIMEOUT ", msg.obj); 516 finishDisconnectedCall((Call) msg.obj); 517 break; 518 default: 519 Log.wtf(this, "Message not expected: " + msg.what); 520 break; 521 } 522 } 523 }; 524 525 /** 526 * Listener interface for any class that wants to be notified of changes 527 * to the call list. 528 */ 529 public interface Listener { 530 /** 531 * Called when a new incoming call comes in. 532 * This is the only method that gets called for incoming calls. Listeners 533 * that want to perform an action on incoming call should respond in this method 534 * because {@link #onCallListChange} does not automatically get called for 535 * incoming calls. 536 */ 537 public void onIncomingCall(Call call); 538 539 /** 540 * Called anytime there are changes to the call list. The change can be switching call 541 * states, updating information, etc. This method will NOT be called for new incoming 542 * calls and for calls that switch to disconnected state. Listeners must add actions 543 * to those method implementations if they want to deal with those actions. 544 */ 545 public void onCallListChange(CallList callList); 546 547 /** 548 * Called when a call switches to the disconnected state. This is the only method 549 * that will get called upon disconnection. 550 */ 551 public void onDisconnect(Call call); 552 } 553 554 public interface CallUpdateListener { 555 // TODO: refactor and limit arg to be call state. Caller info is not needed. 556 public void onCallChanged(Call call); 557 } 558 } 559