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