• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
CallCard(Context context, AttributeSet attrs)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 
setInCallScreenInstance(InCallScreen inCallScreen)138     void setInCallScreenInstance(InCallScreen inCallScreen) {
139         mInCallScreen = inCallScreen;
140     }
141 
onTickForCallTimeElapsed(long timeElapsed)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 */
stopTimer()149     void stopTimer() {
150         mCallTime.cancelTimer();
151     }
152 
153     @Override
onFinishInflate()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      */
updateState(CallManager cm)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      */
updateCallInfoLayout(Phone.State state)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      */
updateForegroundCall(CallManager cm)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      */
updateRingingCall(CallManager cm)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      */
updateNoCall(CallManager cm)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      */
displayMainCallStatus(CallManager cm, Call call)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      */
onQueryComplete(int token, Object cookie, CallerInfo ci)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      */
onImageLoadComplete(int token, Object cookie, ImageView iView, boolean imagePresent)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      */
updateCallStateWidgets(Call call)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      */
updateElapsedTimeWidget(long timeElapsed)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      */
displayOnHoldCallStatus(CallManager cm, Call call)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 
getCallFailedString(Call call)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      */
updateDisplayForPerson(CallerInfo info, int presentation, boolean isTemporary, Call call, Connection conn)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 
getPresentationString(int presentation)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      */
updateDisplayForConference(Call call)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      */
updatePhotoForCallState(Call call)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      */
showCachedImage(ImageView view, CallerInfo ci)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.*/
showImage(ImageView view, int resource)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.*/
showImage(ImageView view, Drawable drawable)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      */
setSideMargins(ViewGroup vg, int margin)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      */
getECMCardTitle(Context context, Phone phone)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      */
updateCallTypeLabel(Call call)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      */
updateSocialStatus(String socialStatusText, Drawable socialStatusBadge, Call call)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      */
hideCallCardElements()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      */
setIncomingCallWidgetHint(int hintTextResId, int hintColorResId)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
dispatchPopulateAccessibilityEvent(AccessibilityEvent event)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 
dispatchPopulateAccessibilityEvent(AccessibilityEvent event, View view)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          */
show(final View view)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          */
hide(final View view, final int visibility)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          */
isFadingOut(final View view)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 
log(String msg)1592     private static void log(String msg) {
1593         Log.d(LOG_TAG, msg);
1594     }
1595 }
1596