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