CallList.java revision 42373eb59cbef15ec61ebb5c919031f293291a53
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.android.collect.Lists; 20import com.google.android.collect.Maps; 21import com.google.android.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; 28 29import java.util.ArrayList; 30import java.util.HashMap; 31import java.util.List; 32import 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 */ 39public class CallList { 40 41 private static final int DISCONNECTED_CALL_TIMEOUT_MS = 2000; 42 43 private static final int EVENT_DISCONNECTED_TIMEOUT = 1; 44 45 private static CallList sInstance = new CallList(); 46 47 private final HashMap<Integer, Call> mCallMap = Maps.newHashMap(); 48 private final HashMap<Integer, ArrayList<String>> mCallTextReponsesMap = 49 Maps.newHashMap(); 50 private final Set<Listener> mListeners = Sets.newArraySet(); 51 private final HashMap<Integer, List<CallUpdateListener>> mCallUpdateListenerMap = Maps 52 .newHashMap(); 53 54 55 /** 56 * Static singleton accessor method. 57 */ 58 public static CallList getInstance() { 59 return sInstance; 60 } 61 62 /** 63 * Private constructor. Instance should only be acquired through getInstance(). 64 */ 65 private CallList() { 66 } 67 68 /** 69 * Called when a single call has changed. 70 */ 71 public void onUpdate(Call call) { 72 Log.d(this, "onUpdate - ", call); 73 74 updateCallInMap(call); 75 notifyListenersOfChange(); 76 } 77 78 /** 79 * Called when a single call disconnects. 80 */ 81 public void onDisconnect(Call call) { 82 Log.d(this, "onDisconnect: ", call); 83 84 updateCallInMap(call); 85 86 notifyListenersOfChange(); 87 } 88 89 /** 90 * Called when a single call has changed. 91 */ 92 public void onIncoming(Call call, List<String> textMessages) { 93 Log.d(this, "onIncoming - " + call); 94 95 updateCallInMap(call); 96 updateCallTextMap(call, textMessages); 97 98 for (Listener listener : mListeners) { 99 listener.onIncomingCall(call); 100 } 101 } 102 103 /** 104 * Called when multiple calls have changed. 105 */ 106 public void onUpdate(List<Call> callsToUpdate) { 107 Log.d(this, "onUpdate(...)"); 108 109 Preconditions.checkNotNull(callsToUpdate); 110 for (Call call : callsToUpdate) { 111 Log.d(this, "\t" + call); 112 113 updateCallInMap(call); 114 updateCallTextMap(call, null); 115 116 notifyCallUpdateListeners(call); 117 } 118 119 notifyListenersOfChange(); 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(int 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(int 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 return getFirstCallWithState(Call.State.DIALING); 188 } 189 190 public Call getActiveCall() { 191 return getFirstCallWithState(Call.State.ACTIVE); 192 } 193 194 public Call getBackgroundCall() { 195 return getFirstCallWithState(Call.State.ONHOLD); 196 } 197 198 public Call getDisconnectedCall() { 199 return getFirstCallWithState(Call.State.DISCONNECTED); 200 } 201 202 public Call getSecondBackgroundCall() { 203 return getCallWithState(Call.State.ONHOLD, 1); 204 } 205 206 public Call getActiveOrBackgroundCall() { 207 Call call = getActiveCall(); 208 if (call == null) { 209 call = getBackgroundCall(); 210 } 211 return call; 212 } 213 214 public Call getIncomingCall() { 215 Call call = getFirstCallWithState(Call.State.INCOMING); 216 if (call == null) { 217 call = getFirstCallWithState(Call.State.CALL_WAITING); 218 } 219 220 return call; 221 } 222 223 224 public Call getFirstCall() { 225 // TODO: should we switch to a simple list and pull the first one? 226 Call result = getIncomingCall(); 227 if (result == null) { 228 result = getFirstCallWithState(Call.State.DIALING); 229 } 230 if (result == null) { 231 result = getFirstCallWithState(Call.State.ACTIVE); 232 } 233 return result; 234 } 235 236 public boolean existsLiveCall() { 237 for (Call call : mCallMap.values()) { 238 if (!isCallDead(call)) { 239 return true; 240 } 241 } 242 return false; 243 } 244 245 public ArrayList<String> getTextResponses(int callId) { 246 return mCallTextReponsesMap.get(callId); 247 } 248 249 /** 250 * Returns first call found in the call map with the specified state. 251 */ 252 public Call getFirstCallWithState(int state) { 253 return getCallWithState(state, 0); 254 } 255 256 /** 257 * Returns the [position]th call found in the call map with the specified state. 258 * TODO(klp): Improve this logic to sort by call time. 259 */ 260 public Call getCallWithState(int state, int positionToFind) { 261 Call retval = null; 262 int position = 0; 263 for (Call call : mCallMap.values()) { 264 if (call.getState() == state) { 265 if (position >= positionToFind) { 266 retval = call; 267 break; 268 } else { 269 position++; 270 } 271 } 272 } 273 274 return retval; 275 } 276 277 /** 278 * This is called when the service disconnects, either expectedly or unexpectedly. 279 * For the expected case, it's because we have no calls left. For the unexpected case, 280 * it is likely a crash of phone and we need to clean up our calls manually. Without phone, 281 * there can be no active calls, so this is relatively safe thing to do. 282 */ 283 public void clearOnDisconnect() { 284 for (Call call : mCallMap.values()) { 285 final int state = call.getState(); 286 if (state != Call.State.IDLE && 287 state != Call.State.INVALID && 288 state != Call.State.DISCONNECTED) { 289 call.setState(Call.State.DISCONNECTED); 290 updateCallInMap(call); 291 } 292 } 293 notifyListenersOfChange(); 294 } 295 296 /** 297 * Sends a generic notification to all listeners that something has changed. 298 * It is up to the listeners to call back to determine what changed. 299 */ 300 private void notifyListenersOfChange() { 301 for (Listener listener : mListeners) { 302 listener.onCallListChange(this); 303 } 304 } 305 306 private void updateCallInMap(Call call) { 307 Preconditions.checkNotNull(call); 308 309 final Integer id = new Integer(call.getCallId()); 310 311 if (call.getState() == Call.State.DISCONNECTED) { 312 313 // update existing (but do not add!!) disconnected calls 314 if (mCallMap.containsKey(id)) { 315 316 // For disconnected calls, we want to keep them alive for a few seconds so that the 317 // UI has a chance to display anything it needs when a call is disconnected. 318 319 // Set up a timer to destroy the call after X seconds. 320 final Message msg = mHandler.obtainMessage(EVENT_DISCONNECTED_TIMEOUT, call); 321 mHandler.sendMessageDelayed(msg, DISCONNECTED_CALL_TIMEOUT_MS); 322 323 mCallMap.put(id, call); 324 } 325 } else if (!isCallDead(call)) { 326 mCallMap.put(id, call); 327 } else if (mCallMap.containsKey(id)) { 328 mCallMap.remove(id); 329 } 330 } 331 332 private void updateCallTextMap(Call call, List<String> textResponses) { 333 Preconditions.checkNotNull(call); 334 335 final Integer id = new Integer(call.getCallId()); 336 337 if (!isCallDead(call)) { 338 if (textResponses != null) { 339 mCallTextReponsesMap.put(id, (ArrayList<String>) textResponses); 340 } 341 } else if (mCallMap.containsKey(id)) { 342 mCallTextReponsesMap.remove(id); 343 } 344 } 345 346 private boolean isCallDead(Call call) { 347 final int state = call.getState(); 348 return Call.State.IDLE == state || Call.State.INVALID == state; 349 } 350 351 /** 352 * Sets up a call for deletion and notifies listeners of change. 353 */ 354 private void finishDisconnectedCall(Call call) { 355 call.setState(Call.State.IDLE); 356 updateCallInMap(call); 357 notifyListenersOfChange(); 358 } 359 360 /** 361 * Handles the timeout for destroying disconnected calls. 362 */ 363 private Handler mHandler = new Handler() { 364 @Override 365 public void handleMessage(Message msg) { 366 switch (msg.what) { 367 case EVENT_DISCONNECTED_TIMEOUT: 368 Log.d(this, "EVENT_DISCONNECTED_TIMEOUT ", msg.obj); 369 finishDisconnectedCall((Call) msg.obj); 370 break; 371 default: 372 Log.wtf(this, "Message not expected: " + msg.what); 373 break; 374 } 375 } 376 }; 377 378 /** 379 * Listener interface for any class that wants to be notified of changes 380 * to the call list. 381 */ 382 public interface Listener { 383 public void onCallListChange(CallList callList); 384 public void onIncomingCall(Call call); 385 } 386 387 public interface CallUpdateListener { 388 // TODO: refactor and limit arg to be call state. Caller info is not needed. 389 public void onCallStateChanged(Call call); 390 } 391} 392