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