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