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