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