CallCard.java revision 8bb467d9a1106dedd79e42166c7b6e9fc9a897a7
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 com.android.internal.telephony.Call; 20import com.android.internal.telephony.CallerInfo; 21import com.android.internal.telephony.CallerInfoAsyncQuery; 22import com.android.internal.telephony.Connection; 23import com.android.internal.telephony.Phone; 24 25import android.content.ContentUris; 26import android.content.Context; 27import android.graphics.drawable.Drawable; 28import android.net.Uri; 29import android.pim.ContactsAsyncHelper; 30import android.provider.Contacts.People; 31import android.text.TextUtils; 32import android.text.format.DateUtils; 33import android.util.AttributeSet; 34import android.util.Log; 35import android.view.LayoutInflater; 36import android.view.MotionEvent; 37import android.view.View; 38import android.view.ViewGroup; 39import android.widget.FrameLayout; 40import android.widget.ImageView; 41import android.widget.TextView; 42 43/** 44 * "Call card" UI element: the in-call screen contains a tiled layout of call 45 * cards, each representing the state of a current "call" (ie. an active call, 46 * a call on hold, or an incoming call.) 47 */ 48public class CallCard extends FrameLayout 49 implements CallTime.OnTickListener, CallerInfoAsyncQuery.OnQueryCompleteListener, 50 ContactsAsyncHelper.OnImageLoadCompleteListener{ 51 private static final String LOG_TAG = "PHONE/CallCard"; 52 private static final boolean DBG = false; 53 private static final boolean PROFILE = true; 54 55 /** 56 * Reference to the InCallScreen activity that owns us. This may be 57 * null if we haven't been initialized yet *or* after the InCallScreen 58 * activity has been destroyed. 59 */ 60 private InCallScreen mInCallScreen; 61 62 // Top-level subviews of the CallCard 63 private ViewGroup mMainCallCard; 64 private ViewGroup mOtherCallOngoingInfoArea; 65 private ViewGroup mOtherCallOnHoldInfoArea; 66 67 // "Upper" and "lower" title widgets 68 private TextView mUpperTitle; 69 private ViewGroup mLowerTitleViewGroup; 70 private TextView mLowerTitle; 71 private ImageView mLowerTitleIcon; 72 private TextView mElapsedTime; 73 74 // Text colors, used with the lower title and "other call" info areas 75 private int mTextColorConnected; 76 private int mTextColorConnectedBluetooth; 77 private int mTextColorEnded; 78 private int mTextColorOnHold; 79 80 private ImageView mPhoto; 81 private TextView mName; 82 private TextView mPhoneNumber; 83 private TextView mLabel; 84 85 // "Other call" info area 86 private ImageView mOtherCallOngoingIcon; 87 private TextView mOtherCallOngoingName; 88 private TextView mOtherCallOngoingStatus; 89 private TextView mOtherCallOnHoldName; 90 private TextView mOtherCallOnHoldStatus; 91 92 // Menu button hint 93 private TextView mMenuButtonHint; 94 95 private boolean mRingerSilenced; 96 97 private CallTime mCallTime; 98 99 // Track the state for the photo. 100 private ContactsAsyncHelper.ImageTracker mPhotoTracker; 101 102 // A few hardwired constants used in our screen layout. 103 // TODO: These should all really come from resources, but that's 104 // nontrivial; see the javadoc for the ConfigurationHelper class. 105 // For now, let's at least keep them all here in one place 106 // rather than sprinkled througout this file. 107 // 108 static final int MAIN_CALLCARD_MIN_HEIGHT_LANDSCAPE = 200; 109 static final int CALLCARD_SIDE_MARGIN_LANDSCAPE = 50; 110 static final float TITLE_TEXT_SIZE_LANDSCAPE = 22F; // scaled pixels 111 112 public CallCard(Context context, AttributeSet attrs) { 113 super(context, attrs); 114 115 if (DBG) log("CallCard constructor..."); 116 if (DBG) log("- this = " + this); 117 if (DBG) log("- context " + context + ", attrs " + attrs); 118 119 // Inflate the contents of this CallCard, and add it (to ourself) as a child. 120 LayoutInflater inflater = LayoutInflater.from(context); 121 inflater.inflate( 122 R.layout.call_card, // resource 123 this, // root 124 true); 125 126 mCallTime = new CallTime(this); 127 128 // create a new object to track the state for the photo. 129 mPhotoTracker = new ContactsAsyncHelper.ImageTracker(); 130 } 131 132 void setInCallScreenInstance(InCallScreen inCallScreen) { 133 mInCallScreen = inCallScreen; 134 } 135 136 void reset() { 137 if (DBG) log("reset()..."); 138 139 mRingerSilenced = false; 140 141 // default to show ACTIVE call style, with empty title and status text 142 showCallConnected(); 143 setUpperTitle(""); 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 LayoutInflater inflater = LayoutInflater.from(getContext()); 164 165 mMainCallCard = (ViewGroup) findViewById(R.id.mainCallCard); 166 mOtherCallOngoingInfoArea = (ViewGroup) findViewById(R.id.otherCallOngoingInfoArea); 167 mOtherCallOnHoldInfoArea = (ViewGroup) findViewById(R.id.otherCallOnHoldInfoArea); 168 169 // "Upper" and "lower" title widgets 170 mUpperTitle = (TextView) findViewById(R.id.upperTitle); 171 mLowerTitleViewGroup = (ViewGroup) findViewById(R.id.lowerTitleViewGroup); 172 mLowerTitle = (TextView) findViewById(R.id.lowerTitle); 173 mLowerTitleIcon = (ImageView) findViewById(R.id.lowerTitleIcon); 174 mElapsedTime = (TextView) findViewById(R.id.elapsedTime); 175 176 // Text colors 177 mTextColorConnected = getResources().getColor(R.color.incall_textConnected); 178 mTextColorConnectedBluetooth = 179 getResources().getColor(R.color.incall_textConnectedBluetooth); 180 mTextColorEnded = getResources().getColor(R.color.incall_textEnded); 181 mTextColorOnHold = getResources().getColor(R.color.incall_textOnHold); 182 183 // "Caller info" area, including photo / name / phone numbers / etc 184 mPhoto = (ImageView) findViewById(R.id.photo); 185 mName = (TextView) findViewById(R.id.name); 186 mPhoneNumber = (TextView) findViewById(R.id.phoneNumber); 187 mLabel = (TextView) findViewById(R.id.label); 188 189 // "Other call" info area 190 mOtherCallOngoingIcon = (ImageView) findViewById(R.id.otherCallOngoingIcon); 191 mOtherCallOngoingName = (TextView) findViewById(R.id.otherCallOngoingName); 192 mOtherCallOngoingStatus = (TextView) findViewById(R.id.otherCallOngoingStatus); 193 mOtherCallOnHoldName = (TextView) findViewById(R.id.otherCallOnHoldName); 194 mOtherCallOnHoldStatus = (TextView) findViewById(R.id.otherCallOnHoldStatus); 195 196 // Menu Button hint 197 mMenuButtonHint = (TextView) findViewById(R.id.menuButtonHint); 198 } 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 // TODO: This code, and updateForegroundCall() / updateRingingCall(), 205 // can probably still be simplified some more. 206 207 Phone.State state = phone.getState(); // IDLE, RINGING, or OFFHOOK 208 if (state == Phone.State.RINGING) { 209 // A phone call is ringing *or* call waiting 210 // (ie. another call may also be active as well.) 211 updateRingingCall(phone); 212 } else if (state == Phone.State.OFFHOOK) { 213 // The phone is off hook. At least one call exists that is 214 // dialing, active, or holding, and no calls are ringing or waiting. 215 updateForegroundCall(phone); 216 } else { 217 // The phone state is IDLE! 218 // 219 // The most common reason for this is if a call just 220 // ended: the phone will be idle, but we *will* still 221 // have a call in the DISCONNECTED state: 222 Call fgCall = phone.getForegroundCall(); 223 Call bgCall = phone.getBackgroundCall(); 224 if ((fgCall.getState() == Call.State.DISCONNECTED) 225 || (bgCall.getState() == Call.State.DISCONNECTED)) { 226 // In this case, we want the main CallCard to display 227 // the "Call ended" state. The normal "foreground call" 228 // code path handles that. 229 updateForegroundCall(phone); 230 } else { 231 // We don't have any DISCONNECTED calls, which means 232 // that the phone is *truly* idle. 233 // 234 // It's very rare to be on the InCallScreen at all in this 235 // state, but it can happen in some cases: 236 // - A stray onPhoneStateChanged() event came in to the 237 // InCallScreen *after* it was dismissed. 238 // - We're allowed to be on the InCallScreen because 239 // an MMI or USSD is running, but there's no actual "call" 240 // to display. 241 // - We're displaying an error dialog to the user 242 // (explaining why the call failed), so we need to stay on 243 // the InCallScreen so that the dialog will be visible. 244 // 245 // In these cases, put the callcard into a sane but "blank" state: 246 updateNoCall(phone); 247 } 248 } 249 } 250 251 /** 252 * Updates the UI for the state where the phone is in use, but not ringing. 253 */ 254 private void updateForegroundCall(Phone phone) { 255 if (DBG) log("updateForegroundCall()..."); 256 257 Call fgCall = phone.getForegroundCall(); 258 Call bgCall = phone.getBackgroundCall(); 259 260 if (fgCall.isIdle() && !fgCall.hasConnections()) { 261 if (DBG) log("updateForegroundCall: no active call, show holding call"); 262 // TODO: make sure this case agrees with the latest UI spec. 263 264 // Display the background call in the main info area of the 265 // CallCard, since there is no foreground call. Note that 266 // displayMainCallStatus() will notice if the call we passed in is on 267 // hold, and display the "on hold" indication. 268 fgCall = bgCall; 269 270 // And be sure to not display anything in the "on hold" box. 271 bgCall = null; 272 } 273 274 displayMainCallStatus(phone, fgCall); 275 displayOnHoldCallStatus(phone, bgCall); 276 displayOngoingCallStatus(phone, null); 277 } 278 279 /** 280 * Updates the UI for the state where an incoming call is ringing (or 281 * call waiting), regardless of whether the phone's already offhook. 282 */ 283 private void updateRingingCall(Phone phone) { 284 if (DBG) log("updateRingingCall()..."); 285 286 Call ringingCall = phone.getRingingCall(); 287 Call fgCall = phone.getForegroundCall(); 288 Call bgCall = phone.getBackgroundCall(); 289 290 displayMainCallStatus(phone, ringingCall); 291 displayOnHoldCallStatus(phone, bgCall); 292 displayOngoingCallStatus(phone, fgCall); 293 } 294 295 /** 296 * Updates the UI for the state where the phone is not in use. 297 298 * This is analogous to updateForegroundCall() and updateRingingCall(), 299 300 * but for the (uncommon) case where the phone is 301 * totally idle. (See comments in updateState() above.) 302 * 303 * This puts the callcard into a sane but "blank" state. 304 */ 305 private void updateNoCall(Phone phone) { 306 if (DBG) log("updateNoCall()..."); 307 308 displayMainCallStatus(phone, null); 309 displayOnHoldCallStatus(phone, null); 310 displayOngoingCallStatus(phone, null); 311 } 312 313 /** 314 * Updates the main block of caller info on the CallCard 315 * (ie. the stuff in the mainCallCard block) based on the specified Call. 316 */ 317 private void displayMainCallStatus(Phone phone, Call call) { 318 if (DBG) log("displayMainCallStatus(phone " + phone 319 + ", call " + call + ")..."); 320 321 if (call == null) { 322 // There's no call to display, presumably because the phone is idle. 323 mMainCallCard.setVisibility(View.GONE); 324 return; 325 } 326 mMainCallCard.setVisibility(View.VISIBLE); 327 328 Call.State state = call.getState(); 329 if (DBG) log(" - call.state: " + call.getState()); 330 331 int callCardBackgroundResid = 0; 332 333 // Background frame resources are different between portrait/landscape. 334 // TODO: Don't do this manually. Instead let the resource system do 335 // it: just move the *_land assets over to the res/drawable-land 336 // directory (but with the same filename as the corresponding 337 // portrait asset.) 338 boolean landscapeMode = InCallScreen.ConfigurationHelper.isLandscape(); 339 340 // Background images are also different if Bluetooth is active. 341 final boolean bluetoothActive = PhoneApp.getInstance().showBluetoothIndication(); 342 343 switch (state) { 344 case ACTIVE: 345 showCallConnected(); 346 347 if (bluetoothActive) { 348 callCardBackgroundResid = 349 landscapeMode ? R.drawable.incall_frame_bluetooth_tall_land 350 : R.drawable.incall_frame_bluetooth_tall_port; 351 } else { 352 callCardBackgroundResid = 353 landscapeMode ? R.drawable.incall_frame_connected_tall_land 354 : R.drawable.incall_frame_connected_tall_port; 355 } 356 357 358 // update timer field 359 if (DBG) log("displayMainCallStatus: start periodicUpdateTimer"); 360 mCallTime.setActiveCallMode(call); 361 mCallTime.reset(); 362 mCallTime.periodicUpdateTimer(); 363 364 break; 365 366 case HOLDING: 367 showCallOnhold(); 368 369 callCardBackgroundResid = 370 landscapeMode ? R.drawable.incall_frame_hold_tall_land 371 : R.drawable.incall_frame_hold_tall_port; 372 373 // update timer field 374 mCallTime.cancelTimer(); 375 376 break; 377 378 case DISCONNECTED: 379 reset(); 380 showCallEnded(); 381 382 callCardBackgroundResid = 383 landscapeMode ? R.drawable.incall_frame_ended_tall_land 384 : R.drawable.incall_frame_ended_tall_port; 385 386 // Stop getting timer ticks from this call 387 mCallTime.cancelTimer(); 388 389 break; 390 391 case DIALING: 392 case ALERTING: 393 showCallConnecting(); 394 395 if (bluetoothActive) { 396 callCardBackgroundResid = 397 landscapeMode ? R.drawable.incall_frame_bluetooth_tall_land 398 : R.drawable.incall_frame_bluetooth_tall_port; 399 } else { 400 callCardBackgroundResid = 401 landscapeMode ? R.drawable.incall_frame_normal_tall_land 402 : R.drawable.incall_frame_normal_tall_port; 403 } 404 405 // Stop getting timer ticks from a previous call 406 mCallTime.cancelTimer(); 407 408 break; 409 410 case INCOMING: 411 case WAITING: 412 showCallIncoming(); 413 414 if (bluetoothActive) { 415 callCardBackgroundResid = 416 landscapeMode ? R.drawable.incall_frame_bluetooth_tall_land 417 : R.drawable.incall_frame_bluetooth_tall_port; 418 } else { 419 callCardBackgroundResid = 420 landscapeMode ? R.drawable.incall_frame_normal_tall_land 421 : R.drawable.incall_frame_normal_tall_port; 422 } 423 424 // Stop getting timer ticks from a previous call 425 mCallTime.cancelTimer(); 426 427 break; 428 429 case IDLE: 430 // The "main CallCard" should never be trying to display 431 // an idle call! In updateState(), if the phone is idle, 432 // we call updateNoCall(), which means that we shouldn't 433 // have passed a call into this method at all. 434 Log.w(LOG_TAG, "displayMainCallStatus: IDLE call in the main call card!"); 435 436 // (It is possible, though, that we had a valid call which 437 // became idle *after* the check in updateState() but 438 // before we get here... So continue the best we can, 439 // with whatever (stale) info we can get from the 440 // passed-in Call object.) 441 442 break; 443 444 default: 445 Log.w(LOG_TAG, "displayMainCallStatus: unexpected call state: " + state); 446 break; 447 } 448 449 updateCardTitleWidgets(phone, call); 450 451 if (PhoneUtils.isConferenceCall(call)) { 452 // Update onscreen info for a conference call. 453 updateDisplayForConference(); 454 } else { 455 // Update onscreen info for a regular call (which presumably 456 // has only one connection.) 457 Connection conn = call.getEarliestConnection(); 458 459 boolean isPrivateNumber = false; // TODO: need isPrivate() API 460 461 if (conn == null) { 462 if (DBG) log("displayMainCallStatus: connection is null, using default values."); 463 // if the connection is null, we run through the behaviour 464 // we had in the past, which breaks down into trivial steps 465 // with the current implementation of getCallerInfo and 466 // updateDisplayForPerson. 467 CallerInfo info = PhoneUtils.getCallerInfo(getContext(), conn); 468 updateDisplayForPerson(info, isPrivateNumber, false, call); 469 } else { 470 if (DBG) log(" - CONN: " + conn + ", state = " + conn.getState()); 471 472 // make sure that we only make a new query when the current 473 // callerinfo differs from what we've been requested to display. 474 boolean runQuery = true; 475 Object o = conn.getUserData(); 476 if (o instanceof PhoneUtils.CallerInfoToken) { 477 runQuery = mPhotoTracker.isDifferentImageRequest( 478 ((PhoneUtils.CallerInfoToken) o).currentInfo); 479 } else { 480 runQuery = mPhotoTracker.isDifferentImageRequest(conn); 481 } 482 483 if (runQuery) { 484 if (DBG) log("- displayMainCallStatus: starting CallerInfo query..."); 485 PhoneUtils.CallerInfoToken info = 486 PhoneUtils.startGetCallerInfo(getContext(), conn, this, call); 487 updateDisplayForPerson(info.currentInfo, isPrivateNumber, !info.isFinal, call); 488 } else { 489 // No need to fire off a new query. We do still need 490 // to update the display, though (since we might have 491 // previously been in the "conference call" state.) 492 if (DBG) log("- displayMainCallStatus: using data we already have..."); 493 if (o instanceof CallerInfo) { 494 CallerInfo ci = (CallerInfo) o; 495 if (DBG) log(" ==> Got CallerInfo; updating display: ci = " + ci); 496 updateDisplayForPerson(ci, false, false, call); 497 } else if (o instanceof PhoneUtils.CallerInfoToken){ 498 CallerInfo ci = ((PhoneUtils.CallerInfoToken) o).currentInfo; 499 if (DBG) log(" ==> Got CallerInfoToken; updating display: ci = " + ci); 500 updateDisplayForPerson(ci, false, true, call); 501 } else { 502 Log.w(LOG_TAG, "displayMainCallStatus: runQuery was false, " 503 + "but we didn't have a cached CallerInfo object! o = " + o); 504 // TODO: any easy way to recover here (given that 505 // the CallCard is probably displaying stale info 506 // right now?) Maybe force the CallCard into the 507 // "Unknown" state? 508 } 509 } 510 } 511 } 512 513 // In some states we override the "photo" ImageView to be an 514 // indication of the current state, rather than displaying the 515 // regular photo as set above. 516 updatePhotoForCallState(call); 517 518 // Set the background frame color based on the state of the call. 519 setMainCallCardBackgroundResource(callCardBackgroundResid); 520 // (Text colors are set in updateCardTitleWidgets().) 521 } 522 523 /** 524 * Implemented for CallerInfoAsyncQuery.OnQueryCompleteListener interface. 525 * refreshes the CallCard data when it called. 526 */ 527 public void onQueryComplete(int token, Object cookie, CallerInfo ci) { 528 if (DBG) log("onQueryComplete: token " + token + ", cookie " + cookie + ", ci " + ci); 529 530 if (cookie instanceof Call) { 531 // grab the call object and update the display for an individual call, 532 // as well as the successive call to update image via call state. 533 // If the object is a textview instead, we update it as we need to. 534 if (DBG) log("callerinfo query complete, updating ui from displayMainCallStatus()"); 535 Call call = (Call) cookie; 536 updateDisplayForPerson(ci, false, false, call); 537 updatePhotoForCallState(call); 538 539 } else if (cookie instanceof TextView){ 540 if (DBG) log("callerinfo query complete, updating ui from ongoing or onhold"); 541 ((TextView) cookie).setText(PhoneUtils.getCompactNameFromCallerInfo(ci, mContext)); 542 } 543 } 544 545 /** 546 * Implemented for ContactsAsyncHelper.OnImageLoadCompleteListener interface. 547 * make sure that the call state is reflected after the image is loaded. 548 */ 549 public void onImageLoadComplete(int token, Object cookie, ImageView iView, 550 boolean imagePresent){ 551 if (cookie != null) { 552 updatePhotoForCallState((Call) cookie); 553 } 554 } 555 556 /** 557 * Updates the "upper" and "lower" titles based on the current state of this call. 558 */ 559 private void updateCardTitleWidgets(Phone phone, Call call) { 560 if (DBG) log("updateCardTitleWidgets(call " + call + ")..."); 561 Call.State state = call.getState(); 562 563 // TODO: Still need clearer spec on exactly how title *and* status get 564 // set in all states. (Then, given that info, refactor the code 565 // here to be more clear about exactly which widgets on the card 566 // need to be set.) 567 568 // Normal "foreground" call card: 569 String cardTitle = getTitleForCallCard(call); 570 571 if (DBG) log("updateCardTitleWidgets: " + cardTitle); 572 573 // We display *either* the "upper title" or the "lower title", but 574 // never both. 575 if (state == Call.State.ACTIVE) { 576 // Use the "lower title" (in green). 577 mLowerTitleViewGroup.setVisibility(View.VISIBLE); 578 579 final boolean bluetoothActive = PhoneApp.getInstance().showBluetoothIndication(); 580 int ongoingCallIcon = bluetoothActive ? R.drawable.ic_incall_ongoing_bluetooth 581 : R.drawable.ic_incall_ongoing; 582 mLowerTitleIcon.setImageResource(ongoingCallIcon); 583 584 mLowerTitle.setText(cardTitle); 585 586 int textColor = bluetoothActive ? mTextColorConnectedBluetooth : mTextColorConnected; 587 mLowerTitle.setTextColor(textColor); 588 mElapsedTime.setTextColor(textColor); 589 setUpperTitle(""); 590 } else if (state == Call.State.DISCONNECTED) { 591 // Use the "lower title" (in red). 592 // TODO: We may not *always* want to use the lower title for 593 // the DISCONNECTED state. "Error" states like BUSY or 594 // CONGESTION (see getCallFailedString()) should probably go 595 // in the upper title, for example. In fact, the lower title 596 // should probably be used *only* for the normal "Call ended" 597 // case. 598 mLowerTitleViewGroup.setVisibility(View.VISIBLE); 599 mLowerTitleIcon.setImageResource(R.drawable.ic_incall_end); 600 mLowerTitle.setText(cardTitle); 601 mLowerTitle.setTextColor(mTextColorEnded); 602 mElapsedTime.setTextColor(mTextColorEnded); 603 setUpperTitle(""); 604 } else { 605 // All other states (DIALING, INCOMING, etc.) use the "upper title": 606 setUpperTitle(cardTitle, state); 607 mLowerTitleViewGroup.setVisibility(View.INVISIBLE); 608 } 609 610 // Draw the onscreen "elapsed time" indication EXCEPT if we're in 611 // the "Call ended" state. (In that case, don't touch the 612 // mElapsedTime widget, so we continue to see the elapsed time of 613 // the call that just ended.) 614 if (call.getState() == Call.State.DISCONNECTED) { 615 // "Call ended" state -- don't touch the onscreen elapsed time. 616 } else { 617 long duration = CallTime.getCallDuration(call); // msec 618 updateElapsedTimeWidget(duration / 1000); 619 // Also see onTickForCallTimeElapsed(), which updates this 620 // widget once per second while the call is active. 621 } 622 } 623 624 /** 625 * Updates mElapsedTime based on the specified number of seconds. 626 * A timeElapsed value of zero means to not show an elapsed time at all. 627 */ 628 private void updateElapsedTimeWidget(long timeElapsed) { 629 // if (DBG) log("updateElapsedTimeWidget: " + timeElapsed); 630 if (timeElapsed == 0) { 631 mElapsedTime.setText(""); 632 } else { 633 mElapsedTime.setText(DateUtils.formatElapsedTime(timeElapsed)); 634 } 635 } 636 637 /** 638 * Returns the "card title" displayed at the top of a foreground 639 * ("active") CallCard to indicate the current state of this call, like 640 * "Dialing" or "In call" or "On hold". A null return value means that 641 * there's no title string for this state. 642 */ 643 private String getTitleForCallCard(Call call) { 644 String retVal = null; 645 Call.State state = call.getState(); 646 Context context = getContext(); 647 int resId; 648 649 if (DBG) log("- getTitleForCallCard(Call " + call + ")..."); 650 651 switch (state) { 652 case IDLE: 653 break; 654 655 case ACTIVE: 656 // Title is "Call in progress". (Note this appears in the 657 // "lower title" area of the CallCard.) 658 retVal = context.getString(R.string.card_title_in_progress); 659 break; 660 661 case HOLDING: 662 retVal = context.getString(R.string.card_title_on_hold); 663 // TODO: if this is a conference call on hold, 664 // maybe have a special title here too? 665 break; 666 667 case DIALING: 668 case ALERTING: 669 retVal = context.getString(R.string.card_title_dialing); 670 break; 671 672 case INCOMING: 673 case WAITING: 674 retVal = context.getString(R.string.card_title_incoming_call); 675 break; 676 677 case DISCONNECTED: 678 retVal = getCallFailedString(call); 679 break; 680 } 681 682 if (DBG) log(" ==> result: " + retVal); 683 return retVal; 684 } 685 686 /** 687 * Updates the "on hold" box in the "other call" info area 688 * (ie. the stuff in the otherCallOnHoldInfo block) 689 * based on the specified Call. 690 * Or, clear out the "on hold" box if the specified call 691 * is null or idle. 692 */ 693 private void displayOnHoldCallStatus(Phone phone, Call call) { 694 if (DBG) log("displayOnHoldCallStatus(call =" + call + ")..."); 695 if (call == null) { 696 mOtherCallOnHoldInfoArea.setVisibility(View.GONE); 697 return; 698 } 699 700 Call.State state = call.getState(); 701 switch (state) { 702 case HOLDING: 703 // Ok, there actually is a background call on hold. 704 // Display the "on hold" box. 705 String name; 706 707 // First, see if we need to query. 708 if (PhoneUtils.isConferenceCall(call)) { 709 if (DBG) log("==> conference call."); 710 name = getContext().getString(R.string.confCall); 711 } else { 712 // perform query and update the name temporarily 713 // make sure we hand the textview we want updated to the 714 // callback function. 715 if (DBG) log("==> NOT a conf call; call startGetCallerInfo..."); 716 PhoneUtils.CallerInfoToken info = PhoneUtils.startGetCallerInfo( 717 getContext(), call, this, mOtherCallOnHoldName); 718 name = PhoneUtils.getCompactNameFromCallerInfo(info.currentInfo, getContext()); 719 } 720 721 mOtherCallOnHoldName.setText(name); 722 723 // The call here is always "on hold", so use the orange "hold" frame 724 // and orange text color: 725 setOnHoldInfoAreaBackgroundResource(R.drawable.incall_frame_hold_short); 726 mOtherCallOnHoldName.setTextColor(mTextColorOnHold); 727 mOtherCallOnHoldStatus.setTextColor(mTextColorOnHold); 728 729 mOtherCallOnHoldInfoArea.setVisibility(View.VISIBLE); 730 731 break; 732 733 default: 734 // There's actually no call on hold. (Presumably this call's 735 // state is IDLE, since any other state is meaningless for the 736 // background call.) 737 mOtherCallOnHoldInfoArea.setVisibility(View.GONE); 738 break; 739 } 740 } 741 742 /** 743 * Updates the "Ongoing call" box in the "other call" info area 744 * (ie. the stuff in the otherCallOngoingInfo block) 745 * based on the specified Call. 746 * Or, clear out the "ongoing call" box if the specified call 747 * is null or idle. 748 */ 749 private void displayOngoingCallStatus(Phone phone, Call call) { 750 if (DBG) log("displayOngoingCallStatus(call =" + call + ")..."); 751 if (call == null) { 752 mOtherCallOngoingInfoArea.setVisibility(View.GONE); 753 return; 754 } 755 756 Call.State state = call.getState(); 757 switch (state) { 758 case ACTIVE: 759 case DIALING: 760 case ALERTING: 761 // Ok, there actually is an ongoing call. 762 // Display the "ongoing call" box. 763 String name; 764 765 // First, see if we need to query. 766 if (PhoneUtils.isConferenceCall(call)) { 767 name = getContext().getString(R.string.confCall); 768 } else { 769 // perform query and update the name temporarily 770 // make sure we hand the textview we want updated to the 771 // callback function. 772 PhoneUtils.CallerInfoToken info = PhoneUtils.startGetCallerInfo( 773 getContext(), call, this, mOtherCallOngoingName); 774 name = PhoneUtils.getCompactNameFromCallerInfo(info.currentInfo, getContext()); 775 } 776 777 mOtherCallOngoingName.setText(name); 778 779 // This is an "ongoing" call: we normally use the green 780 // background frame and text color, but we use blue 781 // instead if bluetooth is in use. 782 boolean bluetoothActive = PhoneApp.getInstance().showBluetoothIndication(); 783 784 int ongoingCallBackground = 785 bluetoothActive ? R.drawable.incall_frame_bluetooth_short 786 : R.drawable.incall_frame_connected_short; 787 setOngoingInfoAreaBackgroundResource(ongoingCallBackground); 788 789 int ongoingCallIcon = bluetoothActive ? R.drawable.ic_incall_ongoing_bluetooth 790 : R.drawable.ic_incall_ongoing; 791 mOtherCallOngoingIcon.setImageResource(ongoingCallIcon); 792 793 int textColor = bluetoothActive ? mTextColorConnectedBluetooth 794 : mTextColorConnected; 795 mOtherCallOngoingName.setTextColor(textColor); 796 mOtherCallOngoingStatus.setTextColor(textColor); 797 798 mOtherCallOngoingInfoArea.setVisibility(View.VISIBLE); 799 800 break; 801 802 default: 803 // There's actually no ongoing call. (Presumably this call's 804 // state is IDLE, since any other state is meaningless for the 805 // foreground call.) 806 mOtherCallOngoingInfoArea.setVisibility(View.GONE); 807 break; 808 } 809 } 810 811 812 private String getCallFailedString(Call call) { 813 Phone phone = PhoneApp.getInstance().phone; 814 Connection c = call.getEarliestConnection(); 815 int resID; 816 817 if (c == null) { 818 if (DBG) log("getCallFailedString: connection is null, using default values."); 819 // if this connection is null, just assume that the 820 // default case occurs. 821 resID = R.string.card_title_call_ended; 822 } else { 823 824 Connection.DisconnectCause cause = c.getDisconnectCause(); 825 826 // TODO: The card *title* should probably be "Call ended" in all 827 // cases, but if the DisconnectCause was an error condition we should 828 // probably also display the specific failure reason somewhere... 829 830 switch (cause) { 831 case BUSY: 832 resID = R.string.callFailed_userBusy; 833 break; 834 835 case CONGESTION: 836 resID = R.string.callFailed_congestion; 837 break; 838 839 case LOST_SIGNAL: 840 resID = R.string.callFailed_noSignal; 841 break; 842 843 case LIMIT_EXCEEDED: 844 resID = R.string.callFailed_limitExceeded; 845 break; 846 847 case POWER_OFF: 848 resID = R.string.callFailed_powerOff; 849 break; 850 851 case SIM_ERROR: 852 resID = R.string.callFailed_simError; 853 break; 854 855 case OUT_OF_SERVICE: 856 resID = R.string.callFailed_outOfService; 857 break; 858 859 default: 860 resID = R.string.card_title_call_ended; 861 break; 862 } 863 } 864 return getContext().getString(resID); 865 } 866 867 private void showCallConnecting() { 868 if (DBG) log("showCallConnecting()..."); 869 // TODO: remove if truly unused 870 } 871 872 private void showCallIncoming() { 873 if (DBG) log("showCallIncoming()..."); 874 // TODO: remove if truly unused 875 } 876 877 private void showCallConnected() { 878 if (DBG) log("showCallConnected()..."); 879 // TODO: remove if truly unused 880 } 881 882 private void showCallEnded() { 883 if (DBG) log("showCallEnded()..."); 884 // TODO: remove if truly unused 885 } 886 private void showCallOnhold() { 887 if (DBG) log("showCallOnhold()..."); 888 // TODO: remove if truly unused 889 } 890 891 /** 892 * Add the Call object to these next 2 apis since the callbacks from 893 * updateImageViewWithContactPhotoAsync call will need to use it. 894 */ 895 896 private void updateDisplayForPerson(CallerInfo info, boolean isPrivateNumber, Call call) { 897 updateDisplayForPerson(info, isPrivateNumber, false, call); 898 } 899 900 /** 901 * Updates the name / photo / number / label fields on the CallCard 902 * based on the specified CallerInfo. 903 * 904 * If the current call is a conference call, use 905 * updateDisplayForConference() instead. 906 */ 907 private void updateDisplayForPerson(CallerInfo info, 908 boolean isPrivateNumber, 909 boolean isTemporary, 910 Call call) { 911 if (DBG) log("updateDisplayForPerson(" + info + ")..."); 912 913 // inform the state machine that we are displaying a photo. 914 mPhotoTracker.setPhotoRequest(info); 915 mPhotoTracker.setPhotoState(ContactsAsyncHelper.ImageTracker.DISPLAY_IMAGE); 916 917 String name; 918 String displayNumber = null; 919 String label = null; 920 Uri personUri = null; 921 922 if (info != null) { 923 // It appears that there is a small change in behaviour with the 924 // PhoneUtils' startGetCallerInfo whereby if we query with an 925 // empty number, we will get a valid CallerInfo object, but with 926 // fields that are all null, and the isTemporary boolean input 927 // parameter as true. 928 929 // In the past, we would see a NULL callerinfo object, but this 930 // ends up causing null pointer exceptions elsewhere down the 931 // line in other cases, so we need to make this fix instead. It 932 // appears that this was the ONLY call to PhoneUtils 933 // .getCallerInfo() that relied on a NULL CallerInfo to indicate 934 // an unknown contact. 935 936 if (TextUtils.isEmpty(info.name)) { 937 if (TextUtils.isEmpty(info.phoneNumber)) { 938 if (isPrivateNumber) { 939 name = getContext().getString(R.string.private_num); 940 } else { 941 name = getContext().getString(R.string.unknown); 942 } 943 } else { 944 name = info.phoneNumber; 945 } 946 } else { 947 name = info.name; 948 displayNumber = info.phoneNumber; 949 label = info.phoneLabel; 950 } 951 personUri = ContentUris.withAppendedId(People.CONTENT_URI, info.person_id); 952 } else { 953 if (isPrivateNumber) { 954 name = getContext().getString(R.string.private_num); 955 } else { 956 name = getContext().getString(R.string.unknown); 957 } 958 } 959 mName.setText(name); 960 mName.setVisibility(View.VISIBLE); 961 962 // Update mPhoto 963 // if the temporary flag is set, we know we'll be getting another call after 964 // the CallerInfo has been correctly updated. So, we can skip the image 965 // loading until then. 966 967 // If the photoResource is filled in for the CallerInfo, (like with the 968 // Emergency Number case), then we can just set the photo image without 969 // requesting for an image load. Please refer to CallerInfoAsyncQuery.java 970 // for cases where CallerInfo.photoResource may be set. We can also avoid 971 // the image load step if the image data is cached. 972 if (isTemporary && (info == null || !info.isCachedPhotoCurrent)) { 973 mPhoto.setVisibility(View.INVISIBLE); 974 } else if (info != null && info.photoResource != 0){ 975 showImage(mPhoto, info.photoResource); 976 } else if (!showCachedImage(mPhoto, info)) { 977 // Load the image with a callback to update the image state. 978 // Use a placeholder image value of -1 to indicate no image. 979 ContactsAsyncHelper.updateImageViewWithContactPhotoAsync(info, 0, this, call, 980 getContext(), mPhoto, personUri, -1); 981 } 982 if (displayNumber != null) { 983 mPhoneNumber.setText(displayNumber); 984 mPhoneNumber.setVisibility(View.VISIBLE); 985 } else { 986 mPhoneNumber.setVisibility(View.GONE); 987 } 988 989 if (label != null) { 990 mLabel.setText(label); 991 mLabel.setVisibility(View.VISIBLE); 992 } else { 993 mLabel.setVisibility(View.GONE); 994 } 995 } 996 997 998 /** 999 * Updates the name / photo / number / label fields 1000 * for the special "conference call" state. 1001 * 1002 * If the current call has only a single connection, use 1003 * updateDisplayForPerson() instead. 1004 */ 1005 private void updateDisplayForConference() { 1006 if (DBG) log("updateDisplayForConference()..."); 1007 1008 // Display the "conference call" image in the photo slot, 1009 // with no other information. 1010 1011 showImage(mPhoto, R.drawable.picture_conference); 1012 1013 mName.setText(R.string.card_title_conf_call); 1014 mName.setVisibility(View.VISIBLE); 1015 1016 // TODO: For a conference call, the "phone number" slot is specced 1017 // to contain a summary of who's on the call, like "Bill Foldes 1018 // and Hazel Nutt" or "Bill Foldes and 2 others". 1019 // But for now, just hide it: 1020 mPhoneNumber.setVisibility(View.GONE); 1021 1022 mLabel.setVisibility(View.GONE); 1023 1024 // TODO: consider also showing names / numbers / photos of some of the 1025 // people on the conference here, so you can see that info without 1026 // having to click "Manage conference". We probably have enough 1027 // space to show info for 2 people, at least. 1028 // 1029 // To do this, our caller would pass us the activeConnections 1030 // list, and we'd call PhoneUtils.getCallerInfo() separately for 1031 // each connection. 1032 } 1033 1034 /** 1035 * Updates the CallCard "photo" IFF the specified Call is in a state 1036 * that needs a special photo (like "busy" or "dialing".) 1037 * 1038 * If the current call does not require a special image in the "photo" 1039 * slot onscreen, don't do anything, since presumably the photo image 1040 * has already been set (to the photo of the person we're talking, or 1041 * the generic "picture_unknown" image, or the "conference call" 1042 * image.) 1043 */ 1044 private void updatePhotoForCallState(Call call) { 1045 if (DBG) log("updatePhotoForCallState(" + call + ")..."); 1046 int photoImageResource = 0; 1047 1048 // Check for the (relatively few) telephony states that need a 1049 // special image in the "photo" slot. 1050 Call.State state = call.getState(); 1051 switch (state) { 1052 case DISCONNECTED: 1053 // Display the special "busy" photo for BUSY or CONGESTION. 1054 // Otherwise (presumably the normal "call ended" state) 1055 // leave the photo alone. 1056 Connection c = call.getEarliestConnection(); 1057 // if the connection is null, we assume the default case, 1058 // otherwise update the image resource normally. 1059 if (c != null) { 1060 Connection.DisconnectCause cause = c.getDisconnectCause(); 1061 if ((cause == Connection.DisconnectCause.BUSY) 1062 || (cause == Connection.DisconnectCause.CONGESTION)) { 1063 photoImageResource = R.drawable.picture_busy; 1064 } 1065 } else if (DBG) { 1066 log("updatePhotoForCallState: connection is null, ignoring."); 1067 } 1068 1069 // TODO: add special images for any other DisconnectCauses? 1070 break; 1071 1072 case DIALING: 1073 case ALERTING: 1074 photoImageResource = R.drawable.picture_dialing; 1075 break; 1076 1077 default: 1078 // Leave the photo alone in all other states. 1079 // If this call is an individual call, and the image is currently 1080 // displaying a state, (rather than a photo), we'll need to update 1081 // the image. 1082 // This is for the case where we've been displaying the state and 1083 // now we need to restore the photo. This can happen because we 1084 // only query the CallerInfo once, and limit the number of times 1085 // the image is loaded. (So a state image may overwrite the photo 1086 // and we would otherwise have no way of displaying the photo when 1087 // the state goes away.) 1088 1089 // if the photoResource field is filled-in in the Connection's 1090 // caller info, then we can just use that instead of requesting 1091 // for a photo load. 1092 1093 // look for the photoResource if it is available. 1094 CallerInfo ci = null; 1095 { 1096 Connection conn = call.getEarliestConnection(); 1097 if (conn != null) { 1098 Object o = conn.getUserData(); 1099 if (o instanceof CallerInfo) { 1100 ci = (CallerInfo) o; 1101 } else if (o instanceof PhoneUtils.CallerInfoToken) { 1102 ci = ((PhoneUtils.CallerInfoToken) o).currentInfo; 1103 } 1104 } 1105 } 1106 1107 if (ci != null) { 1108 photoImageResource = ci.photoResource; 1109 } 1110 1111 // If no photoResource found, check to see if this is a conference call. If 1112 // it is not a conference call: 1113 // 1. Try to show the cached image 1114 // 2. If the image is not cached, check to see if a load request has been 1115 // made already. 1116 // 3. If the load request has not been made [DISPLAY_DEFAULT], start the 1117 // request and note that it has started by updating photo state with 1118 // [DISPLAY_IMAGE]. 1119 // Load requests started in (3) use a placeholder image of -1 to hide the 1120 // image by default. Please refer to CallerInfoAsyncQuery.java for cases 1121 // where CallerInfo.photoResource may be set. 1122 if (photoImageResource == 0) { 1123 if (!PhoneUtils.isConferenceCall(call)) { 1124 if (!showCachedImage(mPhoto, ci) && (mPhotoTracker.getPhotoState() == 1125 ContactsAsyncHelper.ImageTracker.DISPLAY_DEFAULT)) { 1126 ContactsAsyncHelper.updateImageViewWithContactPhotoAsync(ci, 1127 getContext(), mPhoto, mPhotoTracker.getPhotoUri(), -1); 1128 mPhotoTracker.setPhotoState( 1129 ContactsAsyncHelper.ImageTracker.DISPLAY_IMAGE); 1130 } 1131 } 1132 } else { 1133 showImage(mPhoto, photoImageResource); 1134 mPhotoTracker.setPhotoState(ContactsAsyncHelper.ImageTracker.DISPLAY_IMAGE); 1135 return; 1136 } 1137 break; 1138 } 1139 1140 if (photoImageResource != 0) { 1141 if (DBG) log("- overrriding photo image: " + photoImageResource); 1142 showImage(mPhoto, photoImageResource); 1143 // Track the image state. 1144 mPhotoTracker.setPhotoState(ContactsAsyncHelper.ImageTracker.DISPLAY_DEFAULT); 1145 } 1146 } 1147 1148 /** 1149 * Try to display the cached image from the callerinfo object. 1150 * 1151 * @return true if we were able to find the image in the cache, false otherwise. 1152 */ 1153 private static final boolean showCachedImage (ImageView view, CallerInfo ci) { 1154 if ((ci != null) && ci.isCachedPhotoCurrent) { 1155 if (ci.cachedPhoto != null) { 1156 showImage(view, ci.cachedPhoto); 1157 } else { 1158 showImage(view, R.drawable.picture_unknown); 1159 } 1160 return true; 1161 } 1162 return false; 1163 } 1164 1165 /** Helper function to display the resource in the imageview AND ensure its visibility.*/ 1166 private static final void showImage(ImageView view, int resource) { 1167 view.setImageResource(resource); 1168 view.setVisibility(View.VISIBLE); 1169 } 1170 1171 /** Helper function to display the drawable in the imageview AND ensure its visibility.*/ 1172 private static final void showImage(ImageView view, Drawable drawable) { 1173 view.setImageDrawable(drawable); 1174 view.setVisibility(View.VISIBLE); 1175 } 1176 1177 /** 1178 * Intercepts (and discards) any touch events to the CallCard. 1179 */ 1180 @Override 1181 public boolean dispatchTouchEvent(MotionEvent ev) { 1182 // if (DBG) log("CALLCARD: dispatchTouchEvent(): ev = " + ev); 1183 1184 // We *never* let touch events get thru to the UI inside the 1185 // CallCard, since there's nothing touchable there. 1186 return true; 1187 } 1188 1189 /** 1190 * Sets the background drawable of the main call card. 1191 */ 1192 private void setMainCallCardBackgroundResource(int resid) { 1193 mMainCallCard.setBackgroundResource(resid); 1194 } 1195 1196 /** 1197 * Sets the background drawable of the "ongoing call" info area. 1198 */ 1199 private void setOngoingInfoAreaBackgroundResource(int resid) { 1200 mOtherCallOngoingInfoArea.setBackgroundResource(resid); 1201 } 1202 1203 /** 1204 * Sets the background drawable of the "call on hold" info area. 1205 */ 1206 private void setOnHoldInfoAreaBackgroundResource(int resid) { 1207 mOtherCallOnHoldInfoArea.setBackgroundResource(resid); 1208 } 1209 1210 /** 1211 * Returns the "Menu button hint" TextView (which is manipulated 1212 * directly by the InCallScreen.) 1213 * @see InCallScreen.updateMenuButtonHint() 1214 */ 1215 /* package */ TextView getMenuButtonHint() { 1216 return mMenuButtonHint; 1217 } 1218 1219 /** 1220 * Updates anything about our View hierarchy or internal state 1221 * that needs to be different in landscape mode. 1222 * 1223 * @see InCallScreen.applyConfigurationToLayout() 1224 */ 1225 /* package */ void updateForLandscapeMode() { 1226 if (DBG) log("updateForLandscapeMode()..."); 1227 1228 // The main CallCard's minimum height is smaller in landscape mode 1229 // than in portrait mode. 1230 mMainCallCard.setMinimumHeight(MAIN_CALLCARD_MIN_HEIGHT_LANDSCAPE); 1231 1232 // Add some left and right margin to the top-level elements, since 1233 // there's no need to use the full width of the screen (which is 1234 // much wider in landscape mode.) 1235 setSideMargins(mMainCallCard, CALLCARD_SIDE_MARGIN_LANDSCAPE); 1236 setSideMargins(mOtherCallOngoingInfoArea, CALLCARD_SIDE_MARGIN_LANDSCAPE); 1237 setSideMargins(mOtherCallOnHoldInfoArea, CALLCARD_SIDE_MARGIN_LANDSCAPE); 1238 1239 // A couple of TextViews are slightly smaller in landscape mode. 1240 mUpperTitle.setTextSize(TITLE_TEXT_SIZE_LANDSCAPE); 1241 } 1242 1243 /** 1244 * Sets the left and right margins of the specified ViewGroup (whose 1245 * LayoutParams object which must inherit from 1246 * ViewGroup.MarginLayoutParams.) 1247 * 1248 * TODO: Is there already a convenience method like this somewhere? 1249 */ 1250 private void setSideMargins(ViewGroup vg, int margin) { 1251 ViewGroup.MarginLayoutParams lp = 1252 (ViewGroup.MarginLayoutParams) vg.getLayoutParams(); 1253 // Equivalent to setting android:layout_marginLeft/Right in XML 1254 lp.leftMargin = margin; 1255 lp.rightMargin = margin; 1256 vg.setLayoutParams(lp); 1257 } 1258 1259 /** 1260 * Sets the CallCard "upper title" to a plain string, with no icon. 1261 */ 1262 private void setUpperTitle(String title) { 1263 mUpperTitle.setText(title); 1264 mUpperTitle.setCompoundDrawables(null, null, null, null); 1265 } 1266 1267 /** 1268 * Sets the CallCard "upper title". Also, depending on the passed-in 1269 * Call state, possibly display an icon along with the title. 1270 */ 1271 private void setUpperTitle(String title, Call.State state) { 1272 mUpperTitle.setText(title); 1273 1274 int bluetoothIconId = 0; 1275 if (((state == Call.State.INCOMING) || (state == Call.State.WAITING)) 1276 && PhoneApp.getInstance().showBluetoothIndication()) { 1277 // Display the special bluetooth icon also, if this is an incoming 1278 // call and the audio will be routed to bluetooth. 1279 bluetoothIconId = R.drawable.ic_incoming_call_bluetooth; 1280 } 1281 1282 mUpperTitle.setCompoundDrawablesWithIntrinsicBounds(bluetoothIconId, 0, 0, 0); 1283 if (bluetoothIconId != 0) mUpperTitle.setCompoundDrawablePadding(5); 1284 } 1285 1286 1287 // Debugging / testing code 1288 1289 private void log(String msg) { 1290 Log.d(LOG_TAG, "[CallCard " + this + "] " + msg); 1291 } 1292 1293 private static void logErr(String msg) { 1294 Log.e(LOG_TAG, "[CallCard] " + msg); 1295 } 1296} 1297