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