CallCard.java revision 8bb467d9a1106dedd79e42166c7b6e9fc9a897a7
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 com.android.internal.telephony.Call;
20import com.android.internal.telephony.CallerInfo;
21import com.android.internal.telephony.CallerInfoAsyncQuery;
22import com.android.internal.telephony.Connection;
23import com.android.internal.telephony.Phone;
24
25import android.content.ContentUris;
26import android.content.Context;
27import android.graphics.drawable.Drawable;
28import android.net.Uri;
29import android.pim.ContactsAsyncHelper;
30import android.provider.Contacts.People;
31import android.text.TextUtils;
32import android.text.format.DateUtils;
33import android.util.AttributeSet;
34import android.util.Log;
35import android.view.LayoutInflater;
36import android.view.MotionEvent;
37import android.view.View;
38import android.view.ViewGroup;
39import android.widget.FrameLayout;
40import android.widget.ImageView;
41import android.widget.TextView;
42
43/**
44 * "Call card" UI element: the in-call screen contains a tiled layout of call
45 * cards, each representing the state of a current "call" (ie. an active call,
46 * a call on hold, or an incoming call.)
47 */
48public class CallCard extends FrameLayout
49        implements CallTime.OnTickListener, CallerInfoAsyncQuery.OnQueryCompleteListener,
50                ContactsAsyncHelper.OnImageLoadCompleteListener{
51    private static final String LOG_TAG = "PHONE/CallCard";
52    private static final boolean DBG = false;
53    private static final boolean PROFILE = true;
54
55    /**
56     * Reference to the InCallScreen activity that owns us.  This may be
57     * null if we haven't been initialized yet *or* after the InCallScreen
58     * activity has been destroyed.
59     */
60    private InCallScreen mInCallScreen;
61
62    // Top-level subviews of the CallCard
63    private ViewGroup mMainCallCard;
64    private ViewGroup mOtherCallOngoingInfoArea;
65    private ViewGroup mOtherCallOnHoldInfoArea;
66
67    // "Upper" and "lower" title widgets
68    private TextView mUpperTitle;
69    private ViewGroup mLowerTitleViewGroup;
70    private TextView mLowerTitle;
71    private ImageView mLowerTitleIcon;
72    private TextView mElapsedTime;
73
74    // Text colors, used with the lower title and "other call" info areas
75    private int mTextColorConnected;
76    private int mTextColorConnectedBluetooth;
77    private int mTextColorEnded;
78    private int mTextColorOnHold;
79
80    private ImageView mPhoto;
81    private TextView mName;
82    private TextView mPhoneNumber;
83    private TextView mLabel;
84
85    // "Other call" info area
86    private ImageView mOtherCallOngoingIcon;
87    private TextView mOtherCallOngoingName;
88    private TextView mOtherCallOngoingStatus;
89    private TextView mOtherCallOnHoldName;
90    private TextView mOtherCallOnHoldStatus;
91
92    // Menu button hint
93    private TextView mMenuButtonHint;
94
95    private boolean mRingerSilenced;
96
97    private CallTime mCallTime;
98
99    // Track the state for the photo.
100    private ContactsAsyncHelper.ImageTracker mPhotoTracker;
101
102    // A few hardwired constants used in our screen layout.
103    // TODO: These should all really come from resources, but that's
104    // nontrivial; see the javadoc for the ConfigurationHelper class.
105    // For now, let's at least keep them all here in one place
106    // rather than sprinkled througout this file.
107    //
108    static final int MAIN_CALLCARD_MIN_HEIGHT_LANDSCAPE = 200;
109    static final int CALLCARD_SIDE_MARGIN_LANDSCAPE = 50;
110    static final float TITLE_TEXT_SIZE_LANDSCAPE = 22F;  // scaled pixels
111
112    public CallCard(Context context, AttributeSet attrs) {
113        super(context, attrs);
114
115        if (DBG) log("CallCard constructor...");
116        if (DBG) log("- this = " + this);
117        if (DBG) log("- context " + context + ", attrs " + attrs);
118
119        // Inflate the contents of this CallCard, and add it (to ourself) as a child.
120        LayoutInflater inflater = LayoutInflater.from(context);
121        inflater.inflate(
122                R.layout.call_card,  // resource
123                this,                // root
124                true);
125
126        mCallTime = new CallTime(this);
127
128        // create a new object to track the state for the photo.
129        mPhotoTracker = new ContactsAsyncHelper.ImageTracker();
130    }
131
132    void setInCallScreenInstance(InCallScreen inCallScreen) {
133        mInCallScreen = inCallScreen;
134    }
135
136    void reset() {
137        if (DBG) log("reset()...");
138
139        mRingerSilenced = false;
140
141        // default to show ACTIVE call style, with empty title and status text
142        showCallConnected();
143        setUpperTitle("");
144    }
145
146    public void onTickForCallTimeElapsed(long timeElapsed) {
147        // While a call is in progress, update the elapsed time shown
148        // onscreen.
149        updateElapsedTimeWidget(timeElapsed);
150    }
151
152    /* package */
153    void stopTimer() {
154        mCallTime.cancelTimer();
155    }
156
157    @Override
158    protected void onFinishInflate() {
159        super.onFinishInflate();
160
161        if (DBG) log("CallCard onFinishInflate(this = " + this + ")...");
162
163        LayoutInflater inflater = LayoutInflater.from(getContext());
164
165        mMainCallCard = (ViewGroup) findViewById(R.id.mainCallCard);
166        mOtherCallOngoingInfoArea = (ViewGroup) findViewById(R.id.otherCallOngoingInfoArea);
167        mOtherCallOnHoldInfoArea = (ViewGroup) findViewById(R.id.otherCallOnHoldInfoArea);
168
169        // "Upper" and "lower" title widgets
170        mUpperTitle = (TextView) findViewById(R.id.upperTitle);
171        mLowerTitleViewGroup = (ViewGroup) findViewById(R.id.lowerTitleViewGroup);
172        mLowerTitle = (TextView) findViewById(R.id.lowerTitle);
173        mLowerTitleIcon = (ImageView) findViewById(R.id.lowerTitleIcon);
174        mElapsedTime = (TextView) findViewById(R.id.elapsedTime);
175
176        // Text colors
177        mTextColorConnected = getResources().getColor(R.color.incall_textConnected);
178        mTextColorConnectedBluetooth =
179                getResources().getColor(R.color.incall_textConnectedBluetooth);
180        mTextColorEnded = getResources().getColor(R.color.incall_textEnded);
181        mTextColorOnHold = getResources().getColor(R.color.incall_textOnHold);
182
183        // "Caller info" area, including photo / name / phone numbers / etc
184        mPhoto = (ImageView) findViewById(R.id.photo);
185        mName = (TextView) findViewById(R.id.name);
186        mPhoneNumber = (TextView) findViewById(R.id.phoneNumber);
187        mLabel = (TextView) findViewById(R.id.label);
188
189        // "Other call" info area
190        mOtherCallOngoingIcon = (ImageView) findViewById(R.id.otherCallOngoingIcon);
191        mOtherCallOngoingName = (TextView) findViewById(R.id.otherCallOngoingName);
192        mOtherCallOngoingStatus = (TextView) findViewById(R.id.otherCallOngoingStatus);
193        mOtherCallOnHoldName = (TextView) findViewById(R.id.otherCallOnHoldName);
194        mOtherCallOnHoldStatus = (TextView) findViewById(R.id.otherCallOnHoldStatus);
195
196        // Menu Button hint
197        mMenuButtonHint = (TextView) findViewById(R.id.menuButtonHint);
198    }
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        // TODO: This code, and updateForegroundCall() / updateRingingCall(),
205        // can probably still be simplified some more.
206
207        Phone.State state = phone.getState();  // IDLE, RINGING, or OFFHOOK
208        if (state == Phone.State.RINGING) {
209            // A phone call is ringing *or* call waiting
210            // (ie. another call may also be active as well.)
211            updateRingingCall(phone);
212        } else if (state == Phone.State.OFFHOOK) {
213            // The phone is off hook. At least one call exists that is
214            // dialing, active, or holding, and no calls are ringing or waiting.
215            updateForegroundCall(phone);
216        } else {
217            // The phone state is IDLE!
218            //
219            // The most common reason for this is if a call just
220            // ended: the phone will be idle, but we *will* still
221            // have a call in the DISCONNECTED state:
222            Call fgCall = phone.getForegroundCall();
223            Call bgCall = phone.getBackgroundCall();
224            if ((fgCall.getState() == Call.State.DISCONNECTED)
225                || (bgCall.getState() == Call.State.DISCONNECTED)) {
226                // In this case, we want the main CallCard to display
227                // the "Call ended" state.  The normal "foreground call"
228                // code path handles that.
229                updateForegroundCall(phone);
230            } else {
231                // We don't have any DISCONNECTED calls, which means
232                // that the phone is *truly* idle.
233                //
234                // It's very rare to be on the InCallScreen at all in this
235                // state, but it can happen in some cases:
236                // - A stray onPhoneStateChanged() event came in to the
237                //   InCallScreen *after* it was dismissed.
238                // - We're allowed to be on the InCallScreen because
239                //   an MMI or USSD is running, but there's no actual "call"
240                //   to display.
241                // - We're displaying an error dialog to the user
242                //   (explaining why the call failed), so we need to stay on
243                //   the InCallScreen so that the dialog will be visible.
244                //
245                // In these cases, put the callcard into a sane but "blank" state:
246                updateNoCall(phone);
247            }
248        }
249    }
250
251    /**
252     * Updates the UI for the state where the phone is in use, but not ringing.
253     */
254    private void updateForegroundCall(Phone phone) {
255        if (DBG) log("updateForegroundCall()...");
256
257        Call fgCall = phone.getForegroundCall();
258        Call bgCall = phone.getBackgroundCall();
259
260        if (fgCall.isIdle() && !fgCall.hasConnections()) {
261            if (DBG) log("updateForegroundCall: no active call, show holding call");
262            // TODO: make sure this case agrees with the latest UI spec.
263
264            // Display the background call in the main info area of the
265            // CallCard, since there is no foreground call.  Note that
266            // displayMainCallStatus() will notice if the call we passed in is on
267            // hold, and display the "on hold" indication.
268            fgCall = bgCall;
269
270            // And be sure to not display anything in the "on hold" box.
271            bgCall = null;
272        }
273
274        displayMainCallStatus(phone, fgCall);
275        displayOnHoldCallStatus(phone, bgCall);
276        displayOngoingCallStatus(phone, null);
277    }
278
279    /**
280     * Updates the UI for the state where an incoming call is ringing (or
281     * call waiting), regardless of whether the phone's already offhook.
282     */
283    private void updateRingingCall(Phone phone) {
284        if (DBG) log("updateRingingCall()...");
285
286        Call ringingCall = phone.getRingingCall();
287        Call fgCall = phone.getForegroundCall();
288        Call bgCall = phone.getBackgroundCall();
289
290        displayMainCallStatus(phone, ringingCall);
291        displayOnHoldCallStatus(phone, bgCall);
292        displayOngoingCallStatus(phone, fgCall);
293    }
294
295    /**
296     * Updates the UI for the state where the phone is not in use.
297
298     * This is analogous to updateForegroundCall() and updateRingingCall(),
299
300     * but for the (uncommon) case where the phone is
301     * totally idle.  (See comments in updateState() above.)
302     *
303     * This puts the callcard into a sane but "blank" state.
304     */
305    private void updateNoCall(Phone phone) {
306        if (DBG) log("updateNoCall()...");
307
308        displayMainCallStatus(phone, null);
309        displayOnHoldCallStatus(phone, null);
310        displayOngoingCallStatus(phone, null);
311    }
312
313    /**
314     * Updates the main block of caller info on the CallCard
315     * (ie. the stuff in the mainCallCard block) based on the specified Call.
316     */
317    private void displayMainCallStatus(Phone phone, Call call) {
318        if (DBG) log("displayMainCallStatus(phone " + phone
319                     + ", call " + call + ")...");
320
321        if (call == null) {
322            // There's no call to display, presumably because the phone is idle.
323            mMainCallCard.setVisibility(View.GONE);
324            return;
325        }
326        mMainCallCard.setVisibility(View.VISIBLE);
327
328        Call.State state = call.getState();
329        if (DBG) log("  - call.state: " + call.getState());
330
331        int callCardBackgroundResid = 0;
332
333        // Background frame resources are different between portrait/landscape.
334        // TODO: Don't do this manually.  Instead let the resource system do
335        // it: just move the *_land assets over to the res/drawable-land
336        // directory (but with the same filename as the corresponding
337        // portrait asset.)
338        boolean landscapeMode = InCallScreen.ConfigurationHelper.isLandscape();
339
340        // Background images are also different if Bluetooth is active.
341        final boolean bluetoothActive = PhoneApp.getInstance().showBluetoothIndication();
342
343        switch (state) {
344            case ACTIVE:
345                showCallConnected();
346
347                if (bluetoothActive) {
348                    callCardBackgroundResid =
349                            landscapeMode ? R.drawable.incall_frame_bluetooth_tall_land
350                            : R.drawable.incall_frame_bluetooth_tall_port;
351                } else {
352                    callCardBackgroundResid =
353                            landscapeMode ? R.drawable.incall_frame_connected_tall_land
354                            : R.drawable.incall_frame_connected_tall_port;
355                }
356
357
358                // update timer field
359                if (DBG) log("displayMainCallStatus: start periodicUpdateTimer");
360                mCallTime.setActiveCallMode(call);
361                mCallTime.reset();
362                mCallTime.periodicUpdateTimer();
363
364                break;
365
366            case HOLDING:
367                showCallOnhold();
368
369                callCardBackgroundResid =
370                        landscapeMode ? R.drawable.incall_frame_hold_tall_land
371                        : R.drawable.incall_frame_hold_tall_port;
372
373                // update timer field
374                mCallTime.cancelTimer();
375
376                break;
377
378            case DISCONNECTED:
379                reset();
380                showCallEnded();
381
382                callCardBackgroundResid =
383                        landscapeMode ? R.drawable.incall_frame_ended_tall_land
384                        : R.drawable.incall_frame_ended_tall_port;
385
386                // Stop getting timer ticks from this call
387                mCallTime.cancelTimer();
388
389                break;
390
391            case DIALING:
392            case ALERTING:
393                showCallConnecting();
394
395                if (bluetoothActive) {
396                    callCardBackgroundResid =
397                            landscapeMode ? R.drawable.incall_frame_bluetooth_tall_land
398                            : R.drawable.incall_frame_bluetooth_tall_port;
399                } else {
400                    callCardBackgroundResid =
401                            landscapeMode ? R.drawable.incall_frame_normal_tall_land
402                            : R.drawable.incall_frame_normal_tall_port;
403                }
404
405                // Stop getting timer ticks from a previous call
406                mCallTime.cancelTimer();
407
408                break;
409
410            case INCOMING:
411            case WAITING:
412                showCallIncoming();
413
414                if (bluetoothActive) {
415                    callCardBackgroundResid =
416                            landscapeMode ? R.drawable.incall_frame_bluetooth_tall_land
417                            : R.drawable.incall_frame_bluetooth_tall_port;
418                } else {
419                    callCardBackgroundResid =
420                            landscapeMode ? R.drawable.incall_frame_normal_tall_land
421                            : R.drawable.incall_frame_normal_tall_port;
422                }
423
424                // Stop getting timer ticks from a previous call
425                mCallTime.cancelTimer();
426
427                break;
428
429            case IDLE:
430                // The "main CallCard" should never be trying to display
431                // an idle call!  In updateState(), if the phone is idle,
432                // we call updateNoCall(), which means that we shouldn't
433                // have passed a call into this method at all.
434                Log.w(LOG_TAG, "displayMainCallStatus: IDLE call in the main call card!");
435
436                // (It is possible, though, that we had a valid call which
437                // became idle *after* the check in updateState() but
438                // before we get here...  So continue the best we can,
439                // with whatever (stale) info we can get from the
440                // passed-in Call object.)
441
442                break;
443
444            default:
445                Log.w(LOG_TAG, "displayMainCallStatus: unexpected call state: " + state);
446                break;
447        }
448
449        updateCardTitleWidgets(phone, call);
450
451        if (PhoneUtils.isConferenceCall(call)) {
452            // Update onscreen info for a conference call.
453            updateDisplayForConference();
454        } else {
455            // Update onscreen info for a regular call (which presumably
456            // has only one connection.)
457            Connection conn = call.getEarliestConnection();
458
459            boolean isPrivateNumber = false; // TODO: need isPrivate() API
460
461            if (conn == null) {
462                if (DBG) log("displayMainCallStatus: connection is null, using default values.");
463                // if the connection is null, we run through the behaviour
464                // we had in the past, which breaks down into trivial steps
465                // with the current implementation of getCallerInfo and
466                // updateDisplayForPerson.
467                CallerInfo info = PhoneUtils.getCallerInfo(getContext(), conn);
468                updateDisplayForPerson(info, isPrivateNumber, false, call);
469            } else {
470                if (DBG) log("  - CONN: " + conn + ", state = " + conn.getState());
471
472                // make sure that we only make a new query when the current
473                // callerinfo differs from what we've been requested to display.
474                boolean runQuery = true;
475                Object o = conn.getUserData();
476                if (o instanceof PhoneUtils.CallerInfoToken) {
477                    runQuery = mPhotoTracker.isDifferentImageRequest(
478                            ((PhoneUtils.CallerInfoToken) o).currentInfo);
479                } else {
480                    runQuery = mPhotoTracker.isDifferentImageRequest(conn);
481                }
482
483                if (runQuery) {
484                    if (DBG) log("- displayMainCallStatus: starting CallerInfo query...");
485                    PhoneUtils.CallerInfoToken info =
486                            PhoneUtils.startGetCallerInfo(getContext(), conn, this, call);
487                    updateDisplayForPerson(info.currentInfo, isPrivateNumber, !info.isFinal, call);
488                } else {
489                    // No need to fire off a new query.  We do still need
490                    // to update the display, though (since we might have
491                    // previously been in the "conference call" state.)
492                    if (DBG) log("- displayMainCallStatus: using data we already have...");
493                    if (o instanceof CallerInfo) {
494                        CallerInfo ci = (CallerInfo) o;
495                        if (DBG) log("   ==> Got CallerInfo; updating display: ci = " + ci);
496                        updateDisplayForPerson(ci, false, false, call);
497                    } else if (o instanceof PhoneUtils.CallerInfoToken){
498                        CallerInfo ci = ((PhoneUtils.CallerInfoToken) o).currentInfo;
499                        if (DBG) log("   ==> Got CallerInfoToken; updating display: ci = " + ci);
500                        updateDisplayForPerson(ci, false, true, call);
501                    } else {
502                        Log.w(LOG_TAG, "displayMainCallStatus: runQuery was false, "
503                              + "but we didn't have a cached CallerInfo object!  o = " + o);
504                        // TODO: any easy way to recover here (given that
505                        // the CallCard is probably displaying stale info
506                        // right now?)  Maybe force the CallCard into the
507                        // "Unknown" state?
508                    }
509                }
510            }
511        }
512
513        // In some states we override the "photo" ImageView to be an
514        // indication of the current state, rather than displaying the
515        // regular photo as set above.
516        updatePhotoForCallState(call);
517
518        // Set the background frame color based on the state of the call.
519        setMainCallCardBackgroundResource(callCardBackgroundResid);
520        // (Text colors are set in updateCardTitleWidgets().)
521    }
522
523    /**
524     * Implemented for CallerInfoAsyncQuery.OnQueryCompleteListener interface.
525     * refreshes the CallCard data when it called.
526     */
527    public void onQueryComplete(int token, Object cookie, CallerInfo ci) {
528        if (DBG) log("onQueryComplete: token " + token + ", cookie " + cookie + ", ci " + ci);
529
530        if (cookie instanceof Call) {
531            // grab the call object and update the display for an individual call,
532            // as well as the successive call to update image via call state.
533            // If the object is a textview instead, we update it as we need to.
534            if (DBG) log("callerinfo query complete, updating ui from displayMainCallStatus()");
535            Call call = (Call) cookie;
536            updateDisplayForPerson(ci, false, false, call);
537            updatePhotoForCallState(call);
538
539        } else if (cookie instanceof TextView){
540            if (DBG) log("callerinfo query complete, updating ui from ongoing or onhold");
541            ((TextView) cookie).setText(PhoneUtils.getCompactNameFromCallerInfo(ci, mContext));
542        }
543    }
544
545    /**
546     * Implemented for ContactsAsyncHelper.OnImageLoadCompleteListener interface.
547     * make sure that the call state is reflected after the image is loaded.
548     */
549    public void onImageLoadComplete(int token, Object cookie, ImageView iView,
550            boolean imagePresent){
551        if (cookie != null) {
552            updatePhotoForCallState((Call) cookie);
553        }
554    }
555
556    /**
557     * Updates the "upper" and "lower" titles based on the current state of this call.
558     */
559    private void updateCardTitleWidgets(Phone phone, Call call) {
560        if (DBG) log("updateCardTitleWidgets(call " + call + ")...");
561        Call.State state = call.getState();
562
563        // TODO: Still need clearer spec on exactly how title *and* status get
564        // set in all states.  (Then, given that info, refactor the code
565        // here to be more clear about exactly which widgets on the card
566        // need to be set.)
567
568        // Normal "foreground" call card:
569        String cardTitle = getTitleForCallCard(call);
570
571        if (DBG) log("updateCardTitleWidgets: " + cardTitle);
572
573        // We display *either* the "upper title" or the "lower title", but
574        // never both.
575        if (state == Call.State.ACTIVE) {
576            // Use the "lower title" (in green).
577            mLowerTitleViewGroup.setVisibility(View.VISIBLE);
578
579            final boolean bluetoothActive = PhoneApp.getInstance().showBluetoothIndication();
580            int ongoingCallIcon = bluetoothActive ? R.drawable.ic_incall_ongoing_bluetooth
581                    : R.drawable.ic_incall_ongoing;
582            mLowerTitleIcon.setImageResource(ongoingCallIcon);
583
584            mLowerTitle.setText(cardTitle);
585
586            int textColor = bluetoothActive ? mTextColorConnectedBluetooth : mTextColorConnected;
587            mLowerTitle.setTextColor(textColor);
588            mElapsedTime.setTextColor(textColor);
589            setUpperTitle("");
590        } else if (state == Call.State.DISCONNECTED) {
591            // Use the "lower title" (in red).
592            // TODO: We may not *always* want to use the lower title for
593            // the DISCONNECTED state.  "Error" states like BUSY or
594            // CONGESTION (see getCallFailedString()) should probably go
595            // in the upper title, for example.  In fact, the lower title
596            // should probably be used *only* for the normal "Call ended"
597            // case.
598            mLowerTitleViewGroup.setVisibility(View.VISIBLE);
599            mLowerTitleIcon.setImageResource(R.drawable.ic_incall_end);
600            mLowerTitle.setText(cardTitle);
601            mLowerTitle.setTextColor(mTextColorEnded);
602            mElapsedTime.setTextColor(mTextColorEnded);
603            setUpperTitle("");
604        } else {
605            // All other states (DIALING, INCOMING, etc.) use the "upper title":
606            setUpperTitle(cardTitle, state);
607            mLowerTitleViewGroup.setVisibility(View.INVISIBLE);
608        }
609
610        // Draw the onscreen "elapsed time" indication EXCEPT if we're in
611        // the "Call ended" state.  (In that case, don't touch the
612        // mElapsedTime widget, so we continue to see the elapsed time of
613        // the call that just ended.)
614        if (call.getState() == Call.State.DISCONNECTED) {
615            // "Call ended" state -- don't touch the onscreen elapsed time.
616        } else {
617            long duration = CallTime.getCallDuration(call);  // msec
618            updateElapsedTimeWidget(duration / 1000);
619            // Also see onTickForCallTimeElapsed(), which updates this
620            // widget once per second while the call is active.
621        }
622    }
623
624    /**
625     * Updates mElapsedTime based on the specified number of seconds.
626     * A timeElapsed value of zero means to not show an elapsed time at all.
627     */
628    private void updateElapsedTimeWidget(long timeElapsed) {
629        // if (DBG) log("updateElapsedTimeWidget: " + timeElapsed);
630        if (timeElapsed == 0) {
631            mElapsedTime.setText("");
632        } else {
633            mElapsedTime.setText(DateUtils.formatElapsedTime(timeElapsed));
634        }
635    }
636
637    /**
638     * Returns the "card title" displayed at the top of a foreground
639     * ("active") CallCard to indicate the current state of this call, like
640     * "Dialing" or "In call" or "On hold".  A null return value means that
641     * there's no title string for this state.
642     */
643    private String getTitleForCallCard(Call call) {
644        String retVal = null;
645        Call.State state = call.getState();
646        Context context = getContext();
647        int resId;
648
649        if (DBG) log("- getTitleForCallCard(Call " + call + ")...");
650
651        switch (state) {
652            case IDLE:
653                break;
654
655            case ACTIVE:
656                // Title is "Call in progress".  (Note this appears in the
657                // "lower title" area of the CallCard.)
658                retVal = context.getString(R.string.card_title_in_progress);
659                break;
660
661            case HOLDING:
662                retVal = context.getString(R.string.card_title_on_hold);
663                // TODO: if this is a conference call on hold,
664                // maybe have a special title here too?
665                break;
666
667            case DIALING:
668            case ALERTING:
669                retVal = context.getString(R.string.card_title_dialing);
670                break;
671
672            case INCOMING:
673            case WAITING:
674                retVal = context.getString(R.string.card_title_incoming_call);
675                break;
676
677            case DISCONNECTED:
678                retVal = getCallFailedString(call);
679                break;
680        }
681
682        if (DBG) log("  ==> result: " + retVal);
683        return retVal;
684    }
685
686    /**
687     * Updates the "on hold" box in the "other call" info area
688     * (ie. the stuff in the otherCallOnHoldInfo block)
689     * based on the specified Call.
690     * Or, clear out the "on hold" box if the specified call
691     * is null or idle.
692     */
693    private void displayOnHoldCallStatus(Phone phone, Call call) {
694        if (DBG) log("displayOnHoldCallStatus(call =" + call + ")...");
695        if (call == null) {
696            mOtherCallOnHoldInfoArea.setVisibility(View.GONE);
697            return;
698        }
699
700        Call.State state = call.getState();
701        switch (state) {
702            case HOLDING:
703                // Ok, there actually is a background call on hold.
704                // Display the "on hold" box.
705                String name;
706
707                // First, see if we need to query.
708                if (PhoneUtils.isConferenceCall(call)) {
709                    if (DBG) log("==> conference call.");
710                    name = getContext().getString(R.string.confCall);
711                } else {
712                    // perform query and update the name temporarily
713                    // make sure we hand the textview we want updated to the
714                    // callback function.
715                    if (DBG) log("==> NOT a conf call; call startGetCallerInfo...");
716                    PhoneUtils.CallerInfoToken info = PhoneUtils.startGetCallerInfo(
717                            getContext(), call, this, mOtherCallOnHoldName);
718                    name = PhoneUtils.getCompactNameFromCallerInfo(info.currentInfo, getContext());
719                }
720
721                mOtherCallOnHoldName.setText(name);
722
723                // The call here is always "on hold", so use the orange "hold" frame
724                // and orange text color:
725                setOnHoldInfoAreaBackgroundResource(R.drawable.incall_frame_hold_short);
726                mOtherCallOnHoldName.setTextColor(mTextColorOnHold);
727                mOtherCallOnHoldStatus.setTextColor(mTextColorOnHold);
728
729                mOtherCallOnHoldInfoArea.setVisibility(View.VISIBLE);
730
731                break;
732
733            default:
734                // There's actually no call on hold.  (Presumably this call's
735                // state is IDLE, since any other state is meaningless for the
736                // background call.)
737                mOtherCallOnHoldInfoArea.setVisibility(View.GONE);
738                break;
739        }
740    }
741
742    /**
743     * Updates the "Ongoing call" box in the "other call" info area
744     * (ie. the stuff in the otherCallOngoingInfo block)
745     * based on the specified Call.
746     * Or, clear out the "ongoing call" box if the specified call
747     * is null or idle.
748     */
749    private void displayOngoingCallStatus(Phone phone, Call call) {
750        if (DBG) log("displayOngoingCallStatus(call =" + call + ")...");
751        if (call == null) {
752            mOtherCallOngoingInfoArea.setVisibility(View.GONE);
753            return;
754        }
755
756        Call.State state = call.getState();
757        switch (state) {
758            case ACTIVE:
759            case DIALING:
760            case ALERTING:
761                // Ok, there actually is an ongoing call.
762                // Display the "ongoing call" box.
763                String name;
764
765                // First, see if we need to query.
766                if (PhoneUtils.isConferenceCall(call)) {
767                    name = getContext().getString(R.string.confCall);
768                } else {
769                    // perform query and update the name temporarily
770                    // make sure we hand the textview we want updated to the
771                    // callback function.
772                    PhoneUtils.CallerInfoToken info = PhoneUtils.startGetCallerInfo(
773                            getContext(), call, this, mOtherCallOngoingName);
774                    name = PhoneUtils.getCompactNameFromCallerInfo(info.currentInfo, getContext());
775                }
776
777                mOtherCallOngoingName.setText(name);
778
779                // This is an "ongoing" call: we normally use the green
780                // background frame and text color, but we use blue
781                // instead if bluetooth is in use.
782                boolean bluetoothActive = PhoneApp.getInstance().showBluetoothIndication();
783
784                int ongoingCallBackground =
785                        bluetoothActive ? R.drawable.incall_frame_bluetooth_short
786                        : R.drawable.incall_frame_connected_short;
787                setOngoingInfoAreaBackgroundResource(ongoingCallBackground);
788
789                int ongoingCallIcon = bluetoothActive ? R.drawable.ic_incall_ongoing_bluetooth
790                        : R.drawable.ic_incall_ongoing;
791                mOtherCallOngoingIcon.setImageResource(ongoingCallIcon);
792
793                int textColor = bluetoothActive ? mTextColorConnectedBluetooth
794                        : mTextColorConnected;
795                mOtherCallOngoingName.setTextColor(textColor);
796                mOtherCallOngoingStatus.setTextColor(textColor);
797
798                mOtherCallOngoingInfoArea.setVisibility(View.VISIBLE);
799
800                break;
801
802            default:
803                // There's actually no ongoing call.  (Presumably this call's
804                // state is IDLE, since any other state is meaningless for the
805                // foreground call.)
806                mOtherCallOngoingInfoArea.setVisibility(View.GONE);
807                break;
808        }
809    }
810
811
812    private String getCallFailedString(Call call) {
813        Phone phone = PhoneApp.getInstance().phone;
814        Connection c = call.getEarliestConnection();
815        int resID;
816
817        if (c == null) {
818            if (DBG) log("getCallFailedString: connection is null, using default values.");
819            // if this connection is null, just assume that the
820            // default case occurs.
821            resID = R.string.card_title_call_ended;
822        } else {
823
824            Connection.DisconnectCause cause = c.getDisconnectCause();
825
826            // TODO: The card *title* should probably be "Call ended" in all
827            // cases, but if the DisconnectCause was an error condition we should
828            // probably also display the specific failure reason somewhere...
829
830            switch (cause) {
831                case BUSY:
832                    resID = R.string.callFailed_userBusy;
833                    break;
834
835                case CONGESTION:
836                    resID = R.string.callFailed_congestion;
837                    break;
838
839                case LOST_SIGNAL:
840                    resID = R.string.callFailed_noSignal;
841                    break;
842
843                case LIMIT_EXCEEDED:
844                    resID = R.string.callFailed_limitExceeded;
845                    break;
846
847                case POWER_OFF:
848                    resID = R.string.callFailed_powerOff;
849                    break;
850
851                case SIM_ERROR:
852                    resID = R.string.callFailed_simError;
853                    break;
854
855                case OUT_OF_SERVICE:
856                    resID = R.string.callFailed_outOfService;
857                    break;
858
859                default:
860                    resID = R.string.card_title_call_ended;
861                    break;
862            }
863        }
864        return getContext().getString(resID);
865    }
866
867    private void showCallConnecting() {
868        if (DBG) log("showCallConnecting()...");
869        // TODO: remove if truly unused
870    }
871
872    private void showCallIncoming() {
873        if (DBG) log("showCallIncoming()...");
874        // TODO: remove if truly unused
875    }
876
877    private void showCallConnected() {
878        if (DBG) log("showCallConnected()...");
879        // TODO: remove if truly unused
880    }
881
882    private void showCallEnded() {
883        if (DBG) log("showCallEnded()...");
884        // TODO: remove if truly unused
885    }
886    private void showCallOnhold() {
887        if (DBG) log("showCallOnhold()...");
888        // TODO: remove if truly unused
889    }
890
891    /**
892     *  Add the Call object to these next 2 apis since the callbacks from
893     *  updateImageViewWithContactPhotoAsync call will need to use it.
894     */
895
896    private void updateDisplayForPerson(CallerInfo info, boolean isPrivateNumber, Call call) {
897        updateDisplayForPerson(info, isPrivateNumber, false, call);
898    }
899
900    /**
901     * Updates the name / photo / number / label fields on the CallCard
902     * based on the specified CallerInfo.
903     *
904     * If the current call is a conference call, use
905     * updateDisplayForConference() instead.
906     */
907    private void updateDisplayForPerson(CallerInfo info,
908                                        boolean isPrivateNumber,
909                                        boolean isTemporary,
910                                        Call call) {
911        if (DBG) log("updateDisplayForPerson(" + info + ")...");
912
913        // inform the state machine that we are displaying a photo.
914        mPhotoTracker.setPhotoRequest(info);
915        mPhotoTracker.setPhotoState(ContactsAsyncHelper.ImageTracker.DISPLAY_IMAGE);
916
917        String name;
918        String displayNumber = null;
919        String label = null;
920        Uri personUri = null;
921
922        if (info != null) {
923            // It appears that there is a small change in behaviour with the
924            // PhoneUtils' startGetCallerInfo whereby if we query with an
925            // empty number, we will get a valid CallerInfo object, but with
926            // fields that are all null, and the isTemporary boolean input
927            // parameter as true.
928
929            // In the past, we would see a NULL callerinfo object, but this
930            // ends up causing null pointer exceptions elsewhere down the
931            // line in other cases, so we need to make this fix instead. It
932            // appears that this was the ONLY call to PhoneUtils
933            // .getCallerInfo() that relied on a NULL CallerInfo to indicate
934            // an unknown contact.
935
936            if (TextUtils.isEmpty(info.name)) {
937                if (TextUtils.isEmpty(info.phoneNumber)) {
938                    if (isPrivateNumber) {
939                        name = getContext().getString(R.string.private_num);
940                    } else {
941                        name = getContext().getString(R.string.unknown);
942                    }
943                } else {
944                    name = info.phoneNumber;
945                }
946            } else {
947                name = info.name;
948                displayNumber = info.phoneNumber;
949                label = info.phoneLabel;
950            }
951            personUri = ContentUris.withAppendedId(People.CONTENT_URI, info.person_id);
952        } else {
953            if (isPrivateNumber) {
954                name = getContext().getString(R.string.private_num);
955            } else {
956                name = getContext().getString(R.string.unknown);
957            }
958        }
959        mName.setText(name);
960        mName.setVisibility(View.VISIBLE);
961
962        // Update mPhoto
963        // if the temporary flag is set, we know we'll be getting another call after
964        // the CallerInfo has been correctly updated.  So, we can skip the image
965        // loading until then.
966
967        // If the photoResource is filled in for the CallerInfo, (like with the
968        // Emergency Number case), then we can just set the photo image without
969        // requesting for an image load. Please refer to CallerInfoAsyncQuery.java
970        // for cases where CallerInfo.photoResource may be set.  We can also avoid
971        // the image load step if the image data is cached.
972        if (isTemporary && (info == null || !info.isCachedPhotoCurrent)) {
973            mPhoto.setVisibility(View.INVISIBLE);
974        } else if (info != null && info.photoResource != 0){
975            showImage(mPhoto, info.photoResource);
976        } else if (!showCachedImage(mPhoto, info)) {
977            // Load the image with a callback to update the image state.
978            // Use a placeholder image value of -1 to indicate no image.
979            ContactsAsyncHelper.updateImageViewWithContactPhotoAsync(info, 0, this, call,
980                    getContext(), mPhoto, personUri, -1);
981        }
982        if (displayNumber != null) {
983            mPhoneNumber.setText(displayNumber);
984            mPhoneNumber.setVisibility(View.VISIBLE);
985        } else {
986            mPhoneNumber.setVisibility(View.GONE);
987        }
988
989        if (label != null) {
990            mLabel.setText(label);
991            mLabel.setVisibility(View.VISIBLE);
992        } else {
993            mLabel.setVisibility(View.GONE);
994        }
995    }
996
997
998    /**
999     * Updates the name / photo / number / label fields
1000     * for the special "conference call" state.
1001     *
1002     * If the current call has only a single connection, use
1003     * updateDisplayForPerson() instead.
1004     */
1005    private void updateDisplayForConference() {
1006        if (DBG) log("updateDisplayForConference()...");
1007
1008        // Display the "conference call" image in the photo slot,
1009        // with no other information.
1010
1011        showImage(mPhoto, R.drawable.picture_conference);
1012
1013        mName.setText(R.string.card_title_conf_call);
1014        mName.setVisibility(View.VISIBLE);
1015
1016        // TODO: For a conference call, the "phone number" slot is specced
1017        // to contain a summary of who's on the call, like "Bill Foldes
1018        // and Hazel Nutt" or "Bill Foldes and 2 others".
1019        // But for now, just hide it:
1020        mPhoneNumber.setVisibility(View.GONE);
1021
1022        mLabel.setVisibility(View.GONE);
1023
1024        // TODO: consider also showing names / numbers / photos of some of the
1025        // people on the conference here, so you can see that info without
1026        // having to click "Manage conference".  We probably have enough
1027        // space to show info for 2 people, at least.
1028        //
1029        // To do this, our caller would pass us the activeConnections
1030        // list, and we'd call PhoneUtils.getCallerInfo() separately for
1031        // each connection.
1032    }
1033
1034    /**
1035     * Updates the CallCard "photo" IFF the specified Call is in a state
1036     * that needs a special photo (like "busy" or "dialing".)
1037     *
1038     * If the current call does not require a special image in the "photo"
1039     * slot onscreen, don't do anything, since presumably the photo image
1040     * has already been set (to the photo of the person we're talking, or
1041     * the generic "picture_unknown" image, or the "conference call"
1042     * image.)
1043     */
1044    private void updatePhotoForCallState(Call call) {
1045        if (DBG) log("updatePhotoForCallState(" + call + ")...");
1046        int photoImageResource = 0;
1047
1048        // Check for the (relatively few) telephony states that need a
1049        // special image in the "photo" slot.
1050        Call.State state = call.getState();
1051        switch (state) {
1052            case DISCONNECTED:
1053                // Display the special "busy" photo for BUSY or CONGESTION.
1054                // Otherwise (presumably the normal "call ended" state)
1055                // leave the photo alone.
1056                Connection c = call.getEarliestConnection();
1057                // if the connection is null, we assume the default case,
1058                // otherwise update the image resource normally.
1059                if (c != null) {
1060                    Connection.DisconnectCause cause = c.getDisconnectCause();
1061                    if ((cause == Connection.DisconnectCause.BUSY)
1062                        || (cause == Connection.DisconnectCause.CONGESTION)) {
1063                        photoImageResource = R.drawable.picture_busy;
1064                    }
1065                } else if (DBG) {
1066                    log("updatePhotoForCallState: connection is null, ignoring.");
1067                }
1068
1069                // TODO: add special images for any other DisconnectCauses?
1070                break;
1071
1072            case DIALING:
1073            case ALERTING:
1074                photoImageResource = R.drawable.picture_dialing;
1075                break;
1076
1077            default:
1078                // Leave the photo alone in all other states.
1079                // If this call is an individual call, and the image is currently
1080                // displaying a state, (rather than a photo), we'll need to update
1081                // the image.
1082                // This is for the case where we've been displaying the state and
1083                // now we need to restore the photo.  This can happen because we
1084                // only query the CallerInfo once, and limit the number of times
1085                // the image is loaded. (So a state image may overwrite the photo
1086                // and we would otherwise have no way of displaying the photo when
1087                // the state goes away.)
1088
1089                // if the photoResource field is filled-in in the Connection's
1090                // caller info, then we can just use that instead of requesting
1091                // for a photo load.
1092
1093                // look for the photoResource if it is available.
1094                CallerInfo ci = null;
1095                {
1096                    Connection conn = call.getEarliestConnection();
1097                    if (conn != null) {
1098                        Object o = conn.getUserData();
1099                        if (o instanceof CallerInfo) {
1100                            ci = (CallerInfo) o;
1101                        } else if (o instanceof PhoneUtils.CallerInfoToken) {
1102                            ci = ((PhoneUtils.CallerInfoToken) o).currentInfo;
1103                        }
1104                    }
1105                }
1106
1107                if (ci != null) {
1108                    photoImageResource = ci.photoResource;
1109                }
1110
1111                // If no photoResource found, check to see if this is a conference call. If
1112                // it is not a conference call:
1113                //   1. Try to show the cached image
1114                //   2. If the image is not cached, check to see if a load request has been
1115                //      made already.
1116                //   3. If the load request has not been made [DISPLAY_DEFAULT], start the
1117                //      request and note that it has started by updating photo state with
1118                //      [DISPLAY_IMAGE].
1119                // Load requests started in (3) use a placeholder image of -1 to hide the
1120                // image by default.  Please refer to CallerInfoAsyncQuery.java for cases
1121                // where CallerInfo.photoResource may be set.
1122                if (photoImageResource == 0) {
1123                    if (!PhoneUtils.isConferenceCall(call)) {
1124                        if (!showCachedImage(mPhoto, ci) && (mPhotoTracker.getPhotoState() ==
1125                                ContactsAsyncHelper.ImageTracker.DISPLAY_DEFAULT)) {
1126                            ContactsAsyncHelper.updateImageViewWithContactPhotoAsync(ci,
1127                                    getContext(), mPhoto, mPhotoTracker.getPhotoUri(), -1);
1128                            mPhotoTracker.setPhotoState(
1129                                    ContactsAsyncHelper.ImageTracker.DISPLAY_IMAGE);
1130                        }
1131                    }
1132                } else {
1133                    showImage(mPhoto, photoImageResource);
1134                    mPhotoTracker.setPhotoState(ContactsAsyncHelper.ImageTracker.DISPLAY_IMAGE);
1135                    return;
1136                }
1137                break;
1138        }
1139
1140        if (photoImageResource != 0) {
1141            if (DBG) log("- overrriding photo image: " + photoImageResource);
1142            showImage(mPhoto, photoImageResource);
1143            // Track the image state.
1144            mPhotoTracker.setPhotoState(ContactsAsyncHelper.ImageTracker.DISPLAY_DEFAULT);
1145        }
1146    }
1147
1148    /**
1149     * Try to display the cached image from the callerinfo object.
1150     *
1151     *  @return true if we were able to find the image in the cache, false otherwise.
1152     */
1153    private static final boolean showCachedImage (ImageView view, CallerInfo ci) {
1154        if ((ci != null) && ci.isCachedPhotoCurrent) {
1155            if (ci.cachedPhoto != null) {
1156                showImage(view, ci.cachedPhoto);
1157            } else {
1158                showImage(view, R.drawable.picture_unknown);
1159            }
1160            return true;
1161        }
1162        return false;
1163    }
1164
1165    /** Helper function to display the resource in the imageview AND ensure its visibility.*/
1166    private static final void showImage(ImageView view, int resource) {
1167        view.setImageResource(resource);
1168        view.setVisibility(View.VISIBLE);
1169    }
1170
1171    /** Helper function to display the drawable in the imageview AND ensure its visibility.*/
1172    private static final void showImage(ImageView view, Drawable drawable) {
1173        view.setImageDrawable(drawable);
1174        view.setVisibility(View.VISIBLE);
1175    }
1176
1177    /**
1178     * Intercepts (and discards) any touch events to the CallCard.
1179     */
1180    @Override
1181    public boolean dispatchTouchEvent(MotionEvent ev) {
1182        // if (DBG) log("CALLCARD: dispatchTouchEvent(): ev = " + ev);
1183
1184        // We *never* let touch events get thru to the UI inside the
1185        // CallCard, since there's nothing touchable there.
1186        return true;
1187    }
1188
1189    /**
1190     * Sets the background drawable of the main call card.
1191     */
1192    private void setMainCallCardBackgroundResource(int resid) {
1193        mMainCallCard.setBackgroundResource(resid);
1194    }
1195
1196    /**
1197     * Sets the background drawable of the "ongoing call" info area.
1198     */
1199    private void setOngoingInfoAreaBackgroundResource(int resid) {
1200        mOtherCallOngoingInfoArea.setBackgroundResource(resid);
1201    }
1202
1203    /**
1204     * Sets the background drawable of the "call on hold" info area.
1205     */
1206    private void setOnHoldInfoAreaBackgroundResource(int resid) {
1207        mOtherCallOnHoldInfoArea.setBackgroundResource(resid);
1208    }
1209
1210    /**
1211     * Returns the "Menu button hint" TextView (which is manipulated
1212     * directly by the InCallScreen.)
1213     * @see InCallScreen.updateMenuButtonHint()
1214     */
1215    /* package */ TextView getMenuButtonHint() {
1216        return mMenuButtonHint;
1217    }
1218
1219    /**
1220     * Updates anything about our View hierarchy or internal state
1221     * that needs to be different in landscape mode.
1222     *
1223     * @see InCallScreen.applyConfigurationToLayout()
1224     */
1225    /* package */ void updateForLandscapeMode() {
1226        if (DBG) log("updateForLandscapeMode()...");
1227
1228        // The main CallCard's minimum height is smaller in landscape mode
1229        // than in portrait mode.
1230        mMainCallCard.setMinimumHeight(MAIN_CALLCARD_MIN_HEIGHT_LANDSCAPE);
1231
1232        // Add some left and right margin to the top-level elements, since
1233        // there's no need to use the full width of the screen (which is
1234        // much wider in landscape mode.)
1235        setSideMargins(mMainCallCard, CALLCARD_SIDE_MARGIN_LANDSCAPE);
1236        setSideMargins(mOtherCallOngoingInfoArea, CALLCARD_SIDE_MARGIN_LANDSCAPE);
1237        setSideMargins(mOtherCallOnHoldInfoArea, CALLCARD_SIDE_MARGIN_LANDSCAPE);
1238
1239        // A couple of TextViews are slightly smaller in landscape mode.
1240        mUpperTitle.setTextSize(TITLE_TEXT_SIZE_LANDSCAPE);
1241    }
1242
1243    /**
1244     * Sets the left and right margins of the specified ViewGroup (whose
1245     * LayoutParams object which must inherit from
1246     * ViewGroup.MarginLayoutParams.)
1247     *
1248     * TODO: Is there already a convenience method like this somewhere?
1249     */
1250    private void setSideMargins(ViewGroup vg, int margin) {
1251        ViewGroup.MarginLayoutParams lp =
1252                (ViewGroup.MarginLayoutParams) vg.getLayoutParams();
1253        // Equivalent to setting android:layout_marginLeft/Right in XML
1254        lp.leftMargin = margin;
1255        lp.rightMargin = margin;
1256        vg.setLayoutParams(lp);
1257    }
1258
1259    /**
1260     * Sets the CallCard "upper title" to a plain string, with no icon.
1261     */
1262    private void setUpperTitle(String title) {
1263        mUpperTitle.setText(title);
1264        mUpperTitle.setCompoundDrawables(null, null, null, null);
1265    }
1266
1267    /**
1268     * Sets the CallCard "upper title".  Also, depending on the passed-in
1269     * Call state, possibly display an icon along with the title.
1270     */
1271    private void setUpperTitle(String title, Call.State state) {
1272        mUpperTitle.setText(title);
1273
1274        int bluetoothIconId = 0;
1275        if (((state == Call.State.INCOMING) || (state == Call.State.WAITING))
1276                && PhoneApp.getInstance().showBluetoothIndication()) {
1277            // Display the special bluetooth icon also, if this is an incoming
1278            // call and the audio will be routed to bluetooth.
1279            bluetoothIconId = R.drawable.ic_incoming_call_bluetooth;
1280        }
1281
1282        mUpperTitle.setCompoundDrawablesWithIntrinsicBounds(bluetoothIconId, 0, 0, 0);
1283        if (bluetoothIconId != 0) mUpperTitle.setCompoundDrawablePadding(5);
1284    }
1285
1286
1287    // Debugging / testing code
1288
1289    private void log(String msg) {
1290        Log.d(LOG_TAG, "[CallCard " + this + "] " + msg);
1291    }
1292
1293    private static void logErr(String msg) {
1294        Log.e(LOG_TAG, "[CallCard] " + msg);
1295    }
1296}
1297