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