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