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