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