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