/* * 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.content.ContentUris; import android.content.Context; import android.graphics.drawable.Drawable; import android.net.Uri; import android.pim.ContactsAsyncHelper; import android.provider.ContactsContract.Contacts; import android.provider.ContactsContract.RawContacts; import android.text.TextUtils; import android.text.format.DateUtils; import android.util.AttributeSet; import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.view.accessibility.AccessibilityEvent; import android.widget.Button; import android.widget.FrameLayout; import android.widget.ImageView; import android.widget.TextView; import com.android.internal.telephony.Call; 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.CallManager; import java.util.List; import java.util.ArrayList; import android.provider.ContactsContract; /** * "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 FrameLayout implements CallTime.OnTickListener, CallerInfoAsyncQuery.OnQueryCompleteListener, ContactsAsyncHelper.OnImageLoadCompleteListener, View.OnClickListener { private static final String LOG_TAG = "CallCard"; private static final boolean DBG = (PhoneApp.DBG_LEVEL >= 2); /** * 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 PhoneApp mApplication; // Top-level subviews of the CallCard private ViewGroup mPrimaryCallInfo; private ViewGroup mSecondaryCallInfo; // Title and elapsed-time widgets private TextView mUpperTitle; private TextView mElapsedTime; // Text colors, used for various labels / titles private int mTextColorDefaultPrimary; private int mTextColorDefaultSecondary; private int mTextColorConnected; private int mTextColorConnectedBluetooth; private int mTextColorEnded; private int mTextColorOnHold; private int mTextColorCallTypeSip; // The main block of info about the "primary" or "active" call, // including photo / name / phone number / etc. private ImageView mPhoto; private Button mManageConferencePhotoButton; // Possibly shown in place of mPhoto private TextView mName; private TextView mPhoneNumber; private TextView mLabel; private TextView mCallTypeLabel; private TextView mSocialStatus; // Info about the "secondary" call, which is the "call on hold" when // two lines are in use. private TextView mSecondaryCallName; private TextView mSecondaryCallStatus; private ImageView mSecondaryCallPhoto; // Menu button hint private TextView mMenuButtonHint; // Onscreen hint for the incoming call RotarySelector widget. private int mRotarySelectorHintTextResId; private int mRotarySelectorHintColorResId; private CallTime mCallTime; // Track the state for the photo. private ContactsAsyncHelper.ImageTracker mPhotoTracker; // Cached DisplayMetrics density. private float mDensity; // add by cytown private CallFeaturesSetting mSettings; private TextView mOrganization; 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); // Inflate the contents of this CallCard, and add it (to ourself) as a child. LayoutInflater inflater = LayoutInflater.from(context); inflater.inflate( R.layout.call_card, // resource this, // root true); mApplication = PhoneApp.getInstance(); // add by cytown mSettings = CallFeaturesSetting.getInstance(android.preference.PreferenceManager.getDefaultSharedPreferences(context)); 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); } void setInCallScreenInstance(InCallScreen inCallScreen) { mInCallScreen = inCallScreen; } 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 + ")..."); mPrimaryCallInfo = (ViewGroup) findViewById(R.id.primaryCallInfo); mSecondaryCallInfo = (ViewGroup) findViewById(R.id.secondaryCallInfo); // "Upper" and "lower" title widgets mUpperTitle = (TextView) findViewById(R.id.upperTitle); mElapsedTime = (TextView) findViewById(R.id.elapsedTime); // Text colors mTextColorDefaultPrimary = // corresponds to textAppearanceLarge getResources().getColor(android.R.color.primary_text_dark); mTextColorDefaultSecondary = // corresponds to textAppearanceSmall getResources().getColor(android.R.color.secondary_text_dark); mTextColorConnected = getResources().getColor(R.color.incall_textConnected); mTextColorConnectedBluetooth = getResources().getColor(R.color.incall_textConnectedBluetooth); mTextColorEnded = getResources().getColor(R.color.incall_textEnded); mTextColorOnHold = getResources().getColor(R.color.incall_textOnHold); mTextColorCallTypeSip = getResources().getColor(R.color.incall_callTypeSip); // "Caller info" area, including photo / name / phone numbers / etc mPhoto = (ImageView) findViewById(R.id.photo); mManageConferencePhotoButton = (Button) findViewById(R.id.manageConferencePhotoButton); mManageConferencePhotoButton.setOnClickListener(this); 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); mOrganization = (TextView) findViewById(R.id.organization); // "Other call" info area mSecondaryCallName = (TextView) findViewById(R.id.secondaryCallName); mSecondaryCallStatus = (TextView) findViewById(R.id.secondaryCallStatus); mSecondaryCallPhoto = (ImageView) findViewById(R.id.secondaryCallPhoto); // Menu Button hint mMenuButtonHint = (TextView) findViewById(R.id.menuButtonHint); } /** * Updates the state of all UI elements on the CallCard, based on the * current state of the phone. */ void updateState(CallManager cm) { if (DBG) log("updateState(" + cm + ")..."); // Update some internal state based on the current state of the phone. // TODO: clean up this method to just fully update EVERYTHING in // the callcard based on the current phone state: set the overall // type of the CallCard, load up the main caller info area, and // load up and show or hide the "other call" area if necessary. Phone.State state = cm.getState(); // IDLE, RINGING, or OFFHOOK Call ringingCall = cm.getFirstActiveRingingCall(); Call fgCall = cm.getActiveFgCall(); Call bgCall = cm.getFirstActiveBgCall(); // 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. // // 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 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 == Phone.PHONE_TYPE_CDMA) { if ((mApplication.cdmaPhoneCallState.getCurrentCallState() == CdmaPhoneCallState.PhoneCallState.THRWAY_ACTIVE) && mApplication.cdmaPhoneCallState.IsThreeWayCallOrigStateDialing()) { displayOnHoldCallStatus(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. displayOnHoldCallStatus(cm, bgCall); } } else if ((phoneType == Phone.PHONE_TYPE_GSM) || (phoneType == Phone.PHONE_TYPE_SIP)) { displayOnHoldCallStatus(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.) displayOnHoldCallStatus(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); displayOnHoldCallStatus(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; } updateCardTitleWidgets(call.getPhone(), 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 == Phone.PHONE_TYPE_CDMA) { conn = call.getLatestConnection(); } else if ((phoneType == Phone.PHONE_TYPE_GSM) || (phoneType == Phone.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, Connection.PRESENTATION_ALLOWED, false, call); } 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 == Phone.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); } 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); } 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); } 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 (mRotarySelectorHintTextResId != 0) { // Display the hint! mPhoneNumber.setText(mRotarySelectorHintTextResId); mPhoneNumber.setTextColor(getResources().getColor(mRotarySelectorHintColorResId)); 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. */ 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 == Phone.PHONE_TYPE_CDMA) { conn = call.getLatestConnection(); } else if ((phoneType == Phone.PHONE_TYPE_GSM) || (phoneType == Phone.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 = Connection.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, Connection.PRESENTATION_ALLOWED, false, call); } else { updateDisplayForPerson(cit.currentInfo, presentation, false, call); } 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. */ public void onImageLoadComplete(int token, Object cookie, ImageView iView, boolean imagePresent){ if (cookie != null) { updatePhotoForCallState((Call) cookie); } } /** * Updates the "card title" (and also elapsed time widget) based on * the current state of the call. */ // TODO: it's confusing for updateCardTitleWidgets() and // getTitleForCallCard() to be separate methods, since they both // just list out the exact same "phone state" cases. // Let's merge the getTitleForCallCard() logic into here. private void updateCardTitleWidgets(Phone phone, Call call) { if (DBG) log("updateCardTitleWidgets(call " + call + ")..."); Call.State state = call.getState(); // TODO: Still need clearer spec on exactly how title *and* status get // set in all states. (Then, given that info, refactor the code // here to be more clear about exactly which widgets on the card // need to be set.) String cardTitle; int phoneType = phone.getPhoneType(); if (phoneType == Phone.PHONE_TYPE_CDMA) { if (!PhoneApp.getInstance().notifier.getIsCdmaRedialCall()) { cardTitle = getTitleForCallCard(call); // Normal "foreground" call card } else { cardTitle = getContext().getString(R.string.card_title_redialing); } } else if ((phoneType == Phone.PHONE_TYPE_GSM) || (phoneType == Phone.PHONE_TYPE_SIP)) { cardTitle = getTitleForCallCard(call); } else { throw new IllegalStateException("Unexpected phone type: " + phoneType); } if (DBG) log("updateCardTitleWidgets: " + cardTitle); // Update the title and elapsed time widgets based on the current call state. switch (state) { case ACTIVE: case DISCONNECTING: final boolean bluetoothActive = mApplication.showBluetoothIndication(); int ongoingCallIcon = bluetoothActive ? R.drawable.ic_incall_ongoing_bluetooth : R.drawable.ic_incall_ongoing; int connectedTextColor = bluetoothActive ? mTextColorConnectedBluetooth : mTextColorConnected; if (phoneType == Phone.PHONE_TYPE_CDMA) { // Check if the "Dialing" 3Way call needs to be displayed // as the Foreground Call state still remains ACTIVE if (mApplication.cdmaPhoneCallState.IsThreeWayCallOrigStateDialing()) { // Use the "upper title": setUpperTitle(cardTitle, mTextColorDefaultPrimary, state); } else { // Normal "ongoing call" state; don't use any "title" at all. clearUpperTitle(); } } else if ((phoneType == Phone.PHONE_TYPE_GSM) || (phoneType == Phone.PHONE_TYPE_SIP)) { // 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.) // TODO: consider displaying the "Hanging up" state for // CDMA also if the latency there ever gets high enough. if (state == Call.State.DISCONNECTING) { // Display the brief "Hanging up" indication. setUpperTitle(cardTitle, mTextColorDefaultPrimary, state); } else { // state == Call.State.ACTIVE // Normal "ongoing call" state; don't use any "title" at all. clearUpperTitle(); } } // Use the elapsed time widget to show the current call duration. mElapsedTime.setVisibility(View.VISIBLE); mElapsedTime.setTextColor(connectedTextColor); long duration = CallTime.getCallDuration(call); // msec updateElapsedTimeWidget(duration / 1000); // Also see onTickForCallTimeElapsed(), which updates this // widget once per second while the call is active. break; case DISCONNECTED: // Display "Call ended" (or possibly some error indication; // see getCallFailedString()) in the upper title, in red. // TODO: display a "call ended" icon somewhere, like the old // R.drawable.ic_incall_end? setUpperTitle(cardTitle, mTextColorEnded, state); // 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.) mElapsedTime.setVisibility(View.VISIBLE); mElapsedTime.setTextColor(mTextColorEnded); break; case HOLDING: // For a single call on hold, display the title "On hold" in // orange. // (But since the upper title overlaps the label of the // Hold/Unhold button, we actually use the elapsedTime widget // to display the title in this case.) // TODO: display an "On hold" icon somewhere, like the old // R.drawable.ic_incall_onhold? clearUpperTitle(); mElapsedTime.setText(cardTitle); // While on hold, the elapsed time widget displays an // "on hold" indication rather than an amount of time. mElapsedTime.setVisibility(View.VISIBLE); mElapsedTime.setTextColor(mTextColorOnHold); break; default: // All other states (DIALING, INCOMING, etc.) use the "upper title": setUpperTitle(cardTitle, mTextColorDefaultPrimary, state); // ...and we don't show the elapsed time. mElapsedTime.setVisibility(View.INVISIBLE); break; } } /** * Updates mElapsedTime based on the specified number of seconds. * A timeElapsed value of zero means to not show an elapsed time at all. */ private void updateElapsedTimeWidget(long timeElapsed) { // if (DBG) log("updateElapsedTimeWidget: " + timeElapsed); if (timeElapsed == 0) { mElapsedTime.setText(""); } else { mElapsedTime.setText(DateUtils.formatElapsedTime(timeElapsed)); } } /** * Returns the "card title" displayed at the top of a foreground * ("active") CallCard to indicate the current state of this call, like * "Dialing" or "In call" or "On hold". A null return value means that * there's no title string for this state. */ private String getTitleForCallCard(Call call) { String retVal = null; Call.State state = call.getState(); Context context = getContext(); int resId; if (DBG) log("- getTitleForCallCard(Call " + call + ")..."); switch (state) { case IDLE: break; case ACTIVE: // Title is "Call in progress". (Note this appears in the // "lower title" area of the CallCard.) int phoneType = call.getPhone().getPhoneType(); if (phoneType == Phone.PHONE_TYPE_CDMA) { if (mApplication.cdmaPhoneCallState.IsThreeWayCallOrigStateDialing()) { retVal = context.getString(R.string.card_title_dialing); } else { retVal = context.getString(R.string.card_title_in_progress); } } else if ((phoneType == Phone.PHONE_TYPE_GSM) || (phoneType == Phone.PHONE_TYPE_SIP)) { retVal = context.getString(R.string.card_title_in_progress); } else { throw new IllegalStateException("Unexpected phone type: " + phoneType); } break; case HOLDING: retVal = context.getString(R.string.card_title_on_hold); // TODO: if this is a conference call on hold, // maybe have a special title here too? break; case DIALING: case ALERTING: retVal = context.getString(R.string.card_title_dialing); break; case INCOMING: case WAITING: retVal = context.getString(R.string.card_title_incoming_call); break; case DISCONNECTING: retVal = context.getString(R.string.card_title_hanging_up); break; case DISCONNECTED: retVal = getCallFailedString(call); break; } if (DBG) log(" ==> result: " + retVal); return retVal; } /** * 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 displayOnHoldCallStatus(CallManager cm, Call call) { if (DBG) log("displayOnHoldCallStatus(call =" + call + ")..."); if ((call == null) || (PhoneApp.getInstance().isOtaCallInActiveState())) { mSecondaryCallInfo.setVisibility(View.GONE); return; } boolean showSecondaryCallInfo = false; 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.) 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); } } showSecondaryCallInfo = true; 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() == Phone.PHONE_TYPE_CDMA) { 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 PhoneApp 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 != Connection.PRESENTATION_ALLOWED) { name = getPresentationString(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); } } showSecondaryCallInfo = true; } else { // We shouldn't ever get here at all for non-CDMA devices. Log.w(LOG_TAG, "displayOnHoldCallStatus: ACTIVE state on non-CDMA device"); showSecondaryCallInfo = false; } 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.) showSecondaryCallInfo = false; break; } if (showSecondaryCallInfo) { // Ok, we have something useful to display in the "secondary // call" info area. mSecondaryCallInfo.setVisibility(View.VISIBLE); // Watch out: there are some cases where we need to display the // secondary call photo but *not* the two lines of text above it. // Specifically, that's any state where the CallCard "upper title" is // in use, since the title (e.g. "Dialing" or "Call ended") might // collide with the secondaryCallStatus and secondaryCallName widgets. // // We detect this case by simply seeing whether or not there's any text // in mUpperTitle. (This is much simpler than detecting all possible // telephony states where the "upper title" is used! But note it does // rely on the fact that updateCardTitleWidgets() gets called *earlier* // than this method, in the CallCard.updateState() sequence...) boolean okToShowLabels = TextUtils.isEmpty(mUpperTitle.getText()); mSecondaryCallName.setVisibility(okToShowLabels ? View.VISIBLE : View.INVISIBLE); mSecondaryCallStatus.setVisibility(okToShowLabels ? View.VISIBLE : View.INVISIBLE); } else { // Hide the entire "secondary call" info area. mSecondaryCallInfo.setVisibility(View.GONE); } } 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) { 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; boolean updateName = false; if (info != null) { // 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, info.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 leave the "number" slot empty.) if (TextUtils.isEmpty(number)) { displayName = getPresentationString(presentation); } else if (presentation != Connection.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 = getPresentationString(presentation); } else if (!TextUtils.isEmpty(info.cnapName)) { displayName = info.cnapName; info.name = info.cnapName; displayNumber = number; } else { displayName = number; } } 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 != Connection.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 = getPresentationString(presentation); } else { displayName = info.name; displayNumber = number; label = info.phoneLabel; // add by cytown for show organization updateName = true; } } 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 = getPresentationString(presentation); } if (call.isGeneric()) { mName.setText(R.string.card_title_in_call); mOrganization.setVisibility(View.GONE); } else { mName.setText(displayName); if (DBG) log("show ======= " + updateName + ":" + mSettings.mShowOrgan); if (updateName && mSettings.mShowOrgan) { updateOrganization(info.person_id); } else { mOrganization.setVisibility(View.GONE); } } mName.setVisibility(View.VISIBLE); // 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.setVisibility(View.INVISIBLE); } else if (info != null && info.photoResource != 0){ showImage(mPhoto, info.photoResource); } else if (!showCachedImage(mPhoto, info)) { // Load the image with a callback to update the image state. // Use the default unknown picture while the query is running. ContactsAsyncHelper.updateImageViewWithContactPhotoAsync( info, 0, this, call, getContext(), mPhoto, personUri, R.drawable.picture_unknown); } // And no matter what, on all devices, we never see the "manage // conference" button in this state. mManageConferencePhotoButton.setVisibility(View.INVISIBLE); if (displayNumber != null && !call.isGeneric()) { mPhoneNumber.setText(displayNumber); mPhoneNumber.setTextColor(mTextColorDefaultSecondary); mPhoneNumber.setVisibility(View.VISIBLE); } else { mPhoneNumber.setVisibility(View.GONE); } if (label != null && !call.isGeneric()) { mLabel.setText(label); mLabel.setVisibility(View.VISIBLE); } else { mLabel.setVisibility(View.GONE); } // Other text fields: updateCallTypeLabel(call); updateSocialStatus(socialStatusText, socialStatusBadge, call); // Currently unused } private void updateOrganization(final long person_id) { android.database.Cursor c = CallCard.this.getContext().getContentResolver().query(ContactsContract.Data.CONTENT_URI, new String[] { ContactsContract.CommonDataKinds.Organization.COMPANY, ContactsContract.CommonDataKinds.Nickname.NAME }, ContactsContract.Data.CONTACT_ID + " = ? and (" + ContactsContract.Data.MIMETYPE + " = '" + ContactsContract.CommonDataKinds.Organization.CONTENT_ITEM_TYPE + "' or " + ContactsContract.Data.MIMETYPE + " = '" + ContactsContract.CommonDataKinds.Nickname.CONTENT_ITEM_TYPE + "' )", new String[] { person_id + "" }, null); if (c != null) { if (c.moveToNext()) { try { // we have found an organization. set the organization name and exit loop String organ = c.getString(1); if (TextUtils.isEmpty(organ)) { organ = c.getString(0); } if (!TextUtils.isEmpty(organ)) { mOrganization.setText(organ); mOrganization.setVisibility(View.VISIBLE); mOrganization.invalidate(); } else { mOrganization.setVisibility(View.GONE); } } catch (Exception e) {} } else { mOrganization.setVisibility(View.GONE); } c.close(); } } private String getPresentationString(int presentation) { String name = getContext().getString(R.string.unknown); if (presentation == Connection.PRESENTATION_RESTRICTED) { name = getContext().getString(R.string.private_num); } else if (presentation == Connection.PRESENTATION_PAYPHONE) { name = getContext().getString(R.string.payphone); } return name; } /** * 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 == Phone.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 == Phone.PHONE_TYPE_GSM) || (phoneType == Phone.PHONE_TYPE_SIP)) { if (mInCallScreen.isTouchUiEnabled()) { // Display the "manage conference" button in place of the photo. mManageConferencePhotoButton.setVisibility(View.VISIBLE); mPhoto.setVisibility(View.INVISIBLE); // Not GONE, since that would break // other views in our RelativeLayout. } else { // Display the "conference call" image in the photo slot, // with no other information. 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 == Phone.PHONE_TYPE_CDMA) { conn = call.getLatestConnection(); } else if ((phoneType == Phone.PHONE_TYPE_GSM) || (phoneType == Phone.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]. // Load requests started in (3) use a placeholder image of -1 to hide the // image by default. Please refer to CallerInfoAsyncQuery.java for cases // where CallerInfo.photoResource may be set. if (photoImageResource == 0) { if (!PhoneUtils.isConferenceCall(call)) { if (!showCachedImage(mPhoto, ci) && (mPhotoTracker.getPhotoState() == ContactsAsyncHelper.ImageTracker.DISPLAY_DEFAULT)) { ContactsAsyncHelper.updateImageViewWithContactPhotoAsync(ci, getContext(), mPhoto, mPhotoTracker.getPhotoUri(), -1); 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) { view.setImageResource(resource); view.setVisibility(View.VISIBLE); } /** Helper function to display the drawable in the imageview AND ensure its visibility.*/ private static final void showImage(ImageView view, Drawable drawable) { view.setImageDrawable(drawable); view.setVisibility(View.VISIBLE); } /** * Returns the "Menu button hint" TextView (which is manipulated * directly by the InCallScreen.) * @see InCallScreen.updateMenuButtonHint() */ /* package */ TextView getMenuButtonHint() { return mMenuButtonHint; } /** * Sets the left and right margins of the specified ViewGroup (whose * LayoutParams object which must inherit from * ViewGroup.MarginLayoutParams.) * * TODO: Is there already a convenience method like this somewhere? */ private void setSideMargins(ViewGroup vg, int margin) { ViewGroup.MarginLayoutParams lp = (ViewGroup.MarginLayoutParams) vg.getLayoutParams(); // Equivalent to setting android:layout_marginLeft/Right in XML lp.leftMargin = margin; lp.rightMargin = margin; vg.setLayoutParams(lp); } /** * Sets the CallCard "upper title". Also, depending on the passed-in * Call state, possibly display an icon along with the title. */ private void setUpperTitle(String title, int color, Call.State state) { mUpperTitle.setText(title); mUpperTitle.setTextColor(color); int bluetoothIconId = 0; if (!TextUtils.isEmpty(title) && ((state == Call.State.INCOMING) || (state == Call.State.WAITING)) && mApplication.showBluetoothIndication()) { // Display the special bluetooth icon also, if this is an incoming // call and the audio will be routed to bluetooth. bluetoothIconId = R.drawable.ic_incoming_call_bluetooth; } mUpperTitle.setCompoundDrawablesWithIntrinsicBounds(bluetoothIconId, 0, 0, 0); if (bluetoothIconId != 0) mUpperTitle.setCompoundDrawablePadding((int) (mDensity * 5)); } /** * Clears the CallCard "upper title", for states (like a normal * ongoing call) where we don't use any "title" at all. */ private void clearUpperTitle() { setUpperTitle("", 0, Call.State.IDLE); // Use dummy values for "color" and "state" } /** * 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() : Phone.PHONE_TYPE_NONE; if (phoneType == Phone.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 setRotarySelectorHint(int hintTextResId, int hintColorResId) { mRotarySelectorHintTextResId = hintTextResId; mRotarySelectorHintColorResId = hintColorResId; } // View.OnClickListener implementation public void onClick(View view) { int id = view.getId(); if (DBG) log("onClick(View " + view + ", id " + id + ")..."); switch (id) { case R.id.manageConferencePhotoButton: // A click on anything here gets forwarded // straight to the InCallScreen. mInCallScreen.handleOnscreenButtonClick(id); break; default: Log.w(LOG_TAG, "onClick: unexpected click: View " + view + ", id " + id); break; } } // 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) { dispatchPopulateAccessibilityEvent(event, mUpperTitle); dispatchPopulateAccessibilityEvent(event, mPhoto); dispatchPopulateAccessibilityEvent(event, mManageConferencePhotoButton); dispatchPopulateAccessibilityEvent(event, mName); dispatchPopulateAccessibilityEvent(event, mPhoneNumber); dispatchPopulateAccessibilityEvent(event, mLabel); dispatchPopulateAccessibilityEvent(event, mSocialStatus); dispatchPopulateAccessibilityEvent(event, mSecondaryCallName); dispatchPopulateAccessibilityEvent(event, mSecondaryCallStatus); 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); } } // Debugging / testing code private void log(String msg) { Log.d(LOG_TAG, msg); } }