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