blob: 682113fd77d708ddf13c5f95045fa37a8300e8e9 [file] [log] [blame]
/*
* Copyright (C) 2006 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.phone;
import android.animation.LayoutTransition;
import android.content.ContentUris;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.Handler;
import android.os.Message;
import android.provider.ContactsContract.Contacts;
import android.telephony.PhoneNumberUtils;
import android.text.TextUtils;
import android.text.format.DateUtils;
import android.util.AttributeSet;
import android.util.Log;
import android.view.Gravity;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewStub;
import android.view.accessibility.AccessibilityEvent;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
import com.android.internal.telephony.Call;
import com.android.internal.telephony.CallManager;
import com.android.internal.telephony.CallerInfo;
import com.android.internal.telephony.CallerInfoAsyncQuery;
import com.android.internal.telephony.Connection;
import com.android.internal.telephony.Phone;
import com.android.internal.telephony.PhoneConstants;
import java.util.List;
/**
* "Call card" UI element: the in-call screen contains a tiled layout of call
* cards, each representing the state of a current "call" (ie. an active call,
* a call on hold, or an incoming call.)
*/
public class CallCard extends LinearLayout
implements CallTime.OnTickListener, CallerInfoAsyncQuery.OnQueryCompleteListener,
ContactsAsyncHelper.OnImageLoadCompleteListener {
private static final String LOG_TAG = "CallCard";
private static final boolean DBG = (PhoneGlobals.DBG_LEVEL >= 2);
private static final int TOKEN_UPDATE_PHOTO_FOR_CALL_STATE = 0;
private static final int TOKEN_DO_NOTHING = 1;
/**
* Used with {@link ContactsAsyncHelper#startObtainPhotoAsync(int, Context, Uri,
* ContactsAsyncHelper.OnImageLoadCompleteListener, Object)}
*/
private static class AsyncLoadCookie {
public final ImageView imageView;
public final CallerInfo callerInfo;
public final Call call;
public AsyncLoadCookie(ImageView imageView, CallerInfo callerInfo, Call call) {
this.imageView = imageView;
this.callerInfo = callerInfo;
this.call = call;
}
}
/**
* Reference to the InCallScreen activity that owns us. This may be
* null if we haven't been initialized yet *or* after the InCallScreen
* activity has been destroyed.
*/
private InCallScreen mInCallScreen;
// Phone app instance
private PhoneGlobals mApplication;
// Top-level subviews of the CallCard
/** Container for info about the current call(s) */
private ViewGroup mCallInfoContainer;
/** Primary "call info" block (the foreground or ringing call) */
private ViewGroup mPrimaryCallInfo;
/** "Call banner" for the primary call */
private ViewGroup mPrimaryCallBanner;
/** Secondary "call info" block (the background "on hold" call) */
private ViewStub mSecondaryCallInfo;
/**
* Container for both provider info and call state. This will take care of showing/hiding
* animation for those views.
*/
private ViewGroup mSecondaryInfoContainer;
private ViewGroup mProviderInfo;
private TextView mProviderLabel;
private TextView mProviderAddress;
// "Call state" widgets
private TextView mCallStateLabel;
private TextView mElapsedTime;
// Text colors, used for various labels / titles
private int mTextColorCallTypeSip;
// The main block of info about the "primary" or "active" call,
// including photo / name / phone number / etc.
private ImageView mPhoto;
private View mPhotoDimEffect;
private TextView mName;
private TextView mPhoneNumber;
private TextView mLabel;
private TextView mCallTypeLabel;
// private TextView mSocialStatus;
/**
* Uri being used to load contact photo for mPhoto. Will be null when nothing is being loaded,
* or a photo is already loaded.
*/
private Uri mLoadingPersonUri;
// Info about the "secondary" call, which is the "call on hold" when
// two lines are in use.
private TextView mSecondaryCallName;
private ImageView mSecondaryCallPhoto;
private View mSecondaryCallPhotoDimEffect;
// Onscreen hint for the incoming call RotarySelector widget.
private int mIncomingCallWidgetHintTextResId;
private int mIncomingCallWidgetHintColorResId;
private CallTime mCallTime;
// Track the state for the photo.
private ContactsAsyncHelper.ImageTracker mPhotoTracker;
// Cached DisplayMetrics density.
private float mDensity;
/**
* Sent when it takes too long (MESSAGE_DELAY msec) to load a contact photo for the given
* person, at which we just start showing the default avatar picture instead of the person's
* one. Note that we will *not* cancel the ongoing query and eventually replace the avatar
* with the person's photo, when it is available anyway.
*/
private static final int MESSAGE_SHOW_UNKNOWN_PHOTO = 101;
private static final int MESSAGE_DELAY = 500; // msec
private final Handler mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case MESSAGE_SHOW_UNKNOWN_PHOTO:
showImage(mPhoto, R.drawable.picture_unknown);
break;
default:
Log.wtf(LOG_TAG, "mHandler: unexpected message: " + msg);
break;
}
}
};
public CallCard(Context context, AttributeSet attrs) {
super(context, attrs);
if (DBG) log("CallCard constructor...");
if (DBG) log("- this = " + this);
if (DBG) log("- context " + context + ", attrs " + attrs);
mApplication = PhoneGlobals.getInstance();
mCallTime = new CallTime(this);
// create a new object to track the state for the photo.
mPhotoTracker = new ContactsAsyncHelper.ImageTracker();
mDensity = getResources().getDisplayMetrics().density;
if (DBG) log("- Density: " + mDensity);
}
/* package */ void setInCallScreenInstance(InCallScreen inCallScreen) {
mInCallScreen = inCallScreen;
}
@Override
public void onTickForCallTimeElapsed(long timeElapsed) {
// While a call is in progress, update the elapsed time shown
// onscreen.
updateElapsedTimeWidget(timeElapsed);
}
/* package */ void stopTimer() {
mCallTime.cancelTimer();
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
if (DBG) log("CallCard onFinishInflate(this = " + this + ")...");
mCallInfoContainer = (ViewGroup) findViewById(R.id.call_info_container);
mPrimaryCallInfo = (ViewGroup) findViewById(R.id.primary_call_info);
mPrimaryCallBanner = (ViewGroup) findViewById(R.id.primary_call_banner);
mSecondaryInfoContainer = (ViewGroup) findViewById(R.id.secondary_info_container);
mProviderInfo = (ViewGroup) findViewById(R.id.providerInfo);
mProviderLabel = (TextView) findViewById(R.id.providerLabel);
mProviderAddress = (TextView) findViewById(R.id.providerAddress);
mCallStateLabel = (TextView) findViewById(R.id.callStateLabel);
mElapsedTime = (TextView) findViewById(R.id.elapsedTime);
// Text colors
mTextColorCallTypeSip = getResources().getColor(R.color.incall_callTypeSip);
// "Caller info" area, including photo / name / phone numbers / etc
mPhoto = (ImageView) findViewById(R.id.photo);
mPhotoDimEffect = findViewById(R.id.dim_effect_for_primary_photo);
mName = (TextView) findViewById(R.id.name);
mPhoneNumber = (TextView) findViewById(R.id.phoneNumber);
mLabel = (TextView) findViewById(R.id.label);
mCallTypeLabel = (TextView) findViewById(R.id.callTypeLabel);
// mSocialStatus = (TextView) findViewById(R.id.socialStatus);
// Secondary info area, for the background ("on hold") call
mSecondaryCallInfo = (ViewStub) findViewById(R.id.secondary_call_info);
}
/**
* Updates the state of all UI elements on the CallCard, based on the
* current state of the phone.
*/
/* package */ void updateState(CallManager cm) {
if (DBG) log("updateState(" + cm + ")...");
// Update the onscreen UI based on the current state of the phone.
PhoneConstants.State state = cm.getState(); // IDLE, RINGING, or OFFHOOK
Call ringingCall = cm.getFirstActiveRingingCall();
Call fgCall = cm.getActiveFgCall();
Call bgCall = cm.getFirstActiveBgCall();
// Update the overall layout of the onscreen elements, if in PORTRAIT.
// Portrait uses a programatically altered layout, whereas landscape uses layout xml's.
// Landscape view has the views side by side, so no shifting of the picture is needed
if (!PhoneUtils.isLandscape(this.getContext())) {
updateCallInfoLayout(state);
}
// If the FG call is dialing/alerting, we should display for that call
// and ignore the ringing call. This case happens when the telephony
// layer rejects the ringing call while the FG call is dialing/alerting,
// but the incoming call *does* briefly exist in the DISCONNECTING or
// DISCONNECTED state.
if ((ringingCall.getState() != Call.State.IDLE)
&& !fgCall.getState().isDialing()) {
// A phone call is ringing, call waiting *or* being rejected
// (ie. another call may also be active as well.)
updateRingingCall(cm);
} else if ((fgCall.getState() != Call.State.IDLE)
|| (bgCall.getState() != Call.State.IDLE)) {
// We are here because either:
// (1) the phone is off hook. At least one call exists that is
// dialing, active, or holding, and no calls are ringing or waiting,
// or:
// (2) the phone is IDLE but a call just ended and it's still in
// the DISCONNECTING or DISCONNECTED state. In this case, we want
// the main CallCard to display "Hanging up" or "Call ended".
// The normal "foreground call" code path handles both cases.
updateForegroundCall(cm);
} else {
// We don't have any DISCONNECTED calls, which means that the phone
// is *truly* idle.
if (mApplication.inCallUiState.showAlreadyDisconnectedState) {
// showAlreadyDisconnectedState implies the phone call is disconnected
// and we want to show the disconnected phone call for a moment.
//
// This happens when a phone call ends while the screen is off,
// which means the user had no chance to see the last status of
// the call. We'll turn off showAlreadyDisconnectedState flag
// and bail out of the in-call screen soon.
updateAlreadyDisconnected(cm);
} else {
// It's very rare to be on the InCallScreen at all in this
// state, but it can happen in some cases:
// - A stray onPhoneStateChanged() event came in to the
// InCallScreen *after* it was dismissed.
// - We're allowed to be on the InCallScreen because
// an MMI or USSD is running, but there's no actual "call"
// to display.
// - We're displaying an error dialog to the user
// (explaining why the call failed), so we need to stay on
// the InCallScreen so that the dialog will be visible.
//
// In these cases, put the callcard into a sane but "blank" state:
updateNoCall(cm);
}
}
}
/**
* Updates the overall size and positioning of mCallInfoContainer and
* the "Call info" blocks, based on the phone state.
*/
private void updateCallInfoLayout(PhoneConstants.State state) {
boolean ringing = (state == PhoneConstants.State.RINGING);
if (DBG) log("updateCallInfoLayout()... ringing = " + ringing);
// Based on the current state, update the overall
// CallCard layout:
// - Update the bottom margin of mCallInfoContainer to make sure
// the call info area won't overlap with the touchable
// controls on the bottom part of the screen.
int reservedVerticalSpace = mInCallScreen.getInCallTouchUi().getTouchUiHeight();
ViewGroup.MarginLayoutParams callInfoLp =
(ViewGroup.MarginLayoutParams) mCallInfoContainer.getLayoutParams();
callInfoLp.bottomMargin = reservedVerticalSpace; // Equivalent to setting
// android:layout_marginBottom in XML
if (DBG) log(" ==> callInfoLp.bottomMargin: " + reservedVerticalSpace);
mCallInfoContainer.setLayoutParams(callInfoLp);
}
/**
* Updates the UI for the state where the phone is in use, but not ringing.
*/
private void updateForegroundCall(CallManager cm) {
if (DBG) log("updateForegroundCall()...");
// if (DBG) PhoneUtils.dumpCallManager();
Call fgCall = cm.getActiveFgCall();
Call bgCall = cm.getFirstActiveBgCall();
if (fgCall.getState() == Call.State.IDLE) {
if (DBG) log("updateForegroundCall: no active call, show holding call");
// TODO: make sure this case agrees with the latest UI spec.
// Display the background call in the main info area of the
// CallCard, since there is no foreground call. Note that
// displayMainCallStatus() will notice if the call we passed in is on
// hold, and display the "on hold" indication.
fgCall = bgCall;
// And be sure to not display anything in the "on hold" box.
bgCall = null;
}
displayMainCallStatus(cm, fgCall);
Phone phone = fgCall.getPhone();
int phoneType = phone.getPhoneType();
if (phoneType == PhoneConstants.PHONE_TYPE_CDMA) {
if ((mApplication.cdmaPhoneCallState.getCurrentCallState()
== CdmaPhoneCallState.PhoneCallState.THRWAY_ACTIVE)
&& mApplication.cdmaPhoneCallState.IsThreeWayCallOrigStateDialing()) {
displaySecondaryCallStatus(cm, fgCall);
} else {
//This is required so that even if a background call is not present
// we need to clean up the background call area.
displaySecondaryCallStatus(cm, bgCall);
}
} else if ((phoneType == PhoneConstants.PHONE_TYPE_GSM)
|| (phoneType == PhoneConstants.PHONE_TYPE_SIP)) {
displaySecondaryCallStatus(cm, bgCall);
}
}
/**
* Updates the UI for the state where an incoming call is ringing (or
* call waiting), regardless of whether the phone's already offhook.
*/
private void updateRingingCall(CallManager cm) {
if (DBG) log("updateRingingCall()...");
Call ringingCall = cm.getFirstActiveRingingCall();
// Display caller-id info and photo from the incoming call:
displayMainCallStatus(cm, ringingCall);
// And even in the Call Waiting case, *don't* show any info about
// the current ongoing call and/or the current call on hold.
// (Since the caller-id info for the incoming call totally trumps
// any info about the current call(s) in progress.)
displaySecondaryCallStatus(cm, null);
}
/**
* Updates the UI for the state where an incoming call is just disconnected while we want to
* show the screen for a moment.
*
* This case happens when the whole in-call screen is in background when phone calls are hanged
* up, which means there's no way to determine which call was the last call finished. Right now
* this method simply shows the previous primary call status with a photo, closing the
* secondary call status. In most cases (including conference call or misc call happening in
* CDMA) this behaves right.
*
* If there were two phone calls both of which were hung up but the primary call was the
* first, this would behave a bit odd (since the first one still appears as the
* "last disconnected").
*/
private void updateAlreadyDisconnected(CallManager cm) {
// For the foreground call, we manually set up every component based on previous state.
mPrimaryCallInfo.setVisibility(View.VISIBLE);
mSecondaryInfoContainer.setLayoutTransition(null);
mProviderInfo.setVisibility(View.GONE);
mCallStateLabel.setVisibility(View.VISIBLE);
mCallStateLabel.setText(mContext.getString(R.string.card_title_call_ended));
mElapsedTime.setVisibility(View.VISIBLE);
mCallTime.cancelTimer();
// Just hide it.
displaySecondaryCallStatus(cm, null);
}
/**
* Updates the UI for the state where the phone is not in use.
* This is analogous to updateForegroundCall() and updateRingingCall(),
* but for the (uncommon) case where the phone is
* totally idle. (See comments in updateState() above.)
*
* This puts the callcard into a sane but "blank" state.
*/
private void updateNoCall(CallManager cm) {
if (DBG) log("updateNoCall()...");
displayMainCallStatus(cm, null);
displaySecondaryCallStatus(cm, null);
}
/**
* Updates the main block of caller info on the CallCard
* (ie. the stuff in the primaryCallInfo block) based on the specified Call.
*/
private void displayMainCallStatus(CallManager cm, Call call) {
if (DBG) log("displayMainCallStatus(call " + call + ")...");
if (call == null) {
// There's no call to display, presumably because the phone is idle.
mPrimaryCallInfo.setVisibility(View.GONE);
return;
}
mPrimaryCallInfo.setVisibility(View.VISIBLE);
Call.State state = call.getState();
if (DBG) log(" - call.state: " + call.getState());
switch (state) {
case ACTIVE:
case DISCONNECTING:
// update timer field
if (DBG) log("displayMainCallStatus: start periodicUpdateTimer");
mCallTime.setActiveCallMode(call);
mCallTime.reset();
mCallTime.periodicUpdateTimer();
break;
case HOLDING:
// update timer field
mCallTime.cancelTimer();
break;
case DISCONNECTED:
// Stop getting timer ticks from this call
mCallTime.cancelTimer();
break;
case DIALING:
case ALERTING:
// Stop getting timer ticks from a previous call
mCallTime.cancelTimer();
break;
case INCOMING:
case WAITING:
// Stop getting timer ticks from a previous call
mCallTime.cancelTimer();
break;
case IDLE:
// The "main CallCard" should never be trying to display
// an idle call! In updateState(), if the phone is idle,
// we call updateNoCall(), which means that we shouldn't
// have passed a call into this method at all.
Log.w(LOG_TAG, "displayMainCallStatus: IDLE call in the main call card!");
// (It is possible, though, that we had a valid call which
// became idle *after* the check in updateState() but
// before we get here... So continue the best we can,
// with whatever (stale) info we can get from the
// passed-in Call object.)
break;
default:
Log.w(LOG_TAG, "displayMainCallStatus: unexpected call state: " + state);
break;
}
updateCallStateWidgets(call);
if (PhoneUtils.isConferenceCall(call)) {
// Update onscreen info for a conference call.
updateDisplayForConference(call);
} else {
// Update onscreen info for a regular call (which presumably
// has only one connection.)
Connection conn = null;
int phoneType = call.getPhone().getPhoneType();
if (phoneType == PhoneConstants.PHONE_TYPE_CDMA) {
conn = call.getLatestConnection();
} else if ((phoneType == PhoneConstants.PHONE_TYPE_GSM)
|| (phoneType == PhoneConstants.PHONE_TYPE_SIP)) {
conn = call.getEarliestConnection();
} else {
throw new IllegalStateException("Unexpected phone type: " + phoneType);
}
if (conn == null) {
if (DBG) log("displayMainCallStatus: connection is null, using default values.");
// if the connection is null, we run through the behaviour
// we had in the past, which breaks down into trivial steps
// with the current implementation of getCallerInfo and
// updateDisplayForPerson.
CallerInfo info = PhoneUtils.getCallerInfo(getContext(), null /* conn */);
updateDisplayForPerson(info, PhoneConstants.PRESENTATION_ALLOWED, false, call,
conn);
} else {
if (DBG) log(" - CONN: " + conn + ", state = " + conn.getState());
int presentation = conn.getNumberPresentation();
// make sure that we only make a new query when the current
// callerinfo differs from what we've been requested to display.
boolean runQuery = true;
Object o = conn.getUserData();
if (o instanceof PhoneUtils.CallerInfoToken) {
runQuery = mPhotoTracker.isDifferentImageRequest(
((PhoneUtils.CallerInfoToken) o).currentInfo);
} else {
runQuery = mPhotoTracker.isDifferentImageRequest(conn);
}
// Adding a check to see if the update was caused due to a Phone number update
// or CNAP update. If so then we need to start a new query
if (phoneType == PhoneConstants.PHONE_TYPE_CDMA) {
Object obj = conn.getUserData();
String updatedNumber = conn.getAddress();
String updatedCnapName = conn.getCnapName();
CallerInfo info = null;
if (obj instanceof PhoneUtils.CallerInfoToken) {
info = ((PhoneUtils.CallerInfoToken) o).currentInfo;
} else if (o instanceof CallerInfo) {
info = (CallerInfo) o;
}
if (info != null) {
if (updatedNumber != null && !updatedNumber.equals(info.phoneNumber)) {
if (DBG) log("- displayMainCallStatus: updatedNumber = "
+ updatedNumber);
runQuery = true;
}
if (updatedCnapName != null && !updatedCnapName.equals(info.cnapName)) {
if (DBG) log("- displayMainCallStatus: updatedCnapName = "
+ updatedCnapName);
runQuery = true;
}
}
}
if (runQuery) {
if (DBG) log("- displayMainCallStatus: starting CallerInfo query...");
PhoneUtils.CallerInfoToken info =
PhoneUtils.startGetCallerInfo(getContext(), conn, this, call);
updateDisplayForPerson(info.currentInfo, presentation, !info.isFinal,
call, conn);
} else {
// No need to fire off a new query. We do still need
// to update the display, though (since we might have
// previously been in the "conference call" state.)
if (DBG) log("- displayMainCallStatus: using data we already have...");
if (o instanceof CallerInfo) {
CallerInfo ci = (CallerInfo) o;
// Update CNAP information if Phone state change occurred
ci.cnapName = conn.getCnapName();
ci.numberPresentation = conn.getNumberPresentation();
ci.namePresentation = conn.getCnapNamePresentation();
if (DBG) log("- displayMainCallStatus: CNAP data from Connection: "
+ "CNAP name=" + ci.cnapName
+ ", Number/Name Presentation=" + ci.numberPresentation);
if (DBG) log(" ==> Got CallerInfo; updating display: ci = " + ci);
updateDisplayForPerson(ci, presentation, false, call, conn);
} else if (o instanceof PhoneUtils.CallerInfoToken){
CallerInfo ci = ((PhoneUtils.CallerInfoToken) o).currentInfo;
if (DBG) log("- displayMainCallStatus: CNAP data from Connection: "
+ "CNAP name=" + ci.cnapName
+ ", Number/Name Presentation=" + ci.numberPresentation);
if (DBG) log(" ==> Got CallerInfoToken; updating display: ci = " + ci);
updateDisplayForPerson(ci, presentation, true, call, conn);
} else {
Log.w(LOG_TAG, "displayMainCallStatus: runQuery was false, "
+ "but we didn't have a cached CallerInfo object! o = " + o);
// TODO: any easy way to recover here (given that
// the CallCard is probably displaying stale info
// right now?) Maybe force the CallCard into the
// "Unknown" state?
}
}
}
}
// In some states we override the "photo" ImageView to be an
// indication of the current state, rather than displaying the
// regular photo as set above.
updatePhotoForCallState(call);
// One special feature of the "number" text field: For incoming
// calls, while the user is dragging the RotarySelector widget, we
// use mPhoneNumber to display a hint like "Rotate to answer".
if (mIncomingCallWidgetHintTextResId != 0) {
// Display the hint!
mPhoneNumber.setText(mIncomingCallWidgetHintTextResId);
mPhoneNumber.setTextColor(getResources().getColor(mIncomingCallWidgetHintColorResId));
mPhoneNumber.setVisibility(View.VISIBLE);
mLabel.setVisibility(View.GONE);
}
// If we don't have a hint to display, just don't touch
// mPhoneNumber and mLabel. (Their text / color / visibility have
// already been set correctly, by either updateDisplayForPerson()
// or updateDisplayForConference().)
}
/**
* Implemented for CallerInfoAsyncQuery.OnQueryCompleteListener interface.
* refreshes the CallCard data when it called.
*/
@Override
public void onQueryComplete(int token, Object cookie, CallerInfo ci) {
if (DBG) log("onQueryComplete: token " + token + ", cookie " + cookie + ", ci " + ci);
if (cookie instanceof Call) {
// grab the call object and update the display for an individual call,
// as well as the successive call to update image via call state.
// If the object is a textview instead, we update it as we need to.
if (DBG) log("callerinfo query complete, updating ui from displayMainCallStatus()");
Call call = (Call) cookie;
Connection conn = null;
int phoneType = call.getPhone().getPhoneType();
if (phoneType == PhoneConstants.PHONE_TYPE_CDMA) {
conn = call.getLatestConnection();
} else if ((phoneType == PhoneConstants.PHONE_TYPE_GSM)
|| (phoneType == PhoneConstants.PHONE_TYPE_SIP)) {
conn = call.getEarliestConnection();
} else {
throw new IllegalStateException("Unexpected phone type: " + phoneType);
}
PhoneUtils.CallerInfoToken cit =
PhoneUtils.startGetCallerInfo(getContext(), conn, this, null);
int presentation = PhoneConstants.PRESENTATION_ALLOWED;
if (conn != null) presentation = conn.getNumberPresentation();
if (DBG) log("- onQueryComplete: presentation=" + presentation
+ ", contactExists=" + ci.contactExists);
// Depending on whether there was a contact match or not, we want to pass in different
// CallerInfo (for CNAP). Therefore if ci.contactExists then use the ci passed in.
// Otherwise, regenerate the CIT from the Connection and use the CallerInfo from there.
if (ci.contactExists) {
updateDisplayForPerson(ci, PhoneConstants.PRESENTATION_ALLOWED, false, call, conn);
} else {
updateDisplayForPerson(cit.currentInfo, presentation, false, call, conn);
}
updatePhotoForCallState(call);
} else if (cookie instanceof TextView){
if (DBG) log("callerinfo query complete, updating ui from ongoing or onhold");
((TextView) cookie).setText(PhoneUtils.getCompactNameFromCallerInfo(ci, mContext));
}
}
/**
* Implemented for ContactsAsyncHelper.OnImageLoadCompleteListener interface.
* make sure that the call state is reflected after the image is loaded.
*/
@Override
public void onImageLoadComplete(int token, Drawable photo, Bitmap photoIcon, Object cookie) {
mHandler.removeMessages(MESSAGE_SHOW_UNKNOWN_PHOTO);
if (mLoadingPersonUri != null) {
// Start sending view notification after the current request being done.
// New image may possibly be available from the next phone calls.
//
// TODO: may be nice to update the image view again once the newer one
// is available on contacts database.
PhoneUtils.sendViewNotificationAsync(mApplication, mLoadingPersonUri);
} else {
// This should not happen while we need some verbose info if it happens..
Log.w(LOG_TAG, "Person Uri isn't available while Image is successfully loaded.");
}
mLoadingPersonUri = null;
AsyncLoadCookie asyncLoadCookie = (AsyncLoadCookie) cookie;
CallerInfo callerInfo = asyncLoadCookie.callerInfo;
ImageView imageView = asyncLoadCookie.imageView;
Call call = asyncLoadCookie.call;
callerInfo.cachedPhoto = photo;
callerInfo.cachedPhotoIcon = photoIcon;
callerInfo.isCachedPhotoCurrent = true;
// Note: previously ContactsAsyncHelper has done this job.
// TODO: We will need fade-in animation. See issue 5236130.
if (photo != null) {
showImage(imageView, photo);
} else if (photoIcon != null) {
showImage(imageView, photoIcon);
} else {
showImage(imageView, R.drawable.picture_unknown);
}
if (token == TOKEN_UPDATE_PHOTO_FOR_CALL_STATE) {
updatePhotoForCallState(call);
}
}
/**
* Updates the "call state label" and the elapsed time widget based on the
* current state of the call.
*/
private void updateCallStateWidgets(Call call) {
if (DBG) log("updateCallStateWidgets(call " + call + ")...");
final Call.State state = call.getState();
final Context context = getContext();
final Phone phone = call.getPhone();
final int phoneType = phone.getPhoneType();
String callStateLabel = null; // Label to display as part of the call banner
int bluetoothIconId = 0; // Icon to display alongside the call state label
switch (state) {
case IDLE:
// "Call state" is meaningless in this state.
break;
case ACTIVE:
// We normally don't show a "call state label" at all in
// this state (but see below for some special cases).
break;
case HOLDING:
callStateLabel = context.getString(R.string.card_title_on_hold);
break;
case DIALING:
case ALERTING:
callStateLabel = context.getString(R.string.card_title_dialing);
break;
case INCOMING:
case WAITING:
callStateLabel = context.getString(R.string.card_title_incoming_call);
// Also, display a special icon (alongside the "Incoming call"
// label) if there's an incoming call and audio will be routed
// to bluetooth when you answer it.
if (mApplication.showBluetoothIndication()) {
bluetoothIconId = R.drawable.ic_incoming_call_bluetooth;
}
break;
case DISCONNECTING:
// While in the DISCONNECTING state we display a "Hanging up"
// message in order to make the UI feel more responsive. (In
// GSM it's normal to see a delay of a couple of seconds while
// negotiating the disconnect with the network, so the "Hanging
// up" state at least lets the user know that we're doing
// something. This state is currently not used with CDMA.)
callStateLabel = context.getString(R.string.card_title_hanging_up);
break;
case DISCONNECTED:
callStateLabel = getCallFailedString(call);
break;
default:
Log.wtf(LOG_TAG, "updateCallStateWidgets: unexpected call state: " + state);
break;
}
// Check a couple of other special cases (these are all CDMA-specific).
if (phoneType == PhoneConstants.PHONE_TYPE_CDMA) {
if ((state == Call.State.ACTIVE)
&& mApplication.cdmaPhoneCallState.IsThreeWayCallOrigStateDialing()) {
// Display "Dialing" while dialing a 3Way call, even
// though the foreground call state is actually ACTIVE.
callStateLabel = context.getString(R.string.card_title_dialing);
} else if (PhoneGlobals.getInstance().notifier.getIsCdmaRedialCall()) {
callStateLabel = context.getString(R.string.card_title_redialing);
}
}
if (PhoneUtils.isPhoneInEcm(phone)) {
// In emergency callback mode (ECM), use a special label
// that shows your own phone number.
callStateLabel = getECMCardTitle(context, phone);
}
final InCallUiState inCallUiState = mApplication.inCallUiState;
if (DBG) {
log("==> callStateLabel: '" + callStateLabel
+ "', bluetoothIconId = " + bluetoothIconId
+ ", providerInfoVisible = " + inCallUiState.providerInfoVisible);
}
// Animation will be done by mCallerDetail's LayoutTransition, but in some cases, we don't
// want that.
// - DIALING: This is at the beginning of the phone call.
// - DISCONNECTING, DISCONNECTED: Screen will disappear soon; we have no time for animation.
final boolean skipAnimation = (state == Call.State.DIALING
|| state == Call.State.DISCONNECTING
|| state == Call.State.DISCONNECTED);
LayoutTransition layoutTransition = null;
if (skipAnimation) {
// Evict LayoutTransition object to skip animation.
layoutTransition = mSecondaryInfoContainer.getLayoutTransition();
mSecondaryInfoContainer.setLayoutTransition(null);
}
if (inCallUiState.providerInfoVisible) {
mProviderInfo.setVisibility(View.VISIBLE);
mProviderLabel.setText(context.getString(R.string.calling_via_template,
inCallUiState.providerLabel));
mProviderAddress.setText(inCallUiState.providerAddress);
mInCallScreen.requestRemoveProviderInfoWithDelay();
} else {
mProviderInfo.setVisibility(View.GONE);
}
if (!TextUtils.isEmpty(callStateLabel)) {
mCallStateLabel.setVisibility(View.VISIBLE);
mCallStateLabel.setText(callStateLabel);
// ...and display the icon too if necessary.
if (bluetoothIconId != 0) {
mCallStateLabel.setCompoundDrawablesWithIntrinsicBounds(bluetoothIconId, 0, 0, 0);
mCallStateLabel.setCompoundDrawablePadding((int) (mDensity * 5));
} else {
// Clear out any icons
mCallStateLabel.setCompoundDrawablesWithIntrinsicBounds(0, 0, 0, 0);
}
} else {
mCallStateLabel.setVisibility(View.GONE);
// Gravity is aligned left when receiving an incoming call in landscape.
// In that rare case, the gravity needs to be reset to the right.
// Also, setText("") is used since there is a delay in making the view GONE,
// so the user will otherwise see the text jump to the right side before disappearing.
if(mCallStateLabel.getGravity() != Gravity.END) {
mCallStateLabel.setText("");
mCallStateLabel.setGravity(Gravity.END);
}
}
if (skipAnimation) {
// Restore LayoutTransition object to recover animation.
mSecondaryInfoContainer.setLayoutTransition(layoutTransition);
}
// ...and update the elapsed time widget too.
switch (state) {
case ACTIVE:
case DISCONNECTING:
// Show the time with fade-in animation.
AnimationUtils.Fade.show(mElapsedTime);
updateElapsedTimeWidget(call);
break;
case DISCONNECTED:
// In the "Call ended" state, leave the mElapsedTime widget
// visible, but don't touch it (so we continue to see the
// elapsed time of the call that just ended.)
// Check visibility to keep possible fade-in animation.
if (mElapsedTime.getVisibility() != View.VISIBLE) {
mElapsedTime.setVisibility(View.VISIBLE);
}
break;
default:
// Call state here is IDLE, ACTIVE, HOLDING, DIALING, ALERTING,
// INCOMING, or WAITING.
// In all of these states, the "elapsed time" is meaningless, so
// don't show it.
AnimationUtils.Fade.hide(mElapsedTime, View.INVISIBLE);
// Additionally, in call states that can only occur at the start
// of a call, reset the elapsed time to be sure we won't display
// stale info later (like if we somehow go straight from DIALING
// or ALERTING to DISCONNECTED, which can actually happen in
// some failure cases like "line busy").
if ((state == Call.State.DIALING) || (state == Call.State.ALERTING)) {
updateElapsedTimeWidget(0);
}
break;
}
}
/**
* Updates mElapsedTime based on the given {@link Call} object's information.
*
* @see CallTime#getCallDuration(Call)
* @see Connection#getDurationMillis()
*/
/* package */ void updateElapsedTimeWidget(Call call) {
long duration = CallTime.getCallDuration(call); // msec
updateElapsedTimeWidget(duration / 1000);
// Also see onTickForCallTimeElapsed(), which updates this
// widget once per second while the call is active.
}
/**
* Updates mElapsedTime based on the specified number of seconds.
*/
private void updateElapsedTimeWidget(long timeElapsed) {
// if (DBG) log("updateElapsedTimeWidget: " + timeElapsed);
mElapsedTime.setText(DateUtils.formatElapsedTime(timeElapsed));
}
/**
* Updates the "on hold" box in the "other call" info area
* (ie. the stuff in the secondaryCallInfo block)
* based on the specified Call.
* Or, clear out the "on hold" box if the specified call
* is null or idle.
*/
private void displaySecondaryCallStatus(CallManager cm, Call call) {
if (DBG) log("displayOnHoldCallStatus(call =" + call + ")...");
if ((call == null) || (PhoneGlobals.getInstance().isOtaCallInActiveState())) {
mSecondaryCallInfo.setVisibility(View.GONE);
return;
}
Call.State state = call.getState();
switch (state) {
case HOLDING:
// Ok, there actually is a background call on hold.
// Display the "on hold" box.
// Note this case occurs only on GSM devices. (On CDMA,
// the "call on hold" is actually the 2nd connection of
// that ACTIVE call; see the ACTIVE case below.)
showSecondaryCallInfo();
if (PhoneUtils.isConferenceCall(call)) {
if (DBG) log("==> conference call.");
mSecondaryCallName.setText(getContext().getString(R.string.confCall));
showImage(mSecondaryCallPhoto, R.drawable.picture_conference);
} else {
// perform query and update the name temporarily
// make sure we hand the textview we want updated to the
// callback function.
if (DBG) log("==> NOT a conf call; call startGetCallerInfo...");
PhoneUtils.CallerInfoToken infoToken = PhoneUtils.startGetCallerInfo(
getContext(), call, this, mSecondaryCallName);
mSecondaryCallName.setText(
PhoneUtils.getCompactNameFromCallerInfo(infoToken.currentInfo,
getContext()));
// Also pull the photo out of the current CallerInfo.
// (Note we assume we already have a valid photo at
// this point, since *presumably* the caller-id query
// was already run at some point *before* this call
// got put on hold. If there's no cached photo, just
// fall back to the default "unknown" image.)
if (infoToken.isFinal) {
showCachedImage(mSecondaryCallPhoto, infoToken.currentInfo);
} else {
showImage(mSecondaryCallPhoto, R.drawable.picture_unknown);
}
}
AnimationUtils.Fade.show(mSecondaryCallPhotoDimEffect);
break;
case ACTIVE:
// CDMA: This is because in CDMA when the user originates the second call,
// although the Foreground call state is still ACTIVE in reality the network
// put the first call on hold.
if (mApplication.phone.getPhoneType() == PhoneConstants.PHONE_TYPE_CDMA) {
showSecondaryCallInfo();
List<Connection> connections = call.getConnections();
if (connections.size() > 2) {
// This means that current Mobile Originated call is the not the first 3-Way
// call the user is making, which in turn tells the PhoneGlobals that we no
// longer know which previous caller/party had dropped out before the user
// made this call.
mSecondaryCallName.setText(
getContext().getString(R.string.card_title_in_call));
showImage(mSecondaryCallPhoto, R.drawable.picture_unknown);
} else {
// This means that the current Mobile Originated call IS the first 3-Way
// and hence we display the first callers/party's info here.
Connection conn = call.getEarliestConnection();
PhoneUtils.CallerInfoToken infoToken = PhoneUtils.startGetCallerInfo(
getContext(), conn, this, mSecondaryCallName);
// Get the compactName to be displayed, but then check that against
// the number presentation value for the call. If it's not an allowed
// presentation, then display the appropriate presentation string instead.
CallerInfo info = infoToken.currentInfo;
String name = PhoneUtils.getCompactNameFromCallerInfo(info, getContext());
boolean forceGenericPhoto = false;
if (info != null && info.numberPresentation !=
PhoneConstants.PRESENTATION_ALLOWED) {
name = PhoneUtils.getPresentationString(
getContext(), info.numberPresentation);
forceGenericPhoto = true;
}
mSecondaryCallName.setText(name);
// Also pull the photo out of the current CallerInfo.
// (Note we assume we already have a valid photo at
// this point, since *presumably* the caller-id query
// was already run at some point *before* this call
// got put on hold. If there's no cached photo, just
// fall back to the default "unknown" image.)
if (!forceGenericPhoto && infoToken.isFinal) {
showCachedImage(mSecondaryCallPhoto, info);
} else {
showImage(mSecondaryCallPhoto, R.drawable.picture_unknown);
}
}
} else {
// We shouldn't ever get here at all for non-CDMA devices.
Log.w(LOG_TAG, "displayOnHoldCallStatus: ACTIVE state on non-CDMA device");
mSecondaryCallInfo.setVisibility(View.GONE);
}
AnimationUtils.Fade.hide(mSecondaryCallPhotoDimEffect, View.GONE);
break;
default:
// There's actually no call on hold. (Presumably this call's
// state is IDLE, since any other state is meaningless for the
// background call.)
mSecondaryCallInfo.setVisibility(View.GONE);
break;
}
}
private void showSecondaryCallInfo() {
// This will call ViewStub#inflate() when needed.
mSecondaryCallInfo.setVisibility(View.VISIBLE);
if (mSecondaryCallName == null) {
mSecondaryCallName = (TextView) findViewById(R.id.secondaryCallName);
}
if (mSecondaryCallPhoto == null) {
mSecondaryCallPhoto = (ImageView) findViewById(R.id.secondaryCallPhoto);
}
if (mSecondaryCallPhotoDimEffect == null) {
mSecondaryCallPhotoDimEffect = findViewById(R.id.dim_effect_for_secondary_photo);
mSecondaryCallPhotoDimEffect.setOnClickListener(mInCallScreen);
// Add a custom OnTouchListener to manually shrink the "hit target".
mSecondaryCallPhotoDimEffect.setOnTouchListener(new SmallerHitTargetTouchListener());
}
mInCallScreen.updateButtonStateOutsideInCallTouchUi();
}
/**
* Method which is expected to be called from
* {@link InCallScreen#updateButtonStateOutsideInCallTouchUi()}.
*/
/* package */ void setSecondaryCallClickable(boolean clickable) {
if (mSecondaryCallPhotoDimEffect != null) {
mSecondaryCallPhotoDimEffect.setEnabled(clickable);
}
}
private String getCallFailedString(Call call) {
Connection c = call.getEarliestConnection();
int resID;
if (c == null) {
if (DBG) log("getCallFailedString: connection is null, using default values.");
// if this connection is null, just assume that the
// default case occurs.
resID = R.string.card_title_call_ended;
} else {
Connection.DisconnectCause cause = c.getDisconnectCause();
// TODO: The card *title* should probably be "Call ended" in all
// cases, but if the DisconnectCause was an error condition we should
// probably also display the specific failure reason somewhere...
switch (cause) {
case BUSY:
resID = R.string.callFailed_userBusy;
break;
case CONGESTION:
resID = R.string.callFailed_congestion;
break;
case TIMED_OUT:
resID = R.string.callFailed_timedOut;
break;
case SERVER_UNREACHABLE:
resID = R.string.callFailed_server_unreachable;
break;
case NUMBER_UNREACHABLE:
resID = R.string.callFailed_number_unreachable;
break;
case INVALID_CREDENTIALS:
resID = R.string.callFailed_invalid_credentials;
break;
case SERVER_ERROR:
resID = R.string.callFailed_server_error;
break;
case OUT_OF_NETWORK:
resID = R.string.callFailed_out_of_network;
break;
case LOST_SIGNAL:
case CDMA_DROP:
resID = R.string.callFailed_noSignal;
break;
case LIMIT_EXCEEDED:
resID = R.string.callFailed_limitExceeded;
break;
case POWER_OFF:
resID = R.string.callFailed_powerOff;
break;
case ICC_ERROR:
resID = R.string.callFailed_simError;
break;
case OUT_OF_SERVICE:
resID = R.string.callFailed_outOfService;
break;
case INVALID_NUMBER:
case UNOBTAINABLE_NUMBER:
resID = R.string.callFailed_unobtainable_number;
break;
default:
resID = R.string.card_title_call_ended;
break;
}
}
return getContext().getString(resID);
}
/**
* Updates the name / photo / number / label fields on the CallCard
* based on the specified CallerInfo.
*
* If the current call is a conference call, use
* updateDisplayForConference() instead.
*/
private void updateDisplayForPerson(CallerInfo info,
int presentation,
boolean isTemporary,
Call call,
Connection conn) {
if (DBG) log("updateDisplayForPerson(" + info + ")\npresentation:" +
presentation + " isTemporary:" + isTemporary);
// inform the state machine that we are displaying a photo.
mPhotoTracker.setPhotoRequest(info);
mPhotoTracker.setPhotoState(ContactsAsyncHelper.ImageTracker.DISPLAY_IMAGE);
// The actual strings we're going to display onscreen:
String displayName;
String displayNumber = null;
String label = null;
Uri personUri = null;
// String socialStatusText = null;
// Drawable socialStatusBadge = null;
// Gather missing info unless the call is generic, in which case we wouldn't use
// the gathered information anyway.
if (info != null && !call.isGeneric()) {
// It appears that there is a small change in behaviour with the
// PhoneUtils' startGetCallerInfo whereby if we query with an
// empty number, we will get a valid CallerInfo object, but with
// fields that are all null, and the isTemporary boolean input
// parameter as true.
// In the past, we would see a NULL callerinfo object, but this
// ends up causing null pointer exceptions elsewhere down the
// line in other cases, so we need to make this fix instead. It
// appears that this was the ONLY call to PhoneUtils
// .getCallerInfo() that relied on a NULL CallerInfo to indicate
// an unknown contact.
// Currently, infi.phoneNumber may actually be a SIP address, and
// if so, it might sometimes include the "sip:" prefix. That
// prefix isn't really useful to the user, though, so strip it off
// if present. (For any other URI scheme, though, leave the
// prefix alone.)
// TODO: It would be cleaner for CallerInfo to explicitly support
// SIP addresses instead of overloading the "phoneNumber" field.
// Then we could remove this hack, and instead ask the CallerInfo
// for a "user visible" form of the SIP address.
String number = info.phoneNumber;
if ((number != null) && number.startsWith("sip:")) {
number = number.substring(4);
}
if (TextUtils.isEmpty(info.name)) {
// No valid "name" in the CallerInfo, so fall back to
// something else.
// (Typically, we promote the phone number up to the "name" slot
// onscreen, and possibly display a descriptive string in the
// "number" slot.)
if (TextUtils.isEmpty(number)) {
// No name *or* number! Display a generic "unknown" string
// (or potentially some other default based on the presentation.)
displayName = PhoneUtils.getPresentationString(getContext(), presentation);
if (DBG) log(" ==> no name *or* number! displayName = " + displayName);
} else if (presentation != PhoneConstants.PRESENTATION_ALLOWED) {
// This case should never happen since the network should never send a phone #
// AND a restricted presentation. However we leave it here in case of weird
// network behavior
displayName = PhoneUtils.getPresentationString(getContext(), presentation);
if (DBG) log(" ==> presentation not allowed! displayName = " + displayName);
} else if (!TextUtils.isEmpty(info.cnapName)) {
// No name, but we do have a valid CNAP name, so use that.
displayName = info.cnapName;
info.name = info.cnapName;
displayNumber = number;
if (DBG) log(" ==> cnapName available: displayName '"
+ displayName + "', displayNumber '" + displayNumber + "'");
} else {
// No name; all we have is a number. This is the typical
// case when an incoming call doesn't match any contact,
// or if you manually dial an outgoing number using the
// dialpad.
// Promote the phone number up to the "name" slot:
displayName = number;
// ...and use the "number" slot for a geographical description
// string if available (but only for incoming calls.)
if ((conn != null) && (conn.isIncoming())) {
// TODO (CallerInfoAsyncQuery cleanup): Fix the CallerInfo
// query to only do the geoDescription lookup in the first
// place for incoming calls.
displayNumber = info.geoDescription; // may be null
}
if (DBG) log(" ==> no name; falling back to number: displayName '"
+ displayName + "', displayNumber '" + displayNumber + "'");
}
} else {
// We do have a valid "name" in the CallerInfo. Display that
// in the "name" slot, and the phone number in the "number" slot.
if (presentation != PhoneConstants.PRESENTATION_ALLOWED) {
// This case should never happen since the network should never send a name
// AND a restricted presentation. However we leave it here in case of weird
// network behavior
displayName = PhoneUtils.getPresentationString(getContext(), presentation);
if (DBG) log(" ==> valid name, but presentation not allowed!"
+ " displayName = " + displayName);
} else {
displayName = info.name;
displayNumber = number;
label = info.phoneLabel;
if (DBG) log(" ==> name is present in CallerInfo: displayName '"
+ displayName + "', displayNumber '" + displayNumber + "'");
}
}
personUri = ContentUris.withAppendedId(Contacts.CONTENT_URI, info.person_id);
if (DBG) log("- got personUri: '" + personUri
+ "', based on info.person_id: " + info.person_id);
} else {
displayName = PhoneUtils.getPresentationString(getContext(), presentation);
}
if (call.isGeneric()) {
updateGenericInfoUi();
} else {
updateInfoUi(displayName, displayNumber, label);
}
// Update mPhoto
// if the temporary flag is set, we know we'll be getting another call after
// the CallerInfo has been correctly updated. So, we can skip the image
// loading until then.
// If the photoResource is filled in for the CallerInfo, (like with the
// Emergency Number case), then we can just set the photo image without
// requesting for an image load. Please refer to CallerInfoAsyncQuery.java
// for cases where CallerInfo.photoResource may be set. We can also avoid
// the image load step if the image data is cached.
if (isTemporary && (info == null || !info.isCachedPhotoCurrent)) {
mPhoto.setTag(null);
mPhoto.setVisibility(View.INVISIBLE);
} else if (info != null && info.photoResource != 0){
showImage(mPhoto, info.photoResource);
} else if (!showCachedImage(mPhoto, info)) {
if (personUri == null) {
Log.w(LOG_TAG, "personPri is null. Just use Unknown picture.");
showImage(mPhoto, R.drawable.picture_unknown);
} else if (personUri.equals(mLoadingPersonUri)) {
if (DBG) {
log("The requested Uri (" + personUri + ") is being loaded already."
+ " Ignoret the duplicate load request.");
}
} else {
// Remember which person's photo is being loaded right now so that we won't issue
// unnecessary load request multiple times, which will mess up animation around
// the contact photo.
mLoadingPersonUri = personUri;
// Forget the drawable previously used.
mPhoto.setTag(null);
// Show empty screen for a moment.
mPhoto.setVisibility(View.INVISIBLE);
// Load the image with a callback to update the image state.
// When the load is finished, onImageLoadComplete() will be called.
ContactsAsyncHelper.startObtainPhotoAsync(TOKEN_UPDATE_PHOTO_FOR_CALL_STATE,
getContext(), personUri, this, new AsyncLoadCookie(mPhoto, info, call));
// If the image load is too slow, we show a default avatar icon afterward.
// If it is fast enough, this message will be canceled on onImageLoadComplete().
mHandler.removeMessages(MESSAGE_SHOW_UNKNOWN_PHOTO);
mHandler.sendEmptyMessageDelayed(MESSAGE_SHOW_UNKNOWN_PHOTO, MESSAGE_DELAY);
}
}
// If the phone call is on hold, show it with darker status.
// Right now we achieve it by overlaying opaque View.
// Note: See also layout file about why so and what is the other possibilities.
if (call.getState() == Call.State.HOLDING) {
AnimationUtils.Fade.show(mPhotoDimEffect);
} else {
AnimationUtils.Fade.hide(mPhotoDimEffect, View.GONE);
}
// Other text fields:
updateCallTypeLabel(call);
// updateSocialStatus(socialStatusText, socialStatusBadge, call); // Currently unused
}
/**
* Updates the info portion of the UI to be generic. Used for CDMA 3-way calls.
*/
private void updateGenericInfoUi() {
mName.setText(R.string.card_title_in_call);
mPhoneNumber.setVisibility(View.GONE);
mLabel.setVisibility(View.GONE);
}
/**
* Updates the info portion of the call card with passed in values.
*/
private void updateInfoUi(String displayName, String displayNumber, String label) {
mName.setText(displayName);
mName.setVisibility(View.VISIBLE);
if (TextUtils.isEmpty(displayNumber)) {
mPhoneNumber.setVisibility(View.GONE);
// We have a real phone number as "mName" so make it always LTR
mName.setTextDirection(View.TEXT_DIRECTION_LTR);
} else {
mPhoneNumber.setText(displayNumber);
mPhoneNumber.setVisibility(View.VISIBLE);
// We have a real phone number as "mPhoneNumber" so make it always LTR
mPhoneNumber.setTextDirection(View.TEXT_DIRECTION_LTR);
}
if (TextUtils.isEmpty(label)) {
mLabel.setVisibility(View.GONE);
} else {
mLabel.setText(label);
mLabel.setVisibility(View.VISIBLE);
}
}
/**
* Updates the name / photo / number / label fields
* for the special "conference call" state.
*
* If the current call has only a single connection, use
* updateDisplayForPerson() instead.
*/
private void updateDisplayForConference(Call call) {
if (DBG) log("updateDisplayForConference()...");
int phoneType = call.getPhone().getPhoneType();
if (phoneType == PhoneConstants.PHONE_TYPE_CDMA) {
// This state corresponds to both 3-Way merged call and
// Call Waiting accepted call.
// In this case we display the UI in a "generic" state, with
// the generic "dialing" icon and no caller information,
// because in this state in CDMA the user does not really know
// which caller party he is talking to.
showImage(mPhoto, R.drawable.picture_dialing);
mName.setText(R.string.card_title_in_call);
} else if ((phoneType == PhoneConstants.PHONE_TYPE_GSM)
|| (phoneType == PhoneConstants.PHONE_TYPE_SIP)) {
// Normal GSM (or possibly SIP?) conference call.
// Display the "conference call" image as the contact photo.
// TODO: Better visual treatment for contact photos in a
// conference call (see bug 1313252).
showImage(mPhoto, R.drawable.picture_conference);
mName.setText(R.string.card_title_conf_call);
} else {
throw new IllegalStateException("Unexpected phone type: " + phoneType);
}
mName.setVisibility(View.VISIBLE);
// TODO: For a conference call, the "phone number" slot is specced
// to contain a summary of who's on the call, like "Bill Foldes
// and Hazel Nutt" or "Bill Foldes and 2 others".
// But for now, just hide it:
mPhoneNumber.setVisibility(View.GONE);
mLabel.setVisibility(View.GONE);
// Other text fields:
updateCallTypeLabel(call);
// updateSocialStatus(null, null, null); // socialStatus is never visible in this state
// TODO: for a GSM conference call, since we do actually know who
// you're talking to, consider also showing names / numbers /
// photos of some of the people on the conference here, so you can
// see that info without having to click "Manage conference". We
// probably have enough space to show info for 2 people, at least.
//
// To do this, our caller would pass us the activeConnections
// list, and we'd call PhoneUtils.getCallerInfo() separately for
// each connection.
}
/**
* Updates the CallCard "photo" IFF the specified Call is in a state
* that needs a special photo (like "busy" or "dialing".)
*
* If the current call does not require a special image in the "photo"
* slot onscreen, don't do anything, since presumably the photo image
* has already been set (to the photo of the person we're talking, or
* the generic "picture_unknown" image, or the "conference call"
* image.)
*/
private void updatePhotoForCallState(Call call) {
if (DBG) log("updatePhotoForCallState(" + call + ")...");
int photoImageResource = 0;
// Check for the (relatively few) telephony states that need a
// special image in the "photo" slot.
Call.State state = call.getState();
switch (state) {
case DISCONNECTED:
// Display the special "busy" photo for BUSY or CONGESTION.
// Otherwise (presumably the normal "call ended" state)
// leave the photo alone.
Connection c = call.getEarliestConnection();
// if the connection is null, we assume the default case,
// otherwise update the image resource normally.
if (c != null) {
Connection.DisconnectCause cause = c.getDisconnectCause();
if ((cause == Connection.DisconnectCause.BUSY)
|| (cause == Connection.DisconnectCause.CONGESTION)) {
photoImageResource = R.drawable.picture_busy;
}
} else if (DBG) {
log("updatePhotoForCallState: connection is null, ignoring.");
}
// TODO: add special images for any other DisconnectCauses?
break;
case ALERTING:
case DIALING:
default:
// Leave the photo alone in all other states.
// If this call is an individual call, and the image is currently
// displaying a state, (rather than a photo), we'll need to update
// the image.
// This is for the case where we've been displaying the state and
// now we need to restore the photo. This can happen because we
// only query the CallerInfo once, and limit the number of times
// the image is loaded. (So a state image may overwrite the photo
// and we would otherwise have no way of displaying the photo when
// the state goes away.)
// if the photoResource field is filled-in in the Connection's
// caller info, then we can just use that instead of requesting
// for a photo load.
// look for the photoResource if it is available.
CallerInfo ci = null;
{
Connection conn = null;
int phoneType = call.getPhone().getPhoneType();
if (phoneType == PhoneConstants.PHONE_TYPE_CDMA) {
conn = call.getLatestConnection();
} else if ((phoneType == PhoneConstants.PHONE_TYPE_GSM)
|| (phoneType == PhoneConstants.PHONE_TYPE_SIP)) {
conn = call.getEarliestConnection();
} else {
throw new IllegalStateException("Unexpected phone type: " + phoneType);
}
if (conn != null) {
Object o = conn.getUserData();
if (o instanceof CallerInfo) {
ci = (CallerInfo) o;
} else if (o instanceof PhoneUtils.CallerInfoToken) {
ci = ((PhoneUtils.CallerInfoToken) o).currentInfo;
}
}
}
if (ci != null) {
photoImageResource = ci.photoResource;
}
// If no photoResource found, check to see if this is a conference call. If
// it is not a conference call:
// 1. Try to show the cached image
// 2. If the image is not cached, check to see if a load request has been
// made already.
// 3. If the load request has not been made [DISPLAY_DEFAULT], start the
// request and note that it has started by updating photo state with
// [DISPLAY_IMAGE].
if (photoImageResource == 0) {
if (!PhoneUtils.isConferenceCall(call)) {
if (!showCachedImage(mPhoto, ci) && (mPhotoTracker.getPhotoState() ==
ContactsAsyncHelper.ImageTracker.DISPLAY_DEFAULT)) {
Uri photoUri = mPhotoTracker.getPhotoUri();
if (photoUri == null) {
Log.w(LOG_TAG, "photoUri became null. Show default avatar icon");
showImage(mPhoto, R.drawable.picture_unknown);
} else {
if (DBG) {
log("start asynchronous load inside updatePhotoForCallState()");
}
mPhoto.setTag(null);
// Make it invisible for a moment
mPhoto.setVisibility(View.INVISIBLE);
ContactsAsyncHelper.startObtainPhotoAsync(TOKEN_DO_NOTHING,
getContext(), photoUri, this,
new AsyncLoadCookie(mPhoto, ci, null));
}
mPhotoTracker.setPhotoState(
ContactsAsyncHelper.ImageTracker.DISPLAY_IMAGE);
}
}
} else {
showImage(mPhoto, photoImageResource);
mPhotoTracker.setPhotoState(ContactsAsyncHelper.ImageTracker.DISPLAY_IMAGE);
return;
}
break;
}
if (photoImageResource != 0) {
if (DBG) log("- overrriding photo image: " + photoImageResource);
showImage(mPhoto, photoImageResource);
// Track the image state.
mPhotoTracker.setPhotoState(ContactsAsyncHelper.ImageTracker.DISPLAY_DEFAULT);
}
}
/**
* Try to display the cached image from the callerinfo object.
*
* @return true if we were able to find the image in the cache, false otherwise.
*/
private static final boolean showCachedImage(ImageView view, CallerInfo ci) {
if ((ci != null) && ci.isCachedPhotoCurrent) {
if (ci.cachedPhoto != null) {
showImage(view, ci.cachedPhoto);
} else {
showImage(view, R.drawable.picture_unknown);
}
return true;
}
return false;
}
/** Helper function to display the resource in the imageview AND ensure its visibility.*/
private static final void showImage(ImageView view, int resource) {
showImage(view, view.getContext().getResources().getDrawable(resource));
}
private static final void showImage(ImageView view, Bitmap bitmap) {
showImage(view, new BitmapDrawable(view.getContext().getResources(), bitmap));
}
/** Helper function to display the drawable in the imageview AND ensure its visibility.*/
private static final void showImage(ImageView view, Drawable drawable) {
Resources res = view.getContext().getResources();
Drawable current = (Drawable) view.getTag();
if (current == null) {
if (DBG) log("Start fade-in animation for " + view);
view.setImageDrawable(drawable);
AnimationUtils.Fade.show(view);
view.setTag(drawable);
} else {
AnimationUtils.startCrossFade(view, current, drawable);
view.setVisibility(View.VISIBLE);
}
}
/**
* Returns the special card title used in emergency callback mode (ECM),
* which shows your own phone number.
*/
private String getECMCardTitle(Context context, Phone phone) {
String rawNumber = phone.getLine1Number(); // may be null or empty
String formattedNumber;
if (!TextUtils.isEmpty(rawNumber)) {
formattedNumber = PhoneNumberUtils.formatNumber(rawNumber);
} else {
formattedNumber = context.getString(R.string.unknown);
}
String titleFormat = context.getString(R.string.card_title_my_phone_number);
return String.format(titleFormat, formattedNumber);
}
/**
* Updates the "Call type" label, based on the current foreground call.
* This is a special label and/or branding we display for certain
* kinds of calls.
*
* (So far, this is used only for SIP calls, which get an
* "Internet call" label. TODO: But eventually, the telephony
* layer might allow each pluggable "provider" to specify a string
* and/or icon to be displayed here.)
*/
private void updateCallTypeLabel(Call call) {
int phoneType = (call != null) ? call.getPhone().getPhoneType() :
PhoneConstants.PHONE_TYPE_NONE;
if (phoneType == PhoneConstants.PHONE_TYPE_SIP) {
mCallTypeLabel.setVisibility(View.VISIBLE);
mCallTypeLabel.setText(R.string.incall_call_type_label_sip);
mCallTypeLabel.setTextColor(mTextColorCallTypeSip);
// If desired, we could also display a "badge" next to the label, as follows:
// mCallTypeLabel.setCompoundDrawablesWithIntrinsicBounds(
// callTypeSpecificBadge, null, null, null);
// mCallTypeLabel.setCompoundDrawablePadding((int) (mDensity * 6));
} else {
mCallTypeLabel.setVisibility(View.GONE);
}
}
/**
* Updates the "social status" label with the specified text and
* (optional) badge.
*/
/*private void updateSocialStatus(String socialStatusText,
Drawable socialStatusBadge,
Call call) {
// The socialStatus field is *only* visible while an incoming call
// is ringing, never in any other call state.
if ((socialStatusText != null)
&& (call != null)
&& call.isRinging()
&& !call.isGeneric()) {
mSocialStatus.setVisibility(View.VISIBLE);
mSocialStatus.setText(socialStatusText);
mSocialStatus.setCompoundDrawablesWithIntrinsicBounds(
socialStatusBadge, null, null, null);
mSocialStatus.setCompoundDrawablePadding((int) (mDensity * 6));
} else {
mSocialStatus.setVisibility(View.GONE);
}
}*/
/**
* Hides the top-level UI elements of the call card: The "main
* call card" element representing the current active or ringing call,
* and also the info areas for "ongoing" or "on hold" calls in some
* states.
*
* This is intended to be used in special states where the normal
* in-call UI is totally replaced by some other UI, like OTA mode on a
* CDMA device.
*
* To bring back the regular CallCard UI, just re-run the normal
* updateState() call sequence.
*/
public void hideCallCardElements() {
mPrimaryCallInfo.setVisibility(View.GONE);
mSecondaryCallInfo.setVisibility(View.GONE);
}
/*
* Updates the hint (like "Rotate to answer") that we display while
* the user is dragging the incoming call RotarySelector widget.
*/
/* package */ void setIncomingCallWidgetHint(int hintTextResId, int hintColorResId) {
mIncomingCallWidgetHintTextResId = hintTextResId;
mIncomingCallWidgetHintColorResId = hintColorResId;
}
// Accessibility event support.
// Since none of the CallCard elements are focusable, we need to manually
// fill in the AccessibilityEvent here (so that the name / number / etc will
// get pronounced by a screen reader, for example.)
@Override
public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) {
if (event.getEventType() == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED) {
dispatchPopulateAccessibilityEvent(event, mName);
dispatchPopulateAccessibilityEvent(event, mPhoneNumber);
return true;
}
dispatchPopulateAccessibilityEvent(event, mCallStateLabel);
dispatchPopulateAccessibilityEvent(event, mPhoto);
dispatchPopulateAccessibilityEvent(event, mName);
dispatchPopulateAccessibilityEvent(event, mPhoneNumber);
dispatchPopulateAccessibilityEvent(event, mLabel);
// dispatchPopulateAccessibilityEvent(event, mSocialStatus);
if (mSecondaryCallName != null) {
dispatchPopulateAccessibilityEvent(event, mSecondaryCallName);
}
if (mSecondaryCallPhoto != null) {
dispatchPopulateAccessibilityEvent(event, mSecondaryCallPhoto);
}
return true;
}
private void dispatchPopulateAccessibilityEvent(AccessibilityEvent event, View view) {
List<CharSequence> eventText = event.getText();
int size = eventText.size();
view.dispatchPopulateAccessibilityEvent(event);
// if no text added write null to keep relative position
if (size == eventText.size()) {
eventText.add(null);
}
}
public void clear() {
// The existing phone design is to keep an instance of call card forever. Until that
// design changes, this method is needed to clear (reset) the call card for the next call
// so old data is not shown.
// Other elements can also be cleared here. Starting with elapsed time to fix a bug.
mElapsedTime.setVisibility(View.GONE);
mElapsedTime.setText(null);
}
// Debugging / testing code
private static void log(String msg) {
Log.d(LOG_TAG, msg);
}
}