| /* |
| * 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.phone2; |
| |
| 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.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 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 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; |
| |
| // 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 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; |
| |
| 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(); |
| |
| 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); |
| |
| // "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); |
| mSocialStatus = (TextView) findViewById(R.id.socialStatus); |
| |
| // "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(Phone phone) { |
| if (DBG) log("updateState(" + phone + ")..."); |
| |
| // 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 = phone.getState(); // IDLE, RINGING, or OFFHOOK |
| if (state == Phone.State.RINGING) { |
| // A phone call is ringing *or* call waiting |
| // (ie. another call may also be active as well.) |
| updateRingingCall(phone); |
| } else if (state == Phone.State.OFFHOOK) { |
| // The phone is off hook. At least one call exists that is |
| // dialing, active, or holding, and no calls are ringing or waiting. |
| updateForegroundCall(phone); |
| } else { |
| // The phone state is IDLE! |
| // |
| // The most common reason for this is if a call just |
| // ended: the phone will be idle, but we *will* still |
| // have a call in the DISCONNECTED state: |
| Call fgCall = phone.getForegroundCall(); |
| Call bgCall = phone.getBackgroundCall(); |
| if ((fgCall.getState() == Call.State.DISCONNECTED) |
| || (bgCall.getState() == Call.State.DISCONNECTED)) { |
| // In this case, we want the main CallCard to display |
| // the "Call ended" state. The normal "foreground call" |
| // code path handles that. |
| updateForegroundCall(phone); |
| } 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(phone); |
| } |
| } |
| } |
| |
| /** |
| * Updates the UI for the state where the phone is in use, but not ringing. |
| */ |
| private void updateForegroundCall(Phone phone) { |
| if (DBG) log("updateForegroundCall()..."); |
| |
| Call fgCall = phone.getForegroundCall(); |
| Call bgCall = phone.getBackgroundCall(); |
| |
| if (fgCall.isIdle() && !fgCall.hasConnections()) { |
| 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(phone, fgCall); |
| |
| int phoneType = phone.getPhoneType(); |
| if (phoneType == Phone.PHONE_TYPE_CDMA) { |
| if ((mApplication.cdmaPhoneCallState.getCurrentCallState() |
| == CdmaPhoneCallState.PhoneCallState.THRWAY_ACTIVE) |
| && mApplication.cdmaPhoneCallState.IsThreeWayCallOrigStateDialing()) { |
| displayOnHoldCallStatus(phone, 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(phone, bgCall); |
| } |
| } else if (phoneType == Phone.PHONE_TYPE_GSM) { |
| displayOnHoldCallStatus(phone, 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(Phone phone) { |
| if (DBG) log("updateRingingCall()..."); |
| |
| Call ringingCall = phone.getRingingCall(); |
| Call fgCall = phone.getForegroundCall(); |
| Call bgCall = phone.getBackgroundCall(); |
| |
| // Display caller-id info and photo from the incoming call: |
| displayMainCallStatus(phone, 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(phone, 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(Phone phone) { |
| if (DBG) log("updateNoCall()..."); |
| |
| displayMainCallStatus(phone, null); |
| displayOnHoldCallStatus(phone, 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(Phone phone, Call call) { |
| if (DBG) log("displayMainCallStatus(phone " + phone |
| + ", 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(phone, call); |
| |
| if (PhoneUtils.isConferenceCall(call)) { |
| // Update onscreen info for a conference call. |
| updateDisplayForConference(); |
| } else { |
| // Update onscreen info for a regular call (which presumably |
| // has only one connection.) |
| Connection conn = null; |
| int phoneType = phone.getPhoneType(); |
| if (phoneType == Phone.PHONE_TYPE_CDMA) { |
| conn = call.getLatestConnection(); |
| } else if (phoneType == Phone.PHONE_TYPE_GSM) { |
| 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 = mApplication.phone.getPhoneType(); |
| if (phoneType == Phone.PHONE_TYPE_CDMA) { |
| conn = call.getLatestConnection(); |
| } else if (phoneType == Phone.PHONE_TYPE_GSM) { |
| 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 = mApplication.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) { |
| 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) { |
| // 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 = mApplication.phone.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) { |
| 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(Phone phone, 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 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; |
| |
| 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); |
| |
| String name; |
| String displayNumber = null; |
| String label = null; |
| Uri personUri = null; |
| String socialStatusText = null; |
| Drawable socialStatusBadge = null; |
| |
| 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. |
| |
| if (TextUtils.isEmpty(info.name)) { |
| if (TextUtils.isEmpty(info.phoneNumber)) { |
| name = 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 |
| name = getPresentationString(presentation); |
| } else if (!TextUtils.isEmpty(info.cnapName)) { |
| name = info.cnapName; |
| info.name = info.cnapName; |
| displayNumber = info.phoneNumber; |
| } else { |
| name = info.phoneNumber; |
| } |
| } else { |
| 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 |
| name = getPresentationString(presentation); |
| } else { |
| name = info.name; |
| displayNumber = info.phoneNumber; |
| label = info.phoneLabel; |
| } |
| } |
| personUri = ContentUris.withAppendedId(Contacts.CONTENT_URI, info.person_id); |
| } else { |
| name = getPresentationString(presentation); |
| } |
| |
| if (call.isGeneric()) { |
| mName.setText(R.string.card_title_in_call); |
| } else { |
| mName.setText(name); |
| } |
| 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); |
| } |
| |
| // "Social status": currently unused. |
| // Note socialStatus is *only* visible while an incoming |
| // call is ringing, never in any other call state. |
| if ((socialStatusText != 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); |
| } |
| } |
| |
| 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() { |
| if (DBG) log("updateDisplayForConference()..."); |
| |
| int phoneType = mApplication.phone.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) { |
| 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); |
| |
| // socialStatus is never visible in this state. |
| mSocialStatus.setVisibility(View.GONE); |
| |
| // 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 = mApplication.phone.getPhoneType(); |
| if (phoneType == Phone.PHONE_TYPE_CDMA) { |
| conn = call.getLatestConnection(); |
| } else if (phoneType == Phone.PHONE_TYPE_GSM) { |
| 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" |
| } |
| |
| /** |
| * 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); |
| } |
| } |