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