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