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