CallList.java revision 3c5581a67ba89fd96fc19e64f6c4780d0a58f640
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; 23import com.google.common.collect.ImmutableList; 24import com.google.common.collect.ImmutableMap; 25 26import android.os.Handler; 27import android.os.Message; 28 29import com.android.services.telephony.common.Call; 30 31import java.util.AbstractMap; 32import java.util.ArrayList; 33import java.util.HashMap; 34import java.util.List; 35import java.util.Map; 36import java.util.Set; 37 38/** 39 * Maintains the list of active calls received from CallHandlerService and notifies interested 40 * classes of changes to the call list as they are received from the telephony stack. 41 * Primary lister of changes to this class is InCallPresenter. 42 */ 43public class CallList { 44 45 private static final int DISCONNECTED_CALL_TIMEOUT_MS = 2000; 46 47 private static final int EVENT_DISCONNECTED_TIMEOUT = 1; 48 49 private static CallList sInstance; 50 51 private final HashMap<Integer, Call> mCallMap = Maps.newHashMap(); 52 private final HashMap<Integer, ArrayList<String>> mCallTextReponsesMap = 53 Maps.newHashMap(); 54 private final Set<Listener> mListeners = Sets.newArraySet(); 55 56 /** 57 * Static singleton accessor method. 58 */ 59 /*public static synchronized CallList getInstance() { 60 if (sInstance == null) { 61 sInstance = new CallList(); 62 } 63 return sInstance; 64 }*/ 65 66 /** 67 * Private constructor. Instance should only be acquired through getInstance(). 68 */ 69 public CallList() { 70 } 71 72 /** 73 * Called when a single call has changed. 74 */ 75 public void onUpdate(Call call) { 76 Logger.d(this, "onUpdate - ", call); 77 78 updateCallInMap(call); 79 notifyListenersOfChange(); 80 } 81 82 /** 83 * Called when a single call disconnects. 84 */ 85 public void onDisconnect(Call call) { 86 Logger.d(this, "onDisconnect: ", call); 87 88 updateCallInMap(call); 89 90 notifyListenersOfChange(); 91 } 92 93 /** 94 * Called when a single call has changed. 95 */ 96 public void onUpdate(AbstractMap.SimpleEntry<Call, List<String> > incomingCall) { 97 Logger.d(this, "onUpdate - " + incomingCall.getKey()); 98 99 updateCallInMap(incomingCall.getKey()); 100 updateCallTextMap(incomingCall.getKey(), incomingCall.getValue()); 101 102 notifyListenersOfChange(); 103 } 104 105 /** 106 * Called when multiple calls have changed. 107 */ 108 public void onUpdate(List<Call> callsToUpdate) { 109 Logger.d(this, "onUpdate(...)"); 110 111 Preconditions.checkNotNull(callsToUpdate); 112 for (Call call : callsToUpdate) { 113 Logger.d(this, "\t" + call); 114 115 updateCallInMap(call); 116 updateCallTextMap(call, null); 117 } 118 119 notifyListenersOfChange(); 120 } 121 122 public void addListener(Listener listener) { 123 Preconditions.checkNotNull(listener); 124 125 mListeners.add(listener); 126 127 // Let the listener know about the active calls immediately. 128 listener.onCallListChange(this); 129 } 130 131 public void removeListener(Listener listener) { 132 Preconditions.checkNotNull(listener); 133 mListeners.remove(listener); 134 } 135 136 /** 137 * TODO: Change so that this function is not needed. Instead of assuming there is an active 138 * call, the code should rely on the status of a specific Call and allow the presenters to 139 * update the Call object when the active call changes. 140 */ 141 public Call getIncomingOrActive() { 142 Call retval = getIncomingCall(); 143 if (retval == null) { 144 retval = getActiveCall(); 145 } 146 return retval; 147 } 148 149 public Call getOutgoingCall() { 150 return getFirstCallWithState(Call.State.DIALING); 151 } 152 153 public Call getActiveCall() { 154 return getFirstCallWithState(Call.State.ACTIVE); 155 } 156 157 public Call getBackgroundCall() { 158 return getFirstCallWithState(Call.State.ONHOLD); 159 } 160 161 public Call getDisconnectedCall() { 162 return getFirstCallWithState(Call.State.DISCONNECTED); 163 } 164 165 public Call getSecondBackgroundCall() { 166 return getCallWithState(Call.State.ONHOLD, 1); 167 } 168 169 public Call getActiveOrBackgroundCall() { 170 Call call = getActiveCall(); 171 if (call == null) { 172 call = getBackgroundCall(); 173 } 174 return call; 175 } 176 177 public Call getIncomingCall() { 178 Call call = getFirstCallWithState(Call.State.INCOMING); 179 if (call == null) { 180 call = getFirstCallWithState(Call.State.CALL_WAITING); 181 } 182 183 return call; 184 } 185 186 public boolean existsLiveCall() { 187 for (Call call : mCallMap.values()) { 188 if (!isCallDead(call)) { 189 return true; 190 } 191 } 192 return false; 193 } 194 195 public ArrayList<String> getTextResponses(Call call) { 196 return mCallTextReponsesMap.get(call.getCallId()); 197 } 198 199 /** 200 * Returns first call found in the call map with the specified state. 201 */ 202 public Call getFirstCallWithState(int state) { 203 return getCallWithState(state, 0); 204 } 205 206 /** 207 * Returns the [position]th call found in the call map with the specified state. 208 * TODO(klp): Improve this logic to sort by call time. 209 */ 210 public Call getCallWithState(int state, int positionToFind) { 211 Call retval = null; 212 int position = 0; 213 for (Call call : mCallMap.values()) { 214 if (call.getState() == state) { 215 if (position >= positionToFind) { 216 retval = call; 217 break; 218 } else { 219 position++; 220 } 221 } 222 } 223 224 return retval; 225 } 226 227 /** 228 * This is called when the service disconnects, either expectedly or unexpectedly. 229 * For the expected case, it's because we have no calls left. For the unexpected case, 230 * it is likely a crash of phone and we need to clean up our calls manually. Without phone, 231 * there can be no active calls, so this is relatively safe thing to do. 232 */ 233 public void clearOnDisconnect() { 234 for (Call call : mCallMap.values()) { 235 final int state = call.getState(); 236 if (state != Call.State.IDLE && 237 state != Call.State.INVALID && 238 state != Call.State.DISCONNECTED) { 239 call.setState(Call.State.DISCONNECTED); 240 updateCallInMap(call); 241 } 242 } 243 notifyListenersOfChange(); 244 } 245 246 /** 247 * Sends a generic notification to all listeners that something has changed. 248 * It is up to the listeners to call back to determine what changed. 249 */ 250 private void notifyListenersOfChange() { 251 for (Listener listener : mListeners) { 252 listener.onCallListChange(this); 253 } 254 } 255 256 private void updateCallInMap(Call call) { 257 Preconditions.checkNotNull(call); 258 259 final Integer id = new Integer(call.getCallId()); 260 261 if (call.getState() == Call.State.DISCONNECTED) { 262 263 // update existing (but do not add!!) disconnected calls 264 if (mCallMap.containsKey(id)) { 265 266 // For disconnected calls, we want to keep them alive for a few seconds so that the 267 // UI has a chance to display anything it needs when a call is disconnected. 268 269 // Set up a timer to destroy the call after X seconds. 270 final Message msg = mHandler.obtainMessage(EVENT_DISCONNECTED_TIMEOUT, call); 271 mHandler.sendMessageDelayed(msg, DISCONNECTED_CALL_TIMEOUT_MS); 272 273 mCallMap.put(id, call); 274 } 275 } else if (!isCallDead(call)) { 276 mCallMap.put(id, call); 277 } else if (mCallMap.containsKey(id)) { 278 mCallMap.remove(id); 279 } 280 } 281 282 private void updateCallTextMap(Call call, List<String> textResponses) { 283 Preconditions.checkNotNull(call); 284 285 final Integer id = new Integer(call.getCallId()); 286 287 if (!isCallDead(call)) { 288 if (textResponses != null) { 289 mCallTextReponsesMap.put(id, (ArrayList<String>) textResponses); 290 } 291 } else if (mCallMap.containsKey(id)) { 292 mCallTextReponsesMap.remove(id); 293 } 294 } 295 296 private boolean isCallDead(Call call) { 297 final int state = call.getState(); 298 return Call.State.IDLE == state || Call.State.INVALID == state; 299 } 300 301 /** 302 * Sets up a call for deletion and notifies listeners of change. 303 */ 304 private void finishDisconnectedCall(Call call) { 305 call.setState(Call.State.IDLE); 306 updateCallInMap(call); 307 notifyListenersOfChange(); 308 } 309 310 /** 311 * Handles the timeout for destroying disconnected calls. 312 */ 313 private Handler mHandler = new Handler() { 314 @Override 315 public void handleMessage(Message msg) { 316 switch (msg.what) { 317 case EVENT_DISCONNECTED_TIMEOUT: 318 Logger.d(this, "EVENT_DISCONNECTED_TIMEOUT ", msg.obj); 319 finishDisconnectedCall((Call) msg.obj); 320 break; 321 default: 322 Logger.wtf(this, "Message not expected: " + msg.what); 323 break; 324 } 325 } 326 }; 327 328 /** 329 * Listener interface for any class that wants to be notified of changes 330 * to the call list. 331 */ 332 public interface Listener { 333 public void onCallListChange(CallList callList); 334 } 335} 336