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