| /* |
| * 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.app.Activity; |
| import android.app.ActivityOptions; |
| import android.app.AlertDialog; |
| import android.app.Dialog; |
| import android.app.ProgressDialog; |
| import android.bluetooth.BluetoothAdapter; |
| import android.bluetooth.BluetoothDevice; |
| import android.bluetooth.BluetoothHeadset; |
| import android.bluetooth.BluetoothProfile; |
| import android.bluetooth.IBluetoothHeadsetPhone; |
| import android.content.ActivityNotFoundException; |
| import android.content.BroadcastReceiver; |
| import android.content.Context; |
| import android.content.DialogInterface; |
| import android.content.DialogInterface.OnCancelListener; |
| import android.content.Intent; |
| import android.content.IntentFilter; |
| import android.content.res.Configuration; |
| import android.content.res.Resources; |
| import android.graphics.Typeface; |
| import android.media.AudioManager; |
| import android.os.AsyncResult; |
| import android.os.Bundle; |
| import android.os.Handler; |
| import android.os.Message; |
| import android.os.PowerManager; |
| import android.os.RemoteException; |
| import android.os.SystemClock; |
| import android.os.SystemProperties; |
| import android.telephony.ServiceState; |
| import android.text.TextUtils; |
| import android.text.method.DialerKeyListener; |
| import android.util.EventLog; |
| import android.util.Log; |
| import android.view.KeyEvent; |
| import android.view.View; |
| import android.view.ViewGroup; |
| import android.view.ViewStub; |
| import android.view.Window; |
| import android.view.WindowManager; |
| import android.view.accessibility.AccessibilityEvent; |
| import android.widget.EditText; |
| import android.widget.ImageView; |
| import android.widget.LinearLayout; |
| import android.widget.TextView; |
| import android.widget.Toast; |
| |
| import com.android.internal.telephony.Call; |
| import com.android.internal.telephony.CallManager; |
| import com.android.internal.telephony.Connection; |
| import com.android.internal.telephony.MmiCode; |
| import com.android.internal.telephony.Phone; |
| import com.android.internal.telephony.PhoneConstants; |
| import com.android.internal.telephony.TelephonyCapabilities; |
| import com.android.phone.Constants.CallStatusCode; |
| import com.android.phone.InCallUiState.InCallScreenMode; |
| import com.android.phone.OtaUtils.CdmaOtaScreenState; |
| |
| import java.util.List; |
| |
| |
| /** |
| * Phone app "in call" screen. |
| */ |
| public class InCallScreen extends Activity |
| implements View.OnClickListener { |
| private static final String LOG_TAG = "InCallScreen"; |
| |
| private static final boolean DBG = |
| (PhoneGlobals.DBG_LEVEL >= 1) && (SystemProperties.getInt("ro.debuggable", 0) == 1); |
| private static final boolean VDBG = (PhoneGlobals.DBG_LEVEL >= 2); |
| |
| /** |
| * Intent extra used to specify whether the DTMF dialpad should be |
| * initially visible when bringing up the InCallScreen. (If this |
| * extra is present, the dialpad will be initially shown if the extra |
| * has the boolean value true, and initially hidden otherwise.) |
| */ |
| // TODO: Should be EXTRA_SHOW_DIALPAD for consistency. |
| static final String SHOW_DIALPAD_EXTRA = "com.android.phone.ShowDialpad"; |
| |
| /** |
| * Intent extra to specify the package name of the gateway |
| * provider. Used to get the name displayed in the in-call screen |
| * during the call setup. The value is a string. |
| */ |
| // TODO: This extra is currently set by the gateway application as |
| // a temporary measure. Ultimately, the framework will securely |
| // set it. |
| /* package */ static final String EXTRA_GATEWAY_PROVIDER_PACKAGE = |
| "com.android.phone.extra.GATEWAY_PROVIDER_PACKAGE"; |
| |
| /** |
| * Intent extra to specify the URI of the provider to place the |
| * call. The value is a string. It holds the gateway address |
| * (phone gateway URL should start with the 'tel:' scheme) that |
| * will actually be contacted to call the number passed in the |
| * intent URL or in the EXTRA_PHONE_NUMBER extra. |
| */ |
| // TODO: Should the value be a Uri (Parcelable)? Need to make sure |
| // MMI code '#' don't get confused as URI fragments. |
| /* package */ static final String EXTRA_GATEWAY_URI = |
| "com.android.phone.extra.GATEWAY_URI"; |
| |
| // Amount of time (in msec) that we display the "Call ended" state. |
| // The "short" value is for calls ended by the local user, and the |
| // "long" value is for calls ended by the remote caller. |
| private static final int CALL_ENDED_SHORT_DELAY = 200; // msec |
| private static final int CALL_ENDED_LONG_DELAY = 2000; // msec |
| private static final int CALL_ENDED_EXTRA_LONG_DELAY = 5000; // msec |
| |
| // Amount of time that we display the PAUSE alert Dialog showing the |
| // post dial string yet to be send out to the n/w |
| private static final int PAUSE_PROMPT_DIALOG_TIMEOUT = 2000; //msec |
| |
| // Amount of time that we display the provider info if applicable. |
| private static final int PROVIDER_INFO_TIMEOUT = 5000; // msec |
| |
| // These are values for the settings of the auto retry mode: |
| // 0 = disabled |
| // 1 = enabled |
| // TODO (Moto):These constants don't really belong here, |
| // they should be moved to Settings where the value is being looked up in the first place |
| static final int AUTO_RETRY_OFF = 0; |
| static final int AUTO_RETRY_ON = 1; |
| |
| // Message codes; see mHandler below. |
| // Note message codes < 100 are reserved for the PhoneApp. |
| private static final int PHONE_STATE_CHANGED = 101; |
| private static final int PHONE_DISCONNECT = 102; |
| private static final int EVENT_HEADSET_PLUG_STATE_CHANGED = 103; |
| private static final int POST_ON_DIAL_CHARS = 104; |
| private static final int WILD_PROMPT_CHAR_ENTERED = 105; |
| private static final int ADD_VOICEMAIL_NUMBER = 106; |
| private static final int DONT_ADD_VOICEMAIL_NUMBER = 107; |
| private static final int DELAYED_CLEANUP_AFTER_DISCONNECT = 108; |
| private static final int SUPP_SERVICE_FAILED = 110; |
| private static final int REQUEST_UPDATE_BLUETOOTH_INDICATION = 114; |
| private static final int PHONE_CDMA_CALL_WAITING = 115; |
| private static final int REQUEST_CLOSE_SPC_ERROR_NOTICE = 118; |
| private static final int REQUEST_CLOSE_OTA_FAILURE_NOTICE = 119; |
| private static final int EVENT_PAUSE_DIALOG_COMPLETE = 120; |
| private static final int EVENT_HIDE_PROVIDER_INFO = 121; // Time to remove the info. |
| private static final int REQUEST_UPDATE_SCREEN = 122; |
| private static final int PHONE_INCOMING_RING = 123; |
| private static final int PHONE_NEW_RINGING_CONNECTION = 124; |
| |
| // When InCallScreenMode is UNDEFINED set the default action |
| // to ACTION_UNDEFINED so if we are resumed the activity will |
| // know its undefined. In particular checkIsOtaCall will return |
| // false. |
| public static final String ACTION_UNDEFINED = "com.android.phone.InCallScreen.UNDEFINED"; |
| |
| /** Status codes returned from syncWithPhoneState(). */ |
| private enum SyncWithPhoneStateStatus { |
| /** |
| * Successfully updated our internal state based on the telephony state. |
| */ |
| SUCCESS, |
| |
| /** |
| * There was no phone state to sync with (i.e. the phone was |
| * completely idle). In most cases this means that the |
| * in-call UI shouldn't be visible in the first place, unless |
| * we need to remain in the foreground while displaying an |
| * error message. |
| */ |
| PHONE_NOT_IN_USE |
| } |
| |
| private boolean mRegisteredForPhoneStates; |
| |
| private PhoneGlobals mApp; |
| private CallManager mCM; |
| |
| // TODO: need to clean up all remaining uses of mPhone. |
| // (There may be more than one Phone instance on the device, so it's wrong |
| // to just keep a single mPhone field. Instead, any time we need a Phone |
| // reference we should get it dynamically from the CallManager, probably |
| // based on the current foreground Call.) |
| private Phone mPhone; |
| |
| private BluetoothHeadset mBluetoothHeadset; |
| private BluetoothAdapter mBluetoothAdapter; |
| private boolean mBluetoothConnectionPending; |
| private long mBluetoothConnectionRequestTime; |
| |
| /** Main in-call UI elements. */ |
| private CallCard mCallCard; |
| |
| // UI controls: |
| private InCallControlState mInCallControlState; |
| private InCallTouchUi mInCallTouchUi; |
| private RespondViaSmsManager mRespondViaSmsManager; // see internalRespondViaSms() |
| private ManageConferenceUtils mManageConferenceUtils; |
| |
| // DTMF Dialer controller and its view: |
| private DTMFTwelveKeyDialer mDialer; |
| |
| private EditText mWildPromptText; |
| |
| // Various dialogs we bring up (see dismissAllDialogs()). |
| // TODO: convert these all to use the "managed dialogs" framework. |
| // |
| // The MMI started dialog can actually be one of 2 items: |
| // 1. An alert dialog if the MMI code is a normal MMI |
| // 2. A progress dialog if the user requested a USSD |
| private Dialog mMmiStartedDialog; |
| private AlertDialog mMissingVoicemailDialog; |
| private AlertDialog mGenericErrorDialog; |
| private AlertDialog mSuppServiceFailureDialog; |
| private AlertDialog mWaitPromptDialog; |
| private AlertDialog mWildPromptDialog; |
| private AlertDialog mCallLostDialog; |
| private AlertDialog mPausePromptDialog; |
| private AlertDialog mExitingECMDialog; |
| // NOTE: if you add a new dialog here, be sure to add it to dismissAllDialogs() also. |
| |
| // ProgressDialog created by showProgressIndication() |
| private ProgressDialog mProgressDialog; |
| |
| // TODO: If the Activity class ever provides an easy way to get the |
| // current "activity lifecycle" state, we can remove these flags. |
| private boolean mIsDestroyed = false; |
| private boolean mIsForegroundActivity = false; |
| private boolean mIsForegroundActivityForProximity = false; |
| private PowerManager mPowerManager; |
| |
| // For use with Pause/Wait dialogs |
| private String mPostDialStrAfterPause; |
| private boolean mPauseInProgress = false; |
| |
| // Info about the most-recently-disconnected Connection, which is used |
| // to determine what should happen when exiting the InCallScreen after a |
| // call. (This info is set by onDisconnect(), and used by |
| // delayedCleanupAfterDisconnect().) |
| private Connection.DisconnectCause mLastDisconnectCause; |
| |
| /** In-call audio routing options; see switchInCallAudio(). */ |
| public enum InCallAudioMode { |
| SPEAKER, // Speakerphone |
| BLUETOOTH, // Bluetooth headset (if available) |
| EARPIECE, // Handset earpiece (or wired headset, if connected) |
| } |
| |
| |
| private Handler mHandler = new Handler() { |
| @Override |
| public void handleMessage(Message msg) { |
| if (mIsDestroyed) { |
| if (DBG) log("Handler: ignoring message " + msg + "; we're destroyed!"); |
| return; |
| } |
| if (!mIsForegroundActivity) { |
| if (DBG) log("Handler: handling message " + msg + " while not in foreground"); |
| // Continue anyway; some of the messages below *want* to |
| // be handled even if we're not the foreground activity |
| // (like DELAYED_CLEANUP_AFTER_DISCONNECT), and they all |
| // should at least be safe to handle if we're not in the |
| // foreground... |
| } |
| |
| switch (msg.what) { |
| case SUPP_SERVICE_FAILED: |
| onSuppServiceFailed((AsyncResult) msg.obj); |
| break; |
| |
| case PHONE_STATE_CHANGED: |
| onPhoneStateChanged((AsyncResult) msg.obj); |
| break; |
| |
| case PHONE_DISCONNECT: |
| onDisconnect((AsyncResult) msg.obj); |
| break; |
| |
| case EVENT_HEADSET_PLUG_STATE_CHANGED: |
| // Update the in-call UI, since some UI elements (such |
| // as the "Speaker" button) may change state depending on |
| // whether a headset is plugged in. |
| // TODO: A full updateScreen() is overkill here, since |
| // the value of PhoneApp.isHeadsetPlugged() only affects a |
| // single onscreen UI element. (But even a full updateScreen() |
| // is still pretty cheap, so let's keep this simple |
| // for now.) |
| updateScreen(); |
| |
| // Also, force the "audio mode" popup to refresh itself if |
| // it's visible, since one of its items is either "Wired |
| // headset" or "Handset earpiece" depending on whether the |
| // headset is plugged in or not. |
| mInCallTouchUi.refreshAudioModePopup(); // safe even if the popup's not active |
| |
| break; |
| |
| // TODO: sort out MMI code (probably we should remove this method entirely). |
| // See also MMI handling code in onResume() |
| // case PhoneApp.MMI_INITIATE: |
| // onMMIInitiate((AsyncResult) msg.obj); |
| // break; |
| |
| case PhoneGlobals.MMI_CANCEL: |
| onMMICancel(); |
| break; |
| |
| // handle the mmi complete message. |
| // since the message display class has been replaced with |
| // a system dialog in PhoneUtils.displayMMIComplete(), we |
| // should finish the activity here to close the window. |
| case PhoneGlobals.MMI_COMPLETE: |
| onMMIComplete((MmiCode) ((AsyncResult) msg.obj).result); |
| break; |
| |
| case POST_ON_DIAL_CHARS: |
| handlePostOnDialChars((AsyncResult) msg.obj, (char) msg.arg1); |
| break; |
| |
| case ADD_VOICEMAIL_NUMBER: |
| addVoiceMailNumberPanel(); |
| break; |
| |
| case DONT_ADD_VOICEMAIL_NUMBER: |
| dontAddVoiceMailNumber(); |
| break; |
| |
| case DELAYED_CLEANUP_AFTER_DISCONNECT: |
| delayedCleanupAfterDisconnect(); |
| break; |
| |
| case REQUEST_UPDATE_BLUETOOTH_INDICATION: |
| if (VDBG) log("REQUEST_UPDATE_BLUETOOTH_INDICATION..."); |
| // The bluetooth headset state changed, so some UI |
| // elements may need to update. (There's no need to |
| // look up the current state here, since any UI |
| // elements that care about the bluetooth state get it |
| // directly from PhoneApp.showBluetoothIndication().) |
| updateScreen(); |
| break; |
| |
| case PHONE_CDMA_CALL_WAITING: |
| if (DBG) log("Received PHONE_CDMA_CALL_WAITING event ..."); |
| Connection cn = mCM.getFirstActiveRingingCall().getLatestConnection(); |
| |
| // Only proceed if we get a valid connection object |
| if (cn != null) { |
| // Finally update screen with Call waiting info and request |
| // screen to wake up |
| updateScreen(); |
| mApp.updateWakeState(); |
| } |
| break; |
| |
| case REQUEST_CLOSE_SPC_ERROR_NOTICE: |
| if (mApp.otaUtils != null) { |
| mApp.otaUtils.onOtaCloseSpcNotice(); |
| } |
| break; |
| |
| case REQUEST_CLOSE_OTA_FAILURE_NOTICE: |
| if (mApp.otaUtils != null) { |
| mApp.otaUtils.onOtaCloseFailureNotice(); |
| } |
| break; |
| |
| case EVENT_PAUSE_DIALOG_COMPLETE: |
| if (mPausePromptDialog != null) { |
| if (DBG) log("- DISMISSING mPausePromptDialog."); |
| mPausePromptDialog.dismiss(); // safe even if already dismissed |
| mPausePromptDialog = null; |
| } |
| break; |
| |
| case EVENT_HIDE_PROVIDER_INFO: |
| mApp.inCallUiState.providerInfoVisible = false; |
| if (mCallCard != null) { |
| mCallCard.updateState(mCM); |
| } |
| break; |
| case REQUEST_UPDATE_SCREEN: |
| updateScreen(); |
| break; |
| |
| case PHONE_INCOMING_RING: |
| onIncomingRing(); |
| break; |
| |
| case PHONE_NEW_RINGING_CONNECTION: |
| onNewRingingConnection(); |
| break; |
| |
| default: |
| Log.wtf(LOG_TAG, "mHandler: unexpected message: " + msg); |
| break; |
| } |
| } |
| }; |
| |
| private final BroadcastReceiver mReceiver = new BroadcastReceiver() { |
| @Override |
| public void onReceive(Context context, Intent intent) { |
| String action = intent.getAction(); |
| if (action.equals(Intent.ACTION_HEADSET_PLUG)) { |
| // Listen for ACTION_HEADSET_PLUG broadcasts so that we |
| // can update the onscreen UI when the headset state changes. |
| // if (DBG) log("mReceiver: ACTION_HEADSET_PLUG"); |
| // if (DBG) log("==> intent: " + intent); |
| // if (DBG) log(" state: " + intent.getIntExtra("state", 0)); |
| // if (DBG) log(" name: " + intent.getStringExtra("name")); |
| // send the event and add the state as an argument. |
| Message message = Message.obtain(mHandler, EVENT_HEADSET_PLUG_STATE_CHANGED, |
| intent.getIntExtra("state", 0), 0); |
| mHandler.sendMessage(message); |
| } |
| } |
| }; |
| |
| |
| @Override |
| protected void onCreate(Bundle icicle) { |
| Log.i(LOG_TAG, "onCreate()... this = " + this); |
| Profiler.callScreenOnCreate(); |
| super.onCreate(icicle); |
| |
| // Make sure this is a voice-capable device. |
| if (!PhoneGlobals.sVoiceCapable) { |
| // There should be no way to ever reach the InCallScreen on a |
| // non-voice-capable device, since this activity is not exported by |
| // our manifest, and we explicitly disable any other external APIs |
| // like the CALL intent and ITelephony.showCallScreen(). |
| // So the fact that we got here indicates a phone app bug. |
| Log.wtf(LOG_TAG, "onCreate() reached on non-voice-capable device"); |
| finish(); |
| return; |
| } |
| |
| mApp = PhoneGlobals.getInstance(); |
| mApp.setInCallScreenInstance(this); |
| |
| // set this flag so this activity will stay in front of the keyguard |
| int flags = WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED |
| | WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON; |
| if (mApp.getPhoneState() == PhoneConstants.State.OFFHOOK) { |
| // While we are in call, the in-call screen should dismiss the keyguard. |
| // This allows the user to press Home to go directly home without going through |
| // an insecure lock screen. |
| // But we do not want to do this if there is no active call so we do not |
| // bypass the keyguard if the call is not answered or declined. |
| flags |= WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD; |
| } |
| |
| WindowManager.LayoutParams lp = getWindow().getAttributes(); |
| lp.flags |= flags; |
| if (!mApp.proximitySensorModeEnabled()) { |
| // If we don't have a proximity sensor, then the in-call screen explicitly |
| // controls user activity. This is to prevent spurious touches from waking |
| // the display. |
| lp.inputFeatures |= WindowManager.LayoutParams.INPUT_FEATURE_DISABLE_USER_ACTIVITY; |
| } |
| getWindow().setAttributes(lp); |
| |
| setPhone(mApp.phone); // Sets mPhone |
| |
| mCM = mApp.mCM; |
| log("- onCreate: phone state = " + mCM.getState()); |
| |
| mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter(); |
| if (mBluetoothAdapter != null) { |
| mBluetoothAdapter.getProfileProxy(getApplicationContext(), mBluetoothProfileServiceListener, |
| BluetoothProfile.HEADSET); |
| } |
| |
| requestWindowFeature(Window.FEATURE_NO_TITLE); |
| |
| // Inflate everything in incall_screen.xml and add it to the screen. |
| setContentView(R.layout.incall_screen); |
| |
| // If in landscape, then one of the ViewStubs (instead of <include>) is used for the |
| // incall_touch_ui, because CDMA and GSM button layouts are noticeably different. |
| final ViewStub touchUiStub = (ViewStub) findViewById( |
| mPhone.getPhoneType() == PhoneConstants.PHONE_TYPE_CDMA |
| ? R.id.inCallTouchUiCdmaStub : R.id.inCallTouchUiStub); |
| if (touchUiStub != null) touchUiStub.inflate(); |
| |
| initInCallScreen(); |
| |
| registerForPhoneStates(); |
| |
| // No need to change wake state here; that happens in onResume() when we |
| // are actually displayed. |
| |
| // Handle the Intent we were launched with, but only if this is the |
| // the very first time we're being launched (ie. NOT if we're being |
| // re-initialized after previously being shut down.) |
| // Once we're up and running, any future Intents we need |
| // to handle will come in via the onNewIntent() method. |
| if (icicle == null) { |
| if (DBG) log("onCreate(): this is our very first launch, checking intent..."); |
| internalResolveIntent(getIntent()); |
| } |
| |
| Profiler.callScreenCreated(); |
| if (DBG) log("onCreate(): exit"); |
| } |
| |
| private BluetoothProfile.ServiceListener mBluetoothProfileServiceListener = |
| new BluetoothProfile.ServiceListener() { |
| @Override |
| public void onServiceConnected(int profile, BluetoothProfile proxy) { |
| mBluetoothHeadset = (BluetoothHeadset) proxy; |
| if (VDBG) log("- Got BluetoothHeadset: " + mBluetoothHeadset); |
| } |
| |
| @Override |
| public void onServiceDisconnected(int profile) { |
| mBluetoothHeadset = null; |
| } |
| }; |
| |
| /** |
| * Sets the Phone object used internally by the InCallScreen. |
| * |
| * In normal operation this is called from onCreate(), and the |
| * passed-in Phone object comes from the PhoneApp. |
| * For testing, test classes can use this method to |
| * inject a test Phone instance. |
| */ |
| /* package */ void setPhone(Phone phone) { |
| mPhone = phone; |
| } |
| |
| @Override |
| protected void onResume() { |
| if (DBG) log("onResume()..."); |
| super.onResume(); |
| |
| mIsForegroundActivity = true; |
| mIsForegroundActivityForProximity = true; |
| |
| // The flag shouldn't be turned on when there are actual phone calls. |
| if (mCM.hasActiveFgCall() || mCM.hasActiveBgCall() || mCM.hasActiveRingingCall()) { |
| mApp.inCallUiState.showAlreadyDisconnectedState = false; |
| } |
| |
| final InCallUiState inCallUiState = mApp.inCallUiState; |
| if (VDBG) inCallUiState.dumpState(); |
| |
| updateExpandedViewState(); |
| |
| // ...and update the in-call notification too, since the status bar |
| // icon needs to be hidden while we're the foreground activity: |
| mApp.notificationMgr.updateInCallNotification(); |
| |
| // Listen for broadcast intents that might affect the onscreen UI. |
| registerReceiver(mReceiver, new IntentFilter(Intent.ACTION_HEADSET_PLUG)); |
| |
| // Keep a "dialer session" active when we're in the foreground. |
| // (This is needed to play DTMF tones.) |
| mDialer.startDialerSession(); |
| |
| // Restore various other state from the InCallUiState object: |
| |
| // Update the onscreen dialpad state to match the InCallUiState. |
| if (inCallUiState.showDialpad) { |
| openDialpadInternal(false); // no "opening" animation |
| } else { |
| closeDialpadInternal(false); // no "closing" animation |
| } |
| |
| // Reset the dialpad context |
| // TODO: Dialpad digits should be set here as well (once they are saved) |
| mDialer.setDialpadContext(inCallUiState.dialpadContextText); |
| |
| // If there's a "Respond via SMS" popup still around since the |
| // last time we were the foreground activity, make sure it's not |
| // still active! |
| // (The popup should *never* be visible initially when we first |
| // come to the foreground; it only ever comes up in response to |
| // the user selecting the "SMS" option from the incoming call |
| // widget.) |
| mRespondViaSmsManager.dismissPopup(); // safe even if already dismissed |
| |
| // Display an error / diagnostic indication if necessary. |
| // |
| // When the InCallScreen comes to the foreground, we normally we |
| // display the in-call UI in whatever state is appropriate based on |
| // the state of the telephony framework (e.g. an outgoing call in |
| // DIALING state, an incoming call, etc.) |
| // |
| // But if the InCallUiState has a "pending call status code" set, |
| // that means we need to display some kind of status or error |
| // indication to the user instead of the regular in-call UI. (The |
| // most common example of this is when there's some kind of |
| // failure while initiating an outgoing call; see |
| // CallController.placeCall().) |
| // |
| boolean handledStartupError = false; |
| if (inCallUiState.hasPendingCallStatusCode()) { |
| if (DBG) log("- onResume: need to show status indication!"); |
| showStatusIndication(inCallUiState.getPendingCallStatusCode()); |
| |
| // Set handledStartupError to ensure that we won't bail out below. |
| // (We need to stay here in the InCallScreen so that the user |
| // is able to see the error dialog!) |
| handledStartupError = true; |
| } |
| |
| // Set the volume control handler while we are in the foreground. |
| final boolean bluetoothConnected = isBluetoothAudioConnected(); |
| |
| if (bluetoothConnected) { |
| setVolumeControlStream(AudioManager.STREAM_BLUETOOTH_SCO); |
| } else { |
| setVolumeControlStream(AudioManager.STREAM_VOICE_CALL); |
| } |
| |
| takeKeyEvents(true); |
| |
| // If an OTASP call is in progress, use the special OTASP-specific UI. |
| boolean inOtaCall = false; |
| if (TelephonyCapabilities.supportsOtasp(mPhone)) { |
| inOtaCall = checkOtaspStateOnResume(); |
| } |
| if (!inOtaCall) { |
| // Always start off in NORMAL mode |
| setInCallScreenMode(InCallScreenMode.NORMAL); |
| } |
| |
| // Before checking the state of the CallManager, clean up any |
| // connections in the DISCONNECTED state. |
| // (The DISCONNECTED state is used only to drive the "call ended" |
| // UI; it's totally useless when *entering* the InCallScreen.) |
| mCM.clearDisconnected(); |
| |
| // Update the onscreen UI to reflect the current telephony state. |
| SyncWithPhoneStateStatus status = syncWithPhoneState(); |
| |
| // Note there's no need to call updateScreen() here; |
| // syncWithPhoneState() already did that if necessary. |
| |
| if (status != SyncWithPhoneStateStatus.SUCCESS) { |
| if (DBG) log("- onResume: syncWithPhoneState failed! status = " + status); |
| // Couldn't update the UI, presumably because the phone is totally |
| // idle. |
| |
| // Even though the phone is idle, though, we do still need to |
| // stay here on the InCallScreen if we're displaying an |
| // error dialog (see "showStatusIndication()" above). |
| |
| if (handledStartupError) { |
| // Stay here for now. We'll eventually leave the |
| // InCallScreen when the user presses the dialog's OK |
| // button (see bailOutAfterErrorDialog()), or when the |
| // progress indicator goes away. |
| Log.i(LOG_TAG, " ==> syncWithPhoneState failed, but staying here anyway."); |
| } else { |
| // The phone is idle, and we did NOT handle a |
| // startup error during this pass thru onResume. |
| // |
| // This basically means that we're being resumed because of |
| // some action *other* than a new intent. (For example, |
| // the user pressing POWER to wake up the device, causing |
| // the InCallScreen to come back to the foreground.) |
| // |
| // In this scenario we do NOT want to stay here on the |
| // InCallScreen: we're not showing any useful info to the |
| // user (like a dialog), and the in-call UI itself is |
| // useless if there's no active call. So bail out. |
| |
| Log.i(LOG_TAG, " ==> syncWithPhoneState failed; bailing out!"); |
| dismissAllDialogs(); |
| |
| // Force the InCallScreen to truly finish(), rather than just |
| // moving it to the back of the activity stack (which is what |
| // our finish() method usually does.) |
| // This is necessary to avoid an obscure scenario where the |
| // InCallScreen can get stuck in an inconsistent state, somehow |
| // causing a *subsequent* outgoing call to fail (bug 4172599). |
| endInCallScreenSession(true /* force a real finish() call */); |
| return; |
| } |
| } else if (TelephonyCapabilities.supportsOtasp(mPhone)) { |
| if (inCallUiState.inCallScreenMode == InCallScreenMode.OTA_NORMAL || |
| inCallUiState.inCallScreenMode == InCallScreenMode.OTA_ENDED) { |
| if (mCallCard != null) mCallCard.setVisibility(View.GONE); |
| updateScreen(); |
| return; |
| } |
| } |
| |
| // InCallScreen is now active. |
| EventLog.writeEvent(EventLogTags.PHONE_UI_ENTER); |
| |
| // Update the poke lock and wake lock when we move to the foreground. |
| // This will be no-op when prox sensor is effective. |
| mApp.updateWakeState(); |
| |
| // Restore the mute state if the last mute state change was NOT |
| // done by the user. |
| if (mApp.getRestoreMuteOnInCallResume()) { |
| // Mute state is based on the foreground call |
| PhoneUtils.restoreMuteState(); |
| mApp.setRestoreMuteOnInCallResume(false); |
| } |
| |
| Profiler.profileViewCreate(getWindow(), InCallScreen.class.getName()); |
| |
| // If there's a pending MMI code, we'll show a dialog here. |
| // |
| // Note: previously we had shown the dialog when MMI_INITIATE event's coming |
| // from telephony layer, while right now we don't because the event comes |
| // too early (before in-call screen is prepared). |
| // Now we instead check pending MMI code and show the dialog here. |
| // |
| // This *may* cause some problem, e.g. when the user really quickly starts |
| // MMI sequence and calls an actual phone number before the MMI request |
| // being completed, which is rather rare. |
| // |
| // TODO: streamline this logic and have a UX in a better manner. |
| // Right now syncWithPhoneState() above will return SUCCESS based on |
| // mPhone.getPendingMmiCodes().isEmpty(), while we check it again here. |
| // Also we show pre-populated in-call UI under the dialog, which looks |
| // not great. (issue 5210375, 5545506) |
| // After cleaning them, remove commented-out MMI handling code elsewhere. |
| if (!mPhone.getPendingMmiCodes().isEmpty()) { |
| if (mMmiStartedDialog == null) { |
| MmiCode mmiCode = mPhone.getPendingMmiCodes().get(0); |
| Message message = Message.obtain(mHandler, PhoneGlobals.MMI_CANCEL); |
| mMmiStartedDialog = PhoneUtils.displayMMIInitiate(this, mmiCode, |
| message, mMmiStartedDialog); |
| // mInCallScreen needs to receive MMI_COMPLETE/MMI_CANCEL event from telephony, |
| // which will dismiss the entire screen. |
| } |
| } |
| |
| // This means the screen is shown even though there's no connection, which only happens |
| // when the phone call has hung up while the screen is turned off at that moment. |
| // We want to show "disconnected" state with photos with appropriate elapsed time for |
| // the finished phone call. |
| if (mApp.inCallUiState.showAlreadyDisconnectedState) { |
| // if (DBG) { |
| log("onResume(): detected \"show already disconnected state\" situation." |
| + " set up DELAYED_CLEANUP_AFTER_DISCONNECT message with " |
| + CALL_ENDED_LONG_DELAY + " msec delay."); |
| //} |
| mHandler.removeMessages(DELAYED_CLEANUP_AFTER_DISCONNECT); |
| mHandler.sendEmptyMessageDelayed(DELAYED_CLEANUP_AFTER_DISCONNECT, |
| CALL_ENDED_LONG_DELAY); |
| } |
| |
| if (VDBG) log("onResume() done."); |
| } |
| |
| // onPause is guaranteed to be called when the InCallScreen goes |
| // in the background. |
| @Override |
| protected void onPause() { |
| if (DBG) log("onPause()..."); |
| super.onPause(); |
| |
| if (mPowerManager.isScreenOn()) { |
| // Set to false when the screen went background *not* by screen turned off. Probably |
| // the user bailed out of the in-call screen (by pressing BACK, HOME, etc.) |
| mIsForegroundActivityForProximity = false; |
| } |
| mIsForegroundActivity = false; |
| |
| // Force a clear of the provider info frame. Since the |
| // frame is removed using a timed message, it is |
| // possible we missed it if the prev call was interrupted. |
| mApp.inCallUiState.providerInfoVisible = false; |
| |
| // "show-already-disconnected-state" should be effective just during the first wake-up. |
| // We should never allow it to stay true after that. |
| mApp.inCallUiState.showAlreadyDisconnectedState = false; |
| |
| // A safety measure to disable proximity sensor in case call failed |
| // and the telephony state did not change. |
| mApp.setBeginningCall(false); |
| |
| // Make sure the "Manage conference" chronometer is stopped when |
| // we move away from the foreground. |
| mManageConferenceUtils.stopConferenceTime(); |
| |
| // as a catch-all, make sure that any dtmf tones are stopped |
| // when the UI is no longer in the foreground. |
| mDialer.onDialerKeyUp(null); |
| |
| // Release any "dialer session" resources, now that we're no |
| // longer in the foreground. |
| mDialer.stopDialerSession(); |
| |
| // If the device is put to sleep as the phone call is ending, |
| // we may see cases where the DELAYED_CLEANUP_AFTER_DISCONNECT |
| // event gets handled AFTER the device goes to sleep and wakes |
| // up again. |
| |
| // This is because it is possible for a sleep command |
| // (executed with the End Call key) to come during the 2 |
| // seconds that the "Call Ended" screen is up. Sleep then |
| // pauses the device (including the cleanup event) and |
| // resumes the event when it wakes up. |
| |
| // To fix this, we introduce a bit of code that pushes the UI |
| // to the background if we pause and see a request to |
| // DELAYED_CLEANUP_AFTER_DISCONNECT. |
| |
| // Note: We can try to finish directly, by: |
| // 1. Removing the DELAYED_CLEANUP_AFTER_DISCONNECT messages |
| // 2. Calling delayedCleanupAfterDisconnect directly |
| |
| // However, doing so can cause problems between the phone |
| // app and the keyguard - the keyguard is trying to sleep at |
| // the same time that the phone state is changing. This can |
| // end up causing the sleep request to be ignored. |
| if (mHandler.hasMessages(DELAYED_CLEANUP_AFTER_DISCONNECT) |
| && mCM.getState() != PhoneConstants.State.RINGING) { |
| if (DBG) log("DELAYED_CLEANUP_AFTER_DISCONNECT detected, moving UI to background."); |
| endInCallScreenSession(); |
| } |
| |
| EventLog.writeEvent(EventLogTags.PHONE_UI_EXIT); |
| |
| // Dismiss any dialogs we may have brought up, just to be 100% |
| // sure they won't still be around when we get back here. |
| dismissAllDialogs(); |
| |
| updateExpandedViewState(); |
| |
| // ...and the in-call notification too: |
| mApp.notificationMgr.updateInCallNotification(); |
| // ...and *always* reset the system bar back to its normal state |
| // when leaving the in-call UI. |
| // (While we're the foreground activity, we disable navigation in |
| // some call states; see InCallTouchUi.updateState().) |
| mApp.notificationMgr.statusBarHelper.enableSystemBarNavigation(true); |
| |
| // Unregister for broadcast intents. (These affect the visible UI |
| // of the InCallScreen, so we only care about them while we're in the |
| // foreground.) |
| unregisterReceiver(mReceiver); |
| |
| // Make sure we revert the poke lock and wake lock when we move to |
| // the background. |
| mApp.updateWakeState(); |
| |
| // clear the dismiss keyguard flag so we are back to the default state |
| // when we next resume |
| updateKeyguardPolicy(false); |
| |
| // See also PhoneApp#updatePhoneState(), which takes care of all the other release() calls. |
| if (mApp.getUpdateLock().isHeld() && mApp.getPhoneState() == PhoneConstants.State.IDLE) { |
| if (DBG) { |
| log("Release UpdateLock on onPause() because there's no active phone call."); |
| } |
| mApp.getUpdateLock().release(); |
| } |
| } |
| |
| @Override |
| protected void onStop() { |
| if (DBG) log("onStop()..."); |
| super.onStop(); |
| |
| stopTimer(); |
| |
| PhoneConstants.State state = mCM.getState(); |
| if (DBG) log("onStop: state = " + state); |
| |
| if (state == PhoneConstants.State.IDLE) { |
| if (mRespondViaSmsManager.isShowingPopup()) { |
| // This means that the user has been opening the "Respond via SMS" dialog even |
| // after the incoming call hanging up, and the screen finally went background. |
| // In that case we just close the dialog and exit the whole in-call screen. |
| mRespondViaSmsManager.dismissPopup(); |
| } |
| |
| // when OTA Activation, OTA Success/Failure dialog or OTA SPC |
| // failure dialog is running, do not destroy inCallScreen. Because call |
| // is already ended and dialog will not get redrawn on slider event. |
| if ((mApp.cdmaOtaProvisionData != null) && (mApp.cdmaOtaScreenState != null) |
| && ((mApp.cdmaOtaScreenState.otaScreenState != |
| CdmaOtaScreenState.OtaScreenState.OTA_STATUS_ACTIVATION) |
| && (mApp.cdmaOtaScreenState.otaScreenState != |
| CdmaOtaScreenState.OtaScreenState.OTA_STATUS_SUCCESS_FAILURE_DLG) |
| && (!mApp.cdmaOtaProvisionData.inOtaSpcState))) { |
| // we don't want the call screen to remain in the activity history |
| // if there are not active or ringing calls. |
| if (DBG) log("- onStop: calling finish() to clear activity history..."); |
| moveTaskToBack(true); |
| if (mApp.otaUtils != null) { |
| mApp.otaUtils.cleanOtaScreen(true); |
| } |
| } |
| } |
| } |
| |
| @Override |
| protected void onDestroy() { |
| Log.i(LOG_TAG, "onDestroy()... this = " + this); |
| super.onDestroy(); |
| |
| // Set the magic flag that tells us NOT to handle any handler |
| // messages that come in asynchronously after we get destroyed. |
| mIsDestroyed = true; |
| |
| mApp.setInCallScreenInstance(null); |
| |
| // Clear out the InCallScreen references in various helper objects |
| // (to let them know we've been destroyed). |
| if (mCallCard != null) { |
| mCallCard.setInCallScreenInstance(null); |
| } |
| if (mInCallTouchUi != null) { |
| mInCallTouchUi.setInCallScreenInstance(null); |
| } |
| mRespondViaSmsManager.setInCallScreenInstance(null); |
| |
| mDialer.clearInCallScreenReference(); |
| mDialer = null; |
| |
| unregisterForPhoneStates(); |
| // No need to change wake state here; that happens in onPause() when we |
| // are moving out of the foreground. |
| |
| if (mBluetoothHeadset != null) { |
| mBluetoothAdapter.closeProfileProxy(BluetoothProfile.HEADSET, mBluetoothHeadset); |
| mBluetoothHeadset = null; |
| } |
| |
| // Dismiss all dialogs, to be absolutely sure we won't leak any of |
| // them while changing orientation. |
| dismissAllDialogs(); |
| |
| // If there's an OtaUtils instance around, clear out its |
| // references to our internal widgets. |
| if (mApp.otaUtils != null) { |
| mApp.otaUtils.clearUiWidgets(); |
| } |
| } |
| |
| /** |
| * Dismisses the in-call screen. |
| * |
| * We never *really* finish() the InCallScreen, since we don't want to |
| * get destroyed and then have to be re-created from scratch for the |
| * next call. Instead, we just move ourselves to the back of the |
| * activity stack. |
| * |
| * This also means that we'll no longer be reachable via the BACK |
| * button (since moveTaskToBack() puts us behind the Home app, but the |
| * home app doesn't allow the BACK key to move you any farther down in |
| * the history stack.) |
| * |
| * (Since the Phone app itself is never killed, this basically means |
| * that we'll keep a single InCallScreen instance around for the |
| * entire uptime of the device. This noticeably improves the UI |
| * responsiveness for incoming calls.) |
| */ |
| @Override |
| public void finish() { |
| if (DBG) log("finish()..."); |
| moveTaskToBack(true); |
| } |
| |
| /** |
| * End the current in call screen session. |
| * |
| * This must be called when an InCallScreen session has |
| * complete so that the next invocation via an onResume will |
| * not be in an old state. |
| */ |
| public void endInCallScreenSession() { |
| if (DBG) log("endInCallScreenSession()... phone state = " + mCM.getState()); |
| endInCallScreenSession(false); |
| } |
| |
| /** |
| * Internal version of endInCallScreenSession(). |
| * |
| * @param forceFinish If true, force the InCallScreen to |
| * truly finish() rather than just calling moveTaskToBack(). |
| * @see finish() |
| */ |
| private void endInCallScreenSession(boolean forceFinish) { |
| if (DBG) { |
| log("endInCallScreenSession(" + forceFinish + ")... phone state = " + mCM.getState()); |
| } |
| if (forceFinish) { |
| Log.i(LOG_TAG, "endInCallScreenSession(): FORCING a call to super.finish()!"); |
| super.finish(); // Call super.finish() rather than our own finish() method, |
| // which actually just calls moveTaskToBack(). |
| } else { |
| moveTaskToBack(true); |
| } |
| setInCallScreenMode(InCallScreenMode.UNDEFINED); |
| |
| // Call update screen so that the in-call screen goes back to a normal state. |
| // This avoids bugs where a previous state will filcker the next time phone is |
| // opened. |
| updateScreen(); |
| } |
| |
| /** |
| * True when this Activity is in foreground (between onResume() and onPause()). |
| */ |
| /* package */ boolean isForegroundActivity() { |
| return mIsForegroundActivity; |
| } |
| |
| /** |
| * Returns true when the Activity is in foreground (between onResume() and onPause()), |
| * or, is in background due to user's bailing out of the screen, not by screen turning off. |
| * |
| * @see #isForegroundActivity() |
| */ |
| /* package */ boolean isForegroundActivityForProximity() { |
| return mIsForegroundActivityForProximity; |
| } |
| |
| /* package */ void updateKeyguardPolicy(boolean dismissKeyguard) { |
| if (dismissKeyguard) { |
| getWindow().addFlags(WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD); |
| } else { |
| getWindow().clearFlags(WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD); |
| } |
| } |
| |
| private void registerForPhoneStates() { |
| if (!mRegisteredForPhoneStates) { |
| mCM.registerForPreciseCallStateChanged(mHandler, PHONE_STATE_CHANGED, null); |
| mCM.registerForDisconnect(mHandler, PHONE_DISCONNECT, null); |
| // TODO: sort out MMI code (probably we should remove this method entirely). |
| // See also MMI handling code in onResume() |
| // mCM.registerForMmiInitiate(mHandler, PhoneApp.MMI_INITIATE, null); |
| |
| // register for the MMI complete message. Upon completion, |
| // PhoneUtils will bring up a system dialog instead of the |
| // message display class in PhoneUtils.displayMMIComplete(). |
| // We'll listen for that message too, so that we can finish |
| // the activity at the same time. |
| mCM.registerForMmiComplete(mHandler, PhoneGlobals.MMI_COMPLETE, null); |
| mCM.registerForCallWaiting(mHandler, PHONE_CDMA_CALL_WAITING, null); |
| mCM.registerForPostDialCharacter(mHandler, POST_ON_DIAL_CHARS, null); |
| mCM.registerForSuppServiceFailed(mHandler, SUPP_SERVICE_FAILED, null); |
| mCM.registerForIncomingRing(mHandler, PHONE_INCOMING_RING, null); |
| mCM.registerForNewRingingConnection(mHandler, PHONE_NEW_RINGING_CONNECTION, null); |
| mRegisteredForPhoneStates = true; |
| } |
| } |
| |
| private void unregisterForPhoneStates() { |
| mCM.unregisterForPreciseCallStateChanged(mHandler); |
| mCM.unregisterForDisconnect(mHandler); |
| mCM.unregisterForMmiInitiate(mHandler); |
| mCM.unregisterForMmiComplete(mHandler); |
| mCM.unregisterForCallWaiting(mHandler); |
| mCM.unregisterForPostDialCharacter(mHandler); |
| mCM.unregisterForSuppServiceFailed(mHandler); |
| mCM.unregisterForIncomingRing(mHandler); |
| mCM.unregisterForNewRingingConnection(mHandler); |
| mRegisteredForPhoneStates = false; |
| } |
| |
| /* package */ void updateAfterRadioTechnologyChange() { |
| if (DBG) Log.d(LOG_TAG, "updateAfterRadioTechnologyChange()..."); |
| |
| // Reset the call screen since the calls cannot be transferred |
| // across radio technologies. |
| resetInCallScreenMode(); |
| |
| // Unregister for all events from the old obsolete phone |
| unregisterForPhoneStates(); |
| |
| // (Re)register for all events relevant to the new active phone |
| registerForPhoneStates(); |
| |
| // And finally, refresh the onscreen UI. (Note that it's safe |
| // to call requestUpdateScreen() even if the radio change ended up |
| // causing us to exit the InCallScreen.) |
| requestUpdateScreen(); |
| } |
| |
| @Override |
| protected void onNewIntent(Intent intent) { |
| log("onNewIntent: intent = " + intent + ", phone state = " + mCM.getState()); |
| |
| // We're being re-launched with a new Intent. Since it's possible for a |
| // single InCallScreen instance to persist indefinitely (even if we |
| // finish() ourselves), this sequence can potentially happen any time |
| // the InCallScreen needs to be displayed. |
| |
| // Stash away the new intent so that we can get it in the future |
| // by calling getIntent(). (Otherwise getIntent() will return the |
| // original Intent from when we first got created!) |
| setIntent(intent); |
| |
| // Activities are always paused before receiving a new intent, so |
| // we can count on our onResume() method being called next. |
| |
| // Just like in onCreate(), handle the intent. |
| internalResolveIntent(intent); |
| } |
| |
| private void internalResolveIntent(Intent intent) { |
| if (intent == null || intent.getAction() == null) { |
| return; |
| } |
| String action = intent.getAction(); |
| if (DBG) log("internalResolveIntent: action=" + action); |
| |
| // In gingerbread and earlier releases, the InCallScreen used to |
| // directly handle certain intent actions that could initiate phone |
| // calls, namely ACTION_CALL and ACTION_CALL_EMERGENCY, and also |
| // OtaUtils.ACTION_PERFORM_CDMA_PROVISIONING. |
| // |
| // But it doesn't make sense to tie those actions to the InCallScreen |
| // (or especially to the *activity lifecycle* of the InCallScreen). |
| // Instead, the InCallScreen should only be concerned with running the |
| // onscreen UI while in a call. So we've now offloaded the call-control |
| // functionality to a new module called CallController, and OTASP calls |
| // are now launched from the OtaUtils startInteractiveOtasp() or |
| // startNonInteractiveOtasp() methods. |
| // |
| // So now, the InCallScreen is only ever launched using the ACTION_MAIN |
| // action, and (upon launch) performs no functionality other than |
| // displaying the UI in a state that matches the current telephony |
| // state. |
| |
| if (action.equals(intent.ACTION_MAIN)) { |
| // This action is the normal way to bring up the in-call UI. |
| // |
| // Most of the interesting work of updating the onscreen UI (to |
| // match the current telephony state) happens in the |
| // syncWithPhoneState() => updateScreen() sequence that happens in |
| // onResume(). |
| // |
| // But we do check here for one extra that can come along with the |
| // ACTION_MAIN intent: |
| |
| if (intent.hasExtra(SHOW_DIALPAD_EXTRA)) { |
| // SHOW_DIALPAD_EXTRA can be used here to specify whether the DTMF |
| // dialpad should be initially visible. If the extra isn't |
| // present at all, we just leave the dialpad in its previous state. |
| |
| boolean showDialpad = intent.getBooleanExtra(SHOW_DIALPAD_EXTRA, false); |
| if (VDBG) log("- internalResolveIntent: SHOW_DIALPAD_EXTRA: " + showDialpad); |
| |
| // If SHOW_DIALPAD_EXTRA is specified, that overrides whatever |
| // the previous state of inCallUiState.showDialpad was. |
| mApp.inCallUiState.showDialpad = showDialpad; |
| |
| final boolean hasActiveCall = mCM.hasActiveFgCall(); |
| final boolean hasHoldingCall = mCM.hasActiveBgCall(); |
| |
| // There's only one line in use, AND it's on hold, at which we're sure the user |
| // wants to use the dialpad toward the exact line, so un-hold the holding line. |
| if (showDialpad && !hasActiveCall && hasHoldingCall) { |
| PhoneUtils.switchHoldingAndActive(mCM.getFirstActiveBgCall()); |
| } |
| } |
| // ...and in onResume() we'll update the onscreen dialpad state to |
| // match the InCallUiState. |
| |
| return; |
| } |
| |
| if (action.equals(OtaUtils.ACTION_DISPLAY_ACTIVATION_SCREEN)) { |
| // Bring up the in-call UI in the OTASP-specific "activate" state; |
| // see OtaUtils.startInteractiveOtasp(). Note that at this point |
| // the OTASP call has not been started yet; we won't actually make |
| // the call until the user presses the "Activate" button. |
| |
| if (!TelephonyCapabilities.supportsOtasp(mPhone)) { |
| throw new IllegalStateException( |
| "Received ACTION_DISPLAY_ACTIVATION_SCREEN intent on non-OTASP-capable device: " |
| + intent); |
| } |
| |
| setInCallScreenMode(InCallScreenMode.OTA_NORMAL); |
| if ((mApp.cdmaOtaProvisionData != null) |
| && (!mApp.cdmaOtaProvisionData.isOtaCallIntentProcessed)) { |
| mApp.cdmaOtaProvisionData.isOtaCallIntentProcessed = true; |
| mApp.cdmaOtaScreenState.otaScreenState = |
| CdmaOtaScreenState.OtaScreenState.OTA_STATUS_ACTIVATION; |
| } |
| return; |
| } |
| |
| // Various intent actions that should no longer come here directly: |
| if (action.equals(OtaUtils.ACTION_PERFORM_CDMA_PROVISIONING)) { |
| // This intent is now handled by the InCallScreenShowActivation |
| // activity, which translates it into a call to |
| // OtaUtils.startInteractiveOtasp(). |
| throw new IllegalStateException( |
| "Unexpected ACTION_PERFORM_CDMA_PROVISIONING received by InCallScreen: " |
| + intent); |
| } else if (action.equals(Intent.ACTION_CALL) |
| || action.equals(Intent.ACTION_CALL_EMERGENCY)) { |
| // ACTION_CALL* intents go to the OutgoingCallBroadcaster, which now |
| // translates them into CallController.placeCall() calls rather than |
| // launching the InCallScreen directly. |
| throw new IllegalStateException("Unexpected CALL action received by InCallScreen: " |
| + intent); |
| } else if (action.equals(ACTION_UNDEFINED)) { |
| // This action is only used for internal bookkeeping; we should |
| // never actually get launched with it. |
| Log.wtf(LOG_TAG, "internalResolveIntent: got launched with ACTION_UNDEFINED"); |
| return; |
| } else { |
| Log.wtf(LOG_TAG, "internalResolveIntent: unexpected intent action: " + action); |
| // But continue the best we can (basically treating this case |
| // like ACTION_MAIN...) |
| return; |
| } |
| } |
| |
| private void stopTimer() { |
| if (mCallCard != null) mCallCard.stopTimer(); |
| } |
| |
| private void initInCallScreen() { |
| if (VDBG) log("initInCallScreen()..."); |
| |
| // Have the WindowManager filter out touch events that are "too fat". |
| getWindow().addFlags(WindowManager.LayoutParams.FLAG_IGNORE_CHEEK_PRESSES); |
| |
| // Initialize the CallCard. |
| mCallCard = (CallCard) findViewById(R.id.callCard); |
| if (VDBG) log(" - mCallCard = " + mCallCard); |
| mCallCard.setInCallScreenInstance(this); |
| |
| // Initialize the onscreen UI elements. |
| initInCallTouchUi(); |
| |
| // Helper class to keep track of enabledness/state of UI controls |
| mInCallControlState = new InCallControlState(this, mCM); |
| |
| // Helper class to run the "Manage conference" UI |
| mManageConferenceUtils = new ManageConferenceUtils(this, mCM); |
| |
| // The DTMF Dialpad. |
| ViewStub stub = (ViewStub) findViewById(R.id.dtmf_twelve_key_dialer_stub); |
| mDialer = new DTMFTwelveKeyDialer(this, stub); |
| mPowerManager = (PowerManager) getSystemService(Context.POWER_SERVICE); |
| } |
| |
| /** |
| * Returns true if the phone is "in use", meaning that at least one line |
| * is active (ie. off hook or ringing or dialing). Conversely, a return |
| * value of false means there's currently no phone activity at all. |
| */ |
| private boolean phoneIsInUse() { |
| return mCM.getState() != PhoneConstants.State.IDLE; |
| } |
| |
| private boolean handleDialerKeyDown(int keyCode, KeyEvent event) { |
| if (VDBG) log("handleDialerKeyDown: keyCode " + keyCode + ", event " + event + "..."); |
| |
| // As soon as the user starts typing valid dialable keys on the |
| // keyboard (presumably to type DTMF tones) we start passing the |
| // key events to the DTMFDialer's onDialerKeyDown. We do so |
| // only if the okToDialDTMFTones() conditions pass. |
| if (okToDialDTMFTones()) { |
| return mDialer.onDialerKeyDown(event); |
| |
| // TODO: If the dialpad isn't currently visible, maybe |
| // consider automatically bringing it up right now? |
| // (Just to make sure the user sees the digits widget...) |
| // But this probably isn't too critical since it's awkward to |
| // use the hard keyboard while in-call in the first place, |
| // especially now that the in-call UI is portrait-only... |
| } |
| |
| return false; |
| } |
| |
| @Override |
| public void onBackPressed() { |
| if (DBG) log("onBackPressed()..."); |
| |
| // To consume this BACK press, the code here should just do |
| // something and return. Otherwise, call super.onBackPressed() to |
| // get the default implementation (which simply finishes the |
| // current activity.) |
| |
| if (mCM.hasActiveRingingCall()) { |
| // The Back key, just like the Home key, is always disabled |
| // while an incoming call is ringing. (The user *must* either |
| // answer or reject the call before leaving the incoming-call |
| // screen.) |
| if (DBG) log("BACK key while ringing: ignored"); |
| |
| // And consume this event; *don't* call super.onBackPressed(). |
| return; |
| } |
| |
| // BACK is also used to exit out of any "special modes" of the |
| // in-call UI: |
| |
| if (mDialer.isOpened()) { |
| closeDialpadInternal(true); // do the "closing" animation |
| return; |
| } |
| |
| if (mApp.inCallUiState.inCallScreenMode == InCallScreenMode.MANAGE_CONFERENCE) { |
| // Hide the Manage Conference panel, return to NORMAL mode. |
| setInCallScreenMode(InCallScreenMode.NORMAL); |
| requestUpdateScreen(); |
| return; |
| } |
| |
| // Nothing special to do. Fall back to the default behavior. |
| super.onBackPressed(); |
| } |
| |
| /** |
| * Handles the green CALL key while in-call. |
| * @return true if we consumed the event. |
| */ |
| private boolean handleCallKey() { |
| // The green CALL button means either "Answer", "Unhold", or |
| // "Swap calls", or can be a no-op, depending on the current state |
| // of the Phone. |
| |
| final boolean hasRingingCall = mCM.hasActiveRingingCall(); |
| final boolean hasActiveCall = mCM.hasActiveFgCall(); |
| final boolean hasHoldingCall = mCM.hasActiveBgCall(); |
| |
| int phoneType = mPhone.getPhoneType(); |
| if (phoneType == PhoneConstants.PHONE_TYPE_CDMA) { |
| // The green CALL button means either "Answer", "Swap calls/On Hold", or |
| // "Add to 3WC", depending on the current state of the Phone. |
| |
| CdmaPhoneCallState.PhoneCallState currCallState = |
| mApp.cdmaPhoneCallState.getCurrentCallState(); |
| if (hasRingingCall) { |
| //Scenario 1: Accepting the First Incoming and Call Waiting call |
| if (DBG) log("answerCall: First Incoming and Call Waiting scenario"); |
| internalAnswerCall(); // Automatically holds the current active call, |
| // if there is one |
| } else if ((currCallState == CdmaPhoneCallState.PhoneCallState.THRWAY_ACTIVE) |
| && (hasActiveCall)) { |
| //Scenario 2: Merging 3Way calls |
| if (DBG) log("answerCall: Merge 3-way call scenario"); |
| // Merge calls |
| PhoneUtils.mergeCalls(mCM); |
| } else if (currCallState == CdmaPhoneCallState.PhoneCallState.CONF_CALL) { |
| //Scenario 3: Switching between two Call waiting calls or drop the latest |
| // connection if in a 3Way merge scenario |
| if (DBG) log("answerCall: Switch btwn 2 calls scenario"); |
| internalSwapCalls(); |
| } |
| } else if ((phoneType == PhoneConstants.PHONE_TYPE_GSM) |
| || (phoneType == PhoneConstants.PHONE_TYPE_SIP)) { |
| if (hasRingingCall) { |
| // If an incoming call is ringing, the CALL button is actually |
| // handled by the PhoneWindowManager. (We do this to make |
| // sure that we'll respond to the key even if the InCallScreen |
| // hasn't come to the foreground yet.) |
| // |
| // We'd only ever get here in the extremely rare case that the |
| // incoming call started ringing *after* |
| // PhoneWindowManager.interceptKeyTq() but before the event |
| // got here, or else if the PhoneWindowManager had some |
| // problem connecting to the ITelephony service. |
| Log.w(LOG_TAG, "handleCallKey: incoming call is ringing!" |
| + " (PhoneWindowManager should have handled this key.)"); |
| // But go ahead and handle the key as normal, since the |
| // PhoneWindowManager presumably did NOT handle it: |
| |
| // There's an incoming ringing call: CALL means "Answer". |
| internalAnswerCall(); |
| } else if (hasActiveCall && hasHoldingCall) { |
| // Two lines are in use: CALL means "Swap calls". |
| if (DBG) log("handleCallKey: both lines in use ==> swap calls."); |
| internalSwapCalls(); |
| } else if (hasHoldingCall) { |
| // There's only one line in use, AND it's on hold. |
| // In this case CALL is a shortcut for "unhold". |
| if (DBG) log("handleCallKey: call on hold ==> unhold."); |
| PhoneUtils.switchHoldingAndActive( |
| mCM.getFirstActiveBgCall()); // Really means "unhold" in this state |
| } else { |
| // The most common case: there's only one line in use, and |
| // it's an active call (i.e. it's not on hold.) |
| // In this case CALL is a no-op. |
| // (This used to be a shortcut for "add call", but that was a |
| // bad idea because "Add call" is so infrequently-used, and |
| // because the user experience is pretty confusing if you |
| // inadvertently trigger it.) |
| if (VDBG) log("handleCallKey: call in foregound ==> ignoring."); |
| // But note we still consume this key event; see below. |
| } |
| } else { |
| throw new IllegalStateException("Unexpected phone type: " + phoneType); |
| } |
| |
| // We *always* consume the CALL key, since the system-wide default |
| // action ("go to the in-call screen") is useless here. |
| return true; |
| } |
| |
| boolean isKeyEventAcceptableDTMF (KeyEvent event) { |
| return (mDialer != null && mDialer.isKeyEventAcceptable(event)); |
| } |
| |
| /** |
| * Overriden to track relevant focus changes. |
| * |
| * If a key is down and some time later the focus changes, we may |
| * NOT recieve the keyup event; logically the keyup event has not |
| * occured in this window. This issue is fixed by treating a focus |
| * changed event as an interruption to the keydown, making sure |
| * that any code that needs to be run in onKeyUp is ALSO run here. |
| */ |
| @Override |
| public void onWindowFocusChanged(boolean hasFocus) { |
| // the dtmf tones should no longer be played |
| if (VDBG) log("onWindowFocusChanged(" + hasFocus + ")..."); |
| if (!hasFocus && mDialer != null) { |
| if (VDBG) log("- onWindowFocusChanged: faking onDialerKeyUp()..."); |
| mDialer.onDialerKeyUp(null); |
| } |
| } |
| |
| @Override |
| public boolean onKeyUp(int keyCode, KeyEvent event) { |
| // if (DBG) log("onKeyUp(keycode " + keyCode + ")..."); |
| |
| // push input to the dialer. |
| if ((mDialer != null) && (mDialer.onDialerKeyUp(event))){ |
| return true; |
| } else if (keyCode == KeyEvent.KEYCODE_CALL) { |
| // Always consume CALL to be sure the PhoneWindow won't do anything with it |
| return true; |
| } |
| return super.onKeyUp(keyCode, event); |
| } |
| |
| @Override |
| public boolean onKeyDown(int keyCode, KeyEvent event) { |
| // if (DBG) log("onKeyDown(keycode " + keyCode + ")..."); |
| |
| switch (keyCode) { |
| case KeyEvent.KEYCODE_CALL: |
| boolean handled = handleCallKey(); |
| if (!handled) { |
| Log.w(LOG_TAG, "InCallScreen should always handle KEYCODE_CALL in onKeyDown"); |
| } |
| // Always consume CALL to be sure the PhoneWindow won't do anything with it |
| return true; |
| |
| // Note there's no KeyEvent.KEYCODE_ENDCALL case here. |
| // The standard system-wide handling of the ENDCALL key |
| // (see PhoneWindowManager's handling of KEYCODE_ENDCALL) |
| // already implements exactly what the UI spec wants, |
| // namely (1) "hang up" if there's a current active call, |
| // or (2) "don't answer" if there's a current ringing call. |
| |
| case KeyEvent.KEYCODE_CAMERA: |
| // Disable the CAMERA button while in-call since it's too |
| // easy to press accidentally. |
| return true; |
| |
| case KeyEvent.KEYCODE_VOLUME_UP: |
| case KeyEvent.KEYCODE_VOLUME_DOWN: |
| case KeyEvent.KEYCODE_VOLUME_MUTE: |
| if (mCM.getState() == PhoneConstants.State.RINGING) { |
| // If an incoming call is ringing, the VOLUME buttons are |
| // actually handled by the PhoneWindowManager. (We do |
| // this to make sure that we'll respond to them even if |
| // the InCallScreen hasn't come to the foreground yet.) |
| // |
| // We'd only ever get here in the extremely rare case that the |
| // incoming call started ringing *after* |
| // PhoneWindowManager.interceptKeyTq() but before the event |
| // got here, or else if the PhoneWindowManager had some |
| // problem connecting to the ITelephony service. |
| Log.w(LOG_TAG, "VOLUME key: incoming call is ringing!" |
| + " (PhoneWindowManager should have handled this key.)"); |
| // But go ahead and handle the key as normal, since the |
| // PhoneWindowManager presumably did NOT handle it: |
| internalSilenceRinger(); |
| |
| // As long as an incoming call is ringing, we always |
| // consume the VOLUME keys. |
| return true; |
| } |
| break; |
| |
| case KeyEvent.KEYCODE_MUTE: |
| onMuteClick(); |
| return true; |
| |
| // Various testing/debugging features, enabled ONLY when VDBG == true. |
| case KeyEvent.KEYCODE_SLASH: |
| if (VDBG) { |
| log("----------- InCallScreen View dump --------------"); |
| // Dump starting from the top-level view of the entire activity: |
| Window w = this.getWindow(); |
| View decorView = w.getDecorView(); |
| decorView.debug(); |
| return true; |
| } |
| break; |
| case KeyEvent.KEYCODE_EQUALS: |
| if (VDBG) { |
| log("----------- InCallScreen call state dump --------------"); |
| PhoneUtils.dumpCallState(mPhone); |
| PhoneUtils.dumpCallManager(); |
| return true; |
| } |
| break; |
| case KeyEvent.KEYCODE_GRAVE: |
| if (VDBG) { |
| // Placeholder for other misc temp testing |
| log("------------ Temp testing -----------------"); |
| return true; |
| } |
| break; |
| } |
| |
| if (event.getRepeatCount() == 0 && handleDialerKeyDown(keyCode, event)) { |
| return true; |
| } |
| |
| return super.onKeyDown(keyCode, event); |
| } |
| |
| /** |
| * Handle a failure notification for a supplementary service |
| * (i.e. conference, switch, separate, transfer, etc.). |
| */ |
| void onSuppServiceFailed(AsyncResult r) { |
| Phone.SuppService service = (Phone.SuppService) r.result; |
| if (DBG) log("onSuppServiceFailed: " + service); |
| |
| int errorMessageResId; |
| switch (service) { |
| case SWITCH: |
| // Attempt to switch foreground and background/incoming calls failed |
| // ("Failed to switch calls") |
| errorMessageResId = R.string.incall_error_supp_service_switch; |
| break; |
| |
| case SEPARATE: |
| // Attempt to separate a call from a conference call |
| // failed ("Failed to separate out call") |
| errorMessageResId = R.string.incall_error_supp_service_separate; |
| break; |
| |
| case TRANSFER: |
| // Attempt to connect foreground and background calls to |
| // each other (and hanging up user's line) failed ("Call |
| // transfer failed") |
| errorMessageResId = R.string.incall_error_supp_service_transfer; |
| break; |
| |
| case CONFERENCE: |
| // Attempt to add a call to conference call failed |
| // ("Conference call failed") |
| errorMessageResId = R.string.incall_error_supp_service_conference; |
| break; |
| |
| case REJECT: |
| // Attempt to reject an incoming call failed |
| // ("Call rejection failed") |
| errorMessageResId = R.string.incall_error_supp_service_reject; |
| break; |
| |
| case HANGUP: |
| // Attempt to release a call failed ("Failed to release call(s)") |
| errorMessageResId = R.string.incall_error_supp_service_hangup; |
| break; |
| |
| case UNKNOWN: |
| default: |
| // Attempt to use a service we don't recognize or support |
| // ("Unsupported service" or "Selected service failed") |
| errorMessageResId = R.string.incall_error_supp_service_unknown; |
| break; |
| } |
| |
| // mSuppServiceFailureDialog is a generic dialog used for any |
| // supp service failure, and there's only ever have one |
| // instance at a time. So just in case a previous dialog is |
| // still around, dismiss it. |
| if (mSuppServiceFailureDialog != null) { |
| if (DBG) log("- DISMISSING mSuppServiceFailureDialog."); |
| mSuppServiceFailureDialog.dismiss(); // It's safe to dismiss() a dialog |
| // that's already dismissed. |
| mSuppServiceFailureDialog = null; |
| } |
| |
| mSuppServiceFailureDialog = new AlertDialog.Builder(this) |
| .setMessage(errorMessageResId) |
| .setPositiveButton(R.string.ok, null) |
| .create(); |
| mSuppServiceFailureDialog.getWindow().addFlags( |
| WindowManager.LayoutParams.FLAG_BLUR_BEHIND); |
| mSuppServiceFailureDialog.show(); |
| } |
| |
| /** |
| * Something has changed in the phone's state. Update the UI. |
| */ |
| private void onPhoneStateChanged(AsyncResult r) { |
| PhoneConstants.State state = mCM.getState(); |
| if (DBG) log("onPhoneStateChanged: current state = " + state); |
| |
| // There's nothing to do here if we're not the foreground activity. |
| // (When we *do* eventually come to the foreground, we'll do a |
| // full update then.) |
| if (!mIsForegroundActivity) { |
| if (DBG) log("onPhoneStateChanged: Activity not in foreground! Bailing out..."); |
| return; |
| } |
| |
| updateExpandedViewState(); |
| |
| // Update the onscreen UI. |
| // We use requestUpdateScreen() here (which posts a handler message) |
| // instead of calling updateScreen() directly, which allows us to avoid |
| // unnecessary work if multiple onPhoneStateChanged() events come in all |
| // at the same time. |
| |
| requestUpdateScreen(); |
| |
| // Make sure we update the poke lock and wake lock when certain |
| // phone state changes occur. |
| mApp.updateWakeState(); |
| } |
| |
| /** |
| * Updates the UI after a phone connection is disconnected, as follows: |
| * |
| * - If this was a missed or rejected incoming call, and no other |
| * calls are active, dismiss the in-call UI immediately. (The |
| * CallNotifier will still create a "missed call" notification if |
| * necessary.) |
| * |
| * - With any other disconnect cause, if the phone is now totally |
| * idle, display the "Call ended" state for a couple of seconds. |
| * |
| * - Or, if the phone is still in use, stay on the in-call screen |
| * (and update the UI to reflect the current state of the Phone.) |
| * |
| * @param r r.result contains the connection that just ended |
| */ |
| private void onDisconnect(AsyncResult r) { |
| Connection c = (Connection) r.result; |
| Connection.DisconnectCause cause = c.getDisconnectCause(); |
| if (DBG) log("onDisconnect: connection '" + c + "', cause = " + cause |
| + ", showing screen: " + mApp.isShowingCallScreen()); |
| |
| boolean currentlyIdle = !phoneIsInUse(); |
| int autoretrySetting = AUTO_RETRY_OFF; |
| boolean phoneIsCdma = (mPhone.getPhoneType() == PhoneConstants.PHONE_TYPE_CDMA); |
| if (phoneIsCdma) { |
| // Get the Auto-retry setting only if Phone State is IDLE, |
| // else let it stay as AUTO_RETRY_OFF |
| if (currentlyIdle) { |
| autoretrySetting = android.provider.Settings.Global.getInt(mPhone.getContext(). |
| getContentResolver(), android.provider.Settings.Global.CALL_AUTO_RETRY, 0); |
| } |
| } |
| |
| // for OTA Call, only if in OTA NORMAL mode, handle OTA END scenario |
| if ((mApp.inCallUiState.inCallScreenMode == InCallScreenMode.OTA_NORMAL) |
| && ((mApp.cdmaOtaProvisionData != null) |
| && (!mApp.cdmaOtaProvisionData.inOtaSpcState))) { |
| setInCallScreenMode(InCallScreenMode.OTA_ENDED); |
| updateScreen(); |
| return; |
| } else if ((mApp.inCallUiState.inCallScreenMode == InCallScreenMode.OTA_ENDED) |
| || ((mApp.cdmaOtaProvisionData != null) |
| && mApp.cdmaOtaProvisionData.inOtaSpcState)) { |
| if (DBG) log("onDisconnect: OTA Call end already handled"); |
| return; |
| } |
| |
| // Any time a call disconnects, clear out the "history" of DTMF |
| // digits you typed (to make sure it doesn't persist from one call |
| // to the next.) |
| mDialer.clearDigits(); |
| |
| // Under certain call disconnected states, we want to alert the user |
| // with a dialog instead of going through the normal disconnect |
| // routine. |
| if (cause == Connection.DisconnectCause.CALL_BARRED) { |
| showGenericErrorDialog(R.string.callFailed_cb_enabled, false); |
| return; |
| } else if (cause == Connection.DisconnectCause.FDN_BLOCKED) { |
| showGenericErrorDialog(R.string.callFailed_fdn_only, false); |
| return; |
| } else if (cause == Connection.DisconnectCause.CS_RESTRICTED) { |
| showGenericErrorDialog(R.string.callFailed_dsac_restricted, false); |
| return; |
| } else if (cause == Connection.DisconnectCause.CS_RESTRICTED_EMERGENCY) { |
| showGenericErrorDialog(R.string.callFailed_dsac_restricted_emergency, false); |
| return; |
| } else if (cause == Connection.DisconnectCause.CS_RESTRICTED_NORMAL) { |
| showGenericErrorDialog(R.string.callFailed_dsac_restricted_normal, false); |
| return; |
| } |
| |
| if (phoneIsCdma) { |
| Call.State callState = mApp.notifier.getPreviousCdmaCallState(); |
| if ((callState == Call.State.ACTIVE) |
| && (cause != Connection.DisconnectCause.INCOMING_MISSED) |
| && (cause != Connection.DisconnectCause.NORMAL) |
| && (cause != Connection.DisconnectCause.LOCAL) |
| && (cause != Connection.DisconnectCause.INCOMING_REJECTED)) { |
| showCallLostDialog(); |
| } else if ((callState == Call.State.DIALING || callState == Call.State.ALERTING) |
| && (cause != Connection.DisconnectCause.INCOMING_MISSED) |
| && (cause != Connection.DisconnectCause.NORMAL) |
| && (cause != Connection.DisconnectCause.LOCAL) |
| && (cause != Connection.DisconnectCause.INCOMING_REJECTED)) { |
| |
| if (mApp.inCallUiState.needToShowCallLostDialog) { |
| // Show the dialog now since the call that just failed was a retry. |
| showCallLostDialog(); |
| mApp.inCallUiState.needToShowCallLostDialog = false; |
| } else { |
| if (autoretrySetting == AUTO_RETRY_OFF) { |
| // Show the dialog for failed call if Auto Retry is OFF in Settings. |
| showCallLostDialog(); |
| mApp.inCallUiState.needToShowCallLostDialog = false; |
| } else { |
| // Set the needToShowCallLostDialog flag now, so we'll know to show |
| // the dialog if *this* call fails. |
| mApp.inCallUiState.needToShowCallLostDialog = true; |
| } |
| } |
| } |
| } |
| |
| // Explicitly clean up up any DISCONNECTED connections |
| // in a conference call. |
| // [Background: Even after a connection gets disconnected, its |
| // Connection object still stays around for a few seconds, in the |
| // DISCONNECTED state. With regular calls, this state drives the |
| // "call ended" UI. But when a single person disconnects from a |
| // conference call there's no "call ended" state at all; in that |
| // case we blow away any DISCONNECTED connections right now to make sure |
| // the UI updates instantly to reflect the current state.] |
| final Call call = c.getCall(); |
| if (call != null) { |
| // We only care about situation of a single caller |
| // disconnecting from a conference call. In that case, the |
| // call will have more than one Connection (including the one |
| // that just disconnected, which will be in the DISCONNECTED |
| // state) *and* at least one ACTIVE connection. (If the Call |
| // has *no* ACTIVE connections, that means that the entire |
| // conference call just ended, so we *do* want to show the |
| // "Call ended" state.) |
| List<Connection> connections = call.getConnections(); |
| if (connections != null && connections.size() > 1) { |
| for (Connection conn : connections) { |
| if (conn.getState() == Call.State.ACTIVE) { |
| // This call still has at least one ACTIVE connection! |
| // So blow away any DISCONNECTED connections |
| // (including, presumably, the one that just |
| // disconnected from this conference call.) |
| |
| // We also force the wake state to refresh, just in |
| // case the disconnected connections are removed |
| // before the phone state change. |
| if (VDBG) log("- Still-active conf call; clearing DISCONNECTED..."); |
| mApp.updateWakeState(); |
| mCM.clearDisconnected(); // This happens synchronously. |
| break; |
| } |
| } |
| } |
| } |
| |
| // Note: see CallNotifier.onDisconnect() for some other behavior |
| // that might be triggered by a disconnect event, like playing the |
| // busy/congestion tone. |
| |
| // Stash away some info about the call that just disconnected. |
| // (This might affect what happens after we exit the InCallScreen; see |
| // delayedCleanupAfterDisconnect().) |
| // TODO: rather than stashing this away now and then reading it in |
| // delayedCleanupAfterDisconnect(), it would be cleaner to just pass |
| // this as an argument to delayedCleanupAfterDisconnect() (if we call |
| // it directly) or else pass it as a Message argument when we post the |
| // DELAYED_CLEANUP_AFTER_DISCONNECT message. |
| mLastDisconnectCause = cause; |
| |
| // We bail out immediately (and *don't* display the "call ended" |
| // state at all) if this was an incoming call. |
| boolean bailOutImmediately = |
| ((cause == Connection.DisconnectCause.INCOMING_MISSED) |
| || (cause == Connection.DisconnectCause.INCOMING_REJECTED)) |
| && currentlyIdle; |
| |
| boolean showingQuickResponseDialog = |
| mRespondViaSmsManager != null && mRespondViaSmsManager.isShowingPopup(); |
| |
| // Note: we also do some special handling for the case when a call |
| // disconnects with cause==OUT_OF_SERVICE while making an |
| // emergency call from airplane mode. That's handled by |
| // EmergencyCallHelper.onDisconnect(). |
| |
| if (bailOutImmediately && showingQuickResponseDialog) { |
| if (DBG) log("- onDisconnect: Respond-via-SMS dialog is still being displayed..."); |
| |
| // Do *not* exit the in-call UI yet! |
| // If the call was an incoming call that was missed *and* the user is using |
| // quick response screen, we keep showing the screen for a moment, assuming the |
| // user wants to reply the call anyway. |
| // |
| // For this case, we will exit the screen when: |
| // - the message is sent (RespondViaSmsManager) |
| // - the message is canceled (RespondViaSmsManager), or |
| // - when the whole in-call UI becomes background (onPause()) |
| } else if (bailOutImmediately) { |
| if (DBG) log("- onDisconnect: bailOutImmediately..."); |
| |
| // Exit the in-call UI! |
| // (This is basically the same "delayed cleanup" we do below, |
| // just with zero delay. Since the Phone is currently idle, |
| // this call is guaranteed to immediately finish this activity.) |
| delayedCleanupAfterDisconnect(); |
| } else { |
| if (DBG) log("- onDisconnect: delayed bailout..."); |
| // Stay on the in-call screen for now. (Either the phone is |
| // still in use, or the phone is idle but we want to display |
| // the "call ended" state for a couple of seconds.) |
| |
| // Switch to the special "Call ended" state when the phone is idle |
| // but there's still a call in the DISCONNECTED state: |
| if (currentlyIdle |
| && (mCM.hasDisconnectedFgCall() || mCM.hasDisconnectedBgCall())) { |
| if (DBG) log("- onDisconnect: switching to 'Call ended' state..."); |
| setInCallScreenMode(InCallScreenMode.CALL_ENDED); |
| } |
| |
| // Force a UI update in case we need to display anything |
| // special based on this connection's DisconnectCause |
| // (see CallCard.getCallFailedString()). |
| updateScreen(); |
| |
| // Some other misc cleanup that we do if the call that just |
| // disconnected was the foreground call. |
| final boolean hasActiveCall = mCM.hasActiveFgCall(); |
| if (!hasActiveCall) { |
| if (DBG) log("- onDisconnect: cleaning up after FG call disconnect..."); |
| |
| // Dismiss any dialogs which are only meaningful for an |
| // active call *and* which become moot if the call ends. |
| if (mWaitPromptDialog != null) { |
| if (VDBG) log("- DISMISSING mWaitPromptDialog."); |
| mWaitPromptDialog.dismiss(); // safe even if already dismissed |
| mWaitPromptDialog = null; |
| } |
| if (mWildPromptDialog != null) { |
| if (VDBG) log("- DISMISSING mWildPromptDialog."); |
| mWildPromptDialog.dismiss(); // safe even if already dismissed |
| mWildPromptDialog = null; |
| } |
| if (mPausePromptDialog != null) { |
| if (DBG) log("- DISMISSING mPausePromptDialog."); |
| mPausePromptDialog.dismiss(); // safe even if already dismissed |
| mPausePromptDialog = null; |
| } |
| } |
| |
| // Updating the screen wake state is done in onPhoneStateChanged(). |
| |
| |
| // CDMA: We only clean up if the Phone state is IDLE as we might receive an |
| // onDisconnect for a Call Collision case (rare but possible). |
| // For Call collision cases i.e. when the user makes an out going call |
| // and at the same time receives an Incoming Call, the Incoming Call is given |
| // higher preference. At this time framework sends a disconnect for the Out going |
| // call connection hence we should *not* bring down the InCallScreen as the Phone |
| // State would be RINGING |
| if (mPhone.getPhoneType() == PhoneConstants.PHONE_TYPE_CDMA) { |
| if (!currentlyIdle) { |
| // Clean up any connections in the DISCONNECTED state. |
| // This is necessary cause in CallCollision the foreground call might have |
| // connections in DISCONNECTED state which needs to be cleared. |
| mCM.clearDisconnected(); |
| |
| // The phone is still in use. Stay here in this activity. |
| // But we don't need to keep the screen on. |
| if (DBG) log("onDisconnect: Call Collision case - staying on InCallScreen."); |
| if (DBG) PhoneUtils.dumpCallState(mPhone); |
| return; |
| } |
| } |
| |
| // This is onDisconnect() request from the last phone call; no available call anymore. |
| // |
| // When the in-call UI is in background *because* the screen is turned off (unlike the |
| // other case where the other activity is being shown), we wake up the screen and |
| // show "DISCONNECTED" state once, with appropriate elapsed time. After showing that |
| // we *must* bail out of the screen again, showing screen lock if needed. |
| // |
| // See also comments for isForegroundActivityForProximity() |
| // |
| // TODO: Consider moving this to CallNotifier. This code assumes the InCallScreen |
| // never gets destroyed. For this exact case, it works (since InCallScreen won't be |
| // destroyed), while technically this isn't right; Activity may be destroyed when |
| // in background. |
| if (currentlyIdle && !isForegroundActivity() && isForegroundActivityForProximity()) { |
| log("Force waking up the screen to let users see \"disconnected\" state"); |
| if (call != null) { |
| mCallCard.updateElapsedTimeWidget(call); |
| } |
| // This variable will be kept true until the next InCallScreen#onPause(), which |
| // forcibly turns it off regardless of the situation (for avoiding unnecessary |
| // confusion around this special case). |
| mApp.inCallUiState.showAlreadyDisconnectedState = true; |
| |
| // Finally request wake-up.. |
| mApp.wakeUpScreen(); |
| |
| // InCallScreen#onResume() will set DELAYED_CLEANUP_AFTER_DISCONNECT message, |
| // so skip the following section. |
| return; |
| } |
| |
| // Finally, arrange for delayedCleanupAfterDisconnect() to get |
| // called after a short interval (during which we display the |
| // "call ended" state.) At that point, if the |
| // Phone is idle, we'll finish out of this activity. |
| final int callEndedDisplayDelay; |
| switch (cause) { |
| // When the local user hanged up the ongoing call, it is ok to dismiss the screen |
| // soon. In other cases, we show the "hung up" screen longer. |
| // |
| // - For expected reasons we will use CALL_ENDED_LONG_DELAY. |
| // -- when the peer hanged up the call |
| // -- when the local user rejects the incoming call during the other ongoing call |
| // (TODO: there may be other cases which should be in this category) |
| // |
| // - For other unexpected reasons, we will use CALL_ENDED_EXTRA_LONG_DELAY, |
| // assuming the local user wants to confirm the disconnect reason. |
| case LOCAL: |
| callEndedDisplayDelay = CALL_ENDED_SHORT_DELAY; |
| break; |
| case NORMAL: |
| case INCOMING_REJECTED: |
| callEndedDisplayDelay = CALL_ENDED_LONG_DELAY; |
| break; |
| default: |
| callEndedDisplayDelay = CALL_ENDED_EXTRA_LONG_DELAY; |
| break; |
| } |
| mHandler.removeMessages(DELAYED_CLEANUP_AFTER_DISCONNECT); |
| mHandler.sendEmptyMessageDelayed(DELAYED_CLEANUP_AFTER_DISCONNECT, |
| callEndedDisplayDelay); |
| } |
| |
| // Remove 3way timer (only meaningful for CDMA) |
| // TODO: this call needs to happen in the CallController, not here. |
| // (It should probably be triggered by the CallNotifier's onDisconnect method.) |
| // mHandler.removeMessages(THREEWAY_CALLERINFO_DISPLAY_DONE); |
| } |
| |
| /** |
| * Brings up the "MMI Started" dialog. |
| */ |
| /* TODO: sort out MMI code (probably we should remove this method entirely). See also |
| MMI handling code in onResume() |
| private void onMMIInitiate(AsyncResult r) { |
| if (VDBG) log("onMMIInitiate()... AsyncResult r = " + r); |
| |
| // Watch out: don't do this if we're not the foreground activity, |
| // mainly since in the Dialog.show() might fail if we don't have a |
| // valid window token any more... |
| // (Note that this exact sequence can happen if you try to start |
| // an MMI code while the radio is off or out of service.) |
| if (!mIsForegroundActivity) { |
| if (VDBG) log("Activity not in foreground! Bailing out..."); |
| return; |
| } |
| |
| // Also, if any other dialog is up right now (presumably the |
| // generic error dialog displaying the "Starting MMI..." message) |
| // take it down before bringing up the real "MMI Started" dialog |
| // in its place. |
| dismissAllDialogs(); |
| |
| MmiCode mmiCode = (MmiCode) r.result; |
| if (VDBG) log(" - MmiCode: " + mmiCode); |
| |
| Message message = Message.obtain(mHandler, PhoneApp.MMI_CANCEL); |
| mMmiStartedDialog = PhoneUtils.displayMMIInitiate(this, mmiCode, |
| message, mMmiStartedDialog); |
| }*/ |
| |
| /** |
| * Handles an MMI_CANCEL event, which is triggered by the button |
| * (labeled either "OK" or "Cancel") on the "MMI Started" dialog. |
| * @see PhoneUtils#cancelMmiCode(Phone) |
| */ |
| private void onMMICancel() { |
| if (VDBG) log("onMMICancel()..."); |
| |
| // First of all, cancel the outstanding MMI code (if possible.) |
| PhoneUtils.cancelMmiCode(mPhone); |
| |
| // Regardless of whether the current MMI code was cancelable, the |
| // PhoneApp will get an MMI_COMPLETE event very soon, which will |
| // take us to the MMI Complete dialog (see |
| // PhoneUtils.displayMMIComplete().) |
| // |
| // But until that event comes in, we *don't* want to stay here on |
| // the in-call screen, since we'll be visible in a |
| // partially-constructed state as soon as the "MMI Started" dialog |
| // gets dismissed. So let's forcibly bail out right now. |
| if (DBG) log("onMMICancel: finishing InCallScreen..."); |
| dismissAllDialogs(); |
| endInCallScreenSession(); |
| } |
| |
| /** |
| * Handles an MMI_COMPLETE event, which is triggered by telephony, |
| * implying MMI |
| */ |
| private void onMMIComplete(MmiCode mmiCode) { |
| // Check the code to see if the request is ready to |
| // finish, this includes any MMI state that is not |
| // PENDING. |
| |
| // if phone is a CDMA phone display feature code completed message |
| int phoneType = mPhone.getPhoneType(); |
| if (phoneType == PhoneConstants.PHONE_TYPE_CDMA) { |
| PhoneUtils.displayMMIComplete(mPhone, mApp, mmiCode, null, null); |
| } else if (phoneType == PhoneConstants.PHONE_TYPE_GSM) { |
| if (mmiCode.getState() != MmiCode.State.PENDING) { |
| if (DBG) log("Got MMI_COMPLETE, finishing InCallScreen..."); |
| dismissAllDialogs(); |
| endInCallScreenSession(); |
| } |
| } |
| } |
| |
| /** |
| * Handles the POST_ON_DIAL_CHARS message from the Phone |
| * (see our call to mPhone.setOnPostDialCharacter() above.) |
| * |
| * TODO: NEED TO TEST THIS SEQUENCE now that we no longer handle |
| * "dialable" key events here in the InCallScreen: we do directly to the |
| * Dialer UI instead. Similarly, we may now need to go directly to the |
| * Dialer to handle POST_ON_DIAL_CHARS too. |
| */ |
| private void handlePostOnDialChars(AsyncResult r, char ch) { |
| Connection c = (Connection) r.result; |
| |
| if (c != null) { |
| Connection.PostDialState state = |
| (Connection.PostDialState) r.userObj; |
| |
| if (VDBG) log("handlePostOnDialChar: state = " + |
| state + ", ch = " + ch); |
| |
| switch (state) { |
| case STARTED: |
| mDialer.stopLocalToneIfNeeded(); |
| if (mPauseInProgress) { |
| /** |
| * Note that on some devices, this will never happen, |
| * because we will not ever enter the PAUSE state. |
| */ |
| showPausePromptDialog(c, mPostDialStrAfterPause); |
| } |
| mPauseInProgress = false; |
| mDialer.startLocalToneIfNeeded(ch); |
| |
| // TODO: is this needed, now that you can't actually |
| // type DTMF chars or dial directly from here? |
| // If so, we'd need to yank you out of the in-call screen |
| // here too (and take you to the 12-key dialer in "in-call" mode.) |
| // displayPostDialedChar(ch); |
| break; |
| |
| case WAIT: |
| // wait shows a prompt. |
| if (DBG) log("handlePostOnDialChars: show WAIT prompt..."); |
| mDialer.stopLocalToneIfNeeded(); |
| String postDialStr = c.getRemainingPostDialString(); |
| showWaitPromptDialog(c, postDialStr); |
| break; |
| |
| case WILD: |
| if (DBG) log("handlePostOnDialChars: show WILD prompt"); |
| mDialer.stopLocalToneIfNeeded(); |
| showWildPromptDialog(c); |
| break; |
| |
| case COMPLETE: |
| mDialer.stopLocalToneIfNeeded(); |
| break; |
| |
| case PAUSE: |
| // pauses for a brief period of time then continue dialing. |
| mDialer.stopLocalToneIfNeeded(); |
| mPostDialStrAfterPause = c.getRemainingPostDialString(); |
| mPauseInProgress = true; |
| break; |
| |
| default: |
| break; |
| } |
| } |
| } |
| |
| /** |
| * Pop up an alert dialog with OK and Cancel buttons to allow user to |
| * Accept or Reject the WAIT inserted as part of the Dial string. |
| */ |
| private void showWaitPromptDialog(final Connection c, String postDialStr) { |
| if (DBG) log("showWaitPromptDialogChoice: '" + postDialStr + "'..."); |
| |
| Resources r = getResources(); |
| StringBuilder buf = new StringBuilder(); |
| buf.append(r.getText(R.string.wait_prompt_str)); |
| buf.append(postDialStr); |
| |
| // if (DBG) log("- mWaitPromptDialog = " + mWaitPromptDialog); |
| if (mWaitPromptDialog != null) { |
| if (DBG) log("- DISMISSING mWaitPromptDialog."); |
| mWaitPromptDialog.dismiss(); // safe even if already dismissed |
| mWaitPromptDialog = null; |
| } |
| |
| mWaitPromptDialog = new AlertDialog.Builder(this) |
| .setMessage(buf.toString()) |
| .setPositiveButton(R.string.pause_prompt_yes, |
| new DialogInterface.OnClickListener() { |
| @Override |
| public void onClick(DialogInterface dialog, int whichButton) { |
| if (DBG) log("handle WAIT_PROMPT_CONFIRMED, proceed..."); |
| c.proceedAfterWaitChar(); |
| } |
| }) |
| .setNegativeButton(R.string.pause_prompt_no, |
| new DialogInterface.OnClickListener() { |
| @Override |
| public void onClick(DialogInterface dialog, int whichButton) { |
| if (DBG) log("handle POST_DIAL_CANCELED!"); |
| c.cancelPostDial(); |
| } |
| }) |
| .create(); |
| mWaitPromptDialog.getWindow().addFlags( |
| WindowManager.LayoutParams.FLAG_BLUR_BEHIND); |
| |
| mWaitPromptDialog.show(); |
| } |
| |
| /** |
| * Pop up an alert dialog which waits for 2 seconds for each P (Pause) Character entered |
| * as part of the Dial String. |
| */ |
| private void showPausePromptDialog(final Connection c, String postDialStrAfterPause) { |
| Resources r = getResources(); |
| StringBuilder buf = new StringBuilder(); |
| buf.append(r.getText(R.string.pause_prompt_str)); |
| buf.append(postDialStrAfterPause); |
| |
| if (mPausePromptDialog != null) { |
| if (DBG) log("- DISMISSING mPausePromptDialog."); |
| mPausePromptDialog.dismiss(); // safe even if already dismissed |
| mPausePromptDialog = null; |
| } |
| |
| mPausePromptDialog = new AlertDialog.Builder(this) |
| .setMessage(buf.toString()) |
| .create(); |
| mPausePromptDialog.show(); |
| // 2 second timer |
| Message msg = Message.obtain(mHandler, EVENT_PAUSE_DIALOG_COMPLETE); |
| mHandler.sendMessageDelayed(msg, PAUSE_PROMPT_DIALOG_TIMEOUT); |
| } |
| |
| private View createWildPromptView() { |
| LinearLayout result = new LinearLayout(this); |
| result.setOrientation(LinearLayout.VERTICAL); |
| result.setPadding(5, 5, 5, 5); |
| |
| LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams( |
| ViewGroup.LayoutParams.MATCH_PARENT, |
| ViewGroup.LayoutParams.WRAP_CONTENT); |
| |
| TextView promptMsg = new TextView(this); |
| promptMsg.setTextSize(14); |
| promptMsg.setTypeface(Typeface.DEFAULT_BOLD); |
| promptMsg.setText(getResources().getText(R.string.wild_prompt_str)); |
| |
| result.addView(promptMsg, lp); |
| |
| mWildPromptText = new EditText(this); |
| mWildPromptText.setKeyListener(DialerKeyListener.getInstance()); |
| mWildPromptText.setMovementMethod(null); |
| mWildPromptText.setTextSize(14); |
| mWildPromptText.setMaxLines(1); |
| mWildPromptText.setHorizontallyScrolling(true); |
| mWildPromptText.setBackgroundResource(android.R.drawable.editbox_background); |
| |
| LinearLayout.LayoutParams lp2 = new LinearLayout.LayoutParams( |
| ViewGroup.LayoutParams.MATCH_PARENT, |
| ViewGroup.LayoutParams.WRAP_CONTENT); |
| lp2.setMargins(0, 3, 0, 0); |
| |
| result.addView(mWildPromptText, lp2); |
| |
| return result; |
| } |
| |
| private void showWildPromptDialog(final Connection c) { |
| View v = createWildPromptView(); |
| |
| if (mWildPromptDialog != null) { |
| if (VDBG) log("- DISMISSING mWildPromptDialog."); |
| mWildPromptDialog.dismiss(); // safe even if already dismissed |
| mWildPromptDialog = null; |
| } |
| |
| mWildPromptDialog = new AlertDialog.Builder(this) |
| .setView(v) |
| .setPositiveButton( |
| R.string.send_button, |
| new DialogInterface.OnClickListener() { |
| @Override |
| public void onClick(DialogInterface dialog, int whichButton) { |
| if (VDBG) log("handle WILD_PROMPT_CHAR_ENTERED, proceed..."); |
| String replacement = null; |
| if (mWildPromptText != null) { |
| replacement = mWildPromptText.getText().toString(); |
| mWildPromptText = null; |
| } |
| c.proceedAfterWildChar(replacement); |
| mApp.pokeUserActivity(); |
| } |
| }) |
| .setOnCancelListener( |
| new DialogInterface.OnCancelListener() { |
| @Override |
| public void onCancel(DialogInterface dialog) { |
| if (VDBG) log("handle POST_DIAL_CANCELED!"); |
| c.cancelPostDial(); |
| mApp.pokeUserActivity(); |
| } |
| }) |
| .create(); |
| mWildPromptDialog.getWindow().addFlags( |
| WindowManager.LayoutParams.FLAG_BLUR_BEHIND); |
| mWildPromptDialog.show(); |
| |
| mWildPromptText.requestFocus(); |
| } |
| |
| /** |
| * Updates the state of the in-call UI based on the current state of |
| * the Phone. This call has no effect if we're not currently the |
| * foreground activity. |
| * |
| * This method is only allowed to be called from the UI thread (since it |
| * manipulates our View hierarchy). If you need to update the screen from |
| * some other thread, or if you just want to "post a request" for the screen |
| * to be updated (rather than doing it synchronously), call |
| * requestUpdateScreen() instead. |
| * |
| * Right now this method will update UI visibility immediately, with no animation. |
| * TODO: have animate flag here and use it anywhere possible. |
| */ |
| private void updateScreen() { |
| if (DBG) log("updateScreen()..."); |
| final InCallScreenMode inCallScreenMode = mApp.inCallUiState.inCallScreenMode; |
| if (VDBG) { |
| PhoneConstants.State state = mCM.getState(); |
| log(" - phone state = " + state); |
| log(" - inCallScreenMode = " + inCallScreenMode); |
| } |
| |
| // Don't update anything if we're not in the foreground (there's |
| // no point updating our UI widgets since we're not visible!) |
| // Also note this check also ensures we won't update while we're |
| // in the middle of pausing, which could cause a visible glitch in |
| // the "activity ending" transition. |
| if (!mIsForegroundActivity) { |
| if (DBG) log("- updateScreen: not the foreground Activity! Bailing out..."); |
| return; |
| } |
| |
| if (inCallScreenMode == InCallScreenMode.OTA_NORMAL) { |
| if (DBG) log("- updateScreen: OTA call state NORMAL (NOT updating in-call UI)..."); |
| mCallCard.setVisibility(View.GONE); |
| if (mApp.otaUtils != null) { |
| mApp.otaUtils.otaShowProperScreen(); |
| } else { |
| Log.w(LOG_TAG, "OtaUtils object is null, not showing any screen for that."); |
| } |
| return; // Return without updating in-call UI. |
| } else if (inCallScreenMode == InCallScreenMode.OTA_ENDED) { |
| if (DBG) log("- updateScreen: OTA call ended state (NOT updating in-call UI)..."); |
| mCallCard.setVisibility(View.GONE); |
| // Wake up the screen when we get notification, good or bad. |
| mApp.wakeUpScreen(); |
| if (mApp.cdmaOtaScreenState.otaScreenState |
| == CdmaOtaScreenState.OtaScreenState.OTA_STATUS_ACTIVATION) { |
| if (DBG) log("- updateScreen: OTA_STATUS_ACTIVATION"); |
| if (mApp.otaUtils != null) { |
| if (DBG) log("- updateScreen: mApp.otaUtils is not null, " |
| + "call otaShowActivationScreen"); |
| mApp.otaUtils.otaShowActivateScreen(); |
| } |
| } else { |
| if (DBG) log("- updateScreen: OTA Call end state for Dialogs"); |
| if (mApp.otaUtils != null) { |
| if (DBG) log("- updateScreen: Show OTA Success Failure dialog"); |
| mApp.otaUtils.otaShowSuccessFailure(); |
| } |
| } |
| return; // Return without updating in-call UI. |
| } else if (inCallScreenMode == InCallScreenMode.MANAGE_CONFERENCE) { |
| if (DBG) log("- updateScreen: manage conference mode (NOT updating in-call UI)..."); |
| mCallCard.setVisibility(View.GONE); |
| updateManageConferencePanelIfNecessary(); |
| return; // Return without updating in-call UI. |
| } else if (inCallScreenMode == InCallScreenMode.CALL_ENDED) { |
| if (DBG) log("- updateScreen: call ended state..."); |
| // Continue with the rest of updateScreen() as usual, since we do |
| // need to update the background (to the special "call ended" color) |
| // and the CallCard (to show the "Call ended" label.) |
| } |
| |
| if (DBG) log("- updateScreen: updating the in-call UI..."); |
| // Note we update the InCallTouchUi widget before the CallCard, |
| // since the CallCard adjusts its size based on how much vertical |
| // space the InCallTouchUi widget needs. |
| updateInCallTouchUi(); |
| mCallCard.updateState(mCM); |
| |
| // If an incoming call is ringing, make sure the dialpad is |
| // closed. (We do this to make sure we're not covering up the |
| // "incoming call" UI.) |
| if (mCM.getState() == PhoneConstants.State.RINGING) { |
| if (mDialer.isOpened()) { |
| Log.i(LOG_TAG, "During RINGING state we force hiding dialpad."); |
| closeDialpadInternal(false); // don't do the "closing" animation |
| } |
| |
| // At this point, we are guranteed that the dialer is closed. |
| // This means that it is safe to clear out the "history" of DTMF digits |
| // you may have typed into the previous call (so you don't see the |
| // previous call's digits if you answer this call and then bring up the |
| // dialpad.) |
| // |
| // TODO: it would be more precise to do this when you *answer* the |
| // incoming call, rather than as soon as it starts ringing, but |
| // the InCallScreen doesn't keep enough state right now to notice |
| // that specific transition in onPhoneStateChanged(). |
| // TODO: This clears out the dialpad context as well so when a second |
| // call comes in while a voicemail call is happening, the voicemail |
| // dialpad will no longer have the "Voice Mail" context. It's a small |
| // case so not terribly bad, but we need to maintain a better |
| // call-to-callstate mapping before we can fix this. |
| mDialer.clearDigits(); |
| } |
| |
| |
| // Now that we're sure DTMF dialpad is in an appropriate state, reflect |
| // the dialpad state into CallCard |
| updateCallCardVisibilityPerDialerState(false); |
| |
| updateProgressIndication(); |
| |
| // Forcibly take down all dialog if an incoming call is ringing. |
| if (mCM.hasActiveRingingCall()) { |
| dismissAllDialogs(); |
| } else { |
| // Wait prompt dialog is not currently up. But it *should* be |
| // up if the FG call has a connection in the WAIT state and |
| // the phone isn't ringing. |
| String postDialStr = null; |
| List<Connection> fgConnections = mCM.getFgCallConnections(); |
| int phoneType = mCM.getFgPhone().getPhoneType(); |
| if (phoneType == PhoneConstants.PHONE_TYPE_CDMA) { |
| Connection fgLatestConnection = mCM.getFgCallLatestConnection(); |
| if (mApp.cdmaPhoneCallState.getCurrentCallState() == |
| CdmaPhoneCallState.PhoneCallState.CONF_CALL) { |
| for (Connection cn : fgConnections) { |
| if ((cn != null) && (cn.getPostDialState() == |
| Connection.PostDialState.WAIT)) { |
| cn.cancelPostDial(); |
| } |
| } |
| } else if ((fgLatestConnection != null) |
| && (fgLatestConnection.getPostDialState() == Connection.PostDialState.WAIT)) { |
| if(DBG) log("show the Wait dialog for CDMA"); |
| postDialStr = fgLatestConnection.getRemainingPostDialString(); |
| showWaitPromptDialog(fgLatestConnection, postDialStr); |
| } |
| } else if ((phoneType == PhoneConstants.PHONE_TYPE_GSM) |
| || (phoneType == PhoneConstants.PHONE_TYPE_SIP)) { |
| for (Connection cn : fgConnections) { |
| if ((cn != null) && (cn.getPostDialState() == Connection.PostDialState.WAIT)) { |
| postDialStr = cn.getRemainingPostDialString(); |
| showWaitPromptDialog(cn, postDialStr); |
| } |
| } |
| } else { |
| throw new IllegalStateException("Unexpected phone type: " + phoneType); |
| } |
| } |
| } |
| |
| /** |
| * (Re)synchronizes the onscreen UI with the current state of the |
| * telephony framework. |
| * |
| * @return SyncWithPhoneStateStatus.SUCCESS if we successfully updated the UI, or |
| * SyncWithPhoneStateStatus.PHONE_NOT_IN_USE if there was no phone state to sync |
| * with (ie. the phone was completely idle). In the latter case, we |
| * shouldn't even be in the in-call UI in the first place, and it's |
| * the caller's responsibility to bail out of this activity by |
| * calling endInCallScreenSession if appropriate. |
| * |
| * This method directly calls updateScreen() in the normal "phone is |
| * in use" case, so there's no need for the caller to do so. |
| */ |
| private SyncWithPhoneStateStatus syncWithPhoneState() { |
| boolean updateSuccessful = false; |
| if (DBG) log("syncWithPhoneState()..."); |
| if (DBG) PhoneUtils.dumpCallState(mPhone); |
| if (VDBG) dumpBluetoothState(); |
| |
| // Make sure the Phone is "in use". (If not, we shouldn't be on |
| // this screen in the first place.) |
| |
| // An active or just-ended OTA call counts as "in use". |
| if (TelephonyCapabilities.supportsOtasp(mCM.getFgPhone()) |
| && ((mApp.inCallUiState.inCallScreenMode == InCallScreenMode.OTA_NORMAL) |
| || (mApp.inCallUiState.inCallScreenMode == InCallScreenMode.OTA_ENDED))) { |
| // Even when OTA Call ends, need to show OTA End UI, |
| // so return Success to allow UI update. |
| return SyncWithPhoneStateStatus.SUCCESS; |
| } |
| |
| // If an MMI code is running that also counts as "in use". |
| // |
| // TODO: We currently only call getPendingMmiCodes() for GSM |
| // phones. (The code's been that way all along.) But CDMAPhone |
| // does in fact implement getPendingMmiCodes(), so should we |
| // check that here regardless of the phone type? |
| boolean hasPendingMmiCodes = |
| (mPhone.getPhoneType() == PhoneConstants.PHONE_TYPE_GSM) |
| && !mPhone.getPendingMmiCodes().isEmpty(); |
| |
| // Finally, it's also OK to stay here on the InCallScreen if we |
| // need to display a progress indicator while something's |
| // happening in the background. |
| boolean showProgressIndication = mApp.inCallUiState.isProgressIndicationActive(); |
| |
| boolean showScreenEvenAfterDisconnect = mApp.inCallUiState.showAlreadyDisconnectedState; |
| |
| if (mCM.hasActiveFgCall() || mCM.hasActiveBgCall() || mCM.hasActiveRingingCall() |
| || hasPendingMmiCodes || showProgressIndication || showScreenEvenAfterDisconnect) { |
| if (VDBG) log("syncWithPhoneState: it's ok to be here; update the screen..."); |
| updateScreen(); |
| return SyncWithPhoneStateStatus.SUCCESS; |
| } |
| |
| Log.i(LOG_TAG, "syncWithPhoneState: phone is idle (shouldn't be here)"); |
| return SyncWithPhoneStateStatus.PHONE_NOT_IN_USE; |
| } |
| |
| |
| |
| private void handleMissingVoiceMailNumber() { |
| if (DBG) log("handleMissingVoiceMailNumber"); |
| |
| final Message msg = Message.obtain(mHandler); |
| msg.what = DONT_ADD_VOICEMAIL_NUMBER; |
| |
| final Message msg2 = Message.obtain(mHandler); |
| msg2.what = ADD_VOICEMAIL_NUMBER; |
| |
| mMissingVoicemailDialog = new AlertDialog.Builder(this) |
| .setTitle(R.string.no_vm_number) |
| .setMessage(R.string.no_vm_number_msg) |
| .setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() { |
| public void onClick(DialogInterface dialog, int which) { |
| if (VDBG) log("Missing voicemail AlertDialog: POSITIVE click..."); |
| msg.sendToTarget(); // see dontAddVoiceMailNumber() |
| mApp.pokeUserActivity(); |
| }}) |
| .setNegativeButton(R.string.add_vm_number_str, |
| new DialogInterface.OnClickListener() { |
| public void onClick(DialogInterface dialog, int which) { |
| if (VDBG) log("Missing voicemail AlertDialog: NEGATIVE click..."); |
| msg2.sendToTarget(); // see addVoiceMailNumber() |
| mApp.pokeUserActivity(); |
| }}) |
| .setOnCancelListener(new OnCancelListener() { |
| public void onCancel(DialogInterface dialog) { |
| if (VDBG) log("Missing voicemail AlertDialog: CANCEL handler..."); |
| msg.sendToTarget(); // see dontAddVoiceMailNumber() |
| mApp.pokeUserActivity(); |
| }}) |
| .create(); |
| |
| // When the dialog is up, completely hide the in-call UI |
| // underneath (which is in a partially-constructed state). |
| mMissingVoicemailDialog.getWindow().addFlags( |
| WindowManager.LayoutParams.FLAG_DIM_BEHIND); |
| |
| mMissingVoicemailDialog.show(); |
| } |
| |
| private void addVoiceMailNumberPanel() { |
| if (mMissingVoicemailDialog != null) { |
| mMissingVoicemailDialog.dismiss(); |
| mMissingVoicemailDialog = null; |
| } |
| if (DBG) log("addVoiceMailNumberPanel: finishing InCallScreen..."); |
| endInCallScreenSession(); |
| |
| if (DBG) log("show vm setting"); |
| |
| // navigate to the Voicemail setting in the Call Settings activity. |
| Intent intent = new Intent(CallFeaturesSetting.ACTION_ADD_VOICEMAIL); |
| intent.setClass(this, CallFeaturesSetting.class); |
| startActivity(intent); |
| } |
| |
| private void dontAddVoiceMailNumber() { |
| if (mMissingVoicemailDialog != null) { |
| mMissingVoicemailDialog.dismiss(); |
| mMissingVoicemailDialog = null; |
| } |
| if (DBG) log("dontAddVoiceMailNumber: finishing InCallScreen..."); |
| endInCallScreenSession(); |
| } |
| |
| /** |
| * Do some delayed cleanup after a Phone call gets disconnected. |
| * |
| * This method gets called a couple of seconds after any DISCONNECT |
| * event from the Phone; it's triggered by the |
| * DELAYED_CLEANUP_AFTER_DISCONNECT message we send in onDisconnect(). |
| * |
| * If the Phone is totally idle right now, that means we've already |
| * shown the "call ended" state for a couple of seconds, and it's now |
| * time to endInCallScreenSession this activity. |
| * |
| * If the Phone is *not* idle right now, that probably means that one |
| * call ended but the other line is still in use. In that case, do |
| * nothing, and instead stay here on the InCallScreen. |
| */ |
| private void delayedCleanupAfterDisconnect() { |
| if (VDBG) log("delayedCleanupAfterDisconnect()... Phone state = " + mCM.getState()); |
| |
| // Clean up any connections in the DISCONNECTED state. |
| // |
| // [Background: Even after a connection gets disconnected, its |
| // Connection object still stays around, in the special |
| // DISCONNECTED state. This is necessary because we we need the |
| // caller-id information from that Connection to properly draw the |
| // "Call ended" state of the CallCard. |
| // But at this point we truly don't need that connection any |
| // more, so tell the Phone that it's now OK to to clean up any |
| // connections still in that state.] |
| mCM.clearDisconnected(); |
| |
| // There are two cases where we should *not* exit the InCallScreen: |
| // (1) Phone is still in use |
| // or |
| // (2) There's an active progress indication (i.e. the "Retrying..." |
| // progress dialog) that we need to continue to display. |
| |
| boolean stayHere = phoneIsInUse() || mApp.inCallUiState.isProgressIndicationActive(); |
| |
| if (stayHere) { |
| if (DBG) log("- delayedCleanupAfterDisconnect: staying on the InCallScreen..."); |
| } else { |
| // Phone is idle! We should exit the in-call UI now. |
| if (DBG) log("- delayedCleanupAfterDisconnect: phone is idle..."); |
| |
| // And (finally!) exit from the in-call screen |
| // (but not if we're already in the process of pausing...) |
| if (mIsForegroundActivity) { |
| if (DBG) log("- delayedCleanupAfterDisconnect: finishing InCallScreen..."); |
| |
| // In some cases we finish the call by taking the user to the |
| // Call Log. Otherwise, we simply call endInCallScreenSession, |
| // which will take us back to wherever we came from. |
| // |
| // UI note: In eclair and earlier, we went to the Call Log |
| // after outgoing calls initiated on the device, but never for |
| // incoming calls. Now we do it for incoming calls too, as |
| // long as the call was answered by the user. (We always go |
| // back where you came from after a rejected or missed incoming |
| // call.) |
| // |
| // And in any case, *never* go to the call log if we're in |
| // emergency mode (i.e. if the screen is locked and a lock |
| // pattern or PIN/password is set), or if we somehow got here |
| // on a non-voice-capable device. |
| |
| if (VDBG) log("- Post-call behavior:"); |
| if (VDBG) log(" - mLastDisconnectCause = " + mLastDisconnectCause); |
| if (VDBG) log(" - isPhoneStateRestricted() = " + isPhoneStateRestricted()); |
| |
| // DisconnectCause values in the most common scenarios: |
| // - INCOMING_MISSED: incoming ringing call times out, or the |
| // other end hangs up while still ringing |
| // - INCOMING_REJECTED: user rejects the call while ringing |
| // - LOCAL: user hung up while a call was active (after |
| // answering an incoming call, or after making an |
| // outgoing call) |
| // - NORMAL: the other end hung up (after answering an incoming |
| // call, or after making an outgoing call) |
| |
| if ((mLastDisconnectCause != Connection.DisconnectCause.INCOMING_MISSED) |
| && (mLastDisconnectCause != Connection.DisconnectCause.INCOMING_REJECTED) |
| && !isPhoneStateRestricted() |
| && PhoneGlobals.sVoiceCapable) { |
| final Intent intent = mApp.createPhoneEndIntentUsingCallOrigin(); |
| ActivityOptions opts = ActivityOptions.makeCustomAnimation(this, |
| R.anim.activity_close_enter, R.anim.activity_close_exit); |
| if (VDBG) { |
| log("- Show Call Log (or Dialtacts) after disconnect. Current intent: " |
| + intent); |
| } |
| try { |
| startActivity(intent, opts.toBundle()); |
| } catch (ActivityNotFoundException e) { |
| // Don't crash if there's somehow no "Call log" at |
| // all on this device. |
| // (This should never happen, though, since we already |
| // checked PhoneApp.sVoiceCapable above, and any |
| // voice-capable device surely *should* have a call |
| // log activity....) |
| Log.w(LOG_TAG, "delayedCleanupAfterDisconnect: " |
| + "transition to call log failed; intent = " + intent); |
| // ...so just return back where we came from.... |
| } |
| // Even if we did go to the call log, note that we still |
| // call endInCallScreenSession (below) to make sure we don't |
| // stay in the activity history. |
| } |
| |
| } |
| endInCallScreenSession(); |
| |
| // Reset the call origin when the session ends and this in-call UI is being finished. |
| mApp.setLatestActiveCallOrigin(null); |
| } |
| } |
| |
| |
| /** |
| * View.OnClickListener implementation. |
| * |
| * This method handles clicks from UI elements that use the |
| * InCallScreen itself as their OnClickListener. |
| * |
| * Note: Currently this method is used only for a few special buttons: |
| * - the mButtonManageConferenceDone "Back to call" button |
| * - the "dim" effect for the secondary call photo in CallCard as the second "swap" button |
| * - other OTASP-specific buttons managed by OtaUtils.java. |
| * |
| * *Most* in-call controls are handled by the handleOnscreenButtonClick() method, via the |
| * InCallTouchUi widget. |
| */ |
| @Override |
| public void onClick(View view) { |
| int id = view.getId(); |
| if (VDBG) log("onClick(View " + view + ", id " + id + ")..."); |
| |
| switch (id) { |
| case R.id.manage_done: // mButtonManageConferenceDone |
| if (VDBG) log("onClick: mButtonManageConferenceDone..."); |
| // Hide the Manage Conference panel, return to NORMAL mode. |
| setInCallScreenMode(InCallScreenMode.NORMAL); |
| requestUpdateScreen(); |
| break; |
| |
| case R.id.dim_effect_for_secondary_photo: |
| if (mInCallControlState.canSwap) { |
| internalSwapCalls(); |
| } |
| break; |
| |
| default: |
| // Presumably one of the OTASP-specific buttons managed by |
| // OtaUtils.java. |
| // (TODO: It would be cleaner for the OtaUtils instance itself to |
| // be the OnClickListener for its own buttons.) |
| |
| if ((mApp.inCallUiState.inCallScreenMode == InCallScreenMode.OTA_NORMAL |
| || mApp.inCallUiState.inCallScreenMode == InCallScreenMode.OTA_ENDED) |
| && mApp.otaUtils != null) { |
| mApp.otaUtils.onClickHandler(id); |
| } else { |
| // Uh oh: we *should* only receive clicks here from the |
| // buttons managed by OtaUtils.java, but if we're not in one |
| // of the special OTASP modes, those buttons shouldn't have |
| // been visible in the first place. |
| Log.w(LOG_TAG, |
| "onClick: unexpected click from ID " + id + " (View = " + view + ")"); |
| } |
| break; |
| } |
| |
| EventLog.writeEvent(EventLogTags.PHONE_UI_BUTTON_CLICK, |
| (view instanceof TextView) ? ((TextView) view).getText() : ""); |
| |
| // Clicking any onscreen UI element counts as explicit "user activity". |
| mApp.pokeUserActivity(); |
| } |
| |
| private void onHoldClick() { |
| final boolean hasActiveCall = mCM.hasActiveFgCall(); |
| final boolean hasHoldingCall = mCM.hasActiveBgCall(); |
| log("onHoldClick: hasActiveCall = " + hasActiveCall |
| + ", hasHoldingCall = " + hasHoldingCall); |
| boolean newHoldState; |
| boolean holdButtonEnabled; |
| if (hasActiveCall && !hasHoldingCall) { |
| // There's only one line in use, and that line is active. |
| PhoneUtils.switchHoldingAndActive( |
| mCM.getFirstActiveBgCall()); // Really means "hold" in this state |
| newHoldState = true; |
| holdButtonEnabled = true; |
| } else if (!hasActiveCall && hasHoldingCall) { |
| // There's only one line in use, and that line is on hold. |
| PhoneUtils.switchHoldingAndActive( |
| mCM.getFirstActiveBgCall()); // Really means "unhold" in this state |
| newHoldState = false; |
| holdButtonEnabled = true; |
| } else { |
| // Either zero or 2 lines are in use; "hold/unhold" is meaningless. |
| newHoldState = false; |
| holdButtonEnabled = false; |
| } |
| // No need to forcibly update the onscreen UI; just wait for the |
| // onPhoneStateChanged() callback. (This seems to be responsive |
| // enough.) |
| |
| // Also, any time we hold or unhold, force the DTMF dialpad to close. |
| closeDialpadInternal(true); // do the "closing" animation |
| } |
| |
| /** |
| * Toggles in-call audio between speaker and the built-in earpiece (or |
| * wired headset.) |
| */ |
| public void toggleSpeaker() { |
| // TODO: Turning on the speaker seems to enable the mic |
| // whether or not the "mute" feature is active! |
| // Not sure if this is an feature of the telephony API |
| // that I need to handle specially, or just a bug. |
| boolean newSpeakerState = !PhoneUtils.isSpeakerOn(this); |
| log("toggleSpeaker(): newSpeakerState = " + newSpeakerState); |
| |
| if (newSpeakerState && isBluetoothAvailable() && isBluetoothAudioConnected()) { |
| disconnectBluetoothAudio(); |
| } |
| PhoneUtils.turnOnSpeaker(this, newSpeakerState, true); |
| |
| // And update the InCallTouchUi widget (since the "audio mode" |
| // button might need to change its appearance based on the new |
| // audio state.) |
| updateInCallTouchUi(); |
| } |
| |
| /* |
| * onMuteClick is called only when there is a foreground call |
| */ |
| private void onMuteClick() { |
| boolean newMuteState = !PhoneUtils.getMute(); |
| log("onMuteClick(): newMuteState = " + newMuteState); |
| PhoneUtils.setMute(newMuteState); |
| } |
| |
| /** |
| * Toggles whether or not to route in-call audio to the bluetooth |
| * headset, or do nothing (but log a warning) if no bluetooth device |
| * is actually connected. |
| * |
| * TODO: this method is currently unused, but the "audio mode" UI |
| * design is still in flux so let's keep it around for now. |
| * (But if we ultimately end up *not* providing any way for the UI to |
| * simply "toggle bluetooth", we can get rid of this method.) |
| */ |
| public void toggleBluetooth() { |
| if (VDBG) log("toggleBluetooth()..."); |
| |
| if (isBluetoothAvailable()) { |
| // Toggle the bluetooth audio connection state: |
| if (isBluetoothAudioConnected()) { |
| disconnectBluetoothAudio(); |
| } else { |
| // Manually turn the speaker phone off, instead of allowing the |
| // Bluetooth audio routing to handle it, since there's other |
| // important state-updating that needs to happen in the |
| // PhoneUtils.turnOnSpeaker() method. |
| // (Similarly, whenever the user turns *on* the speaker, we |
| // manually disconnect the active bluetooth headset; |
| // see toggleSpeaker() and/or switchInCallAudio().) |
| if (PhoneUtils.isSpeakerOn(this)) { |
| PhoneUtils.turnOnSpeaker(this, false, true); |
| } |
| |
| connectBluetoothAudio(); |
| } |
| } else { |
| // Bluetooth isn't available; the onscreen UI shouldn't have |
| // allowed this request in the first place! |
| Log.w(LOG_TAG, "toggleBluetooth(): bluetooth is unavailable"); |
| } |
| |
| // And update the InCallTouchUi widget (since the "audio mode" |
| // button might need to change its appearance based on the new |
| // audio state.) |
| updateInCallTouchUi(); |
| } |
| |
| /** |
| * Switches the current routing of in-call audio between speaker, |
| * bluetooth, and the built-in earpiece (or wired headset.) |
| * |
| * This method is used on devices that provide a single 3-way switch |
| * for audio routing. For devices that provide separate toggles for |
| * Speaker and Bluetooth, see toggleBluetooth() and toggleSpeaker(). |
| * |
| * TODO: UI design is still in flux. If we end up totally |
| * eliminating the concept of Speaker and Bluetooth toggle buttons, |
| * we can get rid of toggleBluetooth() and toggleSpeaker(). |
| */ |
| public void switchInCallAudio(InCallAudioMode newMode) { |
| log("switchInCallAudio: new mode = " + newMode); |
| switch (newMode) { |
| case SPEAKER: |
| if (!PhoneUtils.isSpeakerOn(this)) { |
| // Switch away from Bluetooth, if it was active. |
| if (isBluetoothAvailable() && isBluetoothAudioConnected()) { |
| disconnectBluetoothAudio(); |
| } |
| PhoneUtils.turnOnSpeaker(this, true, true); |
| } |
| break; |
| |
| case BLUETOOTH: |
| // If already connected to BT, there's nothing to do here. |
| if (isBluetoothAvailable() && !isBluetoothAudioConnected()) { |
| // Manually turn the speaker phone off, instead of allowing the |
| // Bluetooth audio routing to handle it, since there's other |
| // important state-updating that needs to happen in the |
| // PhoneUtils.turnOnSpeaker() method. |
| // (Similarly, whenever the user turns *on* the speaker, we |
| // manually disconnect the active bluetooth headset; |
| // see toggleSpeaker() and/or switchInCallAudio().) |
| if (PhoneUtils.isSpeakerOn(this)) { |
| PhoneUtils.turnOnSpeaker(this, false, true); |
| } |
| connectBluetoothAudio(); |
| } |
| break; |
| |
| case EARPIECE: |
| // Switch to either the handset earpiece, or the wired headset (if connected.) |
| // (Do this by simply making sure both speaker and bluetooth are off.) |
| if (isBluetoothAvailable() && isBluetoothAudioConnected()) { |
| disconnectBluetoothAudio(); |
| } |
| if (PhoneUtils.isSpeakerOn(this)) { |
| PhoneUtils.turnOnSpeaker(this, false, true); |
| } |
| break; |
| |
| default: |
| Log.wtf(LOG_TAG, "switchInCallAudio: unexpected mode " + newMode); |
| break; |
| } |
| |
| // And finally, update the InCallTouchUi widget (since the "audio |
| // mode" button might need to change its appearance based on the |
| // new audio state.) |
| updateInCallTouchUi(); |
| } |
| |
| /** |
| * Handle a click on the "Open/Close dialpad" button. |
| * |
| * @see DTMFTwelveKeyDialer#openDialer(boolean) |
| * @see DTMFTwelveKeyDialer#closeDialer(boolean) |
| */ |
| private void onOpenCloseDialpad() { |
| if (VDBG) log("onOpenCloseDialpad()..."); |
| if (mDialer.isOpened()) { |
| closeDialpadInternal(true); // do the "closing" animation |
| } else { |
| openDialpadInternal(true); // do the "opening" animation |
| } |
| mApp.updateProximitySensorMode(mCM.getState()); |
| } |
| |
| /** Internal wrapper around {@link DTMFTwelveKeyDialer#openDialer(boolean)} */ |
| private void openDialpadInternal(boolean animate) { |
| mDialer.openDialer(animate); |
| // And update the InCallUiState (so that we'll restore the dialpad |
| // to the correct state if we get paused/resumed). |
| mApp.inCallUiState.showDialpad = true; |
| } |
| |
| // Internal wrapper around DTMFTwelveKeyDialer.closeDialer() |
| private void closeDialpadInternal(boolean animate) { |
| mDialer.closeDialer(animate); |
| // And update the InCallUiState (so that we'll restore the dialpad |
| // to the correct state if we get paused/resumed). |
| mApp.inCallUiState.showDialpad = false; |
| } |
| |
| /** |
| * Handles button clicks from the InCallTouchUi widget. |
| */ |
| /* package */ void handleOnscreenButtonClick(int id) { |
| if (DBG) log("handleOnscreenButtonClick(id " + id + ")..."); |
| |
| switch (id) { |
| // Actions while an incoming call is ringing: |
| case R.id.incomingCallAnswer: |
| internalAnswerCall(); |
| break; |
| case R.id.incomingCallReject: |
| hangupRingingCall(); |
| break; |
| case R.id.incomingCallRespondViaSms: |
| internalRespondViaSms(); |
| break; |
| |
| // The other regular (single-tap) buttons used while in-call: |
| case R.id.holdButton: |
| onHoldClick(); |
| break; |
| case R.id.swapButton: |
| internalSwapCalls(); |
| break; |
| case R.id.endButton: |
| internalHangup(); |
| break; |
| case R.id.dialpadButton: |
| onOpenCloseDialpad(); |
| break; |
| case R.id.muteButton: |
| onMuteClick(); |
| break; |
| case R.id.addButton: |
| PhoneUtils.startNewCall(mCM); // Fires off an ACTION_DIAL intent |
| break; |
| case R.id.mergeButton: |
| case R.id.cdmaMergeButton: |
| PhoneUtils.mergeCalls(mCM); |
| break; |
| case R.id.manageConferenceButton: |
| // Show the Manage Conference panel. |
| setInCallScreenMode(InCallScreenMode.MANAGE_CONFERENCE); |
| requestUpdateScreen(); |
| break; |
| |
| default: |
| Log.w(LOG_TAG, "handleOnscreenButtonClick: unexpected ID " + id); |
| break; |
| } |
| |
| // Clicking any onscreen UI element counts as explicit "user activity". |
| mApp.pokeUserActivity(); |
| |
| // Just in case the user clicked a "stateful" UI element (like one |
| // of the toggle buttons), we force the in-call buttons to update, |
| // to make sure the user sees the *new* current state. |
| // |
| // Note that some in-call buttons will *not* immediately change the |
| // state of the UI, namely those that send a request to the telephony |
| // layer (like "Hold" or "End call".) For those buttons, the |
| // updateInCallTouchUi() call here won't have any visible effect. |
| // Instead, the UI will be updated eventually when the next |
| // onPhoneStateChanged() event comes in and triggers an updateScreen() |
| // call. |
| // |
| // TODO: updateInCallTouchUi() is overkill here; it would be |
| // more efficient to update *only* the affected button(s). |
| // (But this isn't a big deal since updateInCallTouchUi() is pretty |
| // cheap anyway...) |
| updateInCallTouchUi(); |
| } |
| |
| /** |
| * Display a status or error indication to the user according to the |
| * specified InCallUiState.CallStatusCode value. |
| */ |
| private void showStatusIndication(CallStatusCode status) { |
| switch (status) { |
| case SUCCESS: |
| // The InCallScreen does not need to display any kind of error indication, |
| // so we shouldn't have gotten here in the first place. |
| Log.wtf(LOG_TAG, "showStatusIndication: nothing to display"); |
| break; |
| |
| case POWER_OFF: |
| // Radio is explictly powered off, presumably because the |
| // device is in airplane mode. |
| // |
| // TODO: For now this UI is ultra-simple: we simply display |
| // a message telling the user to turn off airplane mode. |
| // But it might be nicer for the dialog to offer the option |
| // to turn the radio on right there (and automatically retry |
| // the call once network registration is complete.) |
| showGenericErrorDialog(R.string.incall_error_power_off, |
| true /* isStartupError */); |
| break; |
| |
| case EMERGENCY_ONLY: |
| // Only emergency numbers are allowed, but we tried to dial |
| // a non-emergency number. |
| // (This state is currently unused; see comments above.) |
| showGenericErrorDialog(R.string.incall_error_emergency_only, |
| true /* isStartupError */); |
| break; |
| |
| case OUT_OF_SERVICE: |
| // No network connection. |
| showGenericErrorDialog(R.string.incall_error_out_of_service, |
| true /* isStartupError */); |
| break; |
| |
| case NO_PHONE_NUMBER_SUPPLIED: |
| // The supplied Intent didn't contain a valid phone number. |
| // (This is rare and should only ever happen with broken |
| // 3rd-party apps.) For now just show a generic error. |
| showGenericErrorDialog(R.string.incall_error_no_phone_number_supplied, |
| true /* isStartupError */); |
| break; |
| |
| case DIALED_MMI: |
| // Our initial phone number was actually an MMI sequence. |
| // There's no real "error" here, but we do bring up the |
| // a Toast (as requested of the New UI paradigm). |
| // |
| // In-call MMIs do not trigger the normal MMI Initiate |
| // Notifications, so we should notify the user here. |
| // Otherwise, the code in PhoneUtils.java should handle |
| // user notifications in the form of Toasts or Dialogs. |
| if (mCM.getState() == PhoneConstants.State.OFFHOOK) { |
| Toast.makeText(mApp, R.string.incall_status_dialed_mmi, Toast.LENGTH_SHORT) |
| .show(); |
| } |
| break; |
| |
| case CALL_FAILED: |
| // We couldn't successfully place the call; there was some |
| // failure in the telephony layer. |
| // TODO: Need UI spec for this failure case; for now just |
| // show a generic error. |
| showGenericErrorDialog(R.string.incall_error_call_failed, |
| true /* isStartupError */); |
| break; |
| |
| case VOICEMAIL_NUMBER_MISSING: |
| // We tried to call a voicemail: URI but the device has no |
| // voicemail number configured. |
| handleMissingVoiceMailNumber(); |
| break; |
| |
| case CDMA_CALL_LOST: |
| // This status indicates that InCallScreen should display the |
| // CDMA-specific "call lost" dialog. (If an outgoing call fails, |
| // and the CDMA "auto-retry" feature is enabled, *and* the retried |
| // call fails too, we display this specific dialog.) |
| // |
| // TODO: currently unused; see InCallUiState.needToShowCallLostDialog |
| break; |
| |
| case EXITED_ECM: |
| // This status indicates that InCallScreen needs to display a |
| // warning that we're exiting ECM (emergency callback mode). |
| showExitingECMDialog(); |
| break; |
| |
| default: |
| throw new IllegalStateException( |
| "showStatusIndication: unexpected status code: " + status); |
| } |
| |
| // TODO: still need to make sure that pressing OK or BACK from |
| // *any* of the dialogs we launch here ends up calling |
| // inCallUiState.clearPendingCallStatusCode() |
| // *and* |
| // make sure the Dialog handles both OK *and* cancel by calling |
| // endInCallScreenSession. (See showGenericErrorDialog() for an |
| // example.) |
| // |
| // (showGenericErrorDialog() currently does this correctly, |
| // but handleMissingVoiceMailNumber() probably needs to be fixed too.) |
| // |
| // Also need to make sure that bailing out of any of these dialogs by |
| // pressing Home clears out the pending status code too. (If you do |
| // that, neither the dialog's clickListener *or* cancelListener seems |
| // to run...) |
| } |
| |
| /** |
| * Utility function to bring up a generic "error" dialog, and then bail |
| * out of the in-call UI when the user hits OK (or the BACK button.) |
| */ |
| private void showGenericErrorDialog(int resid, boolean isStartupError) { |
| CharSequence msg = getResources().getText(resid); |
| if (DBG) log("showGenericErrorDialog('" + msg + "')..."); |
| |
| // create the clicklistener and cancel listener as needed. |
| DialogInterface.OnClickListener clickListener; |
| OnCancelListener cancelListener; |
| if (isStartupError) { |
| clickListener = new DialogInterface.OnClickListener() { |
| public void onClick(DialogInterface dialog, int which) { |
| bailOutAfterErrorDialog(); |
| }}; |
| cancelListener = new OnCancelListener() { |
| public void onCancel(DialogInterface dialog) { |
| bailOutAfterErrorDialog(); |
| }}; |
| } else { |
| clickListener = new DialogInterface.OnClickListener() { |
| public void onClick(DialogInterface dialog, int which) { |
| delayedCleanupAfterDisconnect(); |
| }}; |
| cancelListener = new OnCancelListener() { |
| public void onCancel(DialogInterface dialog) { |
| delayedCleanupAfterDisconnect(); |
| }}; |
| } |
| |
| // TODO: Consider adding a setTitle() call here (with some generic |
| // "failure" title?) |
| mGenericErrorDialog = new AlertDialog.Builder(this) |
| .setMessage(msg) |
| .setPositiveButton(R.string.ok, clickListener) |
| .setOnCancelListener(cancelListener) |
| .create(); |
| |
| // When the dialog is up, completely hide the in-call UI |
| // underneath (which is in a partially-constructed state). |
| mGenericErrorDialog.getWindow().addFlags( |
| WindowManager.LayoutParams.FLAG_DIM_BEHIND); |
| |
| mGenericErrorDialog.show(); |
| } |
| |
| private void showCallLostDialog() { |
| if (DBG) log("showCallLostDialog()..."); |
| |
| // Don't need to show the dialog if InCallScreen isn't in the forgeround |
| if (!mIsForegroundActivity) { |
| if (DBG) log("showCallLostDialog: not the foreground Activity! Bailing out..."); |
| return; |
| } |
| |
| // Don't need to show the dialog again, if there is one already. |
| if (mCallLostDialog != null) { |
| if (DBG) log("showCallLostDialog: There is a mCallLostDialog already."); |
| return; |
| } |
| |
| mCallLostDialog = new AlertDialog.Builder(this) |
| .setMessage(R.string.call_lost) |
| .setIconAttribute(android.R.attr.alertDialogIcon) |
| .create(); |
| mCallLostDialog.show(); |
| } |
| |
| /** |
| * Displays the "Exiting ECM" warning dialog. |
| * |
| * Background: If the phone is currently in ECM (Emergency callback |
| * mode) and we dial a non-emergency number, that automatically |
| * *cancels* ECM. (That behavior comes from CdmaCallTracker.dial().) |
| * When that happens, we need to warn the user that they're no longer |
| * in ECM (bug 4207607.) |
| * |
| * So bring up a dialog explaining what's happening. There's nothing |
| * for the user to do, by the way; we're simply providing an |
| * indication that they're exiting ECM. We *could* use a Toast for |
| * this, but toasts are pretty easy to miss, so instead use a dialog |
| * with a single "OK" button. |
| * |
| * TODO: it's ugly that the code here has to make assumptions about |
| * the behavior of the telephony layer (namely that dialing a |
| * non-emergency number while in ECM causes us to exit ECM.) |
| * |
| * Instead, this warning dialog should really be triggered by our |
| * handler for the |
| * TelephonyIntents.ACTION_EMERGENCY_CALLBACK_MODE_CHANGED intent in |
| * PhoneApp.java. But that won't work until that intent also |
| * includes a *reason* why we're exiting ECM, since we need to |
| * display this dialog when exiting ECM because of an outgoing call, |
| * but NOT if we're exiting ECM because the user manually turned it |
| * off via the EmergencyCallbackModeExitDialog. |
| * |
| * Or, it might be simpler to just have outgoing non-emergency calls |
| * *not* cancel ECM. That way the UI wouldn't have to do anything |
| * special here. |
| */ |
| private void showExitingECMDialog() { |
| Log.i(LOG_TAG, "showExitingECMDialog()..."); |
| |
| if (mExitingECMDialog != null) { |
| if (DBG) log("- DISMISSING mExitingECMDialog."); |
| mExitingECMDialog.dismiss(); // safe even if already dismissed |
| mExitingECMDialog = null; |
| } |
| |
| // When the user dismisses the "Exiting ECM" dialog, we clear out |
| // the pending call status code field (since we're done with this |
| // dialog), but do *not* bail out of the InCallScreen. |
| |
| final InCallUiState inCallUiState = mApp.inCallUiState; |
| DialogInterface.OnClickListener clickListener = new DialogInterface.OnClickListener() { |
| public void onClick(DialogInterface dialog, int which) { |
| inCallUiState.clearPendingCallStatusCode(); |
| }}; |
| OnCancelListener cancelListener = new OnCancelListener() { |
| public void onCancel(DialogInterface dialog) { |
| inCallUiState.clearPendingCallStatusCode(); |
| }}; |
| |
| // Ultra-simple AlertDialog with only an OK button: |
| mExitingECMDialog = new AlertDialog.Builder(this) |
| .setMessage(R.string.progress_dialog_exiting_ecm) |
| .setPositiveButton(R.string.ok, clickListener) |
| .setOnCancelListener(cancelListener) |
| .create(); |
| mExitingECMDialog.getWindow().addFlags( |
| WindowManager.LayoutParams.FLAG_BLUR_BEHIND); |
| mExitingECMDialog.show(); |
| } |
| |
| private void bailOutAfterErrorDialog() { |
| if (mGenericErrorDialog != null) { |
| if (DBG) log("bailOutAfterErrorDialog: DISMISSING mGenericErrorDialog."); |
| mGenericErrorDialog.dismiss(); |
| mGenericErrorDialog = null; |
| } |
| if (DBG) log("bailOutAfterErrorDialog(): end InCallScreen session..."); |
| |
| // Now that the user has dismissed the error dialog (presumably by |
| // either hitting the OK button or pressing Back, we can now reset |
| // the pending call status code field. |
| // |
| // (Note that the pending call status is NOT cleared simply |
| // by the InCallScreen being paused or finished, since the resulting |
| // dialog is supposed to persist across orientation changes or if the |
| // screen turns off.) |
| // |
| // See the "Error / diagnostic indications" section of |
| // InCallUiState.java for more detailed info about the |
| // pending call status code field. |
| final InCallUiState inCallUiState = mApp.inCallUiState; |
| inCallUiState.clearPendingCallStatusCode(); |
| |
| // Force the InCallScreen to truly finish(), rather than just |
| // moving it to the back of the activity stack (which is what |
| // our finish() method usually does.) |
| // This is necessary to avoid an obscure scenario where the |
| // InCallScreen can get stuck in an inconsistent state, somehow |
| // causing a *subsequent* outgoing call to fail (bug 4172599). |
| endInCallScreenSession(true /* force a real finish() call */); |
| } |
| |
| /** |
| * Dismisses (and nulls out) all persistent Dialogs managed |
| * by the InCallScreen. Useful if (a) we're about to bring up |
| * a dialog and want to pre-empt any currently visible dialogs, |
| * or (b) as a cleanup step when the Activity is going away. |
| */ |
| private void dismissAllDialogs() { |
| if (DBG) log("dismissAllDialogs()..."); |
| |
| // Note it's safe to dismiss() a dialog that's already dismissed. |
| // (Even if the AlertDialog object(s) below are still around, it's |
| // possible that the actual dialog(s) may have already been |
| // dismissed by the user.) |
| |
| if (mMissingVoicemailDialog != null) { |
| if (VDBG) log("- DISMISSING mMissingVoicemailDialog."); |
| mMissingVoicemailDialog.dismiss(); |
| mMissingVoicemailDialog = null; |
| } |
| if (mMmiStartedDialog != null) { |
| if (VDBG) log("- DISMISSING mMmiStartedDialog."); |
| mMmiStartedDialog.dismiss(); |
| mMmiStartedDialog = null; |
| } |
| if (mGenericErrorDialog != null) { |
| if (VDBG) log("- DISMISSING mGenericErrorDialog."); |
| mGenericErrorDialog.dismiss(); |
| mGenericErrorDialog = null; |
| } |
| if (mSuppServiceFailureDialog != null) { |
| if (VDBG) log("- DISMISSING mSuppServiceFailureDialog."); |
| mSuppServiceFailureDialog.dismiss(); |
| mSuppServiceFailureDialog = null; |
| } |
| if (mWaitPromptDialog != null) { |
| if (VDBG) log("- DISMISSING mWaitPromptDialog."); |
| mWaitPromptDialog.dismiss(); |
| mWaitPromptDialog = null; |
| } |
| if (mWildPromptDialog != null) { |
| if (VDBG) log("- DISMISSING mWildPromptDialog."); |
| mWildPromptDialog.dismiss(); |
| mWildPromptDialog = null; |
| } |
| if (mCallLostDialog != null) { |
| if (VDBG) log("- DISMISSING mCallLostDialog."); |
| mCallLostDialog.dismiss(); |
| mCallLostDialog = null; |
| } |
| if ((mApp.inCallUiState.inCallScreenMode == InCallScreenMode.OTA_NORMAL |
| || mApp.inCallUiState.inCallScreenMode == InCallScreenMode.OTA_ENDED) |
| && mApp.otaUtils != null) { |
| mApp.otaUtils.dismissAllOtaDialogs(); |
| } |
| if (mPausePromptDialog != null) { |
| if (DBG) log("- DISMISSING mPausePromptDialog."); |
| mPausePromptDialog.dismiss(); |
| mPausePromptDialog = null; |
| } |
| if (mExitingECMDialog != null) { |
| if (DBG) log("- DISMISSING mExitingECMDialog."); |
| mExitingECMDialog.dismiss(); |
| mExitingECMDialog = null; |
| } |
| } |
| |
| /** |
| * Updates the state of the onscreen "progress indication" used in |
| * some (relatively rare) scenarios where we need to wait for |
| * something to happen before enabling the in-call UI. |
| * |
| * If necessary, this method will cause a ProgressDialog (i.e. a |
| * spinning wait cursor) to be drawn *on top of* whatever the current |
| * state of the in-call UI is. |
| * |
| * @see InCallUiState.ProgressIndicationType |
| */ |
| private void updateProgressIndication() { |
| // If an incoming call is ringing, that takes priority over any |
| // possible value of inCallUiState.progressIndication. |
| if (mCM.hasActiveRingingCall()) { |
| dismissProgressIndication(); |
| return; |
| } |
| |
| // Otherwise, put up a progress indication if indicated by the |
| // inCallUiState.progressIndication field. |
| final InCallUiState inCallUiState = mApp.inCallUiState; |
| switch (inCallUiState.getProgressIndication()) { |
| case NONE: |
| // No progress indication necessary, so make sure it's dismissed. |
| dismissProgressIndication(); |
| break; |
| |
| case TURNING_ON_RADIO: |
| showProgressIndication( |
| R.string.emergency_enable_radio_dialog_title, |
| R.string.emergency_enable_radio_dialog_message); |
| break; |
| |
| case RETRYING: |
| showProgressIndication( |
| R.string.emergency_enable_radio_dialog_title, |
| R.string.emergency_enable_radio_dialog_retry); |
| break; |
| |
| default: |
| Log.wtf(LOG_TAG, "updateProgressIndication: unexpected value: " |
| + inCallUiState.getProgressIndication()); |
| dismissProgressIndication(); |
| break; |
| } |
| } |
| |
| /** |
| * Show an onscreen "progress indication" with the specified title and message. |
| */ |
| private void showProgressIndication(int titleResId, int messageResId) { |
| if (DBG) log("showProgressIndication(message " + messageResId + ")..."); |
| |
| // TODO: make this be a no-op if the progress indication is |
| // already visible with the exact same title and message. |
| |
| dismissProgressIndication(); // Clean up any prior progress indication |
| mProgressDialog = new ProgressDialog(this); |
| mProgressDialog.setTitle(getText(titleResId)); |
| mProgressDialog.setMessage(getText(messageResId)); |
| mProgressDialog.setIndeterminate(true); |
| mProgressDialog.setCancelable(false); |
| mProgressDialog.getWindow().setType(WindowManager.LayoutParams.TYPE_SYSTEM_DIALOG); |
| mProgressDialog.getWindow().addFlags(WindowManager.LayoutParams.FLAG_BLUR_BEHIND); |
| mProgressDialog.show(); |
| } |
| |
| /** |
| * Dismiss the onscreen "progress indication" (if present). |
| */ |
| private void dismissProgressIndication() { |
| if (DBG) log("dismissProgressIndication()..."); |
| if (mProgressDialog != null) { |
| mProgressDialog.dismiss(); // safe even if already dismissed |
| mProgressDialog = null; |
| } |
| } |
| |
| |
| // |
| // Helper functions for answering incoming calls. |
| // |
| |
| /** |
| * Answer a ringing call. This method does nothing if there's no |
| * ringing or waiting call. |
| */ |
| private void internalAnswerCall() { |
| if (DBG) log("internalAnswerCall()..."); |
| // if (DBG) PhoneUtils.dumpCallState(mPhone); |
| |
| final boolean hasRingingCall = mCM.hasActiveRingingCall(); |
| |
| if (hasRingingCall) { |
| Phone phone = mCM.getRingingPhone(); |
| Call ringing = mCM.getFirstActiveRingingCall(); |
| int phoneType = phone.getPhoneType(); |
| if (phoneType == PhoneConstants.PHONE_TYPE_CDMA) { |
| if (DBG) log("internalAnswerCall: answering (CDMA)..."); |
| if (mCM.hasActiveFgCall() |
| && mCM.getFgPhone().getPhoneType() == PhoneConstants.PHONE_TYPE_SIP) { |
| // The incoming call is CDMA call and the ongoing |
| // call is a SIP call. The CDMA network does not |
| // support holding an active call, so there's no |
| // way to swap between a CDMA call and a SIP call. |
| // So for now, we just don't allow a CDMA call and |
| // a SIP call to be active at the same time.We'll |
| // "answer incoming, end ongoing" in this case. |
| if (DBG) log("internalAnswerCall: answer " |
| + "CDMA incoming and end SIP ongoing"); |
| PhoneUtils.answerAndEndActive(mCM, ringing); |
| } else { |
| PhoneUtils.answerCall(ringing); |
| } |
| } else if (phoneType == PhoneConstants.PHONE_TYPE_SIP) { |
| if (DBG) log("internalAnswerCall: answering (SIP)..."); |
| if (mCM.hasActiveFgCall() |
| && mCM.getFgPhone().getPhoneType() == PhoneConstants.PHONE_TYPE_CDMA) { |
| // Similar to the PHONE_TYPE_CDMA handling. |
| // The incoming call is SIP call and the ongoing |
| // call is a CDMA call. The CDMA network does not |
| // support holding an active call, so there's no |
| // way to swap between a CDMA call and a SIP call. |
| // So for now, we just don't allow a CDMA call and |
| // a SIP call to be active at the same time.We'll |
| // "answer incoming, end ongoing" in this case. |
| if (DBG) log("internalAnswerCall: answer " |
| + "SIP incoming and end CDMA ongoing"); |
| PhoneUtils.answerAndEndActive(mCM, ringing); |
| } else { |
| PhoneUtils.answerCall(ringing); |
| } |
| } else if (phoneType == PhoneConstants.PHONE_TYPE_GSM) { |
| if (DBG) log("internalAnswerCall: answering (GSM)..."); |
| // GSM: this is usually just a wrapper around |
| // PhoneUtils.answerCall(), *but* we also need to do |
| // something special for the "both lines in use" case. |
| |
| final boolean hasActiveCall = mCM.hasActiveFgCall(); |
| final boolean hasHoldingCall = mCM.hasActiveBgCall(); |
| |
| if (hasActiveCall && hasHoldingCall) { |
| if (DBG) log("internalAnswerCall: answering (both lines in use!)..."); |
| // The relatively rare case where both lines are |
| // already in use. We "answer incoming, end ongoing" |
| // in this case, according to the current UI spec. |
| PhoneUtils.answerAndEndActive(mCM, ringing); |
| |
| // Alternatively, we could use |
| // PhoneUtils.answerAndEndHolding(mPhone); |
| // here to end the on-hold call instead. |
| } else { |
| if (DBG) log("internalAnswerCall: answering..."); |
| PhoneUtils.answerCall(ringing); // Automatically holds the current active call, |
| // if there is one |
| } |
| } else { |
| throw new IllegalStateException("Unexpected phone type: " + phoneType); |
| } |
| |
| // Call origin is valid only with outgoing calls. Disable it on incoming calls. |
| mApp.setLatestActiveCallOrigin(null); |
| } |
| } |
| |
| /** |
| * Hang up the ringing call (aka "Don't answer"). |
| */ |
| /* package */ void hangupRingingCall() { |
| if (DBG) log("hangupRingingCall()..."); |
| if (VDBG) PhoneUtils.dumpCallManager(); |
| // In the rare case when multiple calls are ringing, the UI policy |
| // it to always act on the first ringing call. |
| PhoneUtils.hangupRingingCall(mCM.getFirstActiveRingingCall()); |
| } |
| |
| /** |
| * Silence the ringer (if an incoming call is ringing.) |
| */ |
| private void internalSilenceRinger() { |
| if (DBG) log("internalSilenceRinger()..."); |
| final CallNotifier notifier = mApp.notifier; |
| if (notifier.isRinging()) { |
| // ringer is actually playing, so silence it. |
| notifier.silenceRinger(); |
| } |
| } |
| |
| /** |
| * Respond via SMS to the ringing call. |
| * @see RespondViaSmsManager |
| */ |
| private void internalRespondViaSms() { |
| log("internalRespondViaSms()..."); |
| if (VDBG) PhoneUtils.dumpCallManager(); |
| |
| // In the rare case when multiple calls are ringing, the UI policy |
| // it to always act on the first ringing call. |
| Call ringingCall = mCM.getFirstActiveRingingCall(); |
| |
| mRespondViaSmsManager.showRespondViaSmsPopup(ringingCall); |
| |
| // Silence the ringer, since it would be distracting while you're trying |
| // to pick a response. (Note that we'll restart the ringer if you bail |
| // out of the popup, though; see RespondViaSmsCancelListener.) |
| internalSilenceRinger(); |
| } |
| |
| /** |
| * Hang up the current active call. |
| */ |
| private void internalHangup() { |
| PhoneConstants.State state = mCM.getState(); |
| log("internalHangup()... phone state = " + state); |
| |
| // Regardless of the phone state, issue a hangup request. |
| // (If the phone is already idle, this call will presumably have no |
| // effect (but also see the note below.)) |
| PhoneUtils.hangup(mCM); |
| |
| // If the user just hung up the only active call, we'll eventually exit |
| // the in-call UI after the following sequence: |
| // - When the hangup() succeeds, we'll get a DISCONNECT event from |
| // the telephony layer (see onDisconnect()). |
| // - We immediately switch to the "Call ended" state (see the "delayed |
| // bailout" code path in onDisconnect()) and also post a delayed |
| // DELAYED_CLEANUP_AFTER_DISCONNECT message. |
| // - When the DELAYED_CLEANUP_AFTER_DISCONNECT message comes in (see |
| // delayedCleanupAfterDisconnect()) we do some final cleanup, and exit |
| // this activity unless the phone is still in use (i.e. if there's |
| // another call, or something else going on like an active MMI |
| // sequence.) |
| |
| if (state == PhoneConstants.State.IDLE) { |
| // The user asked us to hang up, but the phone was (already) idle! |
| Log.w(LOG_TAG, "internalHangup(): phone is already IDLE!"); |
| |
| // This is rare, but can happen in a few cases: |
| // (a) If the user quickly double-taps the "End" button. In this case |
| // we'll see that 2nd press event during the brief "Call ended" |
| // state (where the phone is IDLE), or possibly even before the |
| // radio has been able to respond to the initial hangup request. |
| // (b) More rarely, this can happen if the user presses "End" at the |
| // exact moment that the call ends on its own (like because of the |
| // other person hanging up.) |
| // (c) Finally, this could also happen if we somehow get stuck here on |
| // the InCallScreen with the phone truly idle, perhaps due to a |
| // bug where we somehow *didn't* exit when the phone became idle |
| // in the first place. |
| |
| // TODO: as a "safety valve" for case (c), consider immediately |
| // bailing out of the in-call UI right here. (The user can always |
| // bail out by pressing Home, of course, but they'll probably try |
| // pressing End first.) |
| // |
| // Log.i(LOG_TAG, "internalHangup(): phone is already IDLE! Bailing out..."); |
| // endInCallScreenSession(); |
| } |
| } |
| |
| /** |
| * InCallScreen-specific wrapper around PhoneUtils.switchHoldingAndActive(). |
| */ |
| private void internalSwapCalls() { |
| if (DBG) log("internalSwapCalls()..."); |
| |
| // Any time we swap calls, force the DTMF dialpad to close. |
| // (We want the regular in-call UI to be visible right now, so the |
| // user can clearly see which call is now in the foreground.) |
| closeDialpadInternal(true); // do the "closing" animation |
| |
| // Also, clear out the "history" of DTMF digits you typed, to make |
| // sure you don't see digits from call #1 while call #2 is active. |
| // (Yes, this does mean that swapping calls twice will cause you |
| // to lose any previous digits from the current call; see the TODO |
| // comment on DTMFTwelvKeyDialer.clearDigits() for more info.) |
| mDialer.clearDigits(); |
| |
| // Swap the fg and bg calls. |
| // In the future we may provides some way for user to choose among |
| // multiple background calls, for now, always act on the first background calll. |
| PhoneUtils.switchHoldingAndActive(mCM.getFirstActiveBgCall()); |
| |
| // If we have a valid BluetoothPhoneService then since CDMA network or |
| // Telephony FW does not send us information on which caller got swapped |
| // we need to update the second call active state in BluetoothPhoneService internally |
| if (mCM.getBgPhone().getPhoneType() == PhoneConstants.PHONE_TYPE_CDMA) { |
| IBluetoothHeadsetPhone btPhone = mApp.getBluetoothPhoneService(); |
| if (btPhone != null) { |
| try { |
| btPhone.cdmaSwapSecondCallState(); |
| } catch (RemoteException e) { |
| Log.e(LOG_TAG, Log.getStackTraceString(new Throwable())); |
| } |
| } |
| } |
| |
| } |
| |
| /** |
| * Sets the current high-level "mode" of the in-call UI. |
| * |
| * NOTE: if newMode is CALL_ENDED, the caller is responsible for |
| * posting a delayed DELAYED_CLEANUP_AFTER_DISCONNECT message, to make |
| * sure the "call ended" state goes away after a couple of seconds. |
| * |
| * Note this method does NOT refresh of the onscreen UI; the caller is |
| * responsible for calling updateScreen() or requestUpdateScreen() if |
| * necessary. |
| */ |
| private void setInCallScreenMode(InCallScreenMode newMode) { |
| if (DBG) log("setInCallScreenMode: " + newMode); |
| mApp.inCallUiState.inCallScreenMode = newMode; |
| |
| switch (newMode) { |
| case MANAGE_CONFERENCE: |
| if (!PhoneUtils.isConferenceCall(mCM.getActiveFgCall())) { |
| Log.w(LOG_TAG, "MANAGE_CONFERENCE: no active conference call!"); |
| // Hide the Manage Conference panel, return to NORMAL mode. |
| setInCallScreenMode(InCallScreenMode.NORMAL); |
| return; |
| } |
| List<Connection> connections = mCM.getFgCallConnections(); |
| // There almost certainly will be > 1 connection, |
| // since isConferenceCall() just returned true. |
| if ((connections == null) || (connections.size() <= 1)) { |
| Log.w(LOG_TAG, |
| "MANAGE_CONFERENCE: Bogus TRUE from isConferenceCall(); connections = " |
| + connections); |
| // Hide the Manage Conference panel, return to NORMAL mode. |
| setInCallScreenMode(InCallScreenMode.NORMAL); |
| return; |
| } |
| |
| // TODO: Don't do this here. The call to |
| // initManageConferencePanel() should instead happen |
| // automagically in ManageConferenceUtils the very first |
| // time you call updateManageConferencePanel() or |
| // setPanelVisible(true). |
| mManageConferenceUtils.initManageConferencePanel(); // if necessary |
| |
| mManageConferenceUtils.updateManageConferencePanel(connections); |
| |
| // The "Manage conference" UI takes up the full main frame, |
| // replacing the CallCard PopupWindow. |
| mManageConferenceUtils.setPanelVisible(true); |
| |
| // Start the chronometer. |
| // TODO: Similarly, we shouldn't expose startConferenceTime() |
| // and stopConferenceTime(); the ManageConferenceUtils |
| // class ought to manage the conferenceTime widget itself |
| // based on setPanelVisible() calls. |
| |
| // Note: there is active Fg call since we are in conference call |
| long callDuration = |
| mCM.getActiveFgCall().getEarliestConnection().getDurationMillis(); |
| mManageConferenceUtils.startConferenceTime( |
| SystemClock.elapsedRealtime() - callDuration); |
| |
| // No need to close the dialer here, since the Manage |
| // Conference UI will just cover it up anyway. |
| |
| break; |
| |
| case CALL_ENDED: |
| case NORMAL: |
| mManageConferenceUtils.setPanelVisible(false); |
| mManageConferenceUtils.stopConferenceTime(); |
| break; |
| |
| case OTA_NORMAL: |
| mApp.otaUtils.setCdmaOtaInCallScreenUiState( |
| OtaUtils.CdmaOtaInCallScreenUiState.State.NORMAL); |
| break; |
| |
| case OTA_ENDED: |
| mApp.otaUtils.setCdmaOtaInCallScreenUiState( |
| OtaUtils.CdmaOtaInCallScreenUiState.State.ENDED); |
| break; |
| |
| case UNDEFINED: |
| // Set our Activities intent to ACTION_UNDEFINED so |
| // that if we get resumed after we've completed a call |
| // the next call will not cause checkIsOtaCall to |
| // return true. |
| // |
| // TODO(OTASP): update these comments |
| // |
| // With the framework as of October 2009 the sequence below |
| // causes the framework to call onResume, onPause, onNewIntent, |
| // onResume. If we don't call setIntent below then when the |
| // first onResume calls checkIsOtaCall via checkOtaspStateOnResume it will |
| // return true and the Activity will be confused. |
| // |
| // 1) Power up Phone A |
| // 2) Place *22899 call and activate Phone A |
| // 3) Press the power key on Phone A to turn off the display |
| // 4) Call Phone A from Phone B answering Phone A |
| // 5) The screen will be blank (Should be normal InCallScreen) |
| // 6) Hang up the Phone B |
| // 7) Phone A displays the activation screen. |
| // |
| // Step 3 is the critical step to cause the onResume, onPause |
| // onNewIntent, onResume sequence. If step 3 is skipped the |
| // sequence will be onNewIntent, onResume and all will be well. |
| setIntent(new Intent(ACTION_UNDEFINED)); |
| |
| // Cleanup Ota Screen if necessary and set the panel |
| // to VISIBLE. |
| if (mCM.getState() != PhoneConstants.State.OFFHOOK) { |
| if (mApp.otaUtils != null) { |
| mApp.otaUtils.cleanOtaScreen(true); |
| } |
| } else { |
| log("WARNING: Setting mode to UNDEFINED but phone is OFFHOOK," |
| + " skip cleanOtaScreen."); |
| } |
| break; |
| } |
| } |
| |
| /** |
| * @return true if the "Manage conference" UI is currently visible. |
| */ |
| /* package */ boolean isManageConferenceMode() { |
| return (mApp.inCallUiState.inCallScreenMode == InCallScreenMode.MANAGE_CONFERENCE); |
| } |
| |
| /** |
| * Checks if the "Manage conference" UI needs to be updated. |
| * If the state of the current conference call has changed |
| * since our previous call to updateManageConferencePanel()), |
| * do a fresh update. Also, if the current call is no longer a |
| * conference call at all, bail out of the "Manage conference" UI and |
| * return to InCallScreenMode.NORMAL mode. |
| */ |
| private void updateManageConferencePanelIfNecessary() { |
| if (VDBG) log("updateManageConferencePanelIfNecessary: " + mCM.getActiveFgCall() + "..."); |
| |
| List<Connection> connections = mCM.getFgCallConnections(); |
| if (connections == null) { |
| if (VDBG) log("==> no connections on foreground call!"); |
| // Hide the Manage Conference panel, return to NORMAL mode. |
| setInCallScreenMode(InCallScreenMode.NORMAL); |
| SyncWithPhoneStateStatus status = syncWithPhoneState(); |
| if (status != SyncWithPhoneStateStatus.SUCCESS) { |
| Log.w(LOG_TAG, "- syncWithPhoneState failed! status = " + status); |
| // We shouldn't even be in the in-call UI in the first |
| // place, so bail out: |
| if (DBG) log("updateManageConferencePanelIfNecessary: endInCallScreenSession... 1"); |
| endInCallScreenSession(); |
| return; |
| } |
| return; |
| } |
| |
| int numConnections = connections.size(); |
| if (numConnections <= 1) { |
| if (VDBG) log("==> foreground call no longer a conference!"); |
| // Hide the Manage Conference panel, return to NORMAL mode. |
| setInCallScreenMode(InCallScreenMode.NORMAL); |
| SyncWithPhoneStateStatus status = syncWithPhoneState(); |
| if (status != SyncWithPhoneStateStatus.SUCCESS) { |
| Log.w(LOG_TAG, "- syncWithPhoneState failed! status = " + status); |
| // We shouldn't even be in the in-call UI in the first |
| // place, so bail out: |
| if (DBG) log("updateManageConferencePanelIfNecessary: endInCallScreenSession... 2"); |
| endInCallScreenSession(); |
| return; |
| } |
| return; |
| } |
| |
| // TODO: the test to see if numConnections has changed can go in |
| // updateManageConferencePanel(), rather than here. |
| if (numConnections != mManageConferenceUtils.getNumCallersInConference()) { |
| if (VDBG) log("==> Conference size has changed; need to rebuild UI!"); |
| mManageConferenceUtils.updateManageConferencePanel(connections); |
| } |
| } |
| |
| /** |
| * Updates {@link #mCallCard}'s visibility state per DTMF dialpad visibility. They |
| * cannot be shown simultaneously and thus we should reflect DTMF dialpad visibility into |
| * another. |
| * |
| * Note: During OTA calls or users' managing conference calls, we should *not* call this method |
| * but manually manage both visibility. |
| * |
| * @see #updateScreen() |
| */ |
| private void updateCallCardVisibilityPerDialerState(boolean animate) { |
| // We need to hide the CallCard while the dialpad is visible. |
| if (isDialerOpened()) { |
| if (VDBG) { |
| log("- updateCallCardVisibilityPerDialerState(animate=" |
| + animate + "): dialpad open, hide mCallCard..."); |
| } |
| if (animate) { |
| AnimationUtils.Fade.hide(mCallCard, View.GONE); |
| } else { |
| mCallCard.setVisibility(View.GONE); |
| } |
| } else { |
| // Dialpad is dismissed; bring back the CallCard if it's supposed to be visible. |
| if ((mApp.inCallUiState.inCallScreenMode == InCallScreenMode.NORMAL) |
| || (mApp.inCallUiState.inCallScreenMode == InCallScreenMode.CALL_ENDED)) { |
| if (VDBG) { |
| log("- updateCallCardVisibilityPerDialerState(animate=" |
| + animate + "): dialpad dismissed, show mCallCard..."); |
| } |
| if (animate) { |
| AnimationUtils.Fade.show(mCallCard); |
| } else { |
| mCallCard.setVisibility(View.VISIBLE); |
| } |
| } |
| } |
| } |
| |
| /** |
| * @see DTMFTwelveKeyDialer#isOpened() |
| */ |
| /* package */ boolean isDialerOpened() { |
| return (mDialer != null && mDialer.isOpened()); |
| } |
| |
| /** |
| * Called any time the DTMF dialpad is opened. |
| * @see DTMFTwelveKeyDialer#openDialer(boolean) |
| */ |
| /* package */ void onDialerOpen(boolean animate) { |
| if (DBG) log("onDialerOpen()..."); |
| |
| // Update the in-call touch UI. |
| updateInCallTouchUi(); |
| |
| // Update CallCard UI, which depends on the dialpad. |
| updateCallCardVisibilityPerDialerState(animate); |
| |
| // This counts as explicit "user activity". |
| mApp.pokeUserActivity(); |
| |
| //If on OTA Call, hide OTA Screen |
| // TODO: This may not be necessary, now that the dialpad is |
| // always visible in OTA mode. |
| if ((mApp.inCallUiState.inCallScreenMode == InCallScreenMode.OTA_NORMAL |
| || mApp.inCallUiState.inCallScreenMode == InCallScreenMode.OTA_ENDED) |
| && mApp.otaUtils != null) { |
| mApp.otaUtils.hideOtaScreen(); |
| } |
| } |
| |
| /** |
| * Called any time the DTMF dialpad is closed. |
| * @see DTMFTwelveKeyDialer#closeDialer(boolean) |
| */ |
| /* package */ void onDialerClose(boolean animate) { |
| if (DBG) log("onDialerClose()..."); |
| |
| // OTA-specific cleanup upon closing the dialpad. |
| if ((mApp.inCallUiState.inCallScreenMode == InCallScreenMode.OTA_NORMAL) |
| || (mApp.inCallUiState.inCallScreenMode == InCallScreenMode.OTA_ENDED) |
| || ((mApp.cdmaOtaScreenState != null) |
| && (mApp.cdmaOtaScreenState.otaScreenState == |
| CdmaOtaScreenState.OtaScreenState.OTA_STATUS_ACTIVATION))) { |
| if (mApp.otaUtils != null) { |
| mApp.otaUtils.otaShowProperScreen(); |
| } |
| } |
| |
| // Update the in-call touch UI. |
| updateInCallTouchUi(); |
| |
| // Update CallCard UI, which depends on the dialpad. |
| updateCallCardVisibilityPerDialerState(animate); |
| |
| // This counts as explicit "user activity". |
| mApp.pokeUserActivity(); |
| } |
| |
| /** |
| * Determines when we can dial DTMF tones. |
| */ |
| /* package */ boolean okToDialDTMFTones() { |
| final boolean hasRingingCall = mCM.hasActiveRingingCall(); |
| final Call.State fgCallState = mCM.getActiveFgCallState(); |
| |
| // We're allowed to send DTMF tones when there's an ACTIVE |
| // foreground call, and not when an incoming call is ringing |
| // (since DTMF tones are useless in that state), or if the |
| // Manage Conference UI is visible (since the tab interferes |
| // with the "Back to call" button.) |
| |
| // We can also dial while in ALERTING state because there are |
| // some connections that never update to an ACTIVE state (no |
| // indication from the network). |
| boolean canDial = |
| (fgCallState == Call.State.ACTIVE || fgCallState == Call.State.ALERTING) |
| && !hasRingingCall |
| && (mApp.inCallUiState.inCallScreenMode != InCallScreenMode.MANAGE_CONFERENCE); |
| |
| if (VDBG) log ("[okToDialDTMFTones] foreground state: " + fgCallState + |
| ", ringing state: " + hasRingingCall + |
| ", call screen mode: " + mApp.inCallUiState.inCallScreenMode + |
| ", result: " + canDial); |
| |
| return canDial; |
| } |
| |
| /** |
| * @return true if the in-call DTMF dialpad should be available to the |
| * user, given the current state of the phone and the in-call UI. |
| * (This is used to control the enabledness of the "Show |
| * dialpad" onscreen button; see InCallControlState.dialpadEnabled.) |
| */ |
| /* package */ boolean okToShowDialpad() { |
| // Very similar to okToDialDTMFTones(), but allow DIALING here. |
| final Call.State fgCallState = mCM.getActiveFgCallState(); |
| return okToDialDTMFTones() || (fgCallState == Call.State.DIALING); |
| } |
| |
| /** |
| * Initializes the in-call touch UI on devices that need it. |
| */ |
| private void initInCallTouchUi() { |
| if (DBG) log("initInCallTouchUi()..."); |
| // TODO: we currently use the InCallTouchUi widget in at least |
| // some states on ALL platforms. But if some devices ultimately |
| // end up not using *any* onscreen touch UI, we should make sure |
| // to not even inflate the InCallTouchUi widget on those devices. |
| mInCallTouchUi = (InCallTouchUi) findViewById(R.id.inCallTouchUi); |
| mInCallTouchUi.setInCallScreenInstance(this); |
| |
| // RespondViaSmsManager implements the "Respond via SMS" |
| // feature that's triggered from the incoming call widget. |
| mRespondViaSmsManager = new RespondViaSmsManager(); |
| mRespondViaSmsManager.setInCallScreenInstance(this); |
| } |
| |
| /** |
| * Updates the state of the in-call touch UI. |
| */ |
| private void updateInCallTouchUi() { |
| if (mInCallTouchUi != null) { |
| mInCallTouchUi.updateState(mCM); |
| } |
| } |
| |
| /** |
| * @return the InCallTouchUi widget |
| */ |
| /* package */ InCallTouchUi getInCallTouchUi() { |
| return mInCallTouchUi; |
| } |
| |
| /** |
| * Posts a handler message telling the InCallScreen to refresh the |
| * onscreen in-call UI. |
| * |
| * This is just a wrapper around updateScreen(), for use by the |
| * rest of the phone app or from a thread other than the UI thread. |
| * |
| * updateScreen() is a no-op if the InCallScreen is not the foreground |
| * activity, so it's safe to call this whether or not the InCallScreen |
| * is currently visible. |
| */ |
| /* package */ void requestUpdateScreen() { |
| if (DBG) log("requestUpdateScreen()..."); |
| mHandler.removeMessages(REQUEST_UPDATE_SCREEN); |
| mHandler.sendEmptyMessage(REQUEST_UPDATE_SCREEN); |
| } |
| |
| /** |
| * @return true if we're in restricted / emergency dialing only mode. |
| */ |
| public boolean isPhoneStateRestricted() { |
| // TODO: This needs to work IN TANDEM with the KeyGuardViewMediator Code. |
| // Right now, it looks like the mInputRestricted flag is INTERNAL to the |
| // KeyGuardViewMediator and SPECIFICALLY set to be FALSE while the emergency |
| // phone call is being made, to allow for input into the InCallScreen. |
| // Having the InCallScreen judge the state of the device from this flag |
| // becomes meaningless since it is always false for us. The mediator should |
| // have an additional API to let this app know that it should be restricted. |
| int serviceState = mCM.getServiceState(); |
| return ((serviceState == ServiceState.STATE_EMERGENCY_ONLY) || |
| (serviceState == ServiceState.STATE_OUT_OF_SERVICE) || |
| (mApp.getKeyguardManager().inKeyguardRestrictedInputMode())); |
| } |
| |
| |
| // |
| // Bluetooth helper methods. |
| // |
| // - BluetoothAdapter is the Bluetooth system service. If |
| // getDefaultAdapter() returns null |
| // then the device is not BT capable. Use BluetoothDevice.isEnabled() |
| // to see if BT is enabled on the device. |
| // |
| // - BluetoothHeadset is the API for the control connection to a |
| // Bluetooth Headset. This lets you completely connect/disconnect a |
| // headset (which we don't do from the Phone UI!) but also lets you |
| // get the address of the currently active headset and see whether |
| // it's currently connected. |
| |
| /** |
| * @return true if the Bluetooth on/off switch in the UI should be |
| * available to the user (i.e. if the device is BT-capable |
| * and a headset is connected.) |
| */ |
| /* package */ boolean isBluetoothAvailable() { |
| if (VDBG) log("isBluetoothAvailable()..."); |
| |
| // There's no need to ask the Bluetooth system service if BT is enabled: |
| // |
| // BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter(); |
| // if ((adapter == null) || !adapter.isEnabled()) { |
| // if (DBG) log(" ==> FALSE (BT not enabled)"); |
| // return false; |
| // } |
| // if (DBG) log(" - BT enabled! device name " + adapter.getName() |
| // + ", address " + adapter.getAddress()); |
| // |
| // ...since we already have a BluetoothHeadset instance. We can just |
| // call isConnected() on that, and assume it'll be false if BT isn't |
| // enabled at all. |
| |
| // Check if there's a connected headset, using the BluetoothHeadset API. |
| boolean isConnected = false; |
| if (mBluetoothHeadset != null) { |
| List<BluetoothDevice> deviceList = mBluetoothHeadset.getConnectedDevices(); |
| |
| if (deviceList.size() > 0) { |
| BluetoothDevice device = deviceList.get(0); |
| isConnected = true; |
| |
| if (VDBG) log(" - headset state = " + |
| mBluetoothHeadset.getConnectionState(device)); |
| if (VDBG) log(" - headset address: " + device); |
| if (VDBG) log(" - isConnected: " + isConnected); |
| } |
| } |
| |
| if (VDBG) log(" ==> " + isConnected); |
| return isConnected; |
| } |
| |
| /** |
| * @return true if a BT Headset is available, and its audio is currently connected. |
| */ |
| /* package */ boolean isBluetoothAudioConnected() { |
| if (mBluetoothHeadset == null) { |
| if (VDBG) log("isBluetoothAudioConnected: ==> FALSE (null mBluetoothHeadset)"); |
| return false; |
| } |
| List<BluetoothDevice> deviceList = mBluetoothHeadset.getConnectedDevices(); |
| |
| if (deviceList.isEmpty()) { |
| return false; |
| } |
| BluetoothDevice device = deviceList.get(0); |
| boolean isAudioOn = mBluetoothHeadset.isAudioConnected(device); |
| if (VDBG) log("isBluetoothAudioConnected: ==> isAudioOn = " + isAudioOn); |
| return isAudioOn; |
| } |
| |
| /** |
| * Helper method used to control the onscreen "Bluetooth" indication; |
| * see InCallControlState.bluetoothIndicatorOn. |
| * |
| * @return true if a BT device is available and its audio is currently connected, |
| * <b>or</b> if we issued a BluetoothHeadset.connectAudio() |
| * call within the last 5 seconds (which presumably means |
| * that the BT audio connection is currently being set |
| * up, and will be connected soon.) |
| */ |
| /* package */ boolean isBluetoothAudioConnectedOrPending() { |
| if (isBluetoothAudioConnected()) { |
| if (VDBG) log("isBluetoothAudioConnectedOrPending: ==> TRUE (really connected)"); |
| return true; |
| } |
| |
| // If we issued a connectAudio() call "recently enough", even |
| // if BT isn't actually connected yet, let's still pretend BT is |
| // on. This makes the onscreen indication more responsive. |
| if (mBluetoothConnectionPending) { |
| long timeSinceRequest = |
| SystemClock.elapsedRealtime() - mBluetoothConnectionRequestTime; |
| if (timeSinceRequest < 5000 /* 5 seconds */) { |
| if (VDBG) log("isBluetoothAudioConnectedOrPending: ==> TRUE (requested " |
| + timeSinceRequest + " msec ago)"); |
| return true; |
| } else { |
| if (VDBG) log("isBluetoothAudioConnectedOrPending: ==> FALSE (request too old: " |
| + timeSinceRequest + " msec ago)"); |
| mBluetoothConnectionPending = false; |
| return false; |
| } |
| } |
| |
| if (VDBG) log("isBluetoothAudioConnectedOrPending: ==> FALSE"); |
| return false; |
| } |
| |
| /** |
| * Posts a message to our handler saying to update the onscreen UI |
| * based on a bluetooth headset state change. |
| */ |
| /* package */ void requestUpdateBluetoothIndication() { |
| if (VDBG) log("requestUpdateBluetoothIndication()..."); |
| // No need to look at the current state here; any UI elements that |
| // care about the bluetooth state (i.e. the CallCard) get |
| // the necessary state directly from PhoneApp.showBluetoothIndication(). |
| mHandler.removeMessages(REQUEST_UPDATE_BLUETOOTH_INDICATION); |
| mHandler.sendEmptyMessage(REQUEST_UPDATE_BLUETOOTH_INDICATION); |
| } |
| |
| private void dumpBluetoothState() { |
| log("============== dumpBluetoothState() ============="); |
| log("= isBluetoothAvailable: " + isBluetoothAvailable()); |
| log("= isBluetoothAudioConnected: " + isBluetoothAudioConnected()); |
| log("= isBluetoothAudioConnectedOrPending: " + isBluetoothAudioConnectedOrPending()); |
| log("= PhoneApp.showBluetoothIndication: " |
| + mApp.showBluetoothIndication()); |
| log("="); |
| if (mBluetoothAdapter != null) { |
| if (mBluetoothHeadset != null) { |
| List<BluetoothDevice> deviceList = mBluetoothHeadset.getConnectedDevices(); |
| |
| if (deviceList.size() > 0) { |
| BluetoothDevice device = deviceList.get(0); |
| log("= BluetoothHeadset.getCurrentDevice: " + device); |
| log("= BluetoothHeadset.State: " |
| + mBluetoothHeadset.getConnectionState(device)); |
| log("= BluetoothHeadset audio connected: " + |
| mBluetoothHeadset.isAudioConnected(device)); |
| } |
| } else { |
| log("= mBluetoothHeadset is null"); |
| } |
| } else { |
| log("= mBluetoothAdapter is null; device is not BT capable"); |
| } |
| } |
| |
| /* package */ void connectBluetoothAudio() { |
| if (VDBG) log("connectBluetoothAudio()..."); |
| if (mBluetoothHeadset != null) { |
| // TODO(BT) check return |
| mBluetoothHeadset.connectAudio(); |
| } |
| |
| // Watch out: The bluetooth connection doesn't happen instantly; |
| // the connectAudio() call returns instantly but does its real |
| // work in another thread. The mBluetoothConnectionPending flag |
| // is just a little trickery to ensure that the onscreen UI updates |
| // instantly. (See isBluetoothAudioConnectedOrPending() above.) |
| mBluetoothConnectionPending = true; |
| mBluetoothConnectionRequestTime = SystemClock.elapsedRealtime(); |
| } |
| |
| /* package */ void disconnectBluetoothAudio() { |
| if (VDBG) log("disconnectBluetoothAudio()..."); |
| if (mBluetoothHeadset != null) { |
| mBluetoothHeadset.disconnectAudio(); |
| } |
| mBluetoothConnectionPending = false; |
| } |
| |
| /** |
| * Posts a handler message telling the InCallScreen to close |
| * the OTA failure notice after the specified delay. |
| * @see OtaUtils.otaShowProgramFailureNotice |
| */ |
| /* package */ void requestCloseOtaFailureNotice(long timeout) { |
| if (DBG) log("requestCloseOtaFailureNotice() with timeout: " + timeout); |
| mHandler.sendEmptyMessageDelayed(REQUEST_CLOSE_OTA_FAILURE_NOTICE, timeout); |
| |
| // TODO: we probably ought to call removeMessages() for this |
| // message code in either onPause or onResume, just to be 100% |
| // sure that the message we just posted has no way to affect a |
| // *different* call if the user quickly backs out and restarts. |
| // (This is also true for requestCloseSpcErrorNotice() below, and |
| // probably anywhere else we use mHandler.sendEmptyMessageDelayed().) |
| } |
| |
| /** |
| * Posts a handler message telling the InCallScreen to close |
| * the SPC error notice after the specified delay. |
| * @see OtaUtils.otaShowSpcErrorNotice |
| */ |
| /* package */ void requestCloseSpcErrorNotice(long timeout) { |
| if (DBG) log("requestCloseSpcErrorNotice() with timeout: " + timeout); |
| mHandler.sendEmptyMessageDelayed(REQUEST_CLOSE_SPC_ERROR_NOTICE, timeout); |
| } |
| |
| public boolean isOtaCallInActiveState() { |
| if ((mApp.inCallUiState.inCallScreenMode == InCallScreenMode.OTA_NORMAL) |
| || ((mApp.cdmaOtaScreenState != null) |
| && (mApp.cdmaOtaScreenState.otaScreenState == |
| CdmaOtaScreenState.OtaScreenState.OTA_STATUS_ACTIVATION))) { |
| return true; |
| } else { |
| return false; |
| } |
| } |
| |
| /** |
| * Handle OTA Call End scenario when display becomes dark during OTA Call |
| * and InCallScreen is in pause mode. CallNotifier will listen for call |
| * end indication and call this api to handle OTA Call end scenario |
| */ |
| public void handleOtaCallEnd() { |
| if (DBG) log("handleOtaCallEnd entering"); |
| if (((mApp.inCallUiState.inCallScreenMode == InCallScreenMode.OTA_NORMAL) |
| || ((mApp.cdmaOtaScreenState != null) |
| && (mApp.cdmaOtaScreenState.otaScreenState != |
| CdmaOtaScreenState.OtaScreenState.OTA_STATUS_UNDEFINED))) |
| && ((mApp.cdmaOtaProvisionData != null) |
| && (!mApp.cdmaOtaProvisionData.inOtaSpcState))) { |
| if (DBG) log("handleOtaCallEnd - Set OTA Call End stater"); |
| setInCallScreenMode(InCallScreenMode.OTA_ENDED); |
| updateScreen(); |
| } |
| } |
| |
| public boolean isOtaCallInEndState() { |
| return (mApp.inCallUiState.inCallScreenMode == InCallScreenMode.OTA_ENDED); |
| } |
| |
| |
| /** |
| * Upon resuming the in-call UI, check to see if an OTASP call is in |
| * progress, and if so enable the special OTASP-specific UI. |
| * |
| * TODO: have a simple single flag in InCallUiState for this rather than |
| * needing to know about all those mApp.cdma*State objects. |
| * |
| * @return true if any OTASP-related UI is active |
| */ |
| private boolean checkOtaspStateOnResume() { |
| // If there's no OtaUtils instance, that means we haven't even tried |
| // to start an OTASP call (yet), so there's definitely nothing to do here. |
| if (mApp.otaUtils == null) { |
| if (DBG) log("checkOtaspStateOnResume: no OtaUtils instance; nothing to do."); |
| return false; |
| } |
| |
| if ((mApp.cdmaOtaScreenState == null) || (mApp.cdmaOtaProvisionData == null)) { |
| // Uh oh -- something wrong with our internal OTASP state. |
| // (Since this is an OTASP-capable device, these objects |
| // *should* have already been created by PhoneApp.onCreate().) |
| throw new IllegalStateException("checkOtaspStateOnResume: " |
| + "app.cdmaOta* objects(s) not initialized"); |
| } |
| |
| // The PhoneApp.cdmaOtaInCallScreenUiState instance is the |
| // authoritative source saying whether or not the in-call UI should |
| // show its OTASP-related UI. |
| |
| OtaUtils.CdmaOtaInCallScreenUiState.State cdmaOtaInCallScreenState = |
| mApp.otaUtils.getCdmaOtaInCallScreenUiState(); |
| // These states are: |
| // - UNDEFINED: no OTASP-related UI is visible |
| // - NORMAL: OTASP call in progress, so show in-progress OTASP UI |
| // - ENDED: OTASP call just ended, so show success/failure indication |
| |
| boolean otaspUiActive = |
| (cdmaOtaInCallScreenState == OtaUtils.CdmaOtaInCallScreenUiState.State.NORMAL) |
| || (cdmaOtaInCallScreenState == OtaUtils.CdmaOtaInCallScreenUiState.State.ENDED); |
| |
| if (otaspUiActive) { |
| // Make sure the OtaUtils instance knows about the InCallScreen's |
| // OTASP-related UI widgets. |
| // |
| // (This call has no effect if the UI widgets have already been set up. |
| // It only really matters the very first time that the InCallScreen instance |
| // is onResume()d after starting an OTASP call.) |
| mApp.otaUtils.updateUiWidgets(this, mInCallTouchUi, mCallCard); |
| |
| // Also update the InCallScreenMode based on the cdmaOtaInCallScreenState. |
| |
| if (cdmaOtaInCallScreenState == OtaUtils.CdmaOtaInCallScreenUiState.State.NORMAL) { |
| if (DBG) log("checkOtaspStateOnResume - in OTA Normal mode"); |
| setInCallScreenMode(InCallScreenMode.OTA_NORMAL); |
| } else if (cdmaOtaInCallScreenState == |
| OtaUtils.CdmaOtaInCallScreenUiState.State.ENDED) { |
| if (DBG) log("checkOtaspStateOnResume - in OTA END mode"); |
| setInCallScreenMode(InCallScreenMode.OTA_ENDED); |
| } |
| |
| // TODO(OTASP): we might also need to go into OTA_ENDED mode |
| // in one extra case: |
| // |
| // else if (mApp.cdmaOtaScreenState.otaScreenState == |
| // CdmaOtaScreenState.OtaScreenState.OTA_STATUS_SUCCESS_FAILURE_DLG) { |
| // if (DBG) log("checkOtaspStateOnResume - set OTA END Mode"); |
| // setInCallScreenMode(InCallScreenMode.OTA_ENDED); |
| // } |
| |
| } else { |
| // OTASP is not active; reset to regular in-call UI. |
| |
| if (DBG) log("checkOtaspStateOnResume - Set OTA NORMAL Mode"); |
| setInCallScreenMode(InCallScreenMode.OTA_NORMAL); |
| |
| if (mApp.otaUtils != null) { |
| mApp.otaUtils.cleanOtaScreen(false); |
| } |
| } |
| |
| // TODO(OTASP): |
| // The original check from checkIsOtaCall() when handling ACTION_MAIN was this: |
| // |
| // [ . . . ] |
| // else if (action.equals(intent.ACTION_MAIN)) { |
| // if (DBG) log("checkIsOtaCall action ACTION_MAIN"); |
| // boolean isRingingCall = mCM.hasActiveRingingCall(); |
| // if (isRingingCall) { |
| // if (DBG) log("checkIsOtaCall isRingingCall: " + isRingingCall); |
| // return false; |
| // } else if ((mApp.cdmaOtaInCallScreenUiState.state |
| // == CdmaOtaInCallScreenUiState.State.NORMAL) |
| // || (mApp.cdmaOtaInCallScreenUiState.state |
| // == CdmaOtaInCallScreenUiState.State.ENDED)) { |
| // if (DBG) log("action ACTION_MAIN, OTA call already in progress"); |
| // isOtaCall = true; |
| // } else { |
| // if (mApp.cdmaOtaScreenState.otaScreenState != |
| // CdmaOtaScreenState.OtaScreenState.OTA_STATUS_UNDEFINED) { |
| // if (DBG) log("checkIsOtaCall action ACTION_MAIN, " |
| // + "OTA call in progress with UNDEFINED"); |
| // isOtaCall = true; |
| // } |
| // } |
| // } |
| // |
| // Also, in internalResolveIntent() we used to do this: |
| // |
| // if ((mApp.inCallUiState.inCallScreenMode == InCallScreenMode.OTA_NORMAL) |
| // || (mApp.inCallUiState.inCallScreenMode == InCallScreenMode.OTA_ENDED)) { |
| // // If in OTA Call, update the OTA UI |
| // updateScreen(); |
| // return; |
| // } |
| // |
| // We still need more cleanup to simplify the mApp.cdma*State objects. |
| |
| return otaspUiActive; |
| } |
| |
| /** |
| * Updates and returns the InCallControlState instance. |
| */ |
| public InCallControlState getUpdatedInCallControlState() { |
| if (VDBG) log("getUpdatedInCallControlState()..."); |
| mInCallControlState.update(); |
| return mInCallControlState; |
| } |
| |
| public void resetInCallScreenMode() { |
| if (DBG) log("resetInCallScreenMode: setting mode to UNDEFINED..."); |
| setInCallScreenMode(InCallScreenMode.UNDEFINED); |
| } |
| |
| /** |
| * Updates the onscreen hint displayed while the user is dragging one |
| * of the handles of the RotarySelector widget used for incoming |
| * calls. |
| * |
| * @param hintTextResId resource ID of the hint text to display, |
| * or 0 if no hint should be visible. |
| * @param hintColorResId resource ID for the color of the hint text |
| */ |
| /* package */ void updateIncomingCallWidgetHint(int hintTextResId, int hintColorResId) { |
| if (VDBG) log("updateIncomingCallWidgetHint(" + hintTextResId + ")..."); |
| if (mCallCard != null) { |
| mCallCard.setIncomingCallWidgetHint(hintTextResId, hintColorResId); |
| mCallCard.updateState(mCM); |
| // TODO: if hintTextResId == 0, consider NOT clearing the onscreen |
| // hint right away, but instead post a delayed handler message to |
| // keep it onscreen for an extra second or two. (This might make |
| // the hint more helpful if the user quickly taps one of the |
| // handles without dragging at all...) |
| // (Or, maybe this should happen completely within the RotarySelector |
| // widget, since the widget itself probably wants to keep the colored |
| // arrow visible for some extra time also...) |
| } |
| } |
| |
| |
| /** |
| * Used when we need to update buttons outside InCallTouchUi's updateInCallControls() along |
| * with that method being called. CallCard may call this too because it doesn't have |
| * enough information to update buttons inside itself (more specifically, the class cannot |
| * obtain mInCallControllState without some side effect. See also |
| * {@link #getUpdatedInCallControlState()}. We probably don't want a method like |
| * getRawCallControlState() which returns raw intance with no side effect just for this |
| * corner case scenario) |
| * |
| * TODO: need better design for buttons outside InCallTouchUi. |
| */ |
| /* package */ void updateButtonStateOutsideInCallTouchUi() { |
| if (mCallCard != null) { |
| mCallCard.setSecondaryCallClickable(mInCallControlState.canSwap); |
| } |
| } |
| |
| @Override |
| public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) { |
| super.dispatchPopulateAccessibilityEvent(event); |
| mCallCard.dispatchPopulateAccessibilityEvent(event); |
| return true; |
| } |
| |
| /** |
| * Manually handle configuration changes. |
| * |
| * Originally android:configChanges was set to "orientation|keyboardHidden|uiMode" |
| * in order "to make sure the system doesn't destroy and re-create us due to the |
| * above config changes". However it is currently set to "keyboardHidden" since |
| * the system needs to handle rotation when inserted into a compatible cardock. |
| * Even without explicitly handling orientation and uiMode, the app still runs |
| * and does not drop the call when rotated. |
| * |
| */ |
| public void onConfigurationChanged(Configuration newConfig) { |
| if (DBG) log("onConfigurationChanged: newConfig = " + newConfig); |
| |
| // Note: At the time this function is called, our Resources object |
| // will have already been updated to return resource values matching |
| // the new configuration. |
| |
| // Watch out: we *can* still get destroyed and recreated if a |
| // configuration change occurs that is *not* listed in the |
| // android:configChanges attribute. TODO: Any others we need to list? |
| |
| super.onConfigurationChanged(newConfig); |
| |
| // Nothing else to do here, since (currently) the InCallScreen looks |
| // exactly the same regardless of configuration. |
| // (Specifically, we'll never be in landscape mode because we set |
| // android:screenOrientation="portrait" in our manifest, and we don't |
| // change our UI at all based on newConfig.keyboardHidden or |
| // newConfig.uiMode.) |
| |
| // TODO: we do eventually want to handle at least some config changes, such as: |
| boolean isKeyboardOpen = (newConfig.keyboardHidden == Configuration.KEYBOARDHIDDEN_NO); |
| if (DBG) log(" - isKeyboardOpen = " + isKeyboardOpen); |
| boolean isLandscape = (newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE); |
| if (DBG) log(" - isLandscape = " + isLandscape); |
| if (DBG) log(" - uiMode = " + newConfig.uiMode); |
| // See bug 2089513. |
| } |
| |
| /** |
| * Handles an incoming RING event from the telephony layer. |
| */ |
| private void onIncomingRing() { |
| if (DBG) log("onIncomingRing()..."); |
| // IFF we're visible, forward this event to the InCallTouchUi |
| // instance (which uses this event to drive the animation of the |
| // incoming-call UI.) |
| if (mIsForegroundActivity && (mInCallTouchUi != null)) { |
| mInCallTouchUi.onIncomingRing(); |
| } |
| } |
| |
| /** |
| * Handles a "new ringing connection" event from the telephony layer. |
| * |
| * This event comes in right at the start of the incoming-call sequence, |
| * exactly once per incoming call. |
| * |
| * Watch out: this won't be called if InCallScreen isn't ready yet, |
| * which typically happens for the first incoming phone call (even before |
| * the possible first outgoing call). |
| */ |
| private void onNewRingingConnection() { |
| if (DBG) log("onNewRingingConnection()..."); |
| |
| // We use this event to reset any incoming-call-related UI elements |
| // that might have been left in an inconsistent state after a prior |
| // incoming call. |
| // (Note we do this whether or not we're the foreground activity, |
| // since this event comes in *before* we actually get launched to |
| // display the incoming-call UI.) |
| |
| // If there's a "Respond via SMS" popup still around since the |
| // last time we were the foreground activity, make sure it's not |
| // still active(!) since that would interfere with *this* incoming |
| // call. |
| // (Note that we also do this same check in onResume(). But we |
| // need it here too, to make sure the popup gets reset in the case |
| // where a call-waiting call comes in while the InCallScreen is |
| // already in the foreground.) |
| mRespondViaSmsManager.dismissPopup(); // safe even if already dismissed |
| } |
| |
| /** |
| * Enables or disables the status bar "window shade" based on the current situation. |
| */ |
| private void updateExpandedViewState() { |
| if (mIsForegroundActivity) { |
| if (mApp.proximitySensorModeEnabled()) { |
| // We should not enable notification's expanded view on RINGING state. |
| mApp.notificationMgr.statusBarHelper.enableExpandedView( |
| mCM.getState() != PhoneConstants.State.RINGING); |
| } else { |
| // If proximity sensor is unavailable on the device, disable it to avoid false |
| // touches toward notifications. |
| mApp.notificationMgr.statusBarHelper.enableExpandedView(false); |
| } |
| } else { |
| mApp.notificationMgr.statusBarHelper.enableExpandedView(true); |
| } |
| } |
| |
| private void log(String msg) { |
| Log.d(LOG_TAG, msg); |
| } |
| |
| /** |
| * Requests to remove provider info frame after having |
| * {@link #PROVIDER_INFO_TIMEOUT}) msec delay. |
| */ |
| /* package */ void requestRemoveProviderInfoWithDelay() { |
| // Remove any zombie messages and then send a message to |
| // self to remove the provider info after some time. |
| mHandler.removeMessages(EVENT_HIDE_PROVIDER_INFO); |
| Message msg = Message.obtain(mHandler, EVENT_HIDE_PROVIDER_INFO); |
| mHandler.sendMessageDelayed(msg, PROVIDER_INFO_TIMEOUT); |
| if (DBG) { |
| log("Requested to remove provider info after " + PROVIDER_INFO_TIMEOUT + " msec."); |
| } |
| } |
| |
| /** |
| * Indicates whether or not the QuickResponseDialog is currently showing in the call screen |
| */ |
| public boolean isQuickResponseDialogShowing() { |
| return mRespondViaSmsManager != null && mRespondViaSmsManager.isShowingPopup(); |
| } |
| } |