1 /* 2 * Copyright (C) 2006 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.phone; 18 19 import android.content.ContentUris; 20 import android.content.Context; 21 import android.graphics.drawable.Drawable; 22 import android.net.Uri; 23 import android.pim.ContactsAsyncHelper; 24 import android.provider.ContactsContract.Contacts; 25 import android.telephony.PhoneNumberUtils; 26 import android.text.TextUtils; 27 import android.text.format.DateUtils; 28 import android.util.AttributeSet; 29 import android.util.Log; 30 import android.view.LayoutInflater; 31 import android.view.View; 32 import android.view.ViewGroup; 33 import android.view.accessibility.AccessibilityEvent; 34 import android.widget.Button; 35 import android.widget.FrameLayout; 36 import android.widget.ImageView; 37 import android.widget.TextView; 38 39 import com.android.internal.telephony.Call; 40 import com.android.internal.telephony.CallerInfo; 41 import com.android.internal.telephony.CallerInfoAsyncQuery; 42 import com.android.internal.telephony.Connection; 43 import com.android.internal.telephony.Phone; 44 import com.android.internal.telephony.CallManager; 45 46 import java.util.List; 47 48 49 /** 50 * "Call card" UI element: the in-call screen contains a tiled layout of call 51 * cards, each representing the state of a current "call" (ie. an active call, 52 * a call on hold, or an incoming call.) 53 */ 54 public class CallCard extends FrameLayout 55 implements CallTime.OnTickListener, CallerInfoAsyncQuery.OnQueryCompleteListener, 56 ContactsAsyncHelper.OnImageLoadCompleteListener, View.OnClickListener { 57 private static final String LOG_TAG = "CallCard"; 58 private static final boolean DBG = (PhoneApp.DBG_LEVEL >= 2); 59 60 /** 61 * Reference to the InCallScreen activity that owns us. This may be 62 * null if we haven't been initialized yet *or* after the InCallScreen 63 * activity has been destroyed. 64 */ 65 private InCallScreen mInCallScreen; 66 67 // Phone app instance 68 private PhoneApp mApplication; 69 70 // Top-level subviews of the CallCard 71 private ViewGroup mPrimaryCallInfo; 72 private ViewGroup mSecondaryCallInfo; 73 74 // Title and elapsed-time widgets 75 private TextView mUpperTitle; 76 private TextView mElapsedTime; 77 78 // Text colors, used for various labels / titles 79 private int mTextColorDefaultPrimary; 80 private int mTextColorDefaultSecondary; 81 private int mTextColorConnected; 82 private int mTextColorConnectedBluetooth; 83 private int mTextColorEnded; 84 private int mTextColorOnHold; 85 private int mTextColorCallTypeSip; 86 87 // The main block of info about the "primary" or "active" call, 88 // including photo / name / phone number / etc. 89 private ImageView mPhoto; 90 private Button mManageConferencePhotoButton; // Possibly shown in place of mPhoto 91 private TextView mName; 92 private TextView mPhoneNumber; 93 private TextView mLabel; 94 private TextView mCallTypeLabel; 95 private TextView mSocialStatus; 96 97 // Info about the "secondary" call, which is the "call on hold" when 98 // two lines are in use. 99 private TextView mSecondaryCallName; 100 private TextView mSecondaryCallStatus; 101 private ImageView mSecondaryCallPhoto; 102 103 // Menu button hint 104 private TextView mMenuButtonHint; 105 106 // Onscreen hint for the incoming call RotarySelector widget. 107 private int mRotarySelectorHintTextResId; 108 private int mRotarySelectorHintColorResId; 109 110 private CallTime mCallTime; 111 112 // Track the state for the photo. 113 private ContactsAsyncHelper.ImageTracker mPhotoTracker; 114 115 // Cached DisplayMetrics density. 116 private float mDensity; 117 CallCard(Context context, AttributeSet attrs)118 public CallCard(Context context, AttributeSet attrs) { 119 super(context, attrs); 120 121 if (DBG) log("CallCard constructor..."); 122 if (DBG) log("- this = " + this); 123 if (DBG) log("- context " + context + ", attrs " + attrs); 124 125 // Inflate the contents of this CallCard, and add it (to ourself) as a child. 126 LayoutInflater inflater = LayoutInflater.from(context); 127 inflater.inflate( 128 R.layout.call_card, // resource 129 this, // root 130 true); 131 132 mApplication = PhoneApp.getInstance(); 133 134 mCallTime = new CallTime(this); 135 136 // create a new object to track the state for the photo. 137 mPhotoTracker = new ContactsAsyncHelper.ImageTracker(); 138 139 mDensity = getResources().getDisplayMetrics().density; 140 if (DBG) log("- Density: " + mDensity); 141 } 142 setInCallScreenInstance(InCallScreen inCallScreen)143 void setInCallScreenInstance(InCallScreen inCallScreen) { 144 mInCallScreen = inCallScreen; 145 } 146 onTickForCallTimeElapsed(long timeElapsed)147 public void onTickForCallTimeElapsed(long timeElapsed) { 148 // While a call is in progress, update the elapsed time shown 149 // onscreen. 150 updateElapsedTimeWidget(timeElapsed); 151 } 152 153 /* package */ stopTimer()154 void stopTimer() { 155 mCallTime.cancelTimer(); 156 } 157 158 @Override onFinishInflate()159 protected void onFinishInflate() { 160 super.onFinishInflate(); 161 162 if (DBG) log("CallCard onFinishInflate(this = " + this + ")..."); 163 164 mPrimaryCallInfo = (ViewGroup) findViewById(R.id.primaryCallInfo); 165 mSecondaryCallInfo = (ViewGroup) findViewById(R.id.secondaryCallInfo); 166 167 // "Upper" and "lower" title widgets 168 mUpperTitle = (TextView) findViewById(R.id.upperTitle); 169 mElapsedTime = (TextView) findViewById(R.id.elapsedTime); 170 171 // Text colors 172 mTextColorDefaultPrimary = // corresponds to textAppearanceLarge 173 getResources().getColor(android.R.color.primary_text_dark); 174 mTextColorDefaultSecondary = // corresponds to textAppearanceSmall 175 getResources().getColor(android.R.color.secondary_text_dark); 176 mTextColorConnected = getResources().getColor(R.color.incall_textConnected); 177 mTextColorConnectedBluetooth = 178 getResources().getColor(R.color.incall_textConnectedBluetooth); 179 mTextColorEnded = getResources().getColor(R.color.incall_textEnded); 180 mTextColorOnHold = getResources().getColor(R.color.incall_textOnHold); 181 mTextColorCallTypeSip = getResources().getColor(R.color.incall_callTypeSip); 182 183 // "Caller info" area, including photo / name / phone numbers / etc 184 mPhoto = (ImageView) findViewById(R.id.photo); 185 mManageConferencePhotoButton = (Button) findViewById(R.id.manageConferencePhotoButton); 186 mManageConferencePhotoButton.setOnClickListener(this); 187 mName = (TextView) findViewById(R.id.name); 188 mPhoneNumber = (TextView) findViewById(R.id.phoneNumber); 189 mLabel = (TextView) findViewById(R.id.label); 190 mCallTypeLabel = (TextView) findViewById(R.id.callTypeLabel); 191 mSocialStatus = (TextView) findViewById(R.id.socialStatus); 192 193 // "Other call" info area 194 mSecondaryCallName = (TextView) findViewById(R.id.secondaryCallName); 195 mSecondaryCallStatus = (TextView) findViewById(R.id.secondaryCallStatus); 196 mSecondaryCallPhoto = (ImageView) findViewById(R.id.secondaryCallPhoto); 197 198 // Menu Button hint 199 mMenuButtonHint = (TextView) findViewById(R.id.menuButtonHint); 200 } 201 202 /** 203 * Updates the state of all UI elements on the CallCard, based on the 204 * current state of the phone. 205 */ updateState(CallManager cm)206 void updateState(CallManager cm) { 207 if (DBG) log("updateState(" + cm + ")..."); 208 209 // Update some internal state based on the current state of the phone. 210 211 // TODO: clean up this method to just fully update EVERYTHING in 212 // the callcard based on the current phone state: set the overall 213 // type of the CallCard, load up the main caller info area, and 214 // load up and show or hide the "other call" area if necessary. 215 216 Phone.State state = cm.getState(); // IDLE, RINGING, or OFFHOOK 217 Call ringingCall = cm.getFirstActiveRingingCall(); 218 Call fgCall = cm.getActiveFgCall(); 219 Call bgCall = cm.getFirstActiveBgCall(); 220 221 // If the FG call is dialing/alerting, we should display for that call 222 // and ignore the ringing call. This case happens when the telephony 223 // layer rejects the ringing call while the FG call is dialing/alerting, 224 // but the incoming call *does* briefly exist in the DISCONNECTING or 225 // DISCONNECTED state. 226 if ((ringingCall.getState() != Call.State.IDLE) 227 && !fgCall.getState().isDialing()) { 228 // A phone call is ringing, call waiting *or* being rejected 229 // (ie. another call may also be active as well.) 230 updateRingingCall(cm); 231 } else if ((fgCall.getState() != Call.State.IDLE) 232 || (bgCall.getState() != Call.State.IDLE)) { 233 // We are here because either: 234 // (1) the phone is off hook. At least one call exists that is 235 // dialing, active, or holding, and no calls are ringing or waiting, 236 // or: 237 // (2) the phone is IDLE but a call just ended and it's still in 238 // the DISCONNECTING or DISCONNECTED state. In this case, we want 239 // the main CallCard to display "Hanging up" or "Call ended". 240 // The normal "foreground call" code path handles both cases. 241 updateForegroundCall(cm); 242 } else { 243 // We don't have any DISCONNECTED calls, which means 244 // that the phone is *truly* idle. 245 // 246 // It's very rare to be on the InCallScreen at all in this 247 // state, but it can happen in some cases: 248 // - A stray onPhoneStateChanged() event came in to the 249 // InCallScreen *after* it was dismissed. 250 // - We're allowed to be on the InCallScreen because 251 // an MMI or USSD is running, but there's no actual "call" 252 // to display. 253 // - We're displaying an error dialog to the user 254 // (explaining why the call failed), so we need to stay on 255 // the InCallScreen so that the dialog will be visible. 256 // 257 // In these cases, put the callcard into a sane but "blank" state: 258 updateNoCall(cm); 259 } 260 } 261 262 /** 263 * Updates the UI for the state where the phone is in use, but not ringing. 264 */ updateForegroundCall(CallManager cm)265 private void updateForegroundCall(CallManager cm) { 266 if (DBG) log("updateForegroundCall()..."); 267 // if (DBG) PhoneUtils.dumpCallManager(); 268 269 Call fgCall = cm.getActiveFgCall(); 270 Call bgCall = cm.getFirstActiveBgCall(); 271 272 if (fgCall.getState() == Call.State.IDLE) { 273 if (DBG) log("updateForegroundCall: no active call, show holding call"); 274 // TODO: make sure this case agrees with the latest UI spec. 275 276 // Display the background call in the main info area of the 277 // CallCard, since there is no foreground call. Note that 278 // displayMainCallStatus() will notice if the call we passed in is on 279 // hold, and display the "on hold" indication. 280 fgCall = bgCall; 281 282 // And be sure to not display anything in the "on hold" box. 283 bgCall = null; 284 } 285 286 displayMainCallStatus(cm, fgCall); 287 288 Phone phone = fgCall.getPhone(); 289 290 int phoneType = phone.getPhoneType(); 291 if (phoneType == Phone.PHONE_TYPE_CDMA) { 292 if ((mApplication.cdmaPhoneCallState.getCurrentCallState() 293 == CdmaPhoneCallState.PhoneCallState.THRWAY_ACTIVE) 294 && mApplication.cdmaPhoneCallState.IsThreeWayCallOrigStateDialing()) { 295 displayOnHoldCallStatus(cm, fgCall); 296 } else { 297 //This is required so that even if a background call is not present 298 // we need to clean up the background call area. 299 displayOnHoldCallStatus(cm, bgCall); 300 } 301 } else if ((phoneType == Phone.PHONE_TYPE_GSM) 302 || (phoneType == Phone.PHONE_TYPE_SIP)) { 303 displayOnHoldCallStatus(cm, bgCall); 304 } 305 } 306 307 /** 308 * Updates the UI for the state where an incoming call is ringing (or 309 * call waiting), regardless of whether the phone's already offhook. 310 */ updateRingingCall(CallManager cm)311 private void updateRingingCall(CallManager cm) { 312 if (DBG) log("updateRingingCall()..."); 313 314 Call ringingCall = cm.getFirstActiveRingingCall(); 315 316 // Display caller-id info and photo from the incoming call: 317 displayMainCallStatus(cm, ringingCall); 318 319 // And even in the Call Waiting case, *don't* show any info about 320 // the current ongoing call and/or the current call on hold. 321 // (Since the caller-id info for the incoming call totally trumps 322 // any info about the current call(s) in progress.) 323 displayOnHoldCallStatus(cm, null); 324 } 325 326 /** 327 * Updates the UI for the state where the phone is not in use. 328 * This is analogous to updateForegroundCall() and updateRingingCall(), 329 * but for the (uncommon) case where the phone is 330 * totally idle. (See comments in updateState() above.) 331 * 332 * This puts the callcard into a sane but "blank" state. 333 */ updateNoCall(CallManager cm)334 private void updateNoCall(CallManager cm) { 335 if (DBG) log("updateNoCall()..."); 336 337 displayMainCallStatus(cm, null); 338 displayOnHoldCallStatus(cm, null); 339 } 340 341 /** 342 * Updates the main block of caller info on the CallCard 343 * (ie. the stuff in the primaryCallInfo block) based on the specified Call. 344 */ displayMainCallStatus(CallManager cm, Call call)345 private void displayMainCallStatus(CallManager cm, Call call) { 346 if (DBG) log("displayMainCallStatus(call " + call + ")..."); 347 348 if (call == null) { 349 // There's no call to display, presumably because the phone is idle. 350 mPrimaryCallInfo.setVisibility(View.GONE); 351 return; 352 } 353 mPrimaryCallInfo.setVisibility(View.VISIBLE); 354 355 Call.State state = call.getState(); 356 if (DBG) log(" - call.state: " + call.getState()); 357 358 switch (state) { 359 case ACTIVE: 360 case DISCONNECTING: 361 // update timer field 362 if (DBG) log("displayMainCallStatus: start periodicUpdateTimer"); 363 mCallTime.setActiveCallMode(call); 364 mCallTime.reset(); 365 mCallTime.periodicUpdateTimer(); 366 367 break; 368 369 case HOLDING: 370 // update timer field 371 mCallTime.cancelTimer(); 372 373 break; 374 375 case DISCONNECTED: 376 // Stop getting timer ticks from this call 377 mCallTime.cancelTimer(); 378 379 break; 380 381 case DIALING: 382 case ALERTING: 383 // Stop getting timer ticks from a previous call 384 mCallTime.cancelTimer(); 385 386 break; 387 388 case INCOMING: 389 case WAITING: 390 // Stop getting timer ticks from a previous call 391 mCallTime.cancelTimer(); 392 393 break; 394 395 case IDLE: 396 // The "main CallCard" should never be trying to display 397 // an idle call! In updateState(), if the phone is idle, 398 // we call updateNoCall(), which means that we shouldn't 399 // have passed a call into this method at all. 400 Log.w(LOG_TAG, "displayMainCallStatus: IDLE call in the main call card!"); 401 402 // (It is possible, though, that we had a valid call which 403 // became idle *after* the check in updateState() but 404 // before we get here... So continue the best we can, 405 // with whatever (stale) info we can get from the 406 // passed-in Call object.) 407 408 break; 409 410 default: 411 Log.w(LOG_TAG, "displayMainCallStatus: unexpected call state: " + state); 412 break; 413 } 414 415 updateCardTitleWidgets(call.getPhone(), call); 416 417 if (PhoneUtils.isConferenceCall(call)) { 418 // Update onscreen info for a conference call. 419 updateDisplayForConference(call); 420 } else { 421 // Update onscreen info for a regular call (which presumably 422 // has only one connection.) 423 Connection conn = null; 424 int phoneType = call.getPhone().getPhoneType(); 425 if (phoneType == Phone.PHONE_TYPE_CDMA) { 426 conn = call.getLatestConnection(); 427 } else if ((phoneType == Phone.PHONE_TYPE_GSM) 428 || (phoneType == Phone.PHONE_TYPE_SIP)) { 429 conn = call.getEarliestConnection(); 430 } else { 431 throw new IllegalStateException("Unexpected phone type: " + phoneType); 432 } 433 434 if (conn == null) { 435 if (DBG) log("displayMainCallStatus: connection is null, using default values."); 436 // if the connection is null, we run through the behaviour 437 // we had in the past, which breaks down into trivial steps 438 // with the current implementation of getCallerInfo and 439 // updateDisplayForPerson. 440 CallerInfo info = PhoneUtils.getCallerInfo(getContext(), null /* conn */); 441 updateDisplayForPerson(info, Connection.PRESENTATION_ALLOWED, false, call); 442 } else { 443 if (DBG) log(" - CONN: " + conn + ", state = " + conn.getState()); 444 int presentation = conn.getNumberPresentation(); 445 446 // make sure that we only make a new query when the current 447 // callerinfo differs from what we've been requested to display. 448 boolean runQuery = true; 449 Object o = conn.getUserData(); 450 if (o instanceof PhoneUtils.CallerInfoToken) { 451 runQuery = mPhotoTracker.isDifferentImageRequest( 452 ((PhoneUtils.CallerInfoToken) o).currentInfo); 453 } else { 454 runQuery = mPhotoTracker.isDifferentImageRequest(conn); 455 } 456 457 // Adding a check to see if the update was caused due to a Phone number update 458 // or CNAP update. If so then we need to start a new query 459 if (phoneType == Phone.PHONE_TYPE_CDMA) { 460 Object obj = conn.getUserData(); 461 String updatedNumber = conn.getAddress(); 462 String updatedCnapName = conn.getCnapName(); 463 CallerInfo info = null; 464 if (obj instanceof PhoneUtils.CallerInfoToken) { 465 info = ((PhoneUtils.CallerInfoToken) o).currentInfo; 466 } else if (o instanceof CallerInfo) { 467 info = (CallerInfo) o; 468 } 469 470 if (info != null) { 471 if (updatedNumber != null && !updatedNumber.equals(info.phoneNumber)) { 472 if (DBG) log("- displayMainCallStatus: updatedNumber = " 473 + updatedNumber); 474 runQuery = true; 475 } 476 if (updatedCnapName != null && !updatedCnapName.equals(info.cnapName)) { 477 if (DBG) log("- displayMainCallStatus: updatedCnapName = " 478 + updatedCnapName); 479 runQuery = true; 480 } 481 } 482 } 483 484 if (runQuery) { 485 if (DBG) log("- displayMainCallStatus: starting CallerInfo query..."); 486 PhoneUtils.CallerInfoToken info = 487 PhoneUtils.startGetCallerInfo(getContext(), conn, this, call); 488 updateDisplayForPerson(info.currentInfo, presentation, !info.isFinal, call); 489 } else { 490 // No need to fire off a new query. We do still need 491 // to update the display, though (since we might have 492 // previously been in the "conference call" state.) 493 if (DBG) log("- displayMainCallStatus: using data we already have..."); 494 if (o instanceof CallerInfo) { 495 CallerInfo ci = (CallerInfo) o; 496 // Update CNAP information if Phone state change occurred 497 ci.cnapName = conn.getCnapName(); 498 ci.numberPresentation = conn.getNumberPresentation(); 499 ci.namePresentation = conn.getCnapNamePresentation(); 500 if (DBG) log("- displayMainCallStatus: CNAP data from Connection: " 501 + "CNAP name=" + ci.cnapName 502 + ", Number/Name Presentation=" + ci.numberPresentation); 503 if (DBG) log(" ==> Got CallerInfo; updating display: ci = " + ci); 504 updateDisplayForPerson(ci, presentation, false, call); 505 } else if (o instanceof PhoneUtils.CallerInfoToken){ 506 CallerInfo ci = ((PhoneUtils.CallerInfoToken) o).currentInfo; 507 if (DBG) log("- displayMainCallStatus: CNAP data from Connection: " 508 + "CNAP name=" + ci.cnapName 509 + ", Number/Name Presentation=" + ci.numberPresentation); 510 if (DBG) log(" ==> Got CallerInfoToken; updating display: ci = " + ci); 511 updateDisplayForPerson(ci, presentation, true, call); 512 } else { 513 Log.w(LOG_TAG, "displayMainCallStatus: runQuery was false, " 514 + "but we didn't have a cached CallerInfo object! o = " + o); 515 // TODO: any easy way to recover here (given that 516 // the CallCard is probably displaying stale info 517 // right now?) Maybe force the CallCard into the 518 // "Unknown" state? 519 } 520 } 521 } 522 } 523 524 // In some states we override the "photo" ImageView to be an 525 // indication of the current state, rather than displaying the 526 // regular photo as set above. 527 updatePhotoForCallState(call); 528 529 // One special feature of the "number" text field: For incoming 530 // calls, while the user is dragging the RotarySelector widget, we 531 // use mPhoneNumber to display a hint like "Rotate to answer". 532 if (mRotarySelectorHintTextResId != 0) { 533 // Display the hint! 534 mPhoneNumber.setText(mRotarySelectorHintTextResId); 535 mPhoneNumber.setTextColor(getResources().getColor(mRotarySelectorHintColorResId)); 536 mPhoneNumber.setVisibility(View.VISIBLE); 537 mLabel.setVisibility(View.GONE); 538 } 539 // If we don't have a hint to display, just don't touch 540 // mPhoneNumber and mLabel. (Their text / color / visibility have 541 // already been set correctly, by either updateDisplayForPerson() 542 // or updateDisplayForConference().) 543 } 544 545 /** 546 * Implemented for CallerInfoAsyncQuery.OnQueryCompleteListener interface. 547 * refreshes the CallCard data when it called. 548 */ onQueryComplete(int token, Object cookie, CallerInfo ci)549 public void onQueryComplete(int token, Object cookie, CallerInfo ci) { 550 if (DBG) log("onQueryComplete: token " + token + ", cookie " + cookie + ", ci " + ci); 551 552 if (cookie instanceof Call) { 553 // grab the call object and update the display for an individual call, 554 // as well as the successive call to update image via call state. 555 // If the object is a textview instead, we update it as we need to. 556 if (DBG) log("callerinfo query complete, updating ui from displayMainCallStatus()"); 557 Call call = (Call) cookie; 558 Connection conn = null; 559 int phoneType = call.getPhone().getPhoneType(); 560 if (phoneType == Phone.PHONE_TYPE_CDMA) { 561 conn = call.getLatestConnection(); 562 } else if ((phoneType == Phone.PHONE_TYPE_GSM) 563 || (phoneType == Phone.PHONE_TYPE_SIP)) { 564 conn = call.getEarliestConnection(); 565 } else { 566 throw new IllegalStateException("Unexpected phone type: " + phoneType); 567 } 568 PhoneUtils.CallerInfoToken cit = 569 PhoneUtils.startGetCallerInfo(getContext(), conn, this, null); 570 571 int presentation = Connection.PRESENTATION_ALLOWED; 572 if (conn != null) presentation = conn.getNumberPresentation(); 573 if (DBG) log("- onQueryComplete: presentation=" + presentation 574 + ", contactExists=" + ci.contactExists); 575 576 // Depending on whether there was a contact match or not, we want to pass in different 577 // CallerInfo (for CNAP). Therefore if ci.contactExists then use the ci passed in. 578 // Otherwise, regenerate the CIT from the Connection and use the CallerInfo from there. 579 if (ci.contactExists) { 580 updateDisplayForPerson(ci, Connection.PRESENTATION_ALLOWED, false, call); 581 } else { 582 updateDisplayForPerson(cit.currentInfo, presentation, false, call); 583 } 584 updatePhotoForCallState(call); 585 586 } else if (cookie instanceof TextView){ 587 if (DBG) log("callerinfo query complete, updating ui from ongoing or onhold"); 588 ((TextView) cookie).setText(PhoneUtils.getCompactNameFromCallerInfo(ci, mContext)); 589 } 590 } 591 592 /** 593 * Implemented for ContactsAsyncHelper.OnImageLoadCompleteListener interface. 594 * make sure that the call state is reflected after the image is loaded. 595 */ onImageLoadComplete(int token, Object cookie, ImageView iView, boolean imagePresent)596 public void onImageLoadComplete(int token, Object cookie, ImageView iView, 597 boolean imagePresent){ 598 if (cookie != null) { 599 updatePhotoForCallState((Call) cookie); 600 } 601 } 602 603 /** 604 * Updates the "card title" (and also elapsed time widget) based on 605 * the current state of the call. 606 */ 607 // TODO: it's confusing for updateCardTitleWidgets() and 608 // getTitleForCallCard() to be separate methods, since they both 609 // just list out the exact same "phone state" cases. 610 // Let's merge the getTitleForCallCard() logic into here. updateCardTitleWidgets(Phone phone, Call call)611 private void updateCardTitleWidgets(Phone phone, Call call) { 612 if (DBG) log("updateCardTitleWidgets(call " + call + ")..."); 613 Call.State state = call.getState(); 614 Context context = getContext(); 615 616 // TODO: Still need clearer spec on exactly how title *and* status get 617 // set in all states. (Then, given that info, refactor the code 618 // here to be more clear about exactly which widgets on the card 619 // need to be set.) 620 621 String cardTitle; 622 int phoneType = phone.getPhoneType(); 623 if (phoneType == Phone.PHONE_TYPE_CDMA) { 624 if (!PhoneApp.getInstance().notifier.getIsCdmaRedialCall()) { 625 cardTitle = getTitleForCallCard(call); // Normal "foreground" call card 626 } else { 627 cardTitle = context.getString(R.string.card_title_redialing); 628 } 629 } else if ((phoneType == Phone.PHONE_TYPE_GSM) 630 || (phoneType == Phone.PHONE_TYPE_SIP)) { 631 cardTitle = getTitleForCallCard(call); 632 } else { 633 throw new IllegalStateException("Unexpected phone type: " + phoneType); 634 } 635 if (DBG) log("updateCardTitleWidgets: " + cardTitle); 636 637 // Update the title and elapsed time widgets based on the current call state. 638 switch (state) { 639 case ACTIVE: 640 case DISCONNECTING: 641 final boolean bluetoothActive = mApplication.showBluetoothIndication(); 642 int ongoingCallIcon = bluetoothActive ? R.drawable.ic_incall_ongoing_bluetooth 643 : R.drawable.ic_incall_ongoing; 644 int connectedTextColor = bluetoothActive 645 ? mTextColorConnectedBluetooth : mTextColorConnected; 646 647 if (phoneType == Phone.PHONE_TYPE_CDMA) { 648 // In normal operation we don't use an "upper title" at all, 649 // except for a couple of special cases: 650 if (mApplication.cdmaPhoneCallState.IsThreeWayCallOrigStateDialing()) { 651 // Display "Dialing" while dialing a 3Way call, even 652 // though the foreground call state is still ACTIVE. 653 setUpperTitle(cardTitle, mTextColorDefaultPrimary, state); 654 } else if (PhoneUtils.isPhoneInEcm(phone)) { 655 // In emergency callback mode (ECM), use a special title 656 // that shows your own phone number. 657 cardTitle = getECMCardTitle(context, phone); 658 setUpperTitle(cardTitle, mTextColorDefaultPrimary, state); 659 } else { 660 // Normal "ongoing call" state; don't use any "title" at all. 661 clearUpperTitle(); 662 } 663 } else if ((phoneType == Phone.PHONE_TYPE_GSM) 664 || (phoneType == Phone.PHONE_TYPE_SIP)) { 665 // While in the DISCONNECTING state we display a 666 // "Hanging up" message in order to make the UI feel more 667 // responsive. (In GSM it's normal to see a delay of a 668 // couple of seconds while negotiating the disconnect with 669 // the network, so the "Hanging up" state at least lets 670 // the user know that we're doing something.) 671 // TODO: consider displaying the "Hanging up" state for 672 // CDMA also if the latency there ever gets high enough. 673 if (state == Call.State.DISCONNECTING) { 674 // Display the brief "Hanging up" indication. 675 setUpperTitle(cardTitle, mTextColorDefaultPrimary, state); 676 } else { // state == Call.State.ACTIVE 677 // Normal "ongoing call" state; don't use any "title" at all. 678 clearUpperTitle(); 679 } 680 } 681 682 // Use the elapsed time widget to show the current call duration. 683 mElapsedTime.setVisibility(View.VISIBLE); 684 mElapsedTime.setTextColor(connectedTextColor); 685 long duration = CallTime.getCallDuration(call); // msec 686 updateElapsedTimeWidget(duration / 1000); 687 // Also see onTickForCallTimeElapsed(), which updates this 688 // widget once per second while the call is active. 689 break; 690 691 case DISCONNECTED: 692 // Display "Call ended" (or possibly some error indication; 693 // see getCallFailedString()) in the upper title, in red. 694 695 // TODO: display a "call ended" icon somewhere, like the old 696 // R.drawable.ic_incall_end? 697 698 setUpperTitle(cardTitle, mTextColorEnded, state); 699 700 // In the "Call ended" state, leave the mElapsedTime widget 701 // visible, but don't touch it (so we continue to see the elapsed time of 702 // the call that just ended.) 703 mElapsedTime.setVisibility(View.VISIBLE); 704 mElapsedTime.setTextColor(mTextColorEnded); 705 break; 706 707 case HOLDING: 708 // For a single call on hold, display the title "On hold" in 709 // orange. 710 // (But since the upper title overlaps the label of the 711 // Hold/Unhold button, we actually use the elapsedTime widget 712 // to display the title in this case.) 713 714 // TODO: display an "On hold" icon somewhere, like the old 715 // R.drawable.ic_incall_onhold? 716 717 clearUpperTitle(); 718 mElapsedTime.setText(cardTitle); 719 720 // While on hold, the elapsed time widget displays an 721 // "on hold" indication rather than an amount of time. 722 mElapsedTime.setVisibility(View.VISIBLE); 723 mElapsedTime.setTextColor(mTextColorOnHold); 724 break; 725 726 default: 727 // All other states (DIALING, INCOMING, etc.) use the "upper title": 728 setUpperTitle(cardTitle, mTextColorDefaultPrimary, state); 729 730 // ...and we don't show the elapsed time. 731 mElapsedTime.setVisibility(View.INVISIBLE); 732 break; 733 } 734 } 735 736 /** 737 * Updates mElapsedTime based on the specified number of seconds. 738 * A timeElapsed value of zero means to not show an elapsed time at all. 739 */ updateElapsedTimeWidget(long timeElapsed)740 private void updateElapsedTimeWidget(long timeElapsed) { 741 // if (DBG) log("updateElapsedTimeWidget: " + timeElapsed); 742 if (timeElapsed == 0) { 743 mElapsedTime.setText(""); 744 } else { 745 mElapsedTime.setText(DateUtils.formatElapsedTime(timeElapsed)); 746 } 747 } 748 749 /** 750 * Returns the "card title" displayed at the top of a foreground 751 * ("active") CallCard to indicate the current state of this call, like 752 * "Dialing" or "In call" or "On hold". A null return value means that 753 * there's no title string for this state. 754 */ getTitleForCallCard(Call call)755 private String getTitleForCallCard(Call call) { 756 String retVal = null; 757 Call.State state = call.getState(); 758 Context context = getContext(); 759 760 if (DBG) log("- getTitleForCallCard(Call " + call + ")..."); 761 762 switch (state) { 763 case IDLE: 764 break; 765 766 case ACTIVE: 767 // Title is "Call in progress". (Note this appears in the 768 // "lower title" area of the CallCard.) 769 int phoneType = call.getPhone().getPhoneType(); 770 if (phoneType == Phone.PHONE_TYPE_CDMA) { 771 if (mApplication.cdmaPhoneCallState.IsThreeWayCallOrigStateDialing()) { 772 retVal = context.getString(R.string.card_title_dialing); 773 } else { 774 retVal = context.getString(R.string.card_title_in_progress); 775 } 776 } else if ((phoneType == Phone.PHONE_TYPE_GSM) 777 || (phoneType == Phone.PHONE_TYPE_SIP)) { 778 retVal = context.getString(R.string.card_title_in_progress); 779 } else { 780 throw new IllegalStateException("Unexpected phone type: " + phoneType); 781 } 782 break; 783 784 case HOLDING: 785 retVal = context.getString(R.string.card_title_on_hold); 786 // TODO: if this is a conference call on hold, 787 // maybe have a special title here too? 788 break; 789 790 case DIALING: 791 case ALERTING: 792 retVal = context.getString(R.string.card_title_dialing); 793 break; 794 795 case INCOMING: 796 case WAITING: 797 retVal = context.getString(R.string.card_title_incoming_call); 798 break; 799 800 case DISCONNECTING: 801 retVal = context.getString(R.string.card_title_hanging_up); 802 break; 803 804 case DISCONNECTED: 805 retVal = getCallFailedString(call); 806 break; 807 } 808 809 if (DBG) log(" ==> result: " + retVal); 810 return retVal; 811 } 812 813 /** 814 * Updates the "on hold" box in the "other call" info area 815 * (ie. the stuff in the secondaryCallInfo block) 816 * based on the specified Call. 817 * Or, clear out the "on hold" box if the specified call 818 * is null or idle. 819 */ displayOnHoldCallStatus(CallManager cm, Call call)820 private void displayOnHoldCallStatus(CallManager cm, Call call) { 821 if (DBG) log("displayOnHoldCallStatus(call =" + call + ")..."); 822 823 if ((call == null) || (PhoneApp.getInstance().isOtaCallInActiveState())) { 824 mSecondaryCallInfo.setVisibility(View.GONE); 825 return; 826 } 827 828 boolean showSecondaryCallInfo = false; 829 Call.State state = call.getState(); 830 switch (state) { 831 case HOLDING: 832 // Ok, there actually is a background call on hold. 833 // Display the "on hold" box. 834 835 // Note this case occurs only on GSM devices. (On CDMA, 836 // the "call on hold" is actually the 2nd connection of 837 // that ACTIVE call; see the ACTIVE case below.) 838 839 if (PhoneUtils.isConferenceCall(call)) { 840 if (DBG) log("==> conference call."); 841 mSecondaryCallName.setText(getContext().getString(R.string.confCall)); 842 showImage(mSecondaryCallPhoto, R.drawable.picture_conference); 843 } else { 844 // perform query and update the name temporarily 845 // make sure we hand the textview we want updated to the 846 // callback function. 847 if (DBG) log("==> NOT a conf call; call startGetCallerInfo..."); 848 PhoneUtils.CallerInfoToken infoToken = PhoneUtils.startGetCallerInfo( 849 getContext(), call, this, mSecondaryCallName); 850 mSecondaryCallName.setText( 851 PhoneUtils.getCompactNameFromCallerInfo(infoToken.currentInfo, 852 getContext())); 853 854 // Also pull the photo out of the current CallerInfo. 855 // (Note we assume we already have a valid photo at 856 // this point, since *presumably* the caller-id query 857 // was already run at some point *before* this call 858 // got put on hold. If there's no cached photo, just 859 // fall back to the default "unknown" image.) 860 if (infoToken.isFinal) { 861 showCachedImage(mSecondaryCallPhoto, infoToken.currentInfo); 862 } else { 863 showImage(mSecondaryCallPhoto, R.drawable.picture_unknown); 864 } 865 } 866 867 showSecondaryCallInfo = true; 868 869 break; 870 871 case ACTIVE: 872 // CDMA: This is because in CDMA when the user originates the second call, 873 // although the Foreground call state is still ACTIVE in reality the network 874 // put the first call on hold. 875 if (mApplication.phone.getPhoneType() == Phone.PHONE_TYPE_CDMA) { 876 List<Connection> connections = call.getConnections(); 877 if (connections.size() > 2) { 878 // This means that current Mobile Originated call is the not the first 3-Way 879 // call the user is making, which in turn tells the PhoneApp that we no 880 // longer know which previous caller/party had dropped out before the user 881 // made this call. 882 mSecondaryCallName.setText( 883 getContext().getString(R.string.card_title_in_call)); 884 showImage(mSecondaryCallPhoto, R.drawable.picture_unknown); 885 } else { 886 // This means that the current Mobile Originated call IS the first 3-Way 887 // and hence we display the first callers/party's info here. 888 Connection conn = call.getEarliestConnection(); 889 PhoneUtils.CallerInfoToken infoToken = PhoneUtils.startGetCallerInfo( 890 getContext(), conn, this, mSecondaryCallName); 891 892 // Get the compactName to be displayed, but then check that against 893 // the number presentation value for the call. If it's not an allowed 894 // presentation, then display the appropriate presentation string instead. 895 CallerInfo info = infoToken.currentInfo; 896 897 String name = PhoneUtils.getCompactNameFromCallerInfo(info, getContext()); 898 boolean forceGenericPhoto = false; 899 if (info != null && info.numberPresentation != 900 Connection.PRESENTATION_ALLOWED) { 901 name = getPresentationString(info.numberPresentation); 902 forceGenericPhoto = true; 903 } 904 mSecondaryCallName.setText(name); 905 906 // Also pull the photo out of the current CallerInfo. 907 // (Note we assume we already have a valid photo at 908 // this point, since *presumably* the caller-id query 909 // was already run at some point *before* this call 910 // got put on hold. If there's no cached photo, just 911 // fall back to the default "unknown" image.) 912 if (!forceGenericPhoto && infoToken.isFinal) { 913 showCachedImage(mSecondaryCallPhoto, info); 914 } else { 915 showImage(mSecondaryCallPhoto, R.drawable.picture_unknown); 916 } 917 } 918 showSecondaryCallInfo = true; 919 920 } else { 921 // We shouldn't ever get here at all for non-CDMA devices. 922 Log.w(LOG_TAG, "displayOnHoldCallStatus: ACTIVE state on non-CDMA device"); 923 showSecondaryCallInfo = false; 924 } 925 break; 926 927 default: 928 // There's actually no call on hold. (Presumably this call's 929 // state is IDLE, since any other state is meaningless for the 930 // background call.) 931 showSecondaryCallInfo = false; 932 break; 933 } 934 935 if (showSecondaryCallInfo) { 936 // Ok, we have something useful to display in the "secondary 937 // call" info area. 938 mSecondaryCallInfo.setVisibility(View.VISIBLE); 939 940 // Watch out: there are some cases where we need to display the 941 // secondary call photo but *not* the two lines of text above it. 942 // Specifically, that's any state where the CallCard "upper title" is 943 // in use, since the title (e.g. "Dialing" or "Call ended") might 944 // collide with the secondaryCallStatus and secondaryCallName widgets. 945 // 946 // We detect this case by simply seeing whether or not there's any text 947 // in mUpperTitle. (This is much simpler than detecting all possible 948 // telephony states where the "upper title" is used! But note it does 949 // rely on the fact that updateCardTitleWidgets() gets called *earlier* 950 // than this method, in the CallCard.updateState() sequence...) 951 boolean okToShowLabels = TextUtils.isEmpty(mUpperTitle.getText()); 952 mSecondaryCallName.setVisibility(okToShowLabels ? View.VISIBLE : View.INVISIBLE); 953 mSecondaryCallStatus.setVisibility(okToShowLabels ? View.VISIBLE : View.INVISIBLE); 954 } else { 955 // Hide the entire "secondary call" info area. 956 mSecondaryCallInfo.setVisibility(View.GONE); 957 } 958 } 959 getCallFailedString(Call call)960 private String getCallFailedString(Call call) { 961 Connection c = call.getEarliestConnection(); 962 int resID; 963 964 if (c == null) { 965 if (DBG) log("getCallFailedString: connection is null, using default values."); 966 // if this connection is null, just assume that the 967 // default case occurs. 968 resID = R.string.card_title_call_ended; 969 } else { 970 971 Connection.DisconnectCause cause = c.getDisconnectCause(); 972 973 // TODO: The card *title* should probably be "Call ended" in all 974 // cases, but if the DisconnectCause was an error condition we should 975 // probably also display the specific failure reason somewhere... 976 977 switch (cause) { 978 case BUSY: 979 resID = R.string.callFailed_userBusy; 980 break; 981 982 case CONGESTION: 983 resID = R.string.callFailed_congestion; 984 break; 985 986 case TIMED_OUT: 987 resID = R.string.callFailed_timedOut; 988 break; 989 990 case SERVER_UNREACHABLE: 991 resID = R.string.callFailed_server_unreachable; 992 break; 993 994 case NUMBER_UNREACHABLE: 995 resID = R.string.callFailed_number_unreachable; 996 break; 997 998 case INVALID_CREDENTIALS: 999 resID = R.string.callFailed_invalid_credentials; 1000 break; 1001 1002 case SERVER_ERROR: 1003 resID = R.string.callFailed_server_error; 1004 break; 1005 1006 case OUT_OF_NETWORK: 1007 resID = R.string.callFailed_out_of_network; 1008 break; 1009 1010 case LOST_SIGNAL: 1011 case CDMA_DROP: 1012 resID = R.string.callFailed_noSignal; 1013 break; 1014 1015 case LIMIT_EXCEEDED: 1016 resID = R.string.callFailed_limitExceeded; 1017 break; 1018 1019 case POWER_OFF: 1020 resID = R.string.callFailed_powerOff; 1021 break; 1022 1023 case ICC_ERROR: 1024 resID = R.string.callFailed_simError; 1025 break; 1026 1027 case OUT_OF_SERVICE: 1028 resID = R.string.callFailed_outOfService; 1029 break; 1030 1031 case INVALID_NUMBER: 1032 case UNOBTAINABLE_NUMBER: 1033 resID = R.string.callFailed_unobtainable_number; 1034 break; 1035 1036 default: 1037 resID = R.string.card_title_call_ended; 1038 break; 1039 } 1040 } 1041 return getContext().getString(resID); 1042 } 1043 1044 /** 1045 * Updates the name / photo / number / label fields on the CallCard 1046 * based on the specified CallerInfo. 1047 * 1048 * If the current call is a conference call, use 1049 * updateDisplayForConference() instead. 1050 */ updateDisplayForPerson(CallerInfo info, int presentation, boolean isTemporary, Call call)1051 private void updateDisplayForPerson(CallerInfo info, 1052 int presentation, 1053 boolean isTemporary, 1054 Call call) { 1055 if (DBG) log("updateDisplayForPerson(" + info + ")\npresentation:" + 1056 presentation + " isTemporary:" + isTemporary); 1057 1058 // inform the state machine that we are displaying a photo. 1059 mPhotoTracker.setPhotoRequest(info); 1060 mPhotoTracker.setPhotoState(ContactsAsyncHelper.ImageTracker.DISPLAY_IMAGE); 1061 1062 // The actual strings we're going to display onscreen: 1063 String displayName; 1064 String displayNumber = null; 1065 String label = null; 1066 Uri personUri = null; 1067 String socialStatusText = null; 1068 Drawable socialStatusBadge = null; 1069 1070 if (info != null) { 1071 // It appears that there is a small change in behaviour with the 1072 // PhoneUtils' startGetCallerInfo whereby if we query with an 1073 // empty number, we will get a valid CallerInfo object, but with 1074 // fields that are all null, and the isTemporary boolean input 1075 // parameter as true. 1076 1077 // In the past, we would see a NULL callerinfo object, but this 1078 // ends up causing null pointer exceptions elsewhere down the 1079 // line in other cases, so we need to make this fix instead. It 1080 // appears that this was the ONLY call to PhoneUtils 1081 // .getCallerInfo() that relied on a NULL CallerInfo to indicate 1082 // an unknown contact. 1083 1084 // Currently, info.phoneNumber may actually be a SIP address, and 1085 // if so, it might sometimes include the "sip:" prefix. That 1086 // prefix isn't really useful to the user, though, so strip it off 1087 // if present. (For any other URI scheme, though, leave the 1088 // prefix alone.) 1089 // TODO: It would be cleaner for CallerInfo to explicitly support 1090 // SIP addresses instead of overloading the "phoneNumber" field. 1091 // Then we could remove this hack, and instead ask the CallerInfo 1092 // for a "user visible" form of the SIP address. 1093 String number = info.phoneNumber; 1094 if ((number != null) && number.startsWith("sip:")) { 1095 number = number.substring(4); 1096 } 1097 1098 if (TextUtils.isEmpty(info.name)) { 1099 // No valid "name" in the CallerInfo, so fall back to 1100 // something else. 1101 // (Typically, we promote the phone number up to the "name" 1102 // slot onscreen, and leave the "number" slot empty.) 1103 if (TextUtils.isEmpty(number)) { 1104 displayName = getPresentationString(presentation); 1105 } else if (presentation != Connection.PRESENTATION_ALLOWED) { 1106 // This case should never happen since the network should never send a phone # 1107 // AND a restricted presentation. However we leave it here in case of weird 1108 // network behavior 1109 displayName = getPresentationString(presentation); 1110 } else if (!TextUtils.isEmpty(info.cnapName)) { 1111 displayName = info.cnapName; 1112 info.name = info.cnapName; 1113 displayNumber = number; 1114 } else { 1115 displayName = number; 1116 } 1117 } else { 1118 // We do have a valid "name" in the CallerInfo. Display that 1119 // in the "name" slot, and the phone number in the "number" slot. 1120 if (presentation != Connection.PRESENTATION_ALLOWED) { 1121 // This case should never happen since the network should never send a name 1122 // AND a restricted presentation. However we leave it here in case of weird 1123 // network behavior 1124 displayName = getPresentationString(presentation); 1125 } else { 1126 displayName = info.name; 1127 displayNumber = number; 1128 label = info.phoneLabel; 1129 } 1130 } 1131 personUri = ContentUris.withAppendedId(Contacts.CONTENT_URI, info.person_id); 1132 if (DBG) log("- got personUri: '" + personUri 1133 + "', based on info.person_id: " + info.person_id); 1134 } else { 1135 displayName = getPresentationString(presentation); 1136 } 1137 1138 if (call.isGeneric()) { 1139 mName.setText(R.string.card_title_in_call); 1140 } else { 1141 mName.setText(displayName); 1142 } 1143 mName.setVisibility(View.VISIBLE); 1144 1145 // Update mPhoto 1146 // if the temporary flag is set, we know we'll be getting another call after 1147 // the CallerInfo has been correctly updated. So, we can skip the image 1148 // loading until then. 1149 1150 // If the photoResource is filled in for the CallerInfo, (like with the 1151 // Emergency Number case), then we can just set the photo image without 1152 // requesting for an image load. Please refer to CallerInfoAsyncQuery.java 1153 // for cases where CallerInfo.photoResource may be set. We can also avoid 1154 // the image load step if the image data is cached. 1155 if (isTemporary && (info == null || !info.isCachedPhotoCurrent)) { 1156 mPhoto.setVisibility(View.INVISIBLE); 1157 } else if (info != null && info.photoResource != 0){ 1158 showImage(mPhoto, info.photoResource); 1159 } else if (!showCachedImage(mPhoto, info)) { 1160 // Load the image with a callback to update the image state. 1161 // Use the default unknown picture while the query is running. 1162 ContactsAsyncHelper.updateImageViewWithContactPhotoAsync( 1163 info, 0, this, call, getContext(), mPhoto, personUri, R.drawable.picture_unknown); 1164 } 1165 // And no matter what, on all devices, we never see the "manage 1166 // conference" button in this state. 1167 mManageConferencePhotoButton.setVisibility(View.INVISIBLE); 1168 1169 if (displayNumber != null && !call.isGeneric()) { 1170 mPhoneNumber.setText(displayNumber); 1171 mPhoneNumber.setTextColor(mTextColorDefaultSecondary); 1172 mPhoneNumber.setVisibility(View.VISIBLE); 1173 } else { 1174 mPhoneNumber.setVisibility(View.GONE); 1175 } 1176 1177 if (label != null && !call.isGeneric()) { 1178 mLabel.setText(label); 1179 mLabel.setVisibility(View.VISIBLE); 1180 } else { 1181 mLabel.setVisibility(View.GONE); 1182 } 1183 1184 // Other text fields: 1185 updateCallTypeLabel(call); 1186 updateSocialStatus(socialStatusText, socialStatusBadge, call); // Currently unused 1187 } 1188 getPresentationString(int presentation)1189 private String getPresentationString(int presentation) { 1190 String name = getContext().getString(R.string.unknown); 1191 if (presentation == Connection.PRESENTATION_RESTRICTED) { 1192 name = getContext().getString(R.string.private_num); 1193 } else if (presentation == Connection.PRESENTATION_PAYPHONE) { 1194 name = getContext().getString(R.string.payphone); 1195 } 1196 return name; 1197 } 1198 1199 /** 1200 * Updates the name / photo / number / label fields 1201 * for the special "conference call" state. 1202 * 1203 * If the current call has only a single connection, use 1204 * updateDisplayForPerson() instead. 1205 */ updateDisplayForConference(Call call)1206 private void updateDisplayForConference(Call call) { 1207 if (DBG) log("updateDisplayForConference()..."); 1208 1209 int phoneType = call.getPhone().getPhoneType(); 1210 if (phoneType == Phone.PHONE_TYPE_CDMA) { 1211 // This state corresponds to both 3-Way merged call and 1212 // Call Waiting accepted call. 1213 // In this case we display the UI in a "generic" state, with 1214 // the generic "dialing" icon and no caller information, 1215 // because in this state in CDMA the user does not really know 1216 // which caller party he is talking to. 1217 showImage(mPhoto, R.drawable.picture_dialing); 1218 mName.setText(R.string.card_title_in_call); 1219 } else if ((phoneType == Phone.PHONE_TYPE_GSM) 1220 || (phoneType == Phone.PHONE_TYPE_SIP)) { 1221 if (mInCallScreen.isTouchUiEnabled()) { 1222 // Display the "manage conference" button in place of the photo. 1223 mManageConferencePhotoButton.setVisibility(View.VISIBLE); 1224 mPhoto.setVisibility(View.INVISIBLE); // Not GONE, since that would break 1225 // other views in our RelativeLayout. 1226 } else { 1227 // Display the "conference call" image in the photo slot, 1228 // with no other information. 1229 showImage(mPhoto, R.drawable.picture_conference); 1230 } 1231 mName.setText(R.string.card_title_conf_call); 1232 } else { 1233 throw new IllegalStateException("Unexpected phone type: " + phoneType); 1234 } 1235 1236 mName.setVisibility(View.VISIBLE); 1237 1238 // TODO: For a conference call, the "phone number" slot is specced 1239 // to contain a summary of who's on the call, like "Bill Foldes 1240 // and Hazel Nutt" or "Bill Foldes and 2 others". 1241 // But for now, just hide it: 1242 mPhoneNumber.setVisibility(View.GONE); 1243 mLabel.setVisibility(View.GONE); 1244 1245 // Other text fields: 1246 updateCallTypeLabel(call); 1247 updateSocialStatus(null, null, null); // socialStatus is never visible in this state 1248 1249 // TODO: for a GSM conference call, since we do actually know who 1250 // you're talking to, consider also showing names / numbers / 1251 // photos of some of the people on the conference here, so you can 1252 // see that info without having to click "Manage conference". We 1253 // probably have enough space to show info for 2 people, at least. 1254 // 1255 // To do this, our caller would pass us the activeConnections 1256 // list, and we'd call PhoneUtils.getCallerInfo() separately for 1257 // each connection. 1258 } 1259 1260 /** 1261 * Updates the CallCard "photo" IFF the specified Call is in a state 1262 * that needs a special photo (like "busy" or "dialing".) 1263 * 1264 * If the current call does not require a special image in the "photo" 1265 * slot onscreen, don't do anything, since presumably the photo image 1266 * has already been set (to the photo of the person we're talking, or 1267 * the generic "picture_unknown" image, or the "conference call" 1268 * image.) 1269 */ updatePhotoForCallState(Call call)1270 private void updatePhotoForCallState(Call call) { 1271 if (DBG) log("updatePhotoForCallState(" + call + ")..."); 1272 int photoImageResource = 0; 1273 1274 // Check for the (relatively few) telephony states that need a 1275 // special image in the "photo" slot. 1276 Call.State state = call.getState(); 1277 switch (state) { 1278 case DISCONNECTED: 1279 // Display the special "busy" photo for BUSY or CONGESTION. 1280 // Otherwise (presumably the normal "call ended" state) 1281 // leave the photo alone. 1282 Connection c = call.getEarliestConnection(); 1283 // if the connection is null, we assume the default case, 1284 // otherwise update the image resource normally. 1285 if (c != null) { 1286 Connection.DisconnectCause cause = c.getDisconnectCause(); 1287 if ((cause == Connection.DisconnectCause.BUSY) 1288 || (cause == Connection.DisconnectCause.CONGESTION)) { 1289 photoImageResource = R.drawable.picture_busy; 1290 } 1291 } else if (DBG) { 1292 log("updatePhotoForCallState: connection is null, ignoring."); 1293 } 1294 1295 // TODO: add special images for any other DisconnectCauses? 1296 break; 1297 1298 case ALERTING: 1299 case DIALING: 1300 default: 1301 // Leave the photo alone in all other states. 1302 // If this call is an individual call, and the image is currently 1303 // displaying a state, (rather than a photo), we'll need to update 1304 // the image. 1305 // This is for the case where we've been displaying the state and 1306 // now we need to restore the photo. This can happen because we 1307 // only query the CallerInfo once, and limit the number of times 1308 // the image is loaded. (So a state image may overwrite the photo 1309 // and we would otherwise have no way of displaying the photo when 1310 // the state goes away.) 1311 1312 // if the photoResource field is filled-in in the Connection's 1313 // caller info, then we can just use that instead of requesting 1314 // for a photo load. 1315 1316 // look for the photoResource if it is available. 1317 CallerInfo ci = null; 1318 { 1319 Connection conn = null; 1320 int phoneType = call.getPhone().getPhoneType(); 1321 if (phoneType == Phone.PHONE_TYPE_CDMA) { 1322 conn = call.getLatestConnection(); 1323 } else if ((phoneType == Phone.PHONE_TYPE_GSM) 1324 || (phoneType == Phone.PHONE_TYPE_SIP)) { 1325 conn = call.getEarliestConnection(); 1326 } else { 1327 throw new IllegalStateException("Unexpected phone type: " + phoneType); 1328 } 1329 1330 if (conn != null) { 1331 Object o = conn.getUserData(); 1332 if (o instanceof CallerInfo) { 1333 ci = (CallerInfo) o; 1334 } else if (o instanceof PhoneUtils.CallerInfoToken) { 1335 ci = ((PhoneUtils.CallerInfoToken) o).currentInfo; 1336 } 1337 } 1338 } 1339 1340 if (ci != null) { 1341 photoImageResource = ci.photoResource; 1342 } 1343 1344 // If no photoResource found, check to see if this is a conference call. If 1345 // it is not a conference call: 1346 // 1. Try to show the cached image 1347 // 2. If the image is not cached, check to see if a load request has been 1348 // made already. 1349 // 3. If the load request has not been made [DISPLAY_DEFAULT], start the 1350 // request and note that it has started by updating photo state with 1351 // [DISPLAY_IMAGE]. 1352 // Load requests started in (3) use a placeholder image of -1 to hide the 1353 // image by default. Please refer to CallerInfoAsyncQuery.java for cases 1354 // where CallerInfo.photoResource may be set. 1355 if (photoImageResource == 0) { 1356 if (!PhoneUtils.isConferenceCall(call)) { 1357 if (!showCachedImage(mPhoto, ci) && (mPhotoTracker.getPhotoState() == 1358 ContactsAsyncHelper.ImageTracker.DISPLAY_DEFAULT)) { 1359 ContactsAsyncHelper.updateImageViewWithContactPhotoAsync(ci, 1360 getContext(), mPhoto, mPhotoTracker.getPhotoUri(), -1); 1361 mPhotoTracker.setPhotoState( 1362 ContactsAsyncHelper.ImageTracker.DISPLAY_IMAGE); 1363 } 1364 } 1365 } else { 1366 showImage(mPhoto, photoImageResource); 1367 mPhotoTracker.setPhotoState(ContactsAsyncHelper.ImageTracker.DISPLAY_IMAGE); 1368 return; 1369 } 1370 break; 1371 } 1372 1373 if (photoImageResource != 0) { 1374 if (DBG) log("- overrriding photo image: " + photoImageResource); 1375 showImage(mPhoto, photoImageResource); 1376 // Track the image state. 1377 mPhotoTracker.setPhotoState(ContactsAsyncHelper.ImageTracker.DISPLAY_DEFAULT); 1378 } 1379 } 1380 1381 /** 1382 * Try to display the cached image from the callerinfo object. 1383 * 1384 * @return true if we were able to find the image in the cache, false otherwise. 1385 */ showCachedImage(ImageView view, CallerInfo ci)1386 private static final boolean showCachedImage(ImageView view, CallerInfo ci) { 1387 if ((ci != null) && ci.isCachedPhotoCurrent) { 1388 if (ci.cachedPhoto != null) { 1389 showImage(view, ci.cachedPhoto); 1390 } else { 1391 showImage(view, R.drawable.picture_unknown); 1392 } 1393 return true; 1394 } 1395 return false; 1396 } 1397 1398 /** Helper function to display the resource in the imageview AND ensure its visibility.*/ showImage(ImageView view, int resource)1399 private static final void showImage(ImageView view, int resource) { 1400 view.setImageResource(resource); 1401 view.setVisibility(View.VISIBLE); 1402 } 1403 1404 /** Helper function to display the drawable in the imageview AND ensure its visibility.*/ showImage(ImageView view, Drawable drawable)1405 private static final void showImage(ImageView view, Drawable drawable) { 1406 view.setImageDrawable(drawable); 1407 view.setVisibility(View.VISIBLE); 1408 } 1409 1410 /** 1411 * Returns the "Menu button hint" TextView (which is manipulated 1412 * directly by the InCallScreen.) 1413 * @see InCallScreen.updateMenuButtonHint() 1414 */ getMenuButtonHint()1415 /* package */ TextView getMenuButtonHint() { 1416 return mMenuButtonHint; 1417 } 1418 1419 /** 1420 * Sets the left and right margins of the specified ViewGroup (whose 1421 * LayoutParams object which must inherit from 1422 * ViewGroup.MarginLayoutParams.) 1423 * 1424 * TODO: Is there already a convenience method like this somewhere? 1425 */ setSideMargins(ViewGroup vg, int margin)1426 private void setSideMargins(ViewGroup vg, int margin) { 1427 ViewGroup.MarginLayoutParams lp = 1428 (ViewGroup.MarginLayoutParams) vg.getLayoutParams(); 1429 // Equivalent to setting android:layout_marginLeft/Right in XML 1430 lp.leftMargin = margin; 1431 lp.rightMargin = margin; 1432 vg.setLayoutParams(lp); 1433 } 1434 1435 /** 1436 * Sets the CallCard "upper title". Also, depending on the passed-in 1437 * Call state, possibly display an icon along with the title. 1438 */ setUpperTitle(String title, int color, Call.State state)1439 private void setUpperTitle(String title, int color, Call.State state) { 1440 mUpperTitle.setText(title); 1441 mUpperTitle.setTextColor(color); 1442 1443 int bluetoothIconId = 0; 1444 if (!TextUtils.isEmpty(title) 1445 && ((state == Call.State.INCOMING) || (state == Call.State.WAITING)) 1446 && mApplication.showBluetoothIndication()) { 1447 // Display the special bluetooth icon also, if this is an incoming 1448 // call and the audio will be routed to bluetooth. 1449 bluetoothIconId = R.drawable.ic_incoming_call_bluetooth; 1450 } 1451 1452 mUpperTitle.setCompoundDrawablesWithIntrinsicBounds(bluetoothIconId, 0, 0, 0); 1453 if (bluetoothIconId != 0) mUpperTitle.setCompoundDrawablePadding((int) (mDensity * 5)); 1454 } 1455 1456 /** 1457 * Clears the CallCard "upper title", for states (like a normal 1458 * ongoing call) where we don't use any "title" at all. 1459 */ clearUpperTitle()1460 private void clearUpperTitle() { 1461 setUpperTitle("", 0, Call.State.IDLE); // Use dummy values for "color" and "state" 1462 } 1463 1464 /** 1465 * Returns the special card title used in emergency callback mode (ECM), 1466 * which shows your own phone number. 1467 */ getECMCardTitle(Context context, Phone phone)1468 private String getECMCardTitle(Context context, Phone phone) { 1469 String rawNumber = phone.getLine1Number(); // may be null or empty 1470 String formattedNumber; 1471 if (!TextUtils.isEmpty(rawNumber)) { 1472 formattedNumber = PhoneNumberUtils.formatNumber(rawNumber); 1473 } else { 1474 formattedNumber = context.getString(R.string.unknown); 1475 } 1476 String titleFormat = context.getString(R.string.card_title_my_phone_number); 1477 return String.format(titleFormat, formattedNumber); 1478 } 1479 1480 /** 1481 * Updates the "Call type" label, based on the current foreground call. 1482 * This is a special label and/or branding we display for certain 1483 * kinds of calls. 1484 * 1485 * (So far, this is used only for SIP calls, which get an 1486 * "Internet call" label. TODO: But eventually, the telephony 1487 * layer might allow each pluggable "provider" to specify a string 1488 * and/or icon to be displayed here.) 1489 */ updateCallTypeLabel(Call call)1490 private void updateCallTypeLabel(Call call) { 1491 int phoneType = (call != null) ? call.getPhone().getPhoneType() : Phone.PHONE_TYPE_NONE; 1492 if (phoneType == Phone.PHONE_TYPE_SIP) { 1493 mCallTypeLabel.setVisibility(View.VISIBLE); 1494 mCallTypeLabel.setText(R.string.incall_call_type_label_sip); 1495 mCallTypeLabel.setTextColor(mTextColorCallTypeSip); 1496 // If desired, we could also display a "badge" next to the label, as follows: 1497 // mCallTypeLabel.setCompoundDrawablesWithIntrinsicBounds( 1498 // callTypeSpecificBadge, null, null, null); 1499 // mCallTypeLabel.setCompoundDrawablePadding((int) (mDensity * 6)); 1500 } else { 1501 mCallTypeLabel.setVisibility(View.GONE); 1502 } 1503 } 1504 1505 /** 1506 * Updates the "social status" label with the specified text and 1507 * (optional) badge. 1508 */ updateSocialStatus(String socialStatusText, Drawable socialStatusBadge, Call call)1509 private void updateSocialStatus(String socialStatusText, 1510 Drawable socialStatusBadge, 1511 Call call) { 1512 // The socialStatus field is *only* visible while an incoming call 1513 // is ringing, never in any other call state. 1514 if ((socialStatusText != null) 1515 && (call != null) 1516 && call.isRinging() 1517 && !call.isGeneric()) { 1518 mSocialStatus.setVisibility(View.VISIBLE); 1519 mSocialStatus.setText(socialStatusText); 1520 mSocialStatus.setCompoundDrawablesWithIntrinsicBounds( 1521 socialStatusBadge, null, null, null); 1522 mSocialStatus.setCompoundDrawablePadding((int) (mDensity * 6)); 1523 } else { 1524 mSocialStatus.setVisibility(View.GONE); 1525 } 1526 } 1527 1528 /** 1529 * Hides the top-level UI elements of the call card: The "main 1530 * call card" element representing the current active or ringing call, 1531 * and also the info areas for "ongoing" or "on hold" calls in some 1532 * states. 1533 * 1534 * This is intended to be used in special states where the normal 1535 * in-call UI is totally replaced by some other UI, like OTA mode on a 1536 * CDMA device. 1537 * 1538 * To bring back the regular CallCard UI, just re-run the normal 1539 * updateState() call sequence. 1540 */ hideCallCardElements()1541 public void hideCallCardElements() { 1542 mPrimaryCallInfo.setVisibility(View.GONE); 1543 mSecondaryCallInfo.setVisibility(View.GONE); 1544 } 1545 1546 /* 1547 * Updates the hint (like "Rotate to answer") that we display while 1548 * the user is dragging the incoming call RotarySelector widget. 1549 */ setRotarySelectorHint(int hintTextResId, int hintColorResId)1550 /* package */ void setRotarySelectorHint(int hintTextResId, int hintColorResId) { 1551 mRotarySelectorHintTextResId = hintTextResId; 1552 mRotarySelectorHintColorResId = hintColorResId; 1553 } 1554 1555 // View.OnClickListener implementation onClick(View view)1556 public void onClick(View view) { 1557 int id = view.getId(); 1558 if (DBG) log("onClick(View " + view + ", id " + id + ")..."); 1559 1560 switch (id) { 1561 case R.id.manageConferencePhotoButton: 1562 // A click on anything here gets forwarded 1563 // straight to the InCallScreen. 1564 mInCallScreen.handleOnscreenButtonClick(id); 1565 break; 1566 1567 default: 1568 Log.w(LOG_TAG, "onClick: unexpected click: View " + view + ", id " + id); 1569 break; 1570 } 1571 } 1572 1573 // Accessibility event support. 1574 // Since none of the CallCard elements are focusable, we need to manually 1575 // fill in the AccessibilityEvent here (so that the name / number / etc will 1576 // get pronounced by a screen reader, for example.) 1577 @Override dispatchPopulateAccessibilityEvent(AccessibilityEvent event)1578 public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) { 1579 dispatchPopulateAccessibilityEvent(event, mUpperTitle); 1580 dispatchPopulateAccessibilityEvent(event, mPhoto); 1581 dispatchPopulateAccessibilityEvent(event, mManageConferencePhotoButton); 1582 dispatchPopulateAccessibilityEvent(event, mName); 1583 dispatchPopulateAccessibilityEvent(event, mPhoneNumber); 1584 dispatchPopulateAccessibilityEvent(event, mLabel); 1585 dispatchPopulateAccessibilityEvent(event, mSocialStatus); 1586 dispatchPopulateAccessibilityEvent(event, mSecondaryCallName); 1587 dispatchPopulateAccessibilityEvent(event, mSecondaryCallStatus); 1588 dispatchPopulateAccessibilityEvent(event, mSecondaryCallPhoto); 1589 return true; 1590 } 1591 dispatchPopulateAccessibilityEvent(AccessibilityEvent event, View view)1592 private void dispatchPopulateAccessibilityEvent(AccessibilityEvent event, View view) { 1593 List<CharSequence> eventText = event.getText(); 1594 int size = eventText.size(); 1595 view.dispatchPopulateAccessibilityEvent(event); 1596 // if no text added write null to keep relative position 1597 if (size == eventText.size()) { 1598 eventText.add(null); 1599 } 1600 } 1601 1602 1603 // Debugging / testing code 1604 log(String msg)1605 private void log(String msg) { 1606 Log.d(LOG_TAG, msg); 1607 } 1608 } 1609