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