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