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