1/*
2 * Copyright (C) 2006 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.phone;
18
19import android.content.ContentUris;
20import android.content.Context;
21import android.graphics.drawable.Drawable;
22import android.net.Uri;
23import android.pim.ContactsAsyncHelper;
24import android.provider.ContactsContract.Contacts;
25import android.text.TextUtils;
26import android.text.format.DateUtils;
27import android.util.AttributeSet;
28import android.util.Log;
29import android.view.LayoutInflater;
30import android.view.View;
31import android.view.ViewGroup;
32import android.view.accessibility.AccessibilityEvent;
33import android.widget.Button;
34import android.widget.FrameLayout;
35import android.widget.ImageView;
36import android.widget.TextView;
37
38import com.android.internal.telephony.Call;
39import com.android.internal.telephony.CallerInfo;
40import com.android.internal.telephony.CallerInfoAsyncQuery;
41import com.android.internal.telephony.Connection;
42import com.android.internal.telephony.Phone;
43
44import java.util.List;
45
46
47/**
48 * "Call card" UI element: the in-call screen contains a tiled layout of call
49 * cards, each representing the state of a current "call" (ie. an active call,
50 * a call on hold, or an incoming call.)
51 */
52public class CallCard extends FrameLayout
53        implements CallTime.OnTickListener, CallerInfoAsyncQuery.OnQueryCompleteListener,
54                   ContactsAsyncHelper.OnImageLoadCompleteListener, View.OnClickListener {
55    private static final String LOG_TAG = "CallCard";
56    private static final boolean DBG = (PhoneApp.DBG_LEVEL >= 2);
57
58    /**
59     * Reference to the InCallScreen activity that owns us.  This may be
60     * null if we haven't been initialized yet *or* after the InCallScreen
61     * activity has been destroyed.
62     */
63    private InCallScreen mInCallScreen;
64
65    // Phone app instance
66    private PhoneApp mApplication;
67
68    // Top-level subviews of the CallCard
69    private ViewGroup mPrimaryCallInfo;
70    private ViewGroup mSecondaryCallInfo;
71
72    // Title and elapsed-time widgets
73    private TextView mUpperTitle;
74    private TextView mElapsedTime;
75
76    // Text colors, used for various labels / titles
77    private int mTextColorDefaultPrimary;
78    private int mTextColorDefaultSecondary;
79    private int mTextColorConnected;
80    private int mTextColorConnectedBluetooth;
81    private int mTextColorEnded;
82    private int mTextColorOnHold;
83
84    // The main block of info about the "primary" or "active" call,
85    // including photo / name / phone number / etc.
86    private ImageView mPhoto;
87    private Button mManageConferencePhotoButton;  // Possibly shown in place of mPhoto
88    private TextView mName;
89    private TextView mPhoneNumber;
90    private TextView mLabel;
91    private TextView mSocialStatus;
92
93    // Info about the "secondary" call, which is the "call on hold" when
94    // two lines are in use.
95    private TextView mSecondaryCallName;
96    private TextView mSecondaryCallStatus;
97    private ImageView mSecondaryCallPhoto;
98
99    // Menu button hint
100    private TextView mMenuButtonHint;
101
102    // Onscreen hint for the incoming call RotarySelector widget.
103    private int mRotarySelectorHintTextResId;
104    private int mRotarySelectorHintColorResId;
105
106    private CallTime mCallTime;
107
108    // Track the state for the photo.
109    private ContactsAsyncHelper.ImageTracker mPhotoTracker;
110
111    // Cached DisplayMetrics density.
112    private float mDensity;
113
114    public CallCard(Context context, AttributeSet attrs) {
115        super(context, attrs);
116
117        if (DBG) log("CallCard constructor...");
118        if (DBG) log("- this = " + this);
119        if (DBG) log("- context " + context + ", attrs " + attrs);
120
121        // Inflate the contents of this CallCard, and add it (to ourself) as a child.
122        LayoutInflater inflater = LayoutInflater.from(context);
123        inflater.inflate(
124                R.layout.call_card,  // resource
125                this,                // root
126                true);
127
128        mApplication = PhoneApp.getInstance();
129
130        mCallTime = new CallTime(this);
131
132        // create a new object to track the state for the photo.
133        mPhotoTracker = new ContactsAsyncHelper.ImageTracker();
134
135        mDensity = getResources().getDisplayMetrics().density;
136        if (DBG) log("- Density: " + mDensity);
137    }
138
139    void setInCallScreenInstance(InCallScreen inCallScreen) {
140        mInCallScreen = inCallScreen;
141    }
142
143    public void onTickForCallTimeElapsed(long timeElapsed) {
144        // While a call is in progress, update the elapsed time shown
145        // onscreen.
146        updateElapsedTimeWidget(timeElapsed);
147    }
148
149    /* package */
150    void stopTimer() {
151        mCallTime.cancelTimer();
152    }
153
154    @Override
155    protected void onFinishInflate() {
156        super.onFinishInflate();
157
158        if (DBG) log("CallCard onFinishInflate(this = " + this + ")...");
159
160        mPrimaryCallInfo = (ViewGroup) findViewById(R.id.primaryCallInfo);
161        mSecondaryCallInfo = (ViewGroup) findViewById(R.id.secondaryCallInfo);
162
163        // "Upper" and "lower" title widgets
164        mUpperTitle = (TextView) findViewById(R.id.upperTitle);
165        mElapsedTime = (TextView) findViewById(R.id.elapsedTime);
166
167        // Text colors
168        mTextColorDefaultPrimary =  // corresponds to textAppearanceLarge
169                getResources().getColor(android.R.color.primary_text_dark);
170        mTextColorDefaultSecondary =  // corresponds to textAppearanceSmall
171                getResources().getColor(android.R.color.secondary_text_dark);
172        mTextColorConnected = getResources().getColor(R.color.incall_textConnected);
173        mTextColorConnectedBluetooth =
174                getResources().getColor(R.color.incall_textConnectedBluetooth);
175        mTextColorEnded = getResources().getColor(R.color.incall_textEnded);
176        mTextColorOnHold = getResources().getColor(R.color.incall_textOnHold);
177
178        // "Caller info" area, including photo / name / phone numbers / etc
179        mPhoto = (ImageView) findViewById(R.id.photo);
180        mManageConferencePhotoButton = (Button) findViewById(R.id.manageConferencePhotoButton);
181        mManageConferencePhotoButton.setOnClickListener(this);
182        mName = (TextView) findViewById(R.id.name);
183        mPhoneNumber = (TextView) findViewById(R.id.phoneNumber);
184        mLabel = (TextView) findViewById(R.id.label);
185        mSocialStatus = (TextView) findViewById(R.id.socialStatus);
186
187        // "Other call" info area
188        mSecondaryCallName = (TextView) findViewById(R.id.secondaryCallName);
189        mSecondaryCallStatus = (TextView) findViewById(R.id.secondaryCallStatus);
190        mSecondaryCallPhoto = (ImageView) findViewById(R.id.secondaryCallPhoto);
191
192        // Menu Button hint
193        mMenuButtonHint = (TextView) findViewById(R.id.menuButtonHint);
194    }
195
196    /**
197     * Updates the state of all UI elements on the CallCard, based on the
198     * current state of the phone.
199     */
200    void updateState(Phone phone) {
201        if (DBG) log("updateState(" + phone + ")...");
202
203        // Update some internal state based on the current state of the phone.
204
205        // TODO: clean up this method to just fully update EVERYTHING in
206        // the callcard based on the current phone state: set the overall
207        // type of the CallCard, load up the main caller info area, and
208        // load up and show or hide the "other call" area if necessary.
209
210        Phone.State state = phone.getState();  // IDLE, RINGING, or OFFHOOK
211        if (state == Phone.State.RINGING) {
212            // A phone call is ringing *or* call waiting
213            // (ie. another call may also be active as well.)
214            updateRingingCall(phone);
215        } else if (state == Phone.State.OFFHOOK) {
216            // The phone is off hook. At least one call exists that is
217            // dialing, active, or holding, and no calls are ringing or waiting.
218            updateForegroundCall(phone);
219        } else {
220            // The phone state is IDLE!
221            //
222            // The most common reason for this is if a call just
223            // ended: the phone will be idle, but we *will* still
224            // have a call in the DISCONNECTED state:
225            Call fgCall = phone.getForegroundCall();
226            Call bgCall = phone.getBackgroundCall();
227            if ((fgCall.getState() == Call.State.DISCONNECTED)
228                || (bgCall.getState() == Call.State.DISCONNECTED)) {
229                // In this case, we want the main CallCard to display
230                // the "Call ended" state.  The normal "foreground call"
231                // code path handles that.
232                updateForegroundCall(phone);
233            } else {
234                // We don't have any DISCONNECTED calls, which means
235                // that the phone is *truly* idle.
236                //
237                // It's very rare to be on the InCallScreen at all in this
238                // state, but it can happen in some cases:
239                // - A stray onPhoneStateChanged() event came in to the
240                //   InCallScreen *after* it was dismissed.
241                // - We're allowed to be on the InCallScreen because
242                //   an MMI or USSD is running, but there's no actual "call"
243                //   to display.
244                // - We're displaying an error dialog to the user
245                //   (explaining why the call failed), so we need to stay on
246                //   the InCallScreen so that the dialog will be visible.
247                //
248                // In these cases, put the callcard into a sane but "blank" state:
249                updateNoCall(phone);
250            }
251        }
252    }
253
254    /**
255     * Updates the UI for the state where the phone is in use, but not ringing.
256     */
257    private void updateForegroundCall(Phone phone) {
258        if (DBG) log("updateForegroundCall()...");
259
260        Call fgCall = phone.getForegroundCall();
261        Call bgCall = phone.getBackgroundCall();
262
263        if (fgCall.isIdle() && !fgCall.hasConnections()) {
264            if (DBG) log("updateForegroundCall: no active call, show holding call");
265            // TODO: make sure this case agrees with the latest UI spec.
266
267            // Display the background call in the main info area of the
268            // CallCard, since there is no foreground call.  Note that
269            // displayMainCallStatus() will notice if the call we passed in is on
270            // hold, and display the "on hold" indication.
271            fgCall = bgCall;
272
273            // And be sure to not display anything in the "on hold" box.
274            bgCall = null;
275        }
276
277        displayMainCallStatus(phone, fgCall);
278
279        int phoneType = phone.getPhoneType();
280        if (phoneType == Phone.PHONE_TYPE_CDMA) {
281            if ((mApplication.cdmaPhoneCallState.getCurrentCallState()
282                    == CdmaPhoneCallState.PhoneCallState.THRWAY_ACTIVE)
283                    && mApplication.cdmaPhoneCallState.IsThreeWayCallOrigStateDialing()) {
284                displayOnHoldCallStatus(phone, fgCall);
285            } else {
286                //This is required so that even if a background call is not present
287                // we need to clean up the background call area.
288                displayOnHoldCallStatus(phone, bgCall);
289            }
290        } else if (phoneType == Phone.PHONE_TYPE_GSM) {
291            displayOnHoldCallStatus(phone, bgCall);
292        }
293    }
294
295    /**
296     * Updates the UI for the state where an incoming call is ringing (or
297     * call waiting), regardless of whether the phone's already offhook.
298     */
299    private void updateRingingCall(Phone phone) {
300        if (DBG) log("updateRingingCall()...");
301
302        Call ringingCall = phone.getRingingCall();
303        Call fgCall = phone.getForegroundCall();
304        Call bgCall = phone.getBackgroundCall();
305
306        // Display caller-id info and photo from the incoming call:
307        displayMainCallStatus(phone, ringingCall);
308
309        // And even in the Call Waiting case, *don't* show any info about
310        // the current ongoing call and/or the current call on hold.
311        // (Since the caller-id info for the incoming call totally trumps
312        // any info about the current call(s) in progress.)
313        displayOnHoldCallStatus(phone, null);
314    }
315
316    /**
317     * Updates the UI for the state where the phone is not in use.
318     * This is analogous to updateForegroundCall() and updateRingingCall(),
319     * but for the (uncommon) case where the phone is
320     * totally idle.  (See comments in updateState() above.)
321     *
322     * This puts the callcard into a sane but "blank" state.
323     */
324    private void updateNoCall(Phone phone) {
325        if (DBG) log("updateNoCall()...");
326
327        displayMainCallStatus(phone, null);
328        displayOnHoldCallStatus(phone, null);
329    }
330
331    /**
332     * Updates the main block of caller info on the CallCard
333     * (ie. the stuff in the primaryCallInfo block) based on the specified Call.
334     */
335    private void displayMainCallStatus(Phone phone, Call call) {
336        if (DBG) log("displayMainCallStatus(phone " + phone
337                     + ", call " + call + ")...");
338
339        if (call == null) {
340            // There's no call to display, presumably because the phone is idle.
341            mPrimaryCallInfo.setVisibility(View.GONE);
342            return;
343        }
344        mPrimaryCallInfo.setVisibility(View.VISIBLE);
345
346        Call.State state = call.getState();
347        if (DBG) log("  - call.state: " + call.getState());
348
349        switch (state) {
350            case ACTIVE:
351            case DISCONNECTING:
352                // update timer field
353                if (DBG) log("displayMainCallStatus: start periodicUpdateTimer");
354                mCallTime.setActiveCallMode(call);
355                mCallTime.reset();
356                mCallTime.periodicUpdateTimer();
357
358                break;
359
360            case HOLDING:
361                // update timer field
362                mCallTime.cancelTimer();
363
364                break;
365
366            case DISCONNECTED:
367                // Stop getting timer ticks from this call
368                mCallTime.cancelTimer();
369
370                break;
371
372            case DIALING:
373            case ALERTING:
374                // Stop getting timer ticks from a previous call
375                mCallTime.cancelTimer();
376
377                break;
378
379            case INCOMING:
380            case WAITING:
381                // Stop getting timer ticks from a previous call
382                mCallTime.cancelTimer();
383
384                break;
385
386            case IDLE:
387                // The "main CallCard" should never be trying to display
388                // an idle call!  In updateState(), if the phone is idle,
389                // we call updateNoCall(), which means that we shouldn't
390                // have passed a call into this method at all.
391                Log.w(LOG_TAG, "displayMainCallStatus: IDLE call in the main call card!");
392
393                // (It is possible, though, that we had a valid call which
394                // became idle *after* the check in updateState() but
395                // before we get here...  So continue the best we can,
396                // with whatever (stale) info we can get from the
397                // passed-in Call object.)
398
399                break;
400
401            default:
402                Log.w(LOG_TAG, "displayMainCallStatus: unexpected call state: " + state);
403                break;
404        }
405
406        updateCardTitleWidgets(phone, call);
407
408        if (PhoneUtils.isConferenceCall(call)) {
409            // Update onscreen info for a conference call.
410            updateDisplayForConference();
411        } else {
412            // Update onscreen info for a regular call (which presumably
413            // has only one connection.)
414            Connection conn = null;
415            int phoneType = phone.getPhoneType();
416            if (phoneType == Phone.PHONE_TYPE_CDMA) {
417                conn = call.getLatestConnection();
418            } else if (phoneType == Phone.PHONE_TYPE_GSM) {
419                conn = call.getEarliestConnection();
420            } else {
421                throw new IllegalStateException("Unexpected phone type: " + phoneType);
422            }
423
424            if (conn == null) {
425                if (DBG) log("displayMainCallStatus: connection is null, using default values.");
426                // if the connection is null, we run through the behaviour
427                // we had in the past, which breaks down into trivial steps
428                // with the current implementation of getCallerInfo and
429                // updateDisplayForPerson.
430                CallerInfo info = PhoneUtils.getCallerInfo(getContext(), null /* conn */);
431                updateDisplayForPerson(info, Connection.PRESENTATION_ALLOWED, false, call);
432            } else {
433                if (DBG) log("  - CONN: " + conn + ", state = " + conn.getState());
434                int presentation = conn.getNumberPresentation();
435
436                // make sure that we only make a new query when the current
437                // callerinfo differs from what we've been requested to display.
438                boolean runQuery = true;
439                Object o = conn.getUserData();
440                if (o instanceof PhoneUtils.CallerInfoToken) {
441                    runQuery = mPhotoTracker.isDifferentImageRequest(
442                            ((PhoneUtils.CallerInfoToken) o).currentInfo);
443                } else {
444                    runQuery = mPhotoTracker.isDifferentImageRequest(conn);
445                }
446
447                // Adding a check to see if the update was caused due to a Phone number update
448                // or CNAP update. If so then we need to start a new query
449                if (phoneType == Phone.PHONE_TYPE_CDMA) {
450                    Object obj = conn.getUserData();
451                    String updatedNumber = conn.getAddress();
452                    String updatedCnapName = conn.getCnapName();
453                    CallerInfo info = null;
454                    if (obj instanceof PhoneUtils.CallerInfoToken) {
455                        info = ((PhoneUtils.CallerInfoToken) o).currentInfo;
456                    } else if (o instanceof CallerInfo) {
457                        info = (CallerInfo) o;
458                    }
459
460                    if (info != null) {
461                        if (updatedNumber != null && !updatedNumber.equals(info.phoneNumber)) {
462                            if (DBG) log("- displayMainCallStatus: updatedNumber = "
463                                    + updatedNumber);
464                            runQuery = true;
465                        }
466                        if (updatedCnapName != null && !updatedCnapName.equals(info.cnapName)) {
467                            if (DBG) log("- displayMainCallStatus: updatedCnapName = "
468                                    + updatedCnapName);
469                            runQuery = true;
470                        }
471                    }
472                }
473
474                if (runQuery) {
475                    if (DBG) log("- displayMainCallStatus: starting CallerInfo query...");
476                    PhoneUtils.CallerInfoToken info =
477                            PhoneUtils.startGetCallerInfo(getContext(), conn, this, call);
478                    updateDisplayForPerson(info.currentInfo, presentation, !info.isFinal, call);
479                } else {
480                    // No need to fire off a new query.  We do still need
481                    // to update the display, though (since we might have
482                    // previously been in the "conference call" state.)
483                    if (DBG) log("- displayMainCallStatus: using data we already have...");
484                    if (o instanceof CallerInfo) {
485                        CallerInfo ci = (CallerInfo) o;
486                        // Update CNAP information if Phone state change occurred
487                        ci.cnapName = conn.getCnapName();
488                        ci.numberPresentation = conn.getNumberPresentation();
489                        ci.namePresentation = conn.getCnapNamePresentation();
490                        if (DBG) log("- displayMainCallStatus: CNAP data from Connection: "
491                                + "CNAP name=" + ci.cnapName
492                                + ", Number/Name Presentation=" + ci.numberPresentation);
493                        if (DBG) log("   ==> Got CallerInfo; updating display: ci = " + ci);
494                        updateDisplayForPerson(ci, presentation, false, call);
495                    } else if (o instanceof PhoneUtils.CallerInfoToken){
496                        CallerInfo ci = ((PhoneUtils.CallerInfoToken) o).currentInfo;
497                        if (DBG) log("- displayMainCallStatus: CNAP data from Connection: "
498                                + "CNAP name=" + ci.cnapName
499                                + ", Number/Name Presentation=" + ci.numberPresentation);
500                        if (DBG) log("   ==> Got CallerInfoToken; updating display: ci = " + ci);
501                        updateDisplayForPerson(ci, presentation, true, call);
502                    } else {
503                        Log.w(LOG_TAG, "displayMainCallStatus: runQuery was false, "
504                              + "but we didn't have a cached CallerInfo object!  o = " + o);
505                        // TODO: any easy way to recover here (given that
506                        // the CallCard is probably displaying stale info
507                        // right now?)  Maybe force the CallCard into the
508                        // "Unknown" state?
509                    }
510                }
511            }
512        }
513
514        // In some states we override the "photo" ImageView to be an
515        // indication of the current state, rather than displaying the
516        // regular photo as set above.
517        updatePhotoForCallState(call);
518
519        // One special feature of the "number" text field: For incoming
520        // calls, while the user is dragging the RotarySelector widget, we
521        // use mPhoneNumber to display a hint like "Rotate to answer".
522        if (mRotarySelectorHintTextResId != 0) {
523            // Display the hint!
524            mPhoneNumber.setText(mRotarySelectorHintTextResId);
525            mPhoneNumber.setTextColor(getResources().getColor(mRotarySelectorHintColorResId));
526            mPhoneNumber.setVisibility(View.VISIBLE);
527            mLabel.setVisibility(View.GONE);
528        }
529        // If we don't have a hint to display, just don't touch
530        // mPhoneNumber and mLabel. (Their text / color / visibility have
531        // already been set correctly, by either updateDisplayForPerson()
532        // or updateDisplayForConference().)
533    }
534
535    /**
536     * Implemented for CallerInfoAsyncQuery.OnQueryCompleteListener interface.
537     * refreshes the CallCard data when it called.
538     */
539    public void onQueryComplete(int token, Object cookie, CallerInfo ci) {
540        if (DBG) log("onQueryComplete: token " + token + ", cookie " + cookie + ", ci " + ci);
541
542        if (cookie instanceof Call) {
543            // grab the call object and update the display for an individual call,
544            // as well as the successive call to update image via call state.
545            // If the object is a textview instead, we update it as we need to.
546            if (DBG) log("callerinfo query complete, updating ui from displayMainCallStatus()");
547            Call call = (Call) cookie;
548            Connection conn = null;
549            int phoneType = mApplication.phone.getPhoneType();
550            if (phoneType == Phone.PHONE_TYPE_CDMA) {
551                conn = call.getLatestConnection();
552            } else if (phoneType == Phone.PHONE_TYPE_GSM) {
553                conn = call.getEarliestConnection();
554            } else {
555                throw new IllegalStateException("Unexpected phone type: " + phoneType);
556            }
557            PhoneUtils.CallerInfoToken cit =
558                   PhoneUtils.startGetCallerInfo(getContext(), conn, this, null);
559
560            int presentation = Connection.PRESENTATION_ALLOWED;
561            if (conn != null) presentation = conn.getNumberPresentation();
562            if (DBG) log("- onQueryComplete: presentation=" + presentation
563                    + ", contactExists=" + ci.contactExists);
564
565            // Depending on whether there was a contact match or not, we want to pass in different
566            // CallerInfo (for CNAP). Therefore if ci.contactExists then use the ci passed in.
567            // Otherwise, regenerate the CIT from the Connection and use the CallerInfo from there.
568            if (ci.contactExists) {
569                updateDisplayForPerson(ci, Connection.PRESENTATION_ALLOWED, false, call);
570            } else {
571                updateDisplayForPerson(cit.currentInfo, presentation, false, call);
572            }
573            updatePhotoForCallState(call);
574
575        } else if (cookie instanceof TextView){
576            if (DBG) log("callerinfo query complete, updating ui from ongoing or onhold");
577            ((TextView) cookie).setText(PhoneUtils.getCompactNameFromCallerInfo(ci, mContext));
578        }
579    }
580
581    /**
582     * Implemented for ContactsAsyncHelper.OnImageLoadCompleteListener interface.
583     * make sure that the call state is reflected after the image is loaded.
584     */
585    public void onImageLoadComplete(int token, Object cookie, ImageView iView,
586            boolean imagePresent){
587        if (cookie != null) {
588            updatePhotoForCallState((Call) cookie);
589        }
590    }
591
592    /**
593     * Updates the "card title" (and also elapsed time widget) based on
594     * the current state of the call.
595     */
596    // TODO: it's confusing for updateCardTitleWidgets() and
597    // getTitleForCallCard() to be separate methods, since they both
598    // just list out the exact same "phone state" cases.
599    // Let's merge the getTitleForCallCard() logic into here.
600    private void updateCardTitleWidgets(Phone phone, Call call) {
601        if (DBG) log("updateCardTitleWidgets(call " + call + ")...");
602        Call.State state = call.getState();
603
604        // TODO: Still need clearer spec on exactly how title *and* status get
605        // set in all states.  (Then, given that info, refactor the code
606        // here to be more clear about exactly which widgets on the card
607        // need to be set.)
608
609        String cardTitle;
610        int phoneType = mApplication.phone.getPhoneType();
611        if (phoneType == Phone.PHONE_TYPE_CDMA) {
612            if (!PhoneApp.getInstance().notifier.getIsCdmaRedialCall()) {
613                cardTitle = getTitleForCallCard(call);  // Normal "foreground" call card
614            } else {
615                cardTitle = getContext().getString(R.string.card_title_redialing);
616            }
617        } else if (phoneType == Phone.PHONE_TYPE_GSM) {
618            cardTitle = getTitleForCallCard(call);
619        } else {
620            throw new IllegalStateException("Unexpected phone type: " + phoneType);
621        }
622        if (DBG) log("updateCardTitleWidgets: " + cardTitle);
623
624        // Update the title and elapsed time widgets based on the current call state.
625        switch (state) {
626            case ACTIVE:
627            case DISCONNECTING:
628                final boolean bluetoothActive = mApplication.showBluetoothIndication();
629                int ongoingCallIcon = bluetoothActive ? R.drawable.ic_incall_ongoing_bluetooth
630                        : R.drawable.ic_incall_ongoing;
631                int connectedTextColor = bluetoothActive
632                        ? mTextColorConnectedBluetooth : mTextColorConnected;
633
634                if (phoneType == Phone.PHONE_TYPE_CDMA) {
635                    // Check if the "Dialing" 3Way call needs to be displayed
636                    // as the Foreground Call state still remains ACTIVE
637                    if (mApplication.cdmaPhoneCallState.IsThreeWayCallOrigStateDialing()) {
638                        // Use the "upper title":
639                        setUpperTitle(cardTitle, mTextColorDefaultPrimary, state);
640                    } else {
641                        // Normal "ongoing call" state; don't use any "title" at all.
642                        clearUpperTitle();
643                    }
644                } else if (phoneType == Phone.PHONE_TYPE_GSM) {
645                    // While in the DISCONNECTING state we display a
646                    // "Hanging up" message in order to make the UI feel more
647                    // responsive.  (In GSM it's normal to see a delay of a
648                    // couple of seconds while negotiating the disconnect with
649                    // the network, so the "Hanging up" state at least lets
650                    // the user know that we're doing something.)
651                    // TODO: consider displaying the "Hanging up" state for
652                    // CDMA also if the latency there ever gets high enough.
653                    if (state == Call.State.DISCONNECTING) {
654                        // Display the brief "Hanging up" indication.
655                        setUpperTitle(cardTitle, mTextColorDefaultPrimary, state);
656                    } else {  // state == Call.State.ACTIVE
657                        // Normal "ongoing call" state; don't use any "title" at all.
658                        clearUpperTitle();
659                    }
660                }
661
662                // Use the elapsed time widget to show the current call duration.
663                mElapsedTime.setVisibility(View.VISIBLE);
664                mElapsedTime.setTextColor(connectedTextColor);
665                long duration = CallTime.getCallDuration(call);  // msec
666                updateElapsedTimeWidget(duration / 1000);
667                // Also see onTickForCallTimeElapsed(), which updates this
668                // widget once per second while the call is active.
669                break;
670
671            case DISCONNECTED:
672                // Display "Call ended" (or possibly some error indication;
673                // see getCallFailedString()) in the upper title, in red.
674
675                // TODO: display a "call ended" icon somewhere, like the old
676                // R.drawable.ic_incall_end?
677
678                setUpperTitle(cardTitle, mTextColorEnded, state);
679
680                // In the "Call ended" state, leave the mElapsedTime widget
681                // visible, but don't touch it (so  we continue to see the elapsed time of
682                // the call that just ended.)
683                mElapsedTime.setVisibility(View.VISIBLE);
684                mElapsedTime.setTextColor(mTextColorEnded);
685                break;
686
687            case HOLDING:
688                // For a single call on hold, display the title "On hold" in
689                // orange.
690                // (But since the upper title overlaps the label of the
691                // Hold/Unhold button, we actually use the elapsedTime widget
692                // to display the title in this case.)
693
694                // TODO: display an "On hold" icon somewhere, like the old
695                // R.drawable.ic_incall_onhold?
696
697                clearUpperTitle();
698                mElapsedTime.setText(cardTitle);
699
700                // While on hold, the elapsed time widget displays an
701                // "on hold" indication rather than an amount of time.
702                mElapsedTime.setVisibility(View.VISIBLE);
703                mElapsedTime.setTextColor(mTextColorOnHold);
704                break;
705
706            default:
707                // All other states (DIALING, INCOMING, etc.) use the "upper title":
708                setUpperTitle(cardTitle, mTextColorDefaultPrimary, state);
709
710                // ...and we don't show the elapsed time.
711                mElapsedTime.setVisibility(View.INVISIBLE);
712                break;
713        }
714    }
715
716    /**
717     * Updates mElapsedTime based on the specified number of seconds.
718     * A timeElapsed value of zero means to not show an elapsed time at all.
719     */
720    private void updateElapsedTimeWidget(long timeElapsed) {
721        // if (DBG) log("updateElapsedTimeWidget: " + timeElapsed);
722        if (timeElapsed == 0) {
723            mElapsedTime.setText("");
724        } else {
725            mElapsedTime.setText(DateUtils.formatElapsedTime(timeElapsed));
726        }
727    }
728
729    /**
730     * Returns the "card title" displayed at the top of a foreground
731     * ("active") CallCard to indicate the current state of this call, like
732     * "Dialing" or "In call" or "On hold".  A null return value means that
733     * there's no title string for this state.
734     */
735    private String getTitleForCallCard(Call call) {
736        String retVal = null;
737        Call.State state = call.getState();
738        Context context = getContext();
739        int resId;
740
741        if (DBG) log("- getTitleForCallCard(Call " + call + ")...");
742
743        switch (state) {
744            case IDLE:
745                break;
746
747            case ACTIVE:
748                // Title is "Call in progress".  (Note this appears in the
749                // "lower title" area of the CallCard.)
750                int phoneType = mApplication.phone.getPhoneType();
751                if (phoneType == Phone.PHONE_TYPE_CDMA) {
752                    if (mApplication.cdmaPhoneCallState.IsThreeWayCallOrigStateDialing()) {
753                        retVal = context.getString(R.string.card_title_dialing);
754                    } else {
755                        retVal = context.getString(R.string.card_title_in_progress);
756                    }
757                } else if (phoneType == Phone.PHONE_TYPE_GSM) {
758                    retVal = context.getString(R.string.card_title_in_progress);
759                } else {
760                    throw new IllegalStateException("Unexpected phone type: " + phoneType);
761                }
762                break;
763
764            case HOLDING:
765                retVal = context.getString(R.string.card_title_on_hold);
766                // TODO: if this is a conference call on hold,
767                // maybe have a special title here too?
768                break;
769
770            case DIALING:
771            case ALERTING:
772                retVal = context.getString(R.string.card_title_dialing);
773                break;
774
775            case INCOMING:
776            case WAITING:
777                retVal = context.getString(R.string.card_title_incoming_call);
778                break;
779
780            case DISCONNECTING:
781                retVal = context.getString(R.string.card_title_hanging_up);
782                break;
783
784            case DISCONNECTED:
785                retVal = getCallFailedString(call);
786                break;
787        }
788
789        if (DBG) log("  ==> result: " + retVal);
790        return retVal;
791    }
792
793    /**
794     * Updates the "on hold" box in the "other call" info area
795     * (ie. the stuff in the secondaryCallInfo block)
796     * based on the specified Call.
797     * Or, clear out the "on hold" box if the specified call
798     * is null or idle.
799     */
800    private void displayOnHoldCallStatus(Phone phone, Call call) {
801        if (DBG) log("displayOnHoldCallStatus(call =" + call + ")...");
802
803        if ((call == null) || (PhoneApp.getInstance().isOtaCallInActiveState())) {
804            mSecondaryCallInfo.setVisibility(View.GONE);
805            return;
806        }
807
808        boolean showSecondaryCallInfo = false;
809        Call.State state = call.getState();
810        switch (state) {
811            case HOLDING:
812                // Ok, there actually is a background call on hold.
813                // Display the "on hold" box.
814
815                // Note this case occurs only on GSM devices.  (On CDMA,
816                // the "call on hold" is actually the 2nd connection of
817                // that ACTIVE call; see the ACTIVE case below.)
818
819                if (PhoneUtils.isConferenceCall(call)) {
820                    if (DBG) log("==> conference call.");
821                    mSecondaryCallName.setText(getContext().getString(R.string.confCall));
822                    showImage(mSecondaryCallPhoto, R.drawable.picture_conference);
823                } else {
824                    // perform query and update the name temporarily
825                    // make sure we hand the textview we want updated to the
826                    // callback function.
827                    if (DBG) log("==> NOT a conf call; call startGetCallerInfo...");
828                    PhoneUtils.CallerInfoToken infoToken = PhoneUtils.startGetCallerInfo(
829                            getContext(), call, this, mSecondaryCallName);
830                    mSecondaryCallName.setText(
831                            PhoneUtils.getCompactNameFromCallerInfo(infoToken.currentInfo,
832                                                                    getContext()));
833
834                    // Also pull the photo out of the current CallerInfo.
835                    // (Note we assume we already have a valid photo at
836                    // this point, since *presumably* the caller-id query
837                    // was already run at some point *before* this call
838                    // got put on hold.  If there's no cached photo, just
839                    // fall back to the default "unknown" image.)
840                    if (infoToken.isFinal) {
841                        showCachedImage(mSecondaryCallPhoto, infoToken.currentInfo);
842                    } else {
843                        showImage(mSecondaryCallPhoto, R.drawable.picture_unknown);
844                    }
845                }
846
847                showSecondaryCallInfo = true;
848
849                break;
850
851            case ACTIVE:
852                // CDMA: This is because in CDMA when the user originates the second call,
853                // although the Foreground call state is still ACTIVE in reality the network
854                // put the first call on hold.
855                if (mApplication.phone.getPhoneType() == Phone.PHONE_TYPE_CDMA) {
856                    List<Connection> connections = call.getConnections();
857                    if (connections.size() > 2) {
858                        // This means that current Mobile Originated call is the not the first 3-Way
859                        // call the user is making, which in turn tells the PhoneApp that we no
860                        // longer know which previous caller/party had dropped out before the user
861                        // made this call.
862                        mSecondaryCallName.setText(
863                                getContext().getString(R.string.card_title_in_call));
864                        showImage(mSecondaryCallPhoto, R.drawable.picture_unknown);
865                    } else {
866                        // This means that the current Mobile Originated call IS the first 3-Way
867                        // and hence we display the first callers/party's info here.
868                        Connection conn = call.getEarliestConnection();
869                        PhoneUtils.CallerInfoToken infoToken = PhoneUtils.startGetCallerInfo(
870                                getContext(), conn, this, mSecondaryCallName);
871
872                        // Get the compactName to be displayed, but then check that against
873                        // the number presentation value for the call. If it's not an allowed
874                        // presentation, then display the appropriate presentation string instead.
875                        CallerInfo info = infoToken.currentInfo;
876
877                        String name = PhoneUtils.getCompactNameFromCallerInfo(info, getContext());
878                        boolean forceGenericPhoto = false;
879                        if (info != null && info.numberPresentation !=
880                                Connection.PRESENTATION_ALLOWED) {
881                            name = getPresentationString(info.numberPresentation);
882                            forceGenericPhoto = true;
883                        }
884                        mSecondaryCallName.setText(name);
885
886                        // Also pull the photo out of the current CallerInfo.
887                        // (Note we assume we already have a valid photo at
888                        // this point, since *presumably* the caller-id query
889                        // was already run at some point *before* this call
890                        // got put on hold.  If there's no cached photo, just
891                        // fall back to the default "unknown" image.)
892                        if (!forceGenericPhoto && infoToken.isFinal) {
893                            showCachedImage(mSecondaryCallPhoto, info);
894                        } else {
895                            showImage(mSecondaryCallPhoto, R.drawable.picture_unknown);
896                        }
897                    }
898                    showSecondaryCallInfo = true;
899
900                } else {
901                    // We shouldn't ever get here at all for non-CDMA devices.
902                    Log.w(LOG_TAG, "displayOnHoldCallStatus: ACTIVE state on non-CDMA device");
903                    showSecondaryCallInfo = false;
904                }
905                break;
906
907            default:
908                // There's actually no call on hold.  (Presumably this call's
909                // state is IDLE, since any other state is meaningless for the
910                // background call.)
911                showSecondaryCallInfo = false;
912                break;
913        }
914
915        if (showSecondaryCallInfo) {
916            // Ok, we have something useful to display in the "secondary
917            // call" info area.
918            mSecondaryCallInfo.setVisibility(View.VISIBLE);
919
920            // Watch out: there are some cases where we need to display the
921            // secondary call photo but *not* the two lines of text above it.
922            // Specifically, that's any state where the CallCard "upper title" is
923            // in use, since the title (e.g. "Dialing" or "Call ended") might
924            // collide with the secondaryCallStatus and secondaryCallName widgets.
925            //
926            // We detect this case by simply seeing whether or not there's any text
927            // in mUpperTitle.  (This is much simpler than detecting all possible
928            // telephony states where the "upper title" is used!  But note it does
929            // rely on the fact that updateCardTitleWidgets() gets called *earlier*
930            // than this method, in the CallCard.updateState() sequence...)
931            boolean okToShowLabels = TextUtils.isEmpty(mUpperTitle.getText());
932            mSecondaryCallName.setVisibility(okToShowLabels ? View.VISIBLE : View.INVISIBLE);
933            mSecondaryCallStatus.setVisibility(okToShowLabels ? View.VISIBLE : View.INVISIBLE);
934        } else {
935            // Hide the entire "secondary call" info area.
936            mSecondaryCallInfo.setVisibility(View.GONE);
937        }
938    }
939
940    private String getCallFailedString(Call call) {
941        Connection c = call.getEarliestConnection();
942        int resID;
943
944        if (c == null) {
945            if (DBG) log("getCallFailedString: connection is null, using default values.");
946            // if this connection is null, just assume that the
947            // default case occurs.
948            resID = R.string.card_title_call_ended;
949        } else {
950
951            Connection.DisconnectCause cause = c.getDisconnectCause();
952
953            // TODO: The card *title* should probably be "Call ended" in all
954            // cases, but if the DisconnectCause was an error condition we should
955            // probably also display the specific failure reason somewhere...
956
957            switch (cause) {
958                case BUSY:
959                    resID = R.string.callFailed_userBusy;
960                    break;
961
962                case CONGESTION:
963                    resID = R.string.callFailed_congestion;
964                    break;
965
966                case LOST_SIGNAL:
967                case CDMA_DROP:
968                    resID = R.string.callFailed_noSignal;
969                    break;
970
971                case LIMIT_EXCEEDED:
972                    resID = R.string.callFailed_limitExceeded;
973                    break;
974
975                case POWER_OFF:
976                    resID = R.string.callFailed_powerOff;
977                    break;
978
979                case ICC_ERROR:
980                    resID = R.string.callFailed_simError;
981                    break;
982
983                case OUT_OF_SERVICE:
984                    resID = R.string.callFailed_outOfService;
985                    break;
986
987                default:
988                    resID = R.string.card_title_call_ended;
989                    break;
990            }
991        }
992        return getContext().getString(resID);
993    }
994
995    /**
996     * Updates the name / photo / number / label fields on the CallCard
997     * based on the specified CallerInfo.
998     *
999     * If the current call is a conference call, use
1000     * updateDisplayForConference() instead.
1001     */
1002    private void updateDisplayForPerson(CallerInfo info,
1003                                        int presentation,
1004                                        boolean isTemporary,
1005                                        Call call) {
1006        if (DBG) log("updateDisplayForPerson(" + info + ")\npresentation:" +
1007                     presentation + " isTemporary:" + isTemporary);
1008
1009        // inform the state machine that we are displaying a photo.
1010        mPhotoTracker.setPhotoRequest(info);
1011        mPhotoTracker.setPhotoState(ContactsAsyncHelper.ImageTracker.DISPLAY_IMAGE);
1012
1013        String name;
1014        String displayNumber = null;
1015        String label = null;
1016        Uri personUri = null;
1017        String socialStatusText = null;
1018        Drawable socialStatusBadge = null;
1019
1020        if (info != null) {
1021            // It appears that there is a small change in behaviour with the
1022            // PhoneUtils' startGetCallerInfo whereby if we query with an
1023            // empty number, we will get a valid CallerInfo object, but with
1024            // fields that are all null, and the isTemporary boolean input
1025            // parameter as true.
1026
1027            // In the past, we would see a NULL callerinfo object, but this
1028            // ends up causing null pointer exceptions elsewhere down the
1029            // line in other cases, so we need to make this fix instead. It
1030            // appears that this was the ONLY call to PhoneUtils
1031            // .getCallerInfo() that relied on a NULL CallerInfo to indicate
1032            // an unknown contact.
1033
1034            if (TextUtils.isEmpty(info.name)) {
1035                if (TextUtils.isEmpty(info.phoneNumber)) {
1036                    name =  getPresentationString(presentation);
1037                } else if (presentation != Connection.PRESENTATION_ALLOWED) {
1038                    // This case should never happen since the network should never send a phone #
1039                    // AND a restricted presentation. However we leave it here in case of weird
1040                    // network behavior
1041                    name = getPresentationString(presentation);
1042                } else if (!TextUtils.isEmpty(info.cnapName)) {
1043                    name = info.cnapName;
1044                    info.name = info.cnapName;
1045                    displayNumber = info.phoneNumber;
1046                } else {
1047                    name = info.phoneNumber;
1048                }
1049            } else {
1050                if (presentation != Connection.PRESENTATION_ALLOWED) {
1051                    // This case should never happen since the network should never send a name
1052                    // AND a restricted presentation. However we leave it here in case of weird
1053                    // network behavior
1054                    name = getPresentationString(presentation);
1055                } else {
1056                    name = info.name;
1057                    displayNumber = info.phoneNumber;
1058                    label = info.phoneLabel;
1059                }
1060            }
1061            personUri = ContentUris.withAppendedId(Contacts.CONTENT_URI, info.person_id);
1062        } else {
1063            name =  getPresentationString(presentation);
1064        }
1065
1066        if (call.isGeneric()) {
1067            mName.setText(R.string.card_title_in_call);
1068        } else {
1069            mName.setText(name);
1070        }
1071        mName.setVisibility(View.VISIBLE);
1072
1073        // Update mPhoto
1074        // if the temporary flag is set, we know we'll be getting another call after
1075        // the CallerInfo has been correctly updated.  So, we can skip the image
1076        // loading until then.
1077
1078        // If the photoResource is filled in for the CallerInfo, (like with the
1079        // Emergency Number case), then we can just set the photo image without
1080        // requesting for an image load. Please refer to CallerInfoAsyncQuery.java
1081        // for cases where CallerInfo.photoResource may be set.  We can also avoid
1082        // the image load step if the image data is cached.
1083        if (isTemporary && (info == null || !info.isCachedPhotoCurrent)) {
1084            mPhoto.setVisibility(View.INVISIBLE);
1085        } else if (info != null && info.photoResource != 0){
1086            showImage(mPhoto, info.photoResource);
1087        } else if (!showCachedImage(mPhoto, info)) {
1088            // Load the image with a callback to update the image state.
1089            // Use the default unknown picture while the query is running.
1090            ContactsAsyncHelper.updateImageViewWithContactPhotoAsync(
1091                info, 0, this, call, getContext(), mPhoto, personUri, R.drawable.picture_unknown);
1092        }
1093        // And no matter what, on all devices, we never see the "manage
1094        // conference" button in this state.
1095        mManageConferencePhotoButton.setVisibility(View.INVISIBLE);
1096
1097        if (displayNumber != null && !call.isGeneric()) {
1098            mPhoneNumber.setText(displayNumber);
1099            mPhoneNumber.setTextColor(mTextColorDefaultSecondary);
1100            mPhoneNumber.setVisibility(View.VISIBLE);
1101        } else {
1102            mPhoneNumber.setVisibility(View.GONE);
1103        }
1104
1105        if (label != null && !call.isGeneric()) {
1106            mLabel.setText(label);
1107            mLabel.setVisibility(View.VISIBLE);
1108        } else {
1109            mLabel.setVisibility(View.GONE);
1110        }
1111
1112        // "Social status": currently unused.
1113        // Note socialStatus is *only* visible while an incoming
1114        // call is ringing, never in any other call state.
1115        if ((socialStatusText != null) && call.isRinging() && !call.isGeneric()) {
1116            mSocialStatus.setVisibility(View.VISIBLE);
1117            mSocialStatus.setText(socialStatusText);
1118            mSocialStatus.setCompoundDrawablesWithIntrinsicBounds(
1119                    socialStatusBadge, null, null, null);
1120            mSocialStatus.setCompoundDrawablePadding((int) (mDensity * 6));
1121        } else {
1122            mSocialStatus.setVisibility(View.GONE);
1123        }
1124    }
1125
1126    private String getPresentationString(int presentation) {
1127        String name = getContext().getString(R.string.unknown);
1128        if (presentation == Connection.PRESENTATION_RESTRICTED) {
1129            name = getContext().getString(R.string.private_num);
1130        } else if (presentation == Connection.PRESENTATION_PAYPHONE) {
1131            name = getContext().getString(R.string.payphone);
1132        }
1133        return name;
1134    }
1135
1136    /**
1137     * Updates the name / photo / number / label fields
1138     * for the special "conference call" state.
1139     *
1140     * If the current call has only a single connection, use
1141     * updateDisplayForPerson() instead.
1142     */
1143    private void updateDisplayForConference() {
1144        if (DBG) log("updateDisplayForConference()...");
1145
1146        int phoneType = mApplication.phone.getPhoneType();
1147        if (phoneType == Phone.PHONE_TYPE_CDMA) {
1148            // This state corresponds to both 3-Way merged call and
1149            // Call Waiting accepted call.
1150            // In this case we display the UI in a "generic" state, with
1151            // the generic "dialing" icon and no caller information,
1152            // because in this state in CDMA the user does not really know
1153            // which caller party he is talking to.
1154            showImage(mPhoto, R.drawable.picture_dialing);
1155            mName.setText(R.string.card_title_in_call);
1156        } else if (phoneType == Phone.PHONE_TYPE_GSM) {
1157            if (mInCallScreen.isTouchUiEnabled()) {
1158                // Display the "manage conference" button in place of the photo.
1159                mManageConferencePhotoButton.setVisibility(View.VISIBLE);
1160                mPhoto.setVisibility(View.INVISIBLE);  // Not GONE, since that would break
1161                                                       // other views in our RelativeLayout.
1162            } else {
1163                // Display the "conference call" image in the photo slot,
1164                // with no other information.
1165                showImage(mPhoto, R.drawable.picture_conference);
1166            }
1167            mName.setText(R.string.card_title_conf_call);
1168        } else {
1169            throw new IllegalStateException("Unexpected phone type: " + phoneType);
1170        }
1171
1172        mName.setVisibility(View.VISIBLE);
1173
1174        // TODO: For a conference call, the "phone number" slot is specced
1175        // to contain a summary of who's on the call, like "Bill Foldes
1176        // and Hazel Nutt" or "Bill Foldes and 2 others".
1177        // But for now, just hide it:
1178        mPhoneNumber.setVisibility(View.GONE);
1179        mLabel.setVisibility(View.GONE);
1180
1181        // socialStatus is never visible in this state.
1182        mSocialStatus.setVisibility(View.GONE);
1183
1184        // TODO: for a GSM conference call, since we do actually know who
1185        // you're talking to, consider also showing names / numbers /
1186        // photos of some of the people on the conference here, so you can
1187        // see that info without having to click "Manage conference".  We
1188        // probably have enough space to show info for 2 people, at least.
1189        //
1190        // To do this, our caller would pass us the activeConnections
1191        // list, and we'd call PhoneUtils.getCallerInfo() separately for
1192        // each connection.
1193    }
1194
1195    /**
1196     * Updates the CallCard "photo" IFF the specified Call is in a state
1197     * that needs a special photo (like "busy" or "dialing".)
1198     *
1199     * If the current call does not require a special image in the "photo"
1200     * slot onscreen, don't do anything, since presumably the photo image
1201     * has already been set (to the photo of the person we're talking, or
1202     * the generic "picture_unknown" image, or the "conference call"
1203     * image.)
1204     */
1205    private void updatePhotoForCallState(Call call) {
1206        if (DBG) log("updatePhotoForCallState(" + call + ")...");
1207        int photoImageResource = 0;
1208
1209        // Check for the (relatively few) telephony states that need a
1210        // special image in the "photo" slot.
1211        Call.State state = call.getState();
1212        switch (state) {
1213            case DISCONNECTED:
1214                // Display the special "busy" photo for BUSY or CONGESTION.
1215                // Otherwise (presumably the normal "call ended" state)
1216                // leave the photo alone.
1217                Connection c = call.getEarliestConnection();
1218                // if the connection is null, we assume the default case,
1219                // otherwise update the image resource normally.
1220                if (c != null) {
1221                    Connection.DisconnectCause cause = c.getDisconnectCause();
1222                    if ((cause == Connection.DisconnectCause.BUSY)
1223                        || (cause == Connection.DisconnectCause.CONGESTION)) {
1224                        photoImageResource = R.drawable.picture_busy;
1225                    }
1226                } else if (DBG) {
1227                    log("updatePhotoForCallState: connection is null, ignoring.");
1228                }
1229
1230                // TODO: add special images for any other DisconnectCauses?
1231                break;
1232
1233            case ALERTING:
1234            case DIALING:
1235            default:
1236                // Leave the photo alone in all other states.
1237                // If this call is an individual call, and the image is currently
1238                // displaying a state, (rather than a photo), we'll need to update
1239                // the image.
1240                // This is for the case where we've been displaying the state and
1241                // now we need to restore the photo.  This can happen because we
1242                // only query the CallerInfo once, and limit the number of times
1243                // the image is loaded. (So a state image may overwrite the photo
1244                // and we would otherwise have no way of displaying the photo when
1245                // the state goes away.)
1246
1247                // if the photoResource field is filled-in in the Connection's
1248                // caller info, then we can just use that instead of requesting
1249                // for a photo load.
1250
1251                // look for the photoResource if it is available.
1252                CallerInfo ci = null;
1253                {
1254                    Connection conn = null;
1255                    int phoneType = mApplication.phone.getPhoneType();
1256                    if (phoneType == Phone.PHONE_TYPE_CDMA) {
1257                        conn = call.getLatestConnection();
1258                    } else if (phoneType == Phone.PHONE_TYPE_GSM) {
1259                        conn = call.getEarliestConnection();
1260                    } else {
1261                        throw new IllegalStateException("Unexpected phone type: " + phoneType);
1262                    }
1263
1264                    if (conn != null) {
1265                        Object o = conn.getUserData();
1266                        if (o instanceof CallerInfo) {
1267                            ci = (CallerInfo) o;
1268                        } else if (o instanceof PhoneUtils.CallerInfoToken) {
1269                            ci = ((PhoneUtils.CallerInfoToken) o).currentInfo;
1270                        }
1271                    }
1272                }
1273
1274                if (ci != null) {
1275                    photoImageResource = ci.photoResource;
1276                }
1277
1278                // If no photoResource found, check to see if this is a conference call. If
1279                // it is not a conference call:
1280                //   1. Try to show the cached image
1281                //   2. If the image is not cached, check to see if a load request has been
1282                //      made already.
1283                //   3. If the load request has not been made [DISPLAY_DEFAULT], start the
1284                //      request and note that it has started by updating photo state with
1285                //      [DISPLAY_IMAGE].
1286                // Load requests started in (3) use a placeholder image of -1 to hide the
1287                // image by default.  Please refer to CallerInfoAsyncQuery.java for cases
1288                // where CallerInfo.photoResource may be set.
1289                if (photoImageResource == 0) {
1290                    if (!PhoneUtils.isConferenceCall(call)) {
1291                        if (!showCachedImage(mPhoto, ci) && (mPhotoTracker.getPhotoState() ==
1292                                ContactsAsyncHelper.ImageTracker.DISPLAY_DEFAULT)) {
1293                            ContactsAsyncHelper.updateImageViewWithContactPhotoAsync(ci,
1294                                    getContext(), mPhoto, mPhotoTracker.getPhotoUri(), -1);
1295                            mPhotoTracker.setPhotoState(
1296                                    ContactsAsyncHelper.ImageTracker.DISPLAY_IMAGE);
1297                        }
1298                    }
1299                } else {
1300                    showImage(mPhoto, photoImageResource);
1301                    mPhotoTracker.setPhotoState(ContactsAsyncHelper.ImageTracker.DISPLAY_IMAGE);
1302                    return;
1303                }
1304                break;
1305        }
1306
1307        if (photoImageResource != 0) {
1308            if (DBG) log("- overrriding photo image: " + photoImageResource);
1309            showImage(mPhoto, photoImageResource);
1310            // Track the image state.
1311            mPhotoTracker.setPhotoState(ContactsAsyncHelper.ImageTracker.DISPLAY_DEFAULT);
1312        }
1313    }
1314
1315    /**
1316     * Try to display the cached image from the callerinfo object.
1317     *
1318     *  @return true if we were able to find the image in the cache, false otherwise.
1319     */
1320    private static final boolean showCachedImage(ImageView view, CallerInfo ci) {
1321        if ((ci != null) && ci.isCachedPhotoCurrent) {
1322            if (ci.cachedPhoto != null) {
1323                showImage(view, ci.cachedPhoto);
1324            } else {
1325                showImage(view, R.drawable.picture_unknown);
1326            }
1327            return true;
1328        }
1329        return false;
1330    }
1331
1332    /** Helper function to display the resource in the imageview AND ensure its visibility.*/
1333    private static final void showImage(ImageView view, int resource) {
1334        view.setImageResource(resource);
1335        view.setVisibility(View.VISIBLE);
1336    }
1337
1338    /** Helper function to display the drawable in the imageview AND ensure its visibility.*/
1339    private static final void showImage(ImageView view, Drawable drawable) {
1340        view.setImageDrawable(drawable);
1341        view.setVisibility(View.VISIBLE);
1342    }
1343
1344    /**
1345     * Returns the "Menu button hint" TextView (which is manipulated
1346     * directly by the InCallScreen.)
1347     * @see InCallScreen.updateMenuButtonHint()
1348     */
1349    /* package */ TextView getMenuButtonHint() {
1350        return mMenuButtonHint;
1351    }
1352
1353    /**
1354     * Sets the left and right margins of the specified ViewGroup (whose
1355     * LayoutParams object which must inherit from
1356     * ViewGroup.MarginLayoutParams.)
1357     *
1358     * TODO: Is there already a convenience method like this somewhere?
1359     */
1360    private void setSideMargins(ViewGroup vg, int margin) {
1361        ViewGroup.MarginLayoutParams lp =
1362                (ViewGroup.MarginLayoutParams) vg.getLayoutParams();
1363        // Equivalent to setting android:layout_marginLeft/Right in XML
1364        lp.leftMargin = margin;
1365        lp.rightMargin = margin;
1366        vg.setLayoutParams(lp);
1367    }
1368
1369    /**
1370     * Sets the CallCard "upper title".  Also, depending on the passed-in
1371     * Call state, possibly display an icon along with the title.
1372     */
1373    private void setUpperTitle(String title, int color, Call.State state) {
1374        mUpperTitle.setText(title);
1375        mUpperTitle.setTextColor(color);
1376
1377        int bluetoothIconId = 0;
1378        if (!TextUtils.isEmpty(title)
1379                && ((state == Call.State.INCOMING) || (state == Call.State.WAITING))
1380                && mApplication.showBluetoothIndication()) {
1381            // Display the special bluetooth icon also, if this is an incoming
1382            // call and the audio will be routed to bluetooth.
1383            bluetoothIconId = R.drawable.ic_incoming_call_bluetooth;
1384        }
1385
1386        mUpperTitle.setCompoundDrawablesWithIntrinsicBounds(bluetoothIconId, 0, 0, 0);
1387        if (bluetoothIconId != 0) mUpperTitle.setCompoundDrawablePadding((int) (mDensity * 5));
1388    }
1389
1390    /**
1391     * Clears the CallCard "upper title", for states (like a normal
1392     * ongoing call) where we don't use any "title" at all.
1393     */
1394    private void clearUpperTitle() {
1395        setUpperTitle("", 0, Call.State.IDLE);  // Use dummy values for "color" and "state"
1396    }
1397
1398    /**
1399     * Hides the top-level UI elements of the call card:  The "main
1400     * call card" element representing the current active or ringing call,
1401     * and also the info areas for "ongoing" or "on hold" calls in some
1402     * states.
1403     *
1404     * This is intended to be used in special states where the normal
1405     * in-call UI is totally replaced by some other UI, like OTA mode on a
1406     * CDMA device.
1407     *
1408     * To bring back the regular CallCard UI, just re-run the normal
1409     * updateState() call sequence.
1410     */
1411    public void hideCallCardElements() {
1412        mPrimaryCallInfo.setVisibility(View.GONE);
1413        mSecondaryCallInfo.setVisibility(View.GONE);
1414    }
1415
1416    /*
1417     * Updates the hint (like "Rotate to answer") that we display while
1418     * the user is dragging the incoming call RotarySelector widget.
1419     */
1420    /* package */ void setRotarySelectorHint(int hintTextResId, int hintColorResId) {
1421        mRotarySelectorHintTextResId = hintTextResId;
1422        mRotarySelectorHintColorResId = hintColorResId;
1423    }
1424
1425    // View.OnClickListener implementation
1426    public void onClick(View view) {
1427        int id = view.getId();
1428        if (DBG) log("onClick(View " + view + ", id " + id + ")...");
1429
1430        switch (id) {
1431            case R.id.manageConferencePhotoButton:
1432                // A click on anything here gets forwarded
1433                // straight to the InCallScreen.
1434                mInCallScreen.handleOnscreenButtonClick(id);
1435                break;
1436
1437            default:
1438                Log.w(LOG_TAG, "onClick: unexpected click: View " + view + ", id " + id);
1439                break;
1440        }
1441    }
1442
1443    // Accessibility event support.
1444    // Since none of the CallCard elements are focusable, we need to manually
1445    // fill in the AccessibilityEvent here (so that the name / number / etc will
1446    // get pronounced by a screen reader, for example.)
1447    @Override
1448    public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) {
1449        dispatchPopulateAccessibilityEvent(event, mUpperTitle);
1450        dispatchPopulateAccessibilityEvent(event, mPhoto);
1451        dispatchPopulateAccessibilityEvent(event, mManageConferencePhotoButton);
1452        dispatchPopulateAccessibilityEvent(event, mName);
1453        dispatchPopulateAccessibilityEvent(event, mPhoneNumber);
1454        dispatchPopulateAccessibilityEvent(event, mLabel);
1455        dispatchPopulateAccessibilityEvent(event, mSocialStatus);
1456        dispatchPopulateAccessibilityEvent(event, mSecondaryCallName);
1457        dispatchPopulateAccessibilityEvent(event, mSecondaryCallStatus);
1458        dispatchPopulateAccessibilityEvent(event, mSecondaryCallPhoto);
1459        return true;
1460    }
1461
1462    private void dispatchPopulateAccessibilityEvent(AccessibilityEvent event, View view) {
1463        List<CharSequence> eventText = event.getText();
1464        int size = eventText.size();
1465        view.dispatchPopulateAccessibilityEvent(event);
1466        // if no text added write null to keep relative position
1467        if (size == eventText.size()) {
1468            eventText.add(null);
1469        }
1470    }
1471
1472
1473    // Debugging / testing code
1474
1475    private void log(String msg) {
1476        Log.d(LOG_TAG, msg);
1477    }
1478}
1479