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 android.content.Context; 20 import android.content.pm.ApplicationInfo; 21 import android.content.pm.PackageManager; 22 import android.graphics.drawable.Drawable; 23 import android.graphics.Bitmap; 24 import android.telephony.PhoneNumberUtils; 25 import android.text.TextUtils; 26 import android.text.format.DateUtils; 27 28 import com.android.incallui.AudioModeProvider.AudioModeListener; 29 import com.android.incallui.ContactInfoCache.ContactCacheEntry; 30 import com.android.incallui.ContactInfoCache.ContactInfoCacheCallback; 31 import com.android.incallui.InCallPresenter.InCallState; 32 import com.android.incallui.InCallPresenter.InCallStateListener; 33 import com.android.incallui.InCallPresenter.IncomingCallListener; 34 import com.android.services.telephony.common.AudioMode; 35 import com.android.services.telephony.common.Call; 36 import com.android.services.telephony.common.Call.Capabilities; 37 import com.android.services.telephony.common.CallIdentification; 38 import com.google.common.base.Preconditions; 39 40 /** 41 * Presenter for the Call Card Fragment. 42 * <p> 43 * This class listens for changes to InCallState and passes it along to the fragment. 44 */ 45 public class CallCardPresenter extends Presenter<CallCardPresenter.CallCardUi> 46 implements InCallStateListener, AudioModeListener, IncomingCallListener { 47 48 private static final String TAG = CallCardPresenter.class.getSimpleName(); 49 private static final long CALL_TIME_UPDATE_INTERVAL = 1000; // in milliseconds 50 51 private Call mPrimary; 52 private Call mSecondary; 53 private ContactCacheEntry mPrimaryContactInfo; 54 private ContactCacheEntry mSecondaryContactInfo; 55 private CallTimer mCallTimer; 56 private Context mContext; 57 58 public CallCardPresenter() { 59 // create the call timer 60 mCallTimer = new CallTimer(new Runnable() { 61 @Override 62 public void run() { 63 updateCallTime(); 64 } 65 }); 66 } 67 68 69 public void init(Context context, Call call) { 70 mContext = Preconditions.checkNotNull(context); 71 72 // Call may be null if disconnect happened already. 73 if (call != null) { 74 mPrimary = call; 75 76 final CallIdentification identification = call.getIdentification(); 77 78 // start processing lookups right away. 79 if (!call.isConferenceCall()) { 80 startContactInfoSearch(identification, true, 81 call.getState() == Call.State.INCOMING); 82 } else { 83 updateContactEntry(null, true, true); 84 } 85 } 86 } 87 88 @Override 89 public void onUiReady(CallCardUi ui) { 90 super.onUiReady(ui); 91 92 AudioModeProvider.getInstance().addListener(this); 93 94 // Contact search may have completed before ui is ready. 95 if (mPrimaryContactInfo != null) { 96 updatePrimaryDisplayInfo(mPrimaryContactInfo, isConference(mPrimary)); 97 } 98 99 // Register for call state changes last 100 InCallPresenter.getInstance().addListener(this); 101 InCallPresenter.getInstance().addIncomingCallListener(this); 102 } 103 104 @Override 105 public void onUiUnready(CallCardUi ui) { 106 super.onUiUnready(ui); 107 108 // stop getting call state changes 109 InCallPresenter.getInstance().removeListener(this); 110 InCallPresenter.getInstance().removeIncomingCallListener(this); 111 112 AudioModeProvider.getInstance().removeListener(this); 113 114 mPrimary = null; 115 mPrimaryContactInfo = null; 116 mSecondaryContactInfo = null; 117 } 118 119 @Override 120 public void onIncomingCall(InCallState state, Call call) { 121 // same logic should happen as with onStateChange() 122 onStateChange(state, CallList.getInstance()); 123 } 124 125 @Override 126 public void onStateChange(InCallState state, CallList callList) { 127 Log.d(this, "onStateChange() " + state); 128 final CallCardUi ui = getUi(); 129 if (ui == null) { 130 return; 131 } 132 133 Call primary = null; 134 Call secondary = null; 135 136 if (state == InCallState.INCOMING) { 137 primary = callList.getIncomingCall(); 138 } else if (state == InCallState.OUTGOING) { 139 primary = callList.getOutgoingCall(); 140 141 // getCallToDisplay doesn't go through outgoing or incoming calls. It will return the 142 // highest priority call to display as the secondary call. 143 secondary = getCallToDisplay(callList, null, true); 144 } else if (state == InCallState.INCALL) { 145 primary = getCallToDisplay(callList, null, false); 146 secondary = getCallToDisplay(callList, primary, true); 147 } 148 149 Log.d(this, "Primary call: " + primary); 150 Log.d(this, "Secondary call: " + secondary); 151 152 final boolean primaryChanged = !areCallsSame(mPrimary, primary); 153 final boolean secondaryChanged = !areCallsSame(mSecondary, secondary); 154 mSecondary = secondary; 155 mPrimary = primary; 156 157 if (primaryChanged && mPrimary != null) { 158 // primary call has changed 159 mPrimaryContactInfo = ContactInfoCache.buildCacheEntryFromCall(mContext, 160 mPrimary.getIdentification(), mPrimary.getState() == Call.State.INCOMING); 161 updatePrimaryDisplayInfo(mPrimaryContactInfo, isConference(mPrimary)); 162 maybeStartSearch(mPrimary, true); 163 } 164 165 if (mSecondary == null) { 166 // Secondary call may have ended. Update the ui. 167 mSecondaryContactInfo = null; 168 updateSecondaryDisplayInfo(false); 169 } else if (secondaryChanged) { 170 // secondary call has changed 171 mSecondaryContactInfo = ContactInfoCache.buildCacheEntryFromCall(mContext, 172 mSecondary.getIdentification(), mSecondary.getState() == Call.State.INCOMING); 173 updateSecondaryDisplayInfo(mSecondary.isConferenceCall()); 174 maybeStartSearch(mSecondary, false); 175 } 176 177 // Start/Stop the call time update timer 178 if (mPrimary != null && mPrimary.getState() == Call.State.ACTIVE) { 179 Log.d(this, "Starting the calltime timer"); 180 mCallTimer.start(CALL_TIME_UPDATE_INTERVAL); 181 } else { 182 Log.d(this, "Canceling the calltime timer"); 183 mCallTimer.cancel(); 184 ui.setPrimaryCallElapsedTime(false, null); 185 } 186 187 // Set the call state 188 if (mPrimary != null) { 189 final boolean bluetoothOn = 190 (AudioModeProvider.getInstance().getAudioMode() == AudioMode.BLUETOOTH); 191 ui.setCallState(mPrimary.getState(), mPrimary.getDisconnectCause(), bluetoothOn, 192 getGatewayLabel(), getGatewayNumber()); 193 } else { 194 ui.setCallState(Call.State.IDLE, Call.DisconnectCause.UNKNOWN, false, null, null); 195 } 196 } 197 198 @Override 199 public void onAudioMode(int mode) { 200 if (mPrimary != null && getUi() != null) { 201 final boolean bluetoothOn = (AudioMode.BLUETOOTH == mode); 202 203 getUi().setCallState(mPrimary.getState(), mPrimary.getDisconnectCause(), bluetoothOn, 204 getGatewayLabel(), getGatewayNumber()); 205 } 206 } 207 208 @Override 209 public void onSupportedAudioMode(int mask) { 210 } 211 212 @Override 213 public void onMute(boolean muted) { 214 } 215 216 public void updateCallTime() { 217 final CallCardUi ui = getUi(); 218 219 if (ui == null || mPrimary == null || mPrimary.getState() != Call.State.ACTIVE) { 220 if (ui != null) { 221 ui.setPrimaryCallElapsedTime(false, null); 222 } 223 mCallTimer.cancel(); 224 } else { 225 final long callStart = mPrimary.getConnectTime(); 226 final long duration = System.currentTimeMillis() - callStart; 227 ui.setPrimaryCallElapsedTime(true, DateUtils.formatElapsedTime(duration / 1000)); 228 } 229 } 230 231 private boolean areCallsSame(Call call1, Call call2) { 232 if (call1 == null && call2 == null) { 233 return true; 234 } else if (call1 == null || call2 == null) { 235 return false; 236 } 237 238 // otherwise compare call Ids 239 return call1.getCallId() == call2.getCallId(); 240 } 241 242 private void maybeStartSearch(Call call, boolean isPrimary) { 243 // no need to start search for conference calls which show generic info. 244 if (call != null && !call.isConferenceCall()) { 245 startContactInfoSearch(call.getIdentification(), isPrimary, 246 call.getState() == Call.State.INCOMING); 247 } 248 } 249 250 /** 251 * Starts a query for more contact data for the save primary and secondary calls. 252 */ 253 private void startContactInfoSearch(final CallIdentification identification, 254 final boolean isPrimary, boolean isIncoming) { 255 final ContactInfoCache cache = ContactInfoCache.getInstance(mContext); 256 257 cache.findInfo(identification, isIncoming, new ContactInfoCacheCallback() { 258 @Override 259 public void onContactInfoComplete(int callId, ContactCacheEntry entry) { 260 updateContactEntry(entry, isPrimary, false); 261 if (entry.name != null) { 262 Log.d(TAG, "Contact found: " + entry); 263 } 264 if (entry.personUri != null) { 265 CallerInfoUtils.sendViewNotification(mContext, entry.personUri); 266 } 267 } 268 269 @Override 270 public void onImageLoadComplete(int callId, ContactCacheEntry entry) { 271 if (getUi() == null) { 272 return; 273 } 274 if (entry.photo != null) { 275 if (mPrimary != null && callId == mPrimary.getCallId()) { 276 getUi().setPrimaryImage(entry.photo); 277 } else if (mSecondary != null && callId == mSecondary.getCallId()) { 278 getUi().setSecondaryImage(entry.photo); 279 } 280 } 281 } 282 }); 283 } 284 285 private static boolean isConference(Call call) { 286 return call != null && call.isConferenceCall(); 287 } 288 289 private static boolean isGenericConference(Call call) { 290 return call != null && call.can(Capabilities.GENERIC_CONFERENCE); 291 } 292 293 private void updateContactEntry(ContactCacheEntry entry, boolean isPrimary, 294 boolean isConference) { 295 if (isPrimary) { 296 mPrimaryContactInfo = entry; 297 updatePrimaryDisplayInfo(entry, isConference); 298 } else { 299 mSecondaryContactInfo = entry; 300 updateSecondaryDisplayInfo(isConference); 301 } 302 } 303 304 /** 305 * Get the highest priority call to display. 306 * Goes through the calls and chooses which to return based on priority of which type of call 307 * to display to the user. Callers can use the "ignore" feature to get the second best call 308 * by passing a previously found primary call as ignore. 309 * 310 * @param ignore A call to ignore if found. 311 */ 312 private Call getCallToDisplay(CallList callList, Call ignore, boolean skipDisconnected) { 313 314 // Active calls come second. An active call always gets precedent. 315 Call retval = callList.getActiveCall(); 316 if (retval != null && retval != ignore) { 317 return retval; 318 } 319 320 // Disconnected calls get primary position if there are no active calls 321 // to let user know quickly what call has disconnected. Disconnected 322 // calls are very short lived. 323 if (!skipDisconnected) { 324 retval = callList.getDisconnectingCall(); 325 if (retval != null && retval != ignore) { 326 return retval; 327 } 328 retval = callList.getDisconnectedCall(); 329 if (retval != null && retval != ignore) { 330 return retval; 331 } 332 } 333 334 // Then we go to background call (calls on hold) 335 retval = callList.getBackgroundCall(); 336 if (retval != null && retval != ignore) { 337 return retval; 338 } 339 340 // Lastly, we go to a second background call. 341 retval = callList.getSecondBackgroundCall(); 342 343 return retval; 344 } 345 346 private void updatePrimaryDisplayInfo(ContactCacheEntry entry, boolean isConference) { 347 Log.d(TAG, "Update primary display " + entry); 348 final CallCardUi ui = getUi(); 349 if (ui == null) { 350 // TODO: May also occur if search result comes back after ui is destroyed. Look into 351 // removing that case completely. 352 Log.d(TAG, "updatePrimaryDisplayInfo called but ui is null!"); 353 return; 354 } 355 356 final boolean isGenericConf = isGenericConference(mPrimary); 357 if (entry != null) { 358 final String name = getNameForCall(entry); 359 final String number = getNumberForCall(entry); 360 final boolean nameIsNumber = name != null && name.equals(entry.number); 361 ui.setPrimary(number, name, nameIsNumber, entry.label, 362 entry.photo, isConference, isGenericConf, entry.isSipCall); 363 } else { 364 ui.setPrimary(null, null, false, null, null, isConference, isGenericConf, false); 365 } 366 367 } 368 369 private void updateSecondaryDisplayInfo(boolean isConference) { 370 371 final CallCardUi ui = getUi(); 372 if (ui == null) { 373 return; 374 } 375 376 final boolean isGenericConf = isGenericConference(mSecondary); 377 if (mSecondaryContactInfo != null) { 378 Log.d(TAG, "updateSecondaryDisplayInfo() " + mSecondaryContactInfo); 379 final String nameForCall = getNameForCall(mSecondaryContactInfo); 380 381 final boolean nameIsNumber = nameForCall != null && nameForCall.equals( 382 mSecondaryContactInfo.number); 383 ui.setSecondary(true, nameForCall, nameIsNumber, mSecondaryContactInfo.label, 384 mSecondaryContactInfo.photo, isConference, isGenericConf); 385 } else { 386 // reset to nothing so that it starts off blank next time we use it. 387 ui.setSecondary(false, null, false, null, null, isConference, isGenericConf); 388 } 389 } 390 391 /** 392 * Returns the gateway number for any existing outgoing call. 393 */ 394 private String getGatewayNumber() { 395 if (hasOutgoingGatewayCall()) { 396 return mPrimary.getGatewayNumber(); 397 } 398 399 return null; 400 } 401 402 /** 403 * Returns the label for the gateway app for any existing outgoing call. 404 */ 405 private String getGatewayLabel() { 406 if (hasOutgoingGatewayCall() && getUi() != null) { 407 final PackageManager pm = mContext.getPackageManager(); 408 try { 409 final ApplicationInfo info = pm.getApplicationInfo(mPrimary.getGatewayPackage(), 0); 410 return mContext.getString(R.string.calling_via_template, 411 pm.getApplicationLabel(info).toString()); 412 } catch (PackageManager.NameNotFoundException e) { 413 } 414 } 415 return null; 416 } 417 418 private boolean hasOutgoingGatewayCall() { 419 // We only display the gateway information while DIALING so return false for any othe 420 // call state. 421 // TODO: mPrimary can be null because this is called from updatePrimaryDisplayInfo which 422 // is also called after a contact search completes (call is not present yet). Split the 423 // UI update so it can receive independent updates. 424 if (mPrimary == null) { 425 return false; 426 } 427 return (Call.State.isDialing(mPrimary.getState()) && 428 !TextUtils.isEmpty(mPrimary.getGatewayNumber()) && 429 !TextUtils.isEmpty(mPrimary.getGatewayPackage())); 430 } 431 432 /** 433 * Gets the name to display for the call. 434 */ 435 private static String getNameForCall(ContactCacheEntry contactInfo) { 436 if (TextUtils.isEmpty(contactInfo.name)) { 437 return contactInfo.number; 438 } 439 return contactInfo.name; 440 } 441 442 /** 443 * Gets the number to display for a call. 444 */ 445 private static String getNumberForCall(ContactCacheEntry contactInfo) { 446 // If the name is empty, we use the number for the name...so dont show a second 447 // number in the number field 448 if (TextUtils.isEmpty(contactInfo.name)) { 449 return contactInfo.location; 450 } 451 return contactInfo.number; 452 } 453 454 public void secondaryPhotoClicked() { 455 CallCommandClient.getInstance().swap(); 456 } 457 458 public interface CallCardUi extends Ui { 459 void setVisible(boolean on); 460 void setPrimary(String number, String name, boolean nameIsNumber, String label, 461 Drawable photo, boolean isConference, boolean isGeneric, boolean isSipCall); 462 void setSecondary(boolean show, String name, boolean nameIsNumber, String label, 463 Drawable photo, boolean isConference, boolean isGeneric); 464 void setSecondaryImage(Drawable image); 465 void setCallState(int state, Call.DisconnectCause cause, boolean bluetoothOn, 466 String gatewayLabel, String gatewayNumber); 467 void setPrimaryCallElapsedTime(boolean show, String duration); 468 void setPrimaryName(String name, boolean nameIsNumber); 469 void setPrimaryImage(Drawable image); 470 void setPrimaryPhoneNumber(String phoneNumber); 471 void setPrimaryLabel(String label); 472 } 473 } 474