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