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