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