blob: 116f254ae3f2b5c1087e26d9500590aae9705bab [file] [log] [blame]
/*
* Copyright (C) 2006 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.phone;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.Dialog;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothHeadset;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.DialogInterface.OnCancelListener;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.res.Resources;
import android.graphics.PixelFormat;
import android.graphics.Typeface;
import android.graphics.drawable.Drawable;
import android.media.AudioManager;
import android.net.Uri;
import android.os.AsyncResult;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.os.SystemClock;
import android.os.SystemProperties;
import android.provider.Settings;
import android.telephony.PhoneNumberUtils;
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.Menu;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.ViewGroup;
import android.view.Window;
import android.view.WindowManager;
import android.view.accessibility.AccessibilityEvent;
import android.view.animation.Animation;
import android.view.animation.AnimationUtils;
import android.widget.EditText;
import android.widget.LinearLayout;
import android.widget.SlidingDrawer;
import android.widget.TextView;
import android.widget.Toast;
import com.android.internal.telephony.Call;
import com.android.internal.telephony.Connection;
import com.android.internal.telephony.MmiCode;
import com.android.internal.telephony.Phone;
import com.android.phone.OtaUtils.CdmaOtaInCallScreenUiState;
import com.android.phone.OtaUtils.CdmaOtaScreenState;
import java.util.List;
/**
* Phone app "in call" screen.
*/
public class InCallScreen extends Activity
implements View.OnClickListener, View.OnTouchListener {
private static final String LOG_TAG = "InCallScreen";
private static final boolean DBG =
(PhoneApp.DBG_LEVEL >= 1) && (SystemProperties.getInt("ro.debuggable", 0) == 1);
private static final boolean VDBG = (PhoneApp.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
// Amount of time (in msec) that we keep the in-call menu onscreen
// *after* the user changes the state of one of the toggle buttons.
private static final int MENU_DISMISS_DELAY = 1000; // 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
// The "touch lock" overlay timeout comes from Gservices; this is the default.
private static final int TOUCH_LOCK_DELAY_DEFAULT = 6000; // msec
// Amount of time for Displaying "Dialing" for 3way Calling origination
private static final int THREEWAY_CALLERINFO_DISPLAY_TIME = 3000; // msec
// Amount of time that we display the provider's overlay if applicable.
private static final int PROVIDER_OVERLAY_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 DISMISS_MENU = 111;
private static final int ALLOW_SCREEN_ON = 112;
private static final int TOUCH_LOCK_TIMER = 113;
private static final int REQUEST_UPDATE_BLUETOOTH_INDICATION = 114;
private static final int PHONE_CDMA_CALL_WAITING = 115;
private static final int THREEWAY_CALLERINFO_DISPLAY_DONE = 116;
private static final int EVENT_OTA_PROVISION_CHANGE = 117;
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_OVERLAY = 121; // Time to remove the overlay.
private static final int REQUEST_UPDATE_TOUCH_UI = 122;
//following constants are used for OTA Call
public static final String ACTION_SHOW_ACTIVATION =
"com.android.phone.InCallScreen.SHOW_ACTIVATION";
public static final String OTA_NUMBER = "*228";
public static final String EXTRA_OTA_CALL = "android.phone.extra.OTA_CALL";
// 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";
// High-level "modes" of the in-call UI.
private enum InCallScreenMode {
/**
* Normal in-call UI elements visible.
*/
NORMAL,
/**
* "Manage conference" UI is visible, totally replacing the
* normal in-call UI.
*/
MANAGE_CONFERENCE,
/**
* Non-interactive UI state. Call card is visible,
* displaying information about the call that just ended.
*/
CALL_ENDED,
/**
* Normal OTA in-call UI elements visible.
*/
OTA_NORMAL,
/**
* OTA call ended UI visible, replacing normal OTA in-call UI.
*/
OTA_ENDED,
/**
* Default state when not on call
*/
UNDEFINED
}
private InCallScreenMode mInCallScreenMode = InCallScreenMode.UNDEFINED;
// Possible error conditions that can happen on startup.
// These are returned as status codes from the various helper
// functions we call from onCreate() and/or onResume().
// See syncWithPhoneState() and checkIfOkToInitiateOutgoingCall() for details.
private enum InCallInitStatus {
SUCCESS,
VOICEMAIL_NUMBER_MISSING,
POWER_OFF,
EMERGENCY_ONLY,
OUT_OF_SERVICE,
PHONE_NOT_IN_USE,
NO_PHONE_NUMBER_SUPPLIED,
DIALED_MMI,
CALL_FAILED
}
private InCallInitStatus mInCallInitialStatus; // see onResume()
private boolean mRegisteredForPhoneStates;
private boolean mNeedShowCallLostDialog;
private Phone mPhone;
private Call mForegroundCall;
private Call mBackgroundCall;
private Call mRingingCall;
private BluetoothHandsfree mBluetoothHandsfree;
private BluetoothHeadset mBluetoothHeadset;
private boolean mBluetoothConnectionPending;
private long mBluetoothConnectionRequestTime;
// Main in-call UI ViewGroups
private ViewGroup mMainFrame;
private ViewGroup mInCallPanel;
// Main in-call UI elements:
private CallCard mCallCard;
// UI controls:
private InCallControlState mInCallControlState;
private InCallMenu mInCallMenu; // used on some devices
private InCallTouchUi mInCallTouchUi; // used on some devices
private ManageConferenceUtils mManageConferenceUtils;
// DTMF Dialer controller and its view:
private DTMFTwelveKeyDialer mDialer;
private DTMFTwelveKeyDialerView mDialerView;
// TODO: Move these providers related fields in their own class.
// Optional overlay when a 3rd party provider is used.
private boolean mProviderOverlayVisible = false;
private CharSequence mProviderLabel;
private Drawable mProviderIcon;
private Uri mProviderGatewayUri;
// The formated address extracted from mProviderGatewayUri. User visible.
private String mProviderAddress;
// For OTA Call
public OtaUtils otaUtils;
private EditText mWildPromptText;
// "Touch lock overlay" feature
private boolean mUseTouchLockOverlay; // True if we use this feature on the current device
private View mTouchLockOverlay; // The overlay over the whole screen
private View mTouchLockIcon; // The "lock" icon in the middle of the screen
private Animation mTouchLockFadeIn;
private long mTouchLockLastTouchTime; // in SystemClock.uptimeMillis() time base
// 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;
// NOTE: if you add a new dialog here, be sure to add it to dismissAllDialogs() also.
// 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;
// For use with CDMA Pause/Wait dialogs
private String mPostDialStrAfterPause;
private boolean mPauseInProgress = false;
// Flag indicating whether or not we should bring up the Call Log when
// exiting the in-call UI due to the Phone becoming idle. (This is
// true if the most recently disconnected Call was initiated by the
// user, or false if it was an incoming call.)
// This flag is used by delayedCleanupAfterDisconnect(), and is set by
// onDisconnect() (which is the only place that either posts a
// DELAYED_CLEANUP_AFTER_DISCONNECT event *or* calls
// delayedCleanupAfterDisconnect() directly.)
private boolean mShowCallLogAfterDisconnect;
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...
}
PhoneApp app = PhoneApp.getInstance();
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 (in
// particular the "Speaker" menu button) 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 menu item. (But even a full updateScreen()
// is still pretty cheap, so let's keep this simple
// for now.)
if (!isBluetoothAudioConnected()) {
if (msg.arg1 == 1) {
// If the dialpad is open, we need to start the timer that will
// eventually bring up the "touch lock" overlay.
if (mDialer.isOpened() && !isTouchLocked()) {
resetTouchLockTimer();
}
}
}
updateScreen();
break;
case PhoneApp.MMI_INITIATE:
onMMIInitiate((AsyncResult) msg.obj);
break;
case PhoneApp.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 PhoneApp.MMI_COMPLETE:
// Check the code to see if the request is ready to
// finish, this includes any MMI state that is not
// PENDING.
MmiCode mmiCode = (MmiCode) ((AsyncResult) msg.obj).result;
// if phone is a CDMA phone display feature code completed message
int phoneType = mPhone.getPhoneType();
if (phoneType == Phone.PHONE_TYPE_CDMA) {
PhoneUtils.displayMMIComplete(mPhone, app, mmiCode, null, null);
} else if (phoneType == Phone.PHONE_TYPE_GSM) {
if (mmiCode.getState() != MmiCode.State.PENDING) {
if (DBG) log("Got MMI_COMPLETE, finishing InCallScreen...");
endInCallScreenSession();
}
}
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 DISMISS_MENU:
// dismissMenu() has no effect if the menu is already closed.
dismissMenu(true); // dismissImmediate = true
break;
case ALLOW_SCREEN_ON:
if (VDBG) log("ALLOW_SCREEN_ON message...");
// Undo our previous call to preventScreenOn(true).
// (Note this will cause the screen to turn on
// immediately, if it's currently off because of a
// prior preventScreenOn(true) call.)
app.preventScreenOn(false);
break;
case TOUCH_LOCK_TIMER:
if (VDBG) log("TOUCH_LOCK_TIMER...");
touchLockTimerExpired();
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 = mRingingCall.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();
app.updateWakeState();
}
break;
case THREEWAY_CALLERINFO_DISPLAY_DONE:
if (DBG) log("Received THREEWAY_CALLERINFO_DISPLAY_DONE event ...");
if (app.cdmaPhoneCallState.getCurrentCallState()
== CdmaPhoneCallState.PhoneCallState.THRWAY_ACTIVE) {
// Set the mThreeWayCallOrigStateDialing state to true
app.cdmaPhoneCallState.setThreeWayCallOrigState(false);
//Finally update screen with with the current on going call
updateScreen();
}
break;
case EVENT_OTA_PROVISION_CHANGE:
if (otaUtils != null) {
otaUtils.onOtaProvisionStatusChanged((AsyncResult) msg.obj);
}
break;
case REQUEST_CLOSE_SPC_ERROR_NOTICE:
if (otaUtils != null) {
otaUtils.onOtaCloseSpcNotice();
}
break;
case REQUEST_CLOSE_OTA_FAILURE_NOTICE:
if (otaUtils != null) {
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_OVERLAY:
mProviderOverlayVisible = false;
updateProviderOverlay(); // Clear the overlay.
break;
case REQUEST_UPDATE_TOUCH_UI:
updateInCallTouchUi();
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) {
if (DBG) log("onCreate()... this = " + this);
Profiler.callScreenOnCreate();
super.onCreate(icicle);
final PhoneApp app = PhoneApp.getInstance();
app.setInCallScreenInstance(this);
// set this flag so this activity will stay in front of the keyguard
int flags = WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED;
if (app.getPhoneState() == Phone.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;
}
getWindow().addFlags(flags);
setPhone(app.phone); // Sets mPhone and mForegroundCall/mBackgroundCall/mRingingCall
mBluetoothHandsfree = app.getBluetoothHandsfree();
if (VDBG) log("- mBluetoothHandsfree: " + mBluetoothHandsfree);
if (mBluetoothHandsfree != null) {
// The PhoneApp only creates a BluetoothHandsfree instance in the
// first place if BluetoothAdapter.getDefaultAdapter()
// succeeds. So at this point we know the device is BT-capable.
mBluetoothHeadset = new BluetoothHeadset(this, null);
if (VDBG) log("- Got BluetoothHeadset: " + mBluetoothHeadset);
}
requestWindowFeature(Window.FEATURE_NO_TITLE);
// Inflate everything in incall_screen.xml and add it to the screen.
setContentView(R.layout.incall_screen);
initInCallScreen();
// Create the dtmf dialer. The dialer view we use depends on the
// current platform:
//
// - On non-prox-sensor devices, it's the dialpad contained inside
// a SlidingDrawer widget (see dtmf_twelve_key_dialer.xml).
//
// - On "full touch UI" devices, it's the compact non-sliding
// dialpad that appears on the upper half of the screen,
// above the main cluster of InCallTouchUi buttons
// (see non_drawer_dialpad.xml).
//
// TODO: These should both be ViewStubs, and right here we should
// inflate one or the other. (Also, while doing that, let's also
// move this block of code over to initInCallScreen().)
//
SlidingDrawer dialerDrawer;
if (isTouchUiEnabled()) {
// This is a "full touch" device.
mDialerView = (DTMFTwelveKeyDialerView) findViewById(R.id.non_drawer_dtmf_dialer);
if (DBG) log("- Full touch device! Found dialerView: " + mDialerView);
dialerDrawer = null; // No SlidingDrawer used on this device.
} else {
// Use the old-style dialpad contained within the SlidingDrawer.
mDialerView = (DTMFTwelveKeyDialerView) findViewById(R.id.dtmf_dialer);
if (DBG) log("- Using SlidingDrawer-based dialpad. Found dialerView: " + mDialerView);
dialerDrawer = (SlidingDrawer) findViewById(R.id.dialer_container);
if (DBG) log(" ...and the SlidingDrawer: " + dialerDrawer);
}
// Sanity-check that (regardless of the device) at least the
// dialer view is present:
if (mDialerView == null) {
Log.e(LOG_TAG, "onCreate: couldn't find dialerView", new IllegalStateException());
}
// Finally, create the DTMFTwelveKeyDialer instance.
mDialer = new DTMFTwelveKeyDialer(this, mDialerView, dialerDrawer);
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...");
// Stash the result code from internalResolveIntent() in the
// mInCallInitialStatus field. If it's an error code, we'll
// handle it in onResume().
mInCallInitialStatus = internalResolveIntent(getIntent());
if (DBG) log("onCreate(): mInCallInitialStatus = " + mInCallInitialStatus);
if (mInCallInitialStatus != InCallInitStatus.SUCCESS) {
Log.w(LOG_TAG, "onCreate: status " + mInCallInitialStatus
+ " from internalResolveIntent()");
// See onResume() for the actual error handling.
}
} else {
mInCallInitialStatus = InCallInitStatus.SUCCESS;
}
// The "touch lock overlay" feature is used only on devices that
// *don't* use a proximity sensor to turn the screen off while in-call.
mUseTouchLockOverlay = !app.proximitySensorModeEnabled();
Profiler.callScreenCreated();
if (DBG) log("onCreate(): exit");
}
/**
* 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;
// Hang onto the three Call objects too; they're singletons that
// are constant (and never null) for the life of the Phone.
mForegroundCall = mPhone.getForegroundCall();
mBackgroundCall = mPhone.getBackgroundCall();
mRingingCall = mPhone.getRingingCall();
}
@Override
protected void onResume() {
if (DBG) log("onResume()...");
super.onResume();
mIsForegroundActivity = true;
final PhoneApp app = PhoneApp.getInstance();
app.disableStatusBar();
// Touch events are never considered "user activity" while the
// InCallScreen is active, so that unintentional touches won't
// prevent the device from going to sleep.
app.setIgnoreTouchUserActivity(true);
// Disable the status bar "window shade" the entire time we're on
// the in-call screen.
NotificationMgr.getDefault().getStatusBarMgr().enableExpandedView(false);
// 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();
// Check for any failures that happened during onCreate() or onNewIntent().
if (DBG) log("- onResume: initial status = " + mInCallInitialStatus);
boolean handledStartupError = false;
if (mInCallInitialStatus != InCallInitStatus.SUCCESS) {
if (DBG) log("- onResume: failure during startup: " + mInCallInitialStatus);
// Don't bring up the regular Phone UI! Instead bring up
// something more specific to let the user deal with the
// problem.
handleStartupError(mInCallInitialStatus);
handledStartupError = true;
// But it *is* OK to continue with the rest of onResume(),
// since any further setup steps (like updateScreen() and the
// CallCard setup) will fall back to a "blank" state if the
// phone isn't in use.
mInCallInitialStatus = InCallInitStatus.SUCCESS;
}
// 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);
boolean phoneIsCdma = (mPhone.getPhoneType() == Phone.PHONE_TYPE_CDMA);
boolean inOtaCall = false;
if (phoneIsCdma) {
inOtaCall = initOtaState();
}
if (!inOtaCall) {
// Always start off in NORMAL mode
setInCallScreenMode(InCallScreenMode.NORMAL);
}
// Before checking the state of the phone, 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.)
mPhone.clearDisconnected();
InCallInitStatus status = syncWithPhoneState();
if (status != InCallInitStatus.SUCCESS) {
if (DBG) log("- onResume: syncWithPhoneState failed! status = " + status);
// Couldn't update the UI, presumably because the phone is totally
// idle.
if (handledStartupError) {
// Do NOT bail out of the in-call UI, since there's
// presumably a dialog visible right now (see the call to
// handleStartupError() above.)
//
// In this case, stay here for now, and we'll eventually
// leave the InCallScreen when the user presses the
// dialog's OK button (see bailOutAfterErrorDialog()).
if (DBG) log(" ==> 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.
if (DBG) log(" ==> syncWithPhoneState failed; bailing out!");
dismissAllDialogs();
endInCallScreenSession();
return;
}
} else if (phoneIsCdma) {
if (mInCallScreenMode == InCallScreenMode.OTA_NORMAL ||
mInCallScreenMode == InCallScreenMode.OTA_ENDED) {
mDialer.setHandleVisible(false);
if (mInCallPanel != null) mInCallPanel.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.
//
// But we need to do something special if we're coming
// to the foreground while an incoming call is ringing:
if (mPhone.getState() == Phone.State.RINGING) {
// If the phone is ringing, we *should* already be holding a
// full wake lock (which we would have acquired before
// firing off the intent that brought us here; see
// PhoneUtils.showIncomingCallUi().)
//
// We also called preventScreenOn(true) at that point, to
// avoid cosmetic glitches while we were being launched.
// So now we need to post an ALLOW_SCREEN_ON message to
// (eventually) undo the prior preventScreenOn(true) call.
//
// (In principle we shouldn't do this until after our first
// layout/draw pass. But in practice, the delay caused by
// simply waiting for the end of the message queue is long
// enough to avoid any flickering of the lock screen before
// the InCallScreen comes up.)
if (VDBG) log("- posting ALLOW_SCREEN_ON message...");
mHandler.removeMessages(ALLOW_SCREEN_ON);
mHandler.sendEmptyMessage(ALLOW_SCREEN_ON);
// TODO: There ought to be a more elegant way of doing this,
// probably by having the PowerManager and ActivityManager
// work together to let apps request that the screen on/off
// state be synchronized with the Activity lifecycle.
// (See bug 1648751.)
} else {
// The phone isn't ringing; this is either an outgoing call, or
// we're returning to a call in progress. There *shouldn't* be
// any prior preventScreenOn(true) call that we need to undo,
// but let's do this just to be safe:
app.preventScreenOn(false);
}
app.updateWakeState();
// The "touch lock" overlay is NEVER visible when we resume.
// (In particular, this check ensures that we won't still be
// locked after the user wakes up the screen by pressing MENU.)
enableTouchLock(false);
// ...but if the dialpad is open we DO need to start the timer
// that will eventually bring up the "touch lock" overlay.
if (mDialer.isOpened()) resetTouchLockTimer();
// Restore the mute state if the last mute state change was NOT
// done by the user.
if (app.getRestoreMuteOnInCallResume()) {
PhoneUtils.restoreMuteState(mPhone);
app.setRestoreMuteOnInCallResume(false);
}
Profiler.profileViewCreate(getWindow(), InCallScreen.class.getName());
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();
mIsForegroundActivity = false;
// Force a clear of the provider overlay' frame. Since the
// overlay is removed using a timed message, it is
// possible we missed it if the prev call was interrupted.
mProviderOverlayVisible = false;
updateProviderOverlay();
final PhoneApp app = PhoneApp.getInstance();
// A safety measure to disable proximity sensor in case call failed
// and the telephony state did not change.
app.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)
&& mPhone.getState() != Phone.State.RINGING) {
if (DBG) log("DELAYED_CLEANUP_AFTER_DISCONNECT detected, moving UI to background.");
endInCallScreenSession();
}
EventLog.writeEvent(EventLogTags.PHONE_UI_EXIT);
// Clean up the menu, in case we get paused while the menu is up
// for some reason.
dismissMenu(true); // dismiss immediately
// 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();
// Re-enable the status bar (which we disabled in onResume().)
NotificationMgr.getDefault().getStatusBarMgr().enableExpandedView(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);
// Re-enable "user activity" for touch events.
// We actually do this slightly *after* onPause(), to work around a
// race condition where a touch can come in after we've paused
// but before the device actually goes to sleep.
// TODO: The PowerManager itself should prevent this from happening.
mHandler.postDelayed(new Runnable() {
public void run() {
app.setIgnoreTouchUserActivity(false);
}
}, 500);
app.reenableStatusBar();
// Make sure we revert the poke lock and wake lock when we move to
// the background.
app.updateWakeState();
// clear the dismiss keyguard flag so we are back to the default state
// when we next resume
updateKeyguardPolicy(false);
}
@Override
protected void onStop() {
if (DBG) log("onStop()...");
super.onStop();
stopTimer();
Phone.State state = mPhone.getState();
if (DBG) log("onStop: state = " + state);
if (state == Phone.State.IDLE) {
final PhoneApp app = PhoneApp.getInstance();
// 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 ((app.cdmaOtaProvisionData != null) && (app.cdmaOtaScreenState != null)
&& ((app.cdmaOtaScreenState.otaScreenState !=
CdmaOtaScreenState.OtaScreenState.OTA_STATUS_ACTIVATION)
&& (app.cdmaOtaScreenState.otaScreenState !=
CdmaOtaScreenState.OtaScreenState.OTA_STATUS_SUCCESS_FAILURE_DLG)
&& (!app.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 (otaUtils != null) {
otaUtils.cleanOtaScreen(true);
}
}
}
}
@Override
protected void onDestroy() {
if (DBG) log("onDestroy()...");
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;
final PhoneApp app = PhoneApp.getInstance();
app.setInCallScreenInstance(null);
// Clear out the InCallScreen references in various helper objects
// (to let them know we've been destroyed).
if (mInCallMenu != null) {
mInCallMenu.clearInCallScreenReference();
}
if (mCallCard != null) {
mCallCard.setInCallScreenInstance(null);
}
if (mInCallTouchUi != null) {
mInCallTouchUi.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) {
mBluetoothHeadset.close();
mBluetoothHeadset = null;
}
// Dismiss all dialogs, to be absolutely sure we won't leak any of
// them while changing orientation.
dismissAllDialogs();
}
/**
* 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()...");
moveTaskToBack(true);
setInCallScreenMode(InCallScreenMode.UNDEFINED);
}
/* package */ boolean isForegroundActivity() {
return mIsForegroundActivity;
}
/* 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) {
mPhone.registerForPreciseCallStateChanged(mHandler, PHONE_STATE_CHANGED, null);
mPhone.registerForDisconnect(mHandler, PHONE_DISCONNECT, null);
int phoneType = mPhone.getPhoneType();
if (phoneType == Phone.PHONE_TYPE_GSM) {
mPhone.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.
mPhone.registerForMmiComplete(mHandler, PhoneApp.MMI_COMPLETE, null);
} else if (phoneType == Phone.PHONE_TYPE_CDMA) {
if (DBG) log("Registering for Call Waiting.");
mPhone.registerForCallWaiting(mHandler, PHONE_CDMA_CALL_WAITING, null);
} else {
throw new IllegalStateException("Unexpected phone type: " + phoneType);
}
mPhone.setOnPostDialCharacter(mHandler, POST_ON_DIAL_CHARS, null);
mPhone.registerForSuppServiceFailed(mHandler, SUPP_SERVICE_FAILED, null);
if (phoneType == Phone.PHONE_TYPE_CDMA) {
mPhone.registerForCdmaOtaStatusChange(mHandler, EVENT_OTA_PROVISION_CHANGE, null);
}
mRegisteredForPhoneStates = true;
}
}
private void unregisterForPhoneStates() {
mPhone.unregisterForPreciseCallStateChanged(mHandler);
mPhone.unregisterForDisconnect(mHandler);
mPhone.unregisterForMmiInitiate(mHandler);
mPhone.unregisterForCallWaiting(mHandler);
mPhone.setOnPostDialCharacter(null, POST_ON_DIAL_CHARS, null);
mPhone.unregisterForCdmaOtaStatusChange(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();
}
@Override
protected void onNewIntent(Intent intent) {
if (DBG) log("onNewIntent: intent=" + intent);
// We're being re-launched with a new Intent. Since we keep
// around a single InCallScreen instance for the life of the phone
// process (see finish()), this sequence will happen EVERY time
// there's a new incoming or outgoing call except for the very
// first time the InCallScreen gets created. This sequence will
// also happen if the InCallScreen is already in the foreground
// (e.g. getting a new ACTION_CALL intent while we were already
// using the other line.)
// 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 this intent, and stash the
// result code from internalResolveIntent() in the
// mInCallInitialStatus field. If it's an error code, we'll
// handle it in onResume().
mInCallInitialStatus = internalResolveIntent(intent);
if (mInCallInitialStatus != InCallInitStatus.SUCCESS) {
Log.w(LOG_TAG, "onNewIntent: status " + mInCallInitialStatus
+ " from internalResolveIntent()");
// See onResume() for the actual error handling.
}
}
/* package */ InCallInitStatus internalResolveIntent(Intent intent) {
if (intent == null || intent.getAction() == null) {
return InCallInitStatus.SUCCESS;
}
checkIsOtaCall(intent);
String action = intent.getAction();
if (DBG) log("internalResolveIntent: action=" + action);
// The calls to setRestoreMuteOnInCallResume() inform the phone
// that we're dealing with new connections (either a placing an
// outgoing call or answering an incoming one, and NOT handling
// an aborted "Add Call" request), so we should let the mute state
// be handled by the PhoneUtils phone state change handler.
final PhoneApp app = PhoneApp.getInstance();
// If OTA Activation is configured for Power up scenario, then
// InCallScreen UI started with Intent of ACTION_SHOW_ACTIVATION
// to show OTA Activation screen at power up.
if ((action.equals(ACTION_SHOW_ACTIVATION))
&& ((mPhone.getPhoneType() == Phone.PHONE_TYPE_CDMA))) {
setInCallScreenMode(InCallScreenMode.OTA_NORMAL);
if ((app.cdmaOtaProvisionData != null)
&& (!app.cdmaOtaProvisionData.isOtaCallIntentProcessed)) {
app.cdmaOtaProvisionData.isOtaCallIntentProcessed = true;
app.cdmaOtaScreenState.otaScreenState =
CdmaOtaScreenState.OtaScreenState.OTA_STATUS_ACTIVATION;
}
return InCallInitStatus.SUCCESS;
} else if (action.equals(Intent.ACTION_ANSWER)) {
internalAnswerCall();
app.setRestoreMuteOnInCallResume(false);
return InCallInitStatus.SUCCESS;
} else if (action.equals(Intent.ACTION_CALL)
|| action.equals(Intent.ACTION_CALL_EMERGENCY)) {
app.setRestoreMuteOnInCallResume(false);
// If a provider is used, extract the info to build the
// overlay and route the call. The overlay will be
// displayed the first time updateScreen is called.
if (PhoneUtils.hasPhoneProviderExtras(intent)) {
mProviderLabel = PhoneUtils.getProviderLabel(this, intent);
mProviderIcon = PhoneUtils.getProviderIcon(this, intent);
mProviderGatewayUri = PhoneUtils.getProviderGatewayUri(intent);
mProviderAddress = PhoneUtils.formatProviderUri(mProviderGatewayUri);
mProviderOverlayVisible = true;
if (TextUtils.isEmpty(mProviderLabel) || null == mProviderIcon ||
null == mProviderGatewayUri || TextUtils.isEmpty(mProviderAddress)) {
clearProvider();
}
} else {
clearProvider();
}
InCallInitStatus status = placeCall(intent);
if (status == InCallInitStatus.SUCCESS) {
// Notify the phone app that a call is beginning so it can
// enable the proximity sensor
app.setBeginningCall(true);
}
return status;
} else if (action.equals(intent.ACTION_MAIN)) {
// The MAIN action is used to bring up the in-call screen without
// doing any other explicit action, like when you return to the
// current call after previously bailing out of the in-call UI.
// 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.
if ((mInCallScreenMode == InCallScreenMode.OTA_NORMAL)
|| (mInCallScreenMode == InCallScreenMode.OTA_ENDED)) {
// If in OTA Call, update the OTA UI
updateScreen();
return InCallInitStatus.SUCCESS;
}
if (intent.hasExtra(SHOW_DIALPAD_EXTRA)) {
boolean showDialpad = intent.getBooleanExtra(SHOW_DIALPAD_EXTRA, false);
if (VDBG) log("- internalResolveIntent: SHOW_DIALPAD_EXTRA: " + showDialpad);
if (showDialpad) {
mDialer.openDialer(false); // no "opening" animation
} else {
mDialer.closeDialer(false); // no "closing" animation
}
}
return InCallInitStatus.SUCCESS;
} else if (action.equals(ACTION_UNDEFINED)) {
return InCallInitStatus.SUCCESS;
} else {
Log.w(LOG_TAG, "internalResolveIntent: unexpected intent action: " + action);
// But continue the best we can (basically treating this case
// like ACTION_MAIN...)
return InCallInitStatus.SUCCESS;
}
}
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);
// Run in a 32-bit window, which improves the appearance of some
// semitransparent artwork in the in-call UI (like the CallCard
// photo borders).
getWindow().setFormat(PixelFormat.RGBX_8888);
mMainFrame = (ViewGroup) findViewById(R.id.mainFrame);
mInCallPanel = (ViewGroup) findViewById(R.id.inCallPanel);
// Initialize the CallCard.
mCallCard = (CallCard) findViewById(R.id.callCard);
if (VDBG) log(" - mCallCard = " + mCallCard);
mCallCard.setInCallScreenInstance(this);
// Onscreen touch UI elements (used on some platforms)
initInCallTouchUi();
// Helper class to keep track of enabledness/state of UI controls
mInCallControlState = new InCallControlState(this, mPhone);
// Helper class to run the "Manage conference" UI
mManageConferenceUtils = new ManageConferenceUtils(this, mPhone);
}
/**
* 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 mPhone.getState() != Phone.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 (!mRingingCall.isIdle()) {
// While an incoming call is ringing, BACK behaves just like
// ENDCALL: it stops the ringing and rejects the current call.
// (This is only enabled on some platforms, though.)
if (getResources().getBoolean(R.bool.allow_back_key_to_reject_incoming_call)) {
if (DBG) log("BACK key while ringing: reject the call");
internalHangupRingingCall();
// Don't consume the key; instead let the BACK event *also*
// get handled normally by the framework (which presumably
// will cause us to exit out of this activity.)
super.onBackPressed();
return;
} else {
// The BACK key is disabled; don't reject the call, but
// *do* consume the keypress (otherwise we'll exit out of
// this activity.)
if (DBG) log("BACK key while ringing: ignored");
return;
}
}
// BACK is also used to exit out of any "special modes" of the
// in-call UI:
if (mDialer.isOpened()) {
// Take down the "touch lock" overlay *immediately* to let the
// user clearly see the DTMF dialpad's closing animation.
enableTouchLock(false);
mDialer.closeDialer(true); // do the "closing" animation
return;
}
if (mInCallScreenMode == InCallScreenMode.MANAGE_CONFERENCE) {
// Hide the Manage Conference panel, return to NORMAL mode.
setInCallScreenMode(InCallScreenMode.NORMAL);
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 = !mRingingCall.isIdle();
final boolean hasActiveCall = !mForegroundCall.isIdle();
final boolean hasHoldingCall = !mBackgroundCall.isIdle();
int phoneType = mPhone.getPhoneType();
if (phoneType == Phone.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.
PhoneApp app = PhoneApp.getInstance();
CdmaPhoneCallState.PhoneCallState currCallState =
app.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(mPhone);
} 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 == Phone.PHONE_TYPE_GSM) {
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(mPhone); // 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.
*
* Note, this focus change event happens AFTER the in-call menu is
* displayed, so mIsMenuDisplayed should always be correct by the
* time this method is called in the framework, please see:
* {@link onCreatePanelView}, {@link onOptionsMenuClosed}
*/
@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 dispatchKeyEvent(KeyEvent event) {
// if (DBG) log("dispatchKeyEvent(event " + event + ")...");
// Intercept some events before they get dispatched to our views.
switch (event.getKeyCode()) {
case KeyEvent.KEYCODE_DPAD_CENTER:
case KeyEvent.KEYCODE_DPAD_UP:
case KeyEvent.KEYCODE_DPAD_DOWN:
case KeyEvent.KEYCODE_DPAD_LEFT:
case KeyEvent.KEYCODE_DPAD_RIGHT:
// Disable DPAD keys and trackball clicks if the touch lock
// overlay is up, since "touch lock" really means "disable
// the DTMF dialpad" (rather than only disabling touch events.)
if (mDialer.isOpened() && isTouchLocked()) {
if (DBG) log("- ignoring DPAD event while touch-locked...");
return true;
}
break;
default:
break;
}
return super.dispatchKeyEvent(event);
}
@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:
if (mPhone.getState() == Phone.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:
final CallNotifier notifier = PhoneApp.getInstance().notifier;
if (notifier.isRinging()) {
// ringer is actually playing, so silence it.
PhoneUtils.setAudioControlState(PhoneUtils.AUDIO_IDLE);
if (DBG) log("VOLUME key: silence ringer");
notifier.silenceRinger();
}
// As long as an incoming call is ringing, we always
// consume the VOLUME keys.
return true;
}
break;
case KeyEvent.KEYCODE_MENU:
// Special case for the MENU key: if the "touch lock"
// overlay is up (over the DTMF dialpad), allow MENU to
// dismiss the overlay just as if you had double-tapped
// the onscreen icon.
// (We do this because MENU is normally used to bring the
// UI back after the screen turns off, and the touch lock
// overlay "feels" very similar to the screen going off.
// This is also here to be "backward-compatibile" with the
// 1.0 behavior, where you *needed* to hit MENU to bring
// back the dialpad after 6 seconds of idle time.)
if (mDialer.isOpened() && isTouchLocked()) {
if (VDBG) log("- allowing MENU to dismiss touch lock overlay...");
// Take down the touch lock overlay, but post a
// message in the future to bring it back later.
enableTouchLock(false);
resetTouchLockTimer();
return true;
}
break;
case KeyEvent.KEYCODE_MUTE:
PhoneUtils.setMute(mPhone, !PhoneUtils.getMute(mPhone));
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);
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)
.setCancelable(true)
.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) {
if (DBG) log("onPhoneStateChanged()...");
// 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;
}
updateScreen();
// Make sure we update the poke lock and wake lock when certain
// phone state changes occur.
PhoneApp.getInstance().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: " + c + ", cause=" + cause);
boolean currentlyIdle = !phoneIsInUse();
int autoretrySetting = AUTO_RETRY_OFF;
boolean phoneIsCdma = (mPhone.getPhoneType() == Phone.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.System.getInt(mPhone.getContext().
getContentResolver(), android.provider.Settings.System.CALL_AUTO_RETRY, 0);
}
}
// for OTA Call, only if in OTA NORMAL mode, handle OTA END scenario
final PhoneApp app = PhoneApp.getInstance();
if ((mInCallScreenMode == InCallScreenMode.OTA_NORMAL)
&& ((app.cdmaOtaProvisionData != null)
&& (!app.cdmaOtaProvisionData.inOtaSpcState))) {
setInCallScreenMode(InCallScreenMode.OTA_ENDED);
updateScreen();
return;
} else if ((mInCallScreenMode == InCallScreenMode.OTA_ENDED)
|| ((app.cdmaOtaProvisionData != null) && app.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 = PhoneApp.getInstance().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 (mNeedShowCallLostDialog) {
// Show the dialog now since the call that just failed was a retry.
showCallLostDialog();
mNeedShowCallLostDialog = false;
} else {
if (autoretrySetting == AUTO_RETRY_OFF) {
// Show the dialog for failed call if Auto Retry is OFF in Settings.
showCallLostDialog();
mNeedShowCallLostDialog = false;
} else {
// Set the mNeedShowCallLostDialog flag now, so we'll know to show
// the dialog if *this* call fails.
mNeedShowCallLostDialog = 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.]
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...");
app.updateWakeState();
mPhone.clearDisconnected(); // This happens synchronously.
break;
}
}
}
}
// Retrieve the emergency call retry count from this intent, in
// case we need to retry the call again.
int emergencyCallRetryCount = getIntent().getIntExtra(
EmergencyCallHandler.EMERGENCY_CALL_RETRY_KEY,
EmergencyCallHandler.INITIAL_ATTEMPT);
// Note: see CallNotifier.onDisconnect() for some other behavior
// that might be triggered by a disconnect event, like playing the
// busy/congestion tone.
// Keep track of whether this call was user-initiated or not.
// (This affects where we take the user next; see delayedCleanupAfterDisconnect().)
mShowCallLogAfterDisconnect = !c.isIncoming();
// We bail out immediately (and *don't* display the "call ended"
// state at all) in a couple of cases, including those where we
// are waiting for the radio to finish powering up for an
// emergency call:
boolean bailOutImmediately =
((cause == Connection.DisconnectCause.INCOMING_MISSED)
|| (cause == Connection.DisconnectCause.INCOMING_REJECTED)
|| ((cause == Connection.DisconnectCause.OUT_OF_SERVICE)
&& (emergencyCallRetryCount > 0)))
&& currentlyIdle;
if (bailOutImmediately) {
if (VDBG) 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();
// Retry the call, by resending the intent to the emergency
// call handler activity.
if ((cause == Connection.DisconnectCause.OUT_OF_SERVICE)
&& (emergencyCallRetryCount > 0)) {
startActivity(getIntent()
.setClassName(this, EmergencyCallHandler.class.getName()));
}
} else {
if (VDBG) 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.)
// Force a UI update in case we need to display anything
// special given this connection's DisconnectCause (see
// CallCard.getCallFailedString()).
updateScreen();
// Display the special "Call ended" state when the phone is idle
// but there's still a call in the DISCONNECTED state:
if (currentlyIdle
&& ((mForegroundCall.getState() == Call.State.DISCONNECTED)
|| (mBackgroundCall.getState() == Call.State.DISCONNECTED))) {
if (VDBG) log("- onDisconnect: switching to 'Call ended' state...");
setInCallScreenMode(InCallScreenMode.CALL_ENDED);
}
// Some other misc cleanup that we do if the call that just
// disconnected was the foreground call.
final boolean hasActiveCall = !mForegroundCall.isIdle();
if (!hasActiveCall) {
if (VDBG) 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() == Phone.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.
mPhone.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;
}
}
// 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.
int callEndedDisplayDelay =
(cause == Connection.DisconnectCause.LOCAL)
? CALL_ENDED_SHORT_DELAY : CALL_ENDED_LONG_DELAY;
mHandler.removeMessages(DELAYED_CLEANUP_AFTER_DISCONNECT);
mHandler.sendEmptyMessageDelayed(DELAYED_CLEANUP_AFTER_DISCONNECT,
callEndedDisplayDelay);
}
// Remove 3way timer (only meaningful for CDMA)
mHandler.removeMessages(THREEWAY_CALLERINFO_DISPLAY_DONE);
}
/**
* Brings up the "MMI Started" dialog.
*/
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 onMMIInitiate
* @see PhoneUtils.cancelMmiCode
*/
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...");
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);
int phoneType = mPhone.getPhoneType();
switch (state) {
case STARTED:
if (phoneType == Phone.PHONE_TYPE_CDMA) {
mDialer.stopLocalToneCdma();
if (mPauseInProgress) {
showPausePromptDialogCDMA(c, mPostDialStrAfterPause);
}
mPauseInProgress = false;
mDialer.startLocalToneCdma(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:
if (DBG) log("handlePostOnDialChars: show WAIT prompt...");
String postDialStr = c.getRemainingPostDialString();
if (phoneType == Phone.PHONE_TYPE_CDMA) {
mDialer.stopLocalToneCdma();
showWaitPromptDialogCDMA(c, postDialStr);
} else if (phoneType == Phone.PHONE_TYPE_GSM) {
showWaitPromptDialogGSM(c, postDialStr);
} else {
throw new IllegalStateException("Unexpected phone type: " + phoneType);
}
break;
case WILD:
if (DBG) log("handlePostOnDialChars: show WILD prompt");
showWildPromptDialog(c);
break;
case COMPLETE:
if (phoneType == Phone.PHONE_TYPE_CDMA) {
mDialer.stopLocalToneCdma();
}
break;
case PAUSE:
if (phoneType == Phone.PHONE_TYPE_CDMA) {
mPostDialStrAfterPause = c.getRemainingPostDialString();
mDialer.stopLocalToneCdma();
mPauseInProgress = true;
}
break;
default:
break;
}
}
}
private void showWaitPromptDialogGSM(final Connection c, String postDialStr) {
if (DBG) log("showWaitPromptDialogGSM: '" + 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.send_button, new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int whichButton) {
if (DBG) log("handle WAIT_PROMPT_CONFIRMED, proceed...");
c.proceedAfterWaitChar();
PhoneApp.getInstance().pokeUserActivity();
}
})
.setOnCancelListener(new DialogInterface.OnCancelListener() {
public void onCancel(DialogInterface dialog) {
if (DBG) log("handle POST_DIAL_CANCELED!");
c.cancelPostDial();
PhoneApp.getInstance().pokeUserActivity();
}
})
.create();
mWaitPromptDialog.getWindow().addFlags(
WindowManager.LayoutParams.FLAG_BLUR_BEHIND);
mWaitPromptDialog.show();
}
/**
* Processes the CDMA specific requirements of a WAIT character in a
* dial string.
*
* 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 showWaitPromptDialogCDMA(final Connection c, String postDialStr) {
if (DBG) log("showWaitPromptDialogCDMA: '" + 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() {
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() {
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 showPausePromptDialogCDMA(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() {
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);
PhoneApp.getInstance().pokeUserActivity();
}
})
.setOnCancelListener(
new DialogInterface.OnCancelListener() {
public void onCancel(DialogInterface dialog) {
if (VDBG) log("handle POST_DIAL_CANCELED!");
c.cancelPostDial();
PhoneApp.getInstance().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.
*/
private void updateScreen() {
if (DBG) log("updateScreen()...");
// 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;
}
// Update the state of the in-call menu items.
if (mInCallMenu != null) {
// TODO: do this only if the menu is visible!
if (DBG) log("- updateScreen: updating menu items...");
boolean okToShowMenu = mInCallMenu.updateItems(mPhone);
if (!okToShowMenu) {
// Uh oh: we were only trying to update the state of the
// menu items, but the logic in InCallMenu.updateItems()
// just decided the menu shouldn't be visible at all!
// (That's probably means that the call ended
// asynchronously while the menu was up.)
//
// So take the menu down ASAP.
if (DBG) log("- updateScreen: Tried to update menu; now need to dismiss!");
// dismissMenu() has no effect if the menu is already closed.
dismissMenu(true); // dismissImmediate = true
}
}
final PhoneApp app = PhoneApp.getInstance();
if (mInCallScreenMode == InCallScreenMode.OTA_NORMAL) {
if (DBG) log("- updateScreen: OTA call state NORMAL...");
if (otaUtils != null) {
if (DBG) log("- updateScreen: otaUtils is not null, call otaShowProperScreen");
otaUtils.otaShowProperScreen();
}
return;
} else if (mInCallScreenMode == InCallScreenMode.OTA_ENDED) {
if (DBG) log("- updateScreen: OTA call ended state ...");
// Wake up the screen when we get notification, good or bad.
PhoneApp.getInstance().wakeUpScreen();
if (app.cdmaOtaScreenState.otaScreenState
== CdmaOtaScreenState.OtaScreenState.OTA_STATUS_ACTIVATION) {
if (DBG) log("- updateScreen: OTA_STATUS_ACTIVATION");
if (otaUtils != null) {
if (DBG) log("- updateScreen: otaUtils is not null, "
+ "call otaShowActivationScreen");
otaUtils.otaShowActivateScreen();
}
} else {
if (DBG) log("- updateScreen: OTA Call end state for Dialogs");
if (otaUtils != null) {
if (DBG) log("- updateScreen: Show OTA Success Failure dialog");
otaUtils.otaShowSuccessFailure();
}
}
return;
} else if (mInCallScreenMode == InCallScreenMode.MANAGE_CONFERENCE) {
if (DBG) log("- updateScreen: manage conference mode (NOT updating in-call UI)...");
updateManageConferencePanelIfNecessary();
return;
} else if (mInCallScreenMode == InCallScreenMode.CALL_ENDED) {
if (DBG) log("- updateScreen: call ended state (NOT updating in-call UI)...");
// Actually we do need to update one thing: the background.
updateInCallBackground();
return;
}
if (DBG) log("- updateScreen: updating the in-call UI...");
mCallCard.updateState(mPhone);
updateDialpadVisibility();
updateInCallTouchUi();
updateProviderOverlay();
updateMenuButtonHint();
updateInCallBackground();
// Forcibly take down all dialog if an incoming call is ringing.
if (!mRingingCall.isIdle()) {
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 = mForegroundCall.getConnections();
int phoneType = mPhone.getPhoneType();
if (phoneType == Phone.PHONE_TYPE_CDMA) {
Connection fgLatestConnection = mForegroundCall.getLatestConnection();
if (PhoneApp.getInstance().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();
showWaitPromptDialogCDMA(fgLatestConnection, postDialStr);
}
} else if (phoneType == Phone.PHONE_TYPE_GSM) {
for (Connection cn : fgConnections) {
if ((cn != null) && (cn.getPostDialState() == Connection.PostDialState.WAIT)) {
postDialStr = cn.getRemainingPostDialString();
showWaitPromptDialogGSM(cn, postDialStr);
}
}
} else {
throw new IllegalStateException("Unexpected phone type: " + phoneType);
}
}
}
/**
* (Re)synchronizes the onscreen UI with the current state of the
* Phone.
*
* @return InCallInitStatus.SUCCESS if we successfully updated the UI, or
* InCallInitStatus.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.
*/
private InCallInitStatus 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.)
int phoneType = mPhone.getPhoneType();
if ((phoneType == Phone.PHONE_TYPE_CDMA)
&& ((mInCallScreenMode == InCallScreenMode.OTA_NORMAL)
|| (mInCallScreenMode == InCallScreenMode.OTA_ENDED))) {
// Even when OTA Call ends, need to show OTA End UI,
// so return Success to allow UI update.
return InCallInitStatus.SUCCESS;
}
// Need to treat running MMI codes as a connection as well.
// Do not check for getPendingMmiCodes when phone is a CDMA phone
boolean hasPendingMmiCodes =
(phoneType == Phone.PHONE_TYPE_GSM) && !mPhone.getPendingMmiCodes().isEmpty();
if (!mForegroundCall.isIdle() || !mBackgroundCall.isIdle() || !mRingingCall.isIdle()
|| hasPendingMmiCodes) {
if (VDBG) log("syncWithPhoneState: it's ok to be here; update the screen...");
updateScreen();
return InCallInitStatus.SUCCESS;
}
if (DBG) log("syncWithPhoneState: phone is idle; we shouldn't be here!");
return InCallInitStatus.PHONE_NOT_IN_USE;
}
/**
* Given the Intent we were initially launched with,
* figure out the actual phone number we should dial.
*
* @return the phone number corresponding to the
* specified Intent, or null if the Intent is not
* an ACTION_CALL intent or if the intent's data is
* malformed or missing.
*
* @throws VoiceMailNumberMissingException if the intent
* contains a "voicemail" URI, but there's no voicemail
* number configured on the device.
*/
private String getInitialNumber(Intent intent)
throws PhoneUtils.VoiceMailNumberMissingException {
String action = intent.getAction();
if (action == null) {
return null;
}
if (action != null && action.equals(Intent.ACTION_CALL) &&
intent.hasExtra(Intent.EXTRA_PHONE_NUMBER)) {
return intent.getStringExtra(Intent.EXTRA_PHONE_NUMBER);
}
return PhoneUtils.getNumberFromIntent(this, mPhone, intent);
}
/**
* Make a call to whomever the intent tells us to.
*
* @param intent the Intent we were launched with
* @return InCallInitStatus.SUCCESS if we successfully initiated an
* outgoing call. If there was some kind of failure, return one of
* the other InCallInitStatus codes indicating what went wrong.
*/
private InCallInitStatus placeCall(Intent intent) {
if (VDBG) log("placeCall()... intent = " + intent);
String number;
// Check the current ServiceState to make sure it's OK
// to even try making a call.
InCallInitStatus okToCallStatus = checkIfOkToInitiateOutgoingCall();
try {
number = getInitialNumber(intent);
} catch (PhoneUtils.VoiceMailNumberMissingException ex) {
// If the call status is NOT in an acceptable state, it
// may effect the way the voicemail number is being
// retrieved. Mask the VoiceMailNumberMissingException
// with the underlying issue of the phone state.
if (okToCallStatus != InCallInitStatus.SUCCESS) {
if (DBG) log("Voicemail number not reachable in current SIM card state.");
return okToCallStatus;
}
if (DBG) log("VoiceMailNumberMissingException from getInitialNumber()");
return InCallInitStatus.VOICEMAIL_NUMBER_MISSING;
}
if (number == null) {
Log.w(LOG_TAG, "placeCall: couldn't get a phone number from Intent " + intent);
return InCallInitStatus.NO_PHONE_NUMBER_SUPPLIED;
}
boolean isEmergencyNumber = PhoneNumberUtils.isEmergencyNumber(number);
boolean isEmergencyIntent = Intent.ACTION_CALL_EMERGENCY.equals(intent.getAction());
if (isEmergencyNumber && !isEmergencyIntent) {
Log.e(LOG_TAG, "Non-CALL_EMERGENCY Intent " + intent
+ " attempted to call emergency number " + number
+ ".");
return InCallInitStatus.CALL_FAILED;
} else if (!isEmergencyNumber && isEmergencyIntent) {
Log.e(LOG_TAG, "Received CALL_EMERGENCY Intent " + intent
+ " with non-emergency number " + number
+ " -- failing call.");
return InCallInitStatus.CALL_FAILED;
}
// If we're trying to call an emergency number, then it's OK to
// proceed in certain states where we'd usually just bring up
// an error dialog:
// - If we're in EMERGENCY_ONLY mode, then (obviously) you're allowed
// to dial emergency numbers.
// - If we're OUT_OF_SERVICE, we still attempt to make a call,
// since the radio will register to any available network.
if (isEmergencyNumber
&& ((okToCallStatus == InCallInitStatus.EMERGENCY_ONLY)
|| (okToCallStatus == InCallInitStatus.OUT_OF_SERVICE))) {
if (DBG) log("placeCall: Emergency number detected with status = " + okToCallStatus);
okToCallStatus = InCallInitStatus.SUCCESS;
if (DBG) log("==> UPDATING status to: " + okToCallStatus);
}
if (okToCallStatus != InCallInitStatus.SUCCESS) {
// If this is an emergency call, we call the emergency call
// handler activity to turn on the radio and do whatever else
// is needed. For now, we finish the InCallScreen (since were
// expecting a callback when the emergency call handler dictates
// it) and just return the success state.
if (isEmergencyNumber && (okToCallStatus == InCallInitStatus.POWER_OFF)) {
startActivity(intent.setClassName(this, EmergencyCallHandler.class.getName()));
if (DBG) log("placeCall: starting EmergencyCallHandler, finishing InCallScreen...");
endInCallScreenSession();
return InCallInitStatus.SUCCESS;
} else {
return okToCallStatus;
}
}
final PhoneApp app = PhoneApp.getInstance();
if ((mPhone.getPhoneType() == Phone.PHONE_TYPE_CDMA) && (mPhone.isOtaSpNumber(number))) {
if (DBG) log("placeCall: isOtaSpNumber() returns true");
setInCallScreenMode(InCallScreenMode.OTA_NORMAL);
if (app.cdmaOtaProvisionData != null) {
app.cdmaOtaProvisionData.isOtaCallCommitted = false;
}
}
mNeedShowCallLostDialog = false;
// We have a valid number, so try to actually place a call:
// make sure we pass along the intent's URI which is a
// reference to the contact. We may have a provider gateway
// phone number to use for the outgoing call.
int callStatus;
Uri contactUri = intent.getData();
if (null != mProviderGatewayUri &&
!(isEmergencyNumber || isEmergencyIntent) &&
PhoneUtils.isRoutableViaGateway(number)) { // Filter out MMI, OTA and other codes.
callStatus = PhoneUtils.placeCallVia(
this, mPhone, number, contactUri, mProviderGatewayUri);
} else {
callStatus = PhoneUtils.placeCall(mPhone, number, contactUri);
}
switch (callStatus) {
case PhoneUtils.CALL_STATUS_DIALED:
if (VDBG) log("placeCall: PhoneUtils.placeCall() succeeded for regular call '"
+ number + "'.");
if (mInCallScreenMode == InCallScreenMode.OTA_NORMAL) {
app.cdmaOtaScreenState.otaScreenState =
CdmaOtaScreenState.OtaScreenState.OTA_STATUS_LISTENING;
updateScreen();
}
// Any time we initiate a call, force the DTMF dialpad to
// close. (We want to make sure the user can see the regular
// in-call UI while the new call is dialing, and when it
// first gets connected.)
mDialer.closeDialer(false); // no "closing" animation
// Also, in case a previous call was already active (i.e. if
// we just did "Add call"), clear out the "history" of DTMF
// digits you typed, to make sure it doesn't persist from the
// previous call to the new call.
// TODO: it would be more precise to do this when the actual
// phone state change happens (i.e. when a new foreground
// call appears and the previous call moves to the
// background), but the InCallScreen doesn't keep enough
// state right now to notice that specific transition in
// onPhoneStateChanged().
mDialer.clearDigits();
if (mPhone.getPhoneType() == Phone.PHONE_TYPE_CDMA) {
// Start the 2 second timer for 3 Way CallerInfo
if (app.cdmaPhoneCallState.getCurrentCallState()
== CdmaPhoneCallState.PhoneCallState.THRWAY_ACTIVE) {
//Unmute for the second MO call
PhoneUtils.setMuteInternal(mPhone, false);
//Start the timer for displaying "Dialing" for second call
Message msg = Message.obtain(mHandler, THREEWAY_CALLERINFO_DISPLAY_DONE);
mHandler.sendMessageDelayed(msg, THREEWAY_CALLERINFO_DISPLAY_TIME);
// Set the mThreeWayCallOrigStateDialing state to true
app.cdmaPhoneCallState.setThreeWayCallOrigState(true);
//Update screen to show 3way dialing
updateScreen();
}
}
return InCallInitStatus.SUCCESS;
case PhoneUtils.CALL_STATUS_DIALED_MMI:
if (DBG) log("placeCall: specified number was an MMI code: '" + number + "'.");
// The passed-in number was an MMI code, not a regular phone number!
// This isn't really a failure; the Dialer may have deliberately
// fired an ACTION_CALL intent to dial an MMI code, like for a
// USSD call.
//
// Presumably an MMI_INITIATE message will come in shortly
// (and we'll bring up the "MMI Started" dialog), or else
// an MMI_COMPLETE will come in (which will take us to a
// different Activity; see PhoneUtils.displayMMIComplete()).
return InCallInitStatus.DIALED_MMI;
case PhoneUtils.CALL_STATUS_FAILED:
Log.w(LOG_TAG, "placeCall: PhoneUtils.placeCall() FAILED for number '"
+ number + "'.");
// We couldn't successfully place the call; there was some
// failure in the telephony layer.
return InCallInitStatus.CALL_FAILED;
default:
Log.w(LOG_TAG, "placeCall: unknown callStatus " + callStatus
+ " from PhoneUtils.placeCall() for number '" + number + "'.");
return InCallInitStatus.SUCCESS; // Try to continue anyway...
}
}
/**
* Checks the current ServiceState to make sure it's OK
* to try making an outgoing call to the specified number.
*
* @return InCallInitStatus.SUCCESS if it's OK to try calling the specified
* number. If not, like if the radio is powered off or we have no
* signal, return one of the other InCallInitStatus codes indicating what
* the problem is.
*/
private InCallInitStatus checkIfOkToInitiateOutgoingCall() {
// Watch out: do NOT use PhoneStateIntentReceiver.getServiceState() here;
// that's not guaranteed to be fresh. To synchronously get the
// CURRENT service state, ask the Phone object directly:
int state = mPhone.getServiceState().getState();
if (VDBG) log("checkIfOkToInitiateOutgoingCall: ServiceState = " + state);
switch (state) {
case ServiceState.STATE_IN_SERVICE:
// Normal operation. It's OK to make outgoing calls.
return InCallInitStatus.SUCCESS;
case ServiceState.STATE_POWER_OFF:
// Radio is explictly powered off.
return InCallInitStatus.POWER_OFF;
case ServiceState.STATE_EMERGENCY_ONLY:
// The phone is registered, but locked. Only emergency
// numbers are allowed.
// Note that as of Android 2.0 at least, the telephony layer
// does not actually use ServiceState.STATE_EMERGENCY_ONLY,
// mainly since there's no guarantee that the radio/RIL can
// make this distinction. So in practice the
// InCallInitStatus.EMERGENCY_ONLY state and the string
// "incall_error_emergency_only" are totally unused.
return InCallInitStatus.EMERGENCY_ONLY;
case ServiceState.STATE_OUT_OF_SERVICE:
// No network connection.
return InCallInitStatus.OUT_OF_SERVICE;
default:
throw new IllegalStateException("Unexpected ServiceState: " + state);
}
}
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()
PhoneApp.getInstance().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()
PhoneApp.getInstance().pokeUserActivity();
}})
.setOnCancelListener(new OnCancelListener() {
public void onCancel(DialogInterface dialog) {
if (VDBG) log("Missing voicemail AlertDialog: CANCEL handler...");
msg.sendToTarget(); // see dontAddVoiceMailNumber()
PhoneApp.getInstance().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, we
* *don't* exit the in-call screen, but we at least turn off the
* backlight (which we turned on in onDisconnect().)
*/
private void delayedCleanupAfterDisconnect() {
if (VDBG) log("delayedCleanupAfterDisconnect()... Phone state = " + mPhone.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.]
mPhone.clearDisconnected();
if (!phoneIsInUse()) {
// Phone is idle! We should exit this screen 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...");
// If this is a call that was initiated by the user, and
// we're *not* in emergency mode, 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.
if (mShowCallLogAfterDisconnect && !isPhoneStateRestricted()) {
if (VDBG) log("- Show Call Log after disconnect...");
final Intent intent = PhoneApp.createCallLogIntent();
intent.addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION);
startActivity(intent);
// Even in this case we still call endInCallScreenSession (below),
// to make sure we don't stay in the activity history.
}
endInCallScreenSession();
}
} else {
// The phone is still in use. Stay here in this activity, but
// we don't need to keep the screen on.
if (DBG) log("- delayedCleanupAfterDisconnect: staying on the InCallScreen...");
if (DBG) PhoneUtils.dumpCallState(mPhone);
}
}
//
// Callbacks for buttons / menu items.
//
public void onClick(View view) {
int id = view.getId();
if (VDBG) log("onClick(View " + view + ", id " + id + ")...");
if (VDBG && view instanceof InCallMenuItemView) {
InCallMenuItemView item = (InCallMenuItemView) view;
log(" ==> menu item! " + item);
}
// Most menu items dismiss the menu immediately once you click
// them. But some items (the "toggle" buttons) are different:
// they want the menu to stay visible for a second afterwards to
// give you feedback about the state change.
boolean dismissMenuImmediate = true;
switch (id) {
case R.id.menuAnswerAndHold:
if (VDBG) log("onClick: AnswerAndHold...");
internalAnswerCall(); // Automatically holds the current active call
break;
case R.id.menuAnswerAndEnd:
if (VDBG) log("onClick: AnswerAndEnd...");
internalAnswerAndEnd();
break;
case R.id.menuAnswer:
if (DBG) log("onClick: Answer...");
internalAnswerCall();
break;
case R.id.menuIgnore:
if (DBG) log("onClick: Ignore...");
internalHangupRingingCall();
break;
case R.id.menuSwapCalls:
if (DBG) log("onClick: SwapCalls...");
internalSwapCalls();
break;
case R.id.menuMergeCalls:
if (VDBG) log("onClick: MergeCalls...");
PhoneUtils.mergeCalls(mPhone);
break;
case R.id.menuManageConference:
if (VDBG) log("onClick: ManageConference...");
// Show the Manage Conference panel.
setInCallScreenMode(InCallScreenMode.MANAGE_CONFERENCE);
break;
case R.id.menuShowDialpad:
if (VDBG) log("onClick: Show/hide dialpad...");
onShowHideDialpad();
break;
case R.id.manage_done: // mButtonManageConferenceDone
if (VDBG) log("onClick: mButtonManageConferenceDone...");
// Hide the Manage Conference panel, return to NORMAL mode.
setInCallScreenMode(InCallScreenMode.NORMAL);
break;
case R.id.menuSpeaker:
if (VDBG) log("onClick: Speaker...");
onSpeakerClick();
// This is a "toggle" button; let the user see the new state for a moment.
dismissMenuImmediate = false;
break;
case R.id.menuBluetooth:
if (VDBG) log("onClick: Bluetooth...");
onBluetoothClick();
// This is a "toggle" button; let the user see the new state for a moment.
dismissMenuImmediate = false;
break;
case R.id.menuMute:
if (VDBG) log("onClick: Mute...");
onMuteClick();
// This is a "toggle" button; let the user see the new state for a moment.
dismissMenuImmediate = false;
break;
case R.id.menuHold:
if (VDBG) log("onClick: Hold...");
onHoldClick();
// This is a "toggle" button; let the user see the new state for a moment.
dismissMenuImmediate = false;
break;
case R.id.menuAddCall:
if (VDBG) log("onClick: AddCall...");
PhoneUtils.startNewCall(mPhone); // Fires off an ACTION_DIAL intent
break;
case R.id.menuEndCall:
if (VDBG) log("onClick: EndCall...");
internalHangup();
break;
default:
if ((mInCallScreenMode == InCallScreenMode.OTA_NORMAL
|| mInCallScreenMode == InCallScreenMode.OTA_ENDED)
&& otaUtils != null) {
otaUtils.onClickHandler(id);
} else {
Log.w(LOG_TAG,
"Got click from unexpected View ID " + id + " (View = " + view + ")");
}
break;
}
EventLog.writeEvent(EventLogTags.PHONE_UI_BUTTON_CLICK,
(view instanceof TextView) ? ((TextView) view).getText() : "");
// If the user just clicked a "stateful" menu item (i.e. one of
// the toggle buttons), we keep the menu onscreen briefly to
// provide visual feedback. Since we want the user to see the
// *new* current state, force the menu items to update right now.
//
// Note that some toggle buttons ("Hold" in particular) do NOT
// immediately change the state of the Phone. In that case, the
// updateItems() call below won't have any visible effect.
// Instead, the menu will get updated by the updateScreen() call
// that happens from onPhoneStateChanged().
if (!dismissMenuImmediate) {
// TODO: mInCallMenu.updateItems() is a very big hammer; it
// would be more efficient to update *only* the menu item(s)
// we just changed. (Doing it this way doesn't seem to cause
// a noticeable performance problem, though.)
if (VDBG) log("- onClick: updating menu to show 'new' current state...");
boolean okToShowMenu = mInCallMenu.updateItems(mPhone);
if (!okToShowMenu) {
// Uh oh. All we tried to do was update the state of the
// menu items, but the logic in InCallMenu.updateItems()
// just decided the menu shouldn't be visible at all!
// (That probably means that the call ended asynchronously
// while the menu was up.)
//
// That's OK; just make sure to take the menu down ASAP.
if (VDBG) log("onClick: Tried to update menu, but now need to take it down!");
dismissMenuImmediate = true;
}
}
// Any menu item counts as explicit "user activity".
PhoneApp.getInstance().pokeUserActivity();
// Finally, *any* action handled here closes the menu (either
// immediately, or after a short delay).
//
// Note that some of the clicks we handle here aren't even menu
// items in the first place, like the mButtonManageConferenceDone
// button. That's OK; if the menu is already closed, the
// dismissMenu() call does nothing.
dismissMenu(dismissMenuImmediate);
}
private void onHoldClick() {
if (VDBG) log("onHoldClick()...");
final boolean hasActiveCall = !mForegroundCall.isIdle();
final boolean hasHoldingCall = !mBackgroundCall.isIdle();
if (VDBG) log("- 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(mPhone); // 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(mPhone); // 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;
}
// TODO: we *could* now forcibly update the "Hold" button based on
// "newHoldState" and "holdButtonEnabled". But for now, do
// nothing here, and instead let the menu get updated when the
// onPhoneStateChanged() callback comes in. (This seems to be
// responsive enough.)
// Also, any time we hold or unhold, force the DTMF dialpad to close.
mDialer.closeDialer(true); // do the "closing" animation
}
private void onSpeakerClick() {
if (VDBG) log("onSpeakerClick()...");
// 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);
if (newSpeakerState && isBluetoothAvailable() && isBluetoothAudioConnected()) {
disconnectBluetoothAudio();
}
PhoneUtils.turnOnSpeaker(this, newSpeakerState, true);
if (newSpeakerState) {
// The "touch lock" overlay is NEVER used when the speaker is on.
enableTouchLock(false);
} else {
// User just turned the speaker *off*. If the dialpad
// is open, we need to start the timer that will
// eventually bring up the "touch lock" overlay.
if (mDialer.isOpened() && !isTouchLocked()) {
resetTouchLockTimer();
}
}
}
private void onMuteClick() {
if (VDBG) log("onMuteClick()...");
boolean newMuteState = !PhoneUtils.getMute(mPhone);
PhoneUtils.setMute(mPhone, newMuteState);
}
private void onBluetoothClick() {
if (VDBG) log("onBluetoothClick()...");
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 handle it. This ensures that the rest
// of the speakerphone code is executed, and reciprocates the
// menuSpeaker code above in onClick(). The onClick() code
// disconnects the active bluetooth headsets when the
// speakerphone is turned on.
if (PhoneUtils.isSpeakerOn(this)) {
PhoneUtils.turnOnSpeaker(this, false, true);
}
connectBluetoothAudio();
}
} else {
// Bluetooth isn't available; the "Audio" button shouldn't have
// been enabled in the first place!
Log.w(LOG_TAG, "Got onBluetoothClick, but bluetooth is unavailable");
}
}
private void onShowHideDialpad() {
if (VDBG) log("onShowHideDialpad()...");
if (mDialer.isOpened()) {
mDialer.closeDialer(true); // do the "closing" animation
} else {
mDialer.openDialer(true); // do the "opening" animation
}
mDialer.setHandleVisible(true);
}
/**
* Handles button clicks from the InCallTouchUi widget.
*/
/* package */ void handleOnscreenButtonClick(int id) {
if (DBG) log("handleOnscreenButtonClick(id " + id + ")...");
switch (id) {
// TODO: since every button here corresponds to a menu item that we
// already handle in onClick(), maybe merge the guts of these two
// methods into a separate helper that takes an ID (of either a menu
// item *or* touch button) and does the appropriate user action.
// Actions while an incoming call is ringing:
case R.id.answerButton:
internalAnswerCall();
break;
case R.id.rejectButton:
internalHangupRingingCall();
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:
onShowHideDialpad();
break;
case R.id.bluetoothButton:
onBluetoothClick();
break;
case R.id.muteButton:
onMuteClick();
break;
case R.id.speakerButton:
onSpeakerClick();
break;
case R.id.addButton:
PhoneUtils.startNewCall(mPhone); // Fires off an ACTION_DIAL intent
break;
case R.id.mergeButton:
case R.id.cdmaMergeButton:
PhoneUtils.mergeCalls(mPhone);
break;
case R.id.manageConferencePhotoButton:
// Show the Manage Conference panel.
setInCallScreenMode(InCallScreenMode.MANAGE_CONFERENCE);
break;
default:
Log.w(LOG_TAG, "handleOnscreenButtonClick: unexpected ID " + id);
break;
}
// Just in case the user clicked a "stateful" menu item (i.e. one
// of the toggle buttons), we force the in-call buttons to update,
// to make sure the user sees the *new* current state.
//
// (But note that some toggle buttons may *not* immediately change
// the state of the Phone, in which case the updateInCallTouchUi()
// call here won't have any visible effect. Instead, those
// buttons will get updated by the updateScreen() call that gets
// triggered when the onPhoneStateChanged() event comes in.)
//
// TODO: updateInCallTouchUi() is overkill here; it would be
// more efficient to update *only* the affected button(s).
// Consider adding API for that. (This is lo-pri since
// updateInCallTouchUi() is pretty cheap already...)
updateInCallTouchUi();
}
/**
* Update the network provider's overlay based on the value of
* mProviderOverlayVisible.
* If false the overlay is hidden otherwise it is shown. A
* delayed message is posted to take the overalay down after
* PROVIDER_OVERLAY_TIMEOUT. This ensures the user will see the
* overlay even if the call setup phase is very short.
*/
private void updateProviderOverlay() {
if (VDBG) log("updateProviderOverlay: " + mProviderOverlayVisible);
ViewGroup overlay = (ViewGroup) findViewById(R.id.inCallProviderOverlay);
if (mProviderOverlayVisible) {
CharSequence template = getText(R.string.calling_via_template);
CharSequence text = TextUtils.expandTemplate(template, mProviderLabel,
mProviderAddress);
TextView message = (TextView) findViewById(R.id.callingVia);
message.setCompoundDrawablesWithIntrinsicBounds(mProviderIcon, null, null, null);
message.setText(text);
overlay.setVisibility(View.VISIBLE);
// Remove any zombie messages and then send a message to
// self to remove the overlay after some time.
mHandler.removeMessages(EVENT_HIDE_PROVIDER_OVERLAY);
Message msg = Message.obtain(mHandler, EVENT_HIDE_PROVIDER_OVERLAY);
mHandler.sendMessageDelayed(msg, PROVIDER_OVERLAY_TIMEOUT);
} else {
overlay.setVisibility(View.GONE);
}
}
/**
* Updates the "Press Menu for more options" hint based on the current
* state of the Phone.
*/
private void updateMenuButtonHint() {
if (VDBG) log("updateMenuButtonHint()...");
boolean hintVisible = true;
final boolean hasRingingCall = !mRingingCall.isIdle();
final boolean hasActiveCall = !mForegroundCall.isIdle();
final boolean hasHoldingCall = !mBackgroundCall.isIdle();
// The hint is hidden only when there's no menu at all,
// which only happens in a few specific cases:
if (mInCallScreenMode == InCallScreenMode.CALL_ENDED) {
// The "Call ended" state.
hintVisible = false;
} else if (hasRingingCall && !(hasActiveCall && !hasHoldingCall)) {
// An incoming call where you *don't* have the option to
// "answer & end" or "answer & hold".
hintVisible = false;
} else if (!phoneIsInUse()) {
// Or if the phone is totally idle (like if an error dialog
// is up, or an MMI is running.)
hintVisible = false;
}
// The hint is also hidden on devices where we use onscreen
// touchable buttons instead.
if (isTouchUiEnabled()) {
hintVisible = false;
}
// Also, if an incoming call is ringing, hide the hint if the
// "incoming call" touch UI is present (since the SlidingTab
// widget takes up a lot of space and the hint would collide with
// it.)
if (hasRingingCall && isIncomingCallTouchUiEnabled()) {
hintVisible = false;
}
int hintVisibility = (hintVisible) ? View.VISIBLE : View.GONE;
mCallCard.getMenuButtonHint().setVisibility(hintVisibility);
// TODO: Consider hiding the hint(s) whenever the menu is onscreen!
// (Currently, the menu is rendered on top of the hint, but the
// menu is semitransparent so you can still see the hint
// underneath, and the hint is *just* visible enough to be
// distracting.)
}
/**
* Brings up UI to handle the various error conditions that
* can occur when first initializing the in-call UI.
* This is called from onResume() if we encountered
* an error while processing our initial Intent.
*
* @param status one of the InCallInitStatus error codes.
*/
private void handleStartupError(InCallInitStatus status) {
if (DBG) log("handleStartupError(): status = " + status);
// NOTE that the regular Phone UI is in an uninitialized state at
// this point, so we don't ever want the user to see it.
// That means:
// - Any cases here that need to go to some other activity should
// call startActivity() AND immediately call endInCallScreenSession
// on this one.
// - Any cases here that bring up a Dialog must ensure that the
// Dialog handles both OK *and* cancel by calling endInCallScreenSession.
// Activity. (See showGenericErrorDialog() for an example.)
switch (status) {
case VOICEMAIL_NUMBER_MISSING:
// Bring up the "Missing Voicemail Number" dialog, which
// will ultimately take us to some other Activity (or else
// just bail out of this activity.)
handleMissingVoiceMailNumber();
break;
case POWER_OFF:
// Radio is explictly powered off.
// TODO: This UI is ultra-simple for 1.0. It would be nicer
// to bring up a Dialog instead with the option "turn on radio
// now". If selected, we'd turn the radio on, wait for
// network registration to complete, and then make the call.
showGenericErrorDialog(R.string.incall_error_power_off, true);
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);
break;
case OUT_OF_SERVICE:
// No network connection.
showGenericErrorDialog(R.string.incall_error_out_of_service, true);
break;
case PHONE_NOT_IN_USE:
// This error is handled directly in onResume() (by bailing
// out of the activity.) We should never see it here.
Log.w(LOG_TAG,
"handleStartupError: unexpected PHONE_NOT_IN_USE status");
break;
case NO_PHONE_NUMBER_SUPPLIED:
// The supplied Intent didn't contain a valid phone number.
// TODO: Need UI spec for this failure case; for now just
// show a generic error.
showGenericErrorDialog(R.string.incall_error_no_phone_number_supplied, true);
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 (mPhone.getState() == Phone.State.OFFHOOK) {
Toast.makeText(this, 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);
break;
default:
Log.w(LOG_TAG, "handleStartupError: unexpected status code " + status);
showGenericErrorDialog(R.string.incall_error_call_failed, true);
break;
}
}
/**
* 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)
.setIcon(android.R.drawable.ic_dialog_alert)
.create();
mCallLostDialog.show();
}
private void bailOutAfterErrorDialog() {
if (mGenericErrorDialog != null) {
if (DBG) log("bailOutAfterErrorDialog: DISMISSING mGenericErrorDialog.");
mGenericErrorDialog.dismiss();
mGenericErrorDialog = null;
}
if (DBG) log("bailOutAfterErrorDialog(): end InCallScreen session...");
endInCallScreenSession();
}
/**
* 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 ((mInCallScreenMode == InCallScreenMode.OTA_NORMAL
|| mInCallScreenMode == InCallScreenMode.OTA_ENDED)
&& otaUtils != null) {
otaUtils.dismissAllOtaDialogs();
}
if (mPausePromptDialog != null) {
if (DBG) log("- DISMISSING mPausePromptDialog.");
mPausePromptDialog.dismiss();
mPausePromptDialog = null;
}
}
//
// Helper functions for answering incoming calls.
//
/**
* Answer a ringing call. This method does nothing if there's no
* ringing or waiting call.
*/
/* package */ void internalAnswerCall() {
// if (DBG) log("internalAnswerCall()...");
// if (DBG) PhoneUtils.dumpCallState(mPhone);
final boolean hasRingingCall = !mRingingCall.isIdle();
if (hasRingingCall) {
int phoneType = mPhone.getPhoneType();
if (phoneType == Phone.PHONE_TYPE_CDMA) {
if (DBG) log("internalAnswerCall: answering (CDMA)...");
// In CDMA this is simply a wrapper around PhoneUtils.answerCall().
PhoneUtils.answerCall(mPhone); // Automatically holds the current active call,
// if there is one
} else if (phoneType == Phone.PHONE_TYPE_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 = !mForegroundCall.isIdle();
final boolean hasHoldingCall = !mBackgroundCall.isIdle();
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(mPhone);
// Alternatively, we could use
// PhoneUtils.answerAndEndHolding(mPhone);
// here to end the on-hold call instead.
} else {
if (DBG) log("internalAnswerCall: answering...");
PhoneUtils.answerCall(mPhone); // Automatically holds the current active call,
// if there is one
}
} else {
throw new IllegalStateException("Unexpected phone type: " + phoneType);
}
}
}
/**
* Answer the ringing call *and* hang up the ongoing call.
*/
/* package */ void internalAnswerAndEnd() {
if (DBG) log("internalAnswerAndEnd()...");
// if (DBG) PhoneUtils.dumpCallState(mPhone);
PhoneUtils.answerAndEndActive(mPhone);
}
/**
* Hang up the ringing call (aka "Don't answer").
*/
/* package */ void internalHangupRingingCall() {
if (DBG) log("internalHangupRingingCall()...");
PhoneUtils.hangupRingingCall(mPhone);
}
/**
* Hang up the current active call.
*/
/* package */ void internalHangup() {
if (DBG) log("internalHangup()...");
PhoneUtils.hangup(mPhone);
}
/**
* 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.)
mDialer.closeDialer(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.
PhoneUtils.switchHoldingAndActive(mPhone);
// If we have a valid BluetoothHandsfree 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 BluetoothHandsfree internally
if (mPhone.getPhoneType() == Phone.PHONE_TYPE_CDMA) {
BluetoothHandsfree bthf = PhoneApp.getInstance().getBluetoothHandsfree();
if (bthf != null) {
bthf.cdmaSwapSecondCallState();
}
}
}
/**
* 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.
*/
private void setInCallScreenMode(InCallScreenMode newMode) {
if (DBG) log("setInCallScreenMode: " + newMode);
mInCallScreenMode = newMode;
switch (mInCallScreenMode) {
case MANAGE_CONFERENCE:
if (!PhoneUtils.isConferenceCall(mForegroundCall)) {
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 = mForegroundCall.getConnections();
// 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 inCallPanel and 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.
long callDuration = mForegroundCall.getEarliestConnection().getDurationMillis();
mManageConferenceUtils.startConferenceTime(
SystemClock.elapsedRealtime() - callDuration);
mInCallPanel.setVisibility(View.GONE);
// No need to close the dialer here, since the Manage
// Conference UI will just cover it up anyway.
break;
case CALL_ENDED:
// Display the CallCard (in the "Call ended" state)
// and hide all other UI.
mManageConferenceUtils.setPanelVisible(false);
mManageConferenceUtils.stopConferenceTime();
updateMenuButtonHint(); // Hide the Menu button hint
// Make sure the CallCard (which is a child of mInCallPanel) is visible.
mInCallPanel.setVisibility(View.VISIBLE);
break;
case NORMAL:
mInCallPanel.setVisibility(View.VISIBLE);
mManageConferenceUtils.setPanelVisible(false);
mManageConferenceUtils.stopConferenceTime();
break;
case OTA_NORMAL:
otaUtils.setCdmaOtaInCallScreenUiState(
OtaUtils.CdmaOtaInCallScreenUiState.State.NORMAL);
mInCallPanel.setVisibility(View.GONE);
break;
case OTA_ENDED:
otaUtils.setCdmaOtaInCallScreenUiState(
OtaUtils.CdmaOtaInCallScreenUiState.State.ENDED);
mInCallPanel.setVisibility(View.GONE);
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.
//
// 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 initOtaState 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 (mPhone.getState() != Phone.State.OFFHOOK) {
if (otaUtils != null) {
otaUtils.cleanOtaScreen(true);
}
} else {
log("WARNING: Setting mode to UNDEFINED but phone is OFFHOOK,"
+ " skip cleanOtaScreen.");
}
mInCallPanel.setVisibility(View.VISIBLE);
break;
}
// Update the visibility of the DTMF dialer tab on any state
// change.
updateDialpadVisibility();
// Update the in-call touch UI on any state change (since it may
// need to hide or re-show itself.)
updateInCallTouchUi();
}
/**
* @return true if the "Manage conference" UI is currently visible.
*/
/* package */ boolean isManageConferenceMode() {
return (mInCallScreenMode == 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: " + mForegroundCall + "...");
List<Connection> connections = mForegroundCall.getConnections();
if (connections == null) {
if (VDBG) log("==> no connections on foreground call!");
// Hide the Manage Conference panel, return to NORMAL mode.
setInCallScreenMode(InCallScreenMode.NORMAL);
InCallInitStatus status = syncWithPhoneState();
if (status != InCallInitStatus.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);
InCallInitStatus status = syncWithPhoneState();
if (status != InCallInitStatus.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 the visibility of the DTMF dialpad (and its onscreen
* "handle", if applicable), based on the current state of the phone
* and/or the current InCallScreenMode.
*/
private void updateDialpadVisibility() {
//
// (1) The dialpad itself:
//
// 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, and especially to make sure that the "touch
// lock" overlay won't appear.)
if (mPhone.getState() == Phone.State.RINGING) {
mDialer.closeDialer(false); // don't do the "closing" animation
// Also, 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().
mDialer.clearDigits();
}
//
// (2) The onscreen "handle":
//
// The handle is visible only if it's OK to actually open the
// dialpad. (Note this is meaningful only on platforms that use a
// SlidingDrawer as a container for the dialpad.)
mDialer.setHandleVisible(okToShowDialpad());
//
// (3) The main in-call panel (containing the CallCard):
//
// On some platforms(*) we need to hide the CallCard (which is a
// child of mInCallPanel) while the dialpad is visible.
//
// (*) We need to do this when using the dialpad from the
// InCallTouchUi widget, but not when using the
// SlidingDrawer-based dialpad, because the SlidingDrawer itself
// is opaque.)
if (!mDialer.usingSlidingDrawer()) {
if (mDialerView != null) {
mDialerView.setKeysBackgroundResource(
isBluetoothAudioConnected() ? R.drawable.btn_dial_blue
: R.drawable.btn_dial_green);
}
if (isDialerOpened()) {
mInCallPanel.setVisibility(View.GONE);
} else {
// Dialpad is dismissed; bring back the CallCard if
// it's supposed to be visible.
if ((mInCallScreenMode == InCallScreenMode.NORMAL)
|| (mInCallScreenMode == InCallScreenMode.CALL_ENDED)) {
mInCallPanel.setVisibility(View.VISIBLE);
}
}
}
}
/**
* @return true if the DTMF dialpad is currently visible.
*/
/* package */ boolean isDialerOpened() {
return (mDialer != null && mDialer.isOpened());
}
/**
* Called any time the DTMF dialpad is opened.
* @see DTMFTwelveKeyDialer.onDialerOpen()
*/
/* package */ void onDialerOpen() {
if (DBG) log("onDialerOpen()...");
// ANY time the dialpad becomes visible, start the timer that will
// eventually bring up the "touch lock" overlay.
resetTouchLockTimer();
// Update the in-call touch UI (which may need to hide itself, if
// it's enabled.)
updateInCallTouchUi();
// Update any other onscreen UI elements that depend on the dialpad.
updateDialpadVisibility();
// This counts as explicit "user activity".
PhoneApp.getInstance().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 ((mInCallScreenMode == InCallScreenMode.OTA_NORMAL
|| mInCallScreenMode == InCallScreenMode.OTA_ENDED)
&& otaUtils != null) {
otaUtils.hideOtaScreen();
}
}
/**
* Called any time the DTMF dialpad is closed.
* @see DTMFTwelveKeyDialer.onDialerClose()
*/
/* package */ void onDialerClose() {
if (DBG) log("onDialerClose()...");
final PhoneApp app = PhoneApp.getInstance();
// OTA-specific cleanup upon closing the dialpad.
if ((mInCallScreenMode == InCallScreenMode.OTA_NORMAL)
|| (mInCallScreenMode == InCallScreenMode.OTA_ENDED)
|| ((app.cdmaOtaScreenState != null)
&& (app.cdmaOtaScreenState.otaScreenState ==
CdmaOtaScreenState.OtaScreenState.OTA_STATUS_ACTIVATION))) {
mDialer.setHandleVisible(false);
if (otaUtils != null) {
otaUtils.otaShowProperScreen();
}
}
// Dismiss the "touch lock" overlay if it was visible.
// (The overlay is only ever used on top of the dialpad).
enableTouchLock(false);
// Update the in-call touch UI (which may need to re-show itself.)
updateInCallTouchUi();
// Update the visibility of the dialpad itself (and any other
// onscreen UI elements that depend on it.)
updateDialpadVisibility();
// This counts as explicit "user activity".
app.getInstance().pokeUserActivity();
}
/**
* Determines when we can dial DTMF tones.
*/
private boolean okToDialDTMFTones() {
final boolean hasRingingCall = !mRingingCall.isIdle();
final Call.State fgCallState = mForegroundCall.getState();
// 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
&& (mInCallScreenMode != InCallScreenMode.MANAGE_CONFERENCE);
if (VDBG) log ("[okToDialDTMFTones] foreground state: " + fgCallState +
", ringing state: " + hasRingingCall +
", call screen mode: " + mInCallScreenMode +
", 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 visibility of the dialer's
* onscreen handle, if applicable, and the enabledness of the "Show
* dialpad" onscreen button or menu item.)
*/
/* package */ boolean okToShowDialpad() {
// The dialpad is available only when it's OK to dial DTMF
// tones given the current state of the current call.
return okToDialDTMFTones();
}
/**
* 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);
}
/**
* Updates the state of the in-call touch UI.
*/
private void updateInCallTouchUi() {
if (mInCallTouchUi != null) {
mInCallTouchUi.updateState(mPhone);
}
}
/**
* @return true if the onscreen touch UI is enabled (for regular
* "ongoing call" states) on the current device.
*/
public boolean isTouchUiEnabled() {
return (mInCallTouchUi != null) && mInCallTouchUi.isTouchUiEnabled();
}
/**
* @return true if the onscreen touch UI is enabled for
* the "incoming call" state on the current device.
*/
public boolean isIncomingCallTouchUiEnabled() {
return (mInCallTouchUi != null) && mInCallTouchUi.isIncomingCallTouchUiEnabled();
}
/**
* Posts a handler message telling the InCallScreen to update the
* onscreen in-call touch UI.
*
* This is just a wrapper around updateInCallTouchUi(), for use by the
* rest of the phone app or from a thread other than the UI thread.
*/
/* package */ void requestUpdateTouchUi() {
if (DBG) log("requestUpdateTouchUi()...");
mHandler.removeMessages(REQUEST_UPDATE_TOUCH_UI);
mHandler.sendEmptyMessage(REQUEST_UPDATE_TOUCH_UI);
}
/**
* @return true if it's OK to display the in-call touch UI, given the
* current state of the InCallScreen.
*/
/* package */ boolean okToShowInCallTouchUi() {
// Note that this method is concerned only with the internal state
// of the InCallScreen. (The InCallTouchUi widget has separate
// logic to make sure it's OK to display the touch UI given the
// current telephony state, and that it's allowed on the current
// device in the first place.)
// The touch UI is NOT available if:
// - we're in some InCallScreenMode other than NORMAL
// (like CALL_ENDED or one of the OTA modes)
return (mInCallScreenMode == InCallScreenMode.NORMAL);
}
/**
* @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.
return ((mPhone.getServiceState().getState() == ServiceState.STATE_EMERGENCY_ONLY) ||
(mPhone.getServiceState().getState() == ServiceState.STATE_OUT_OF_SERVICE) ||
(PhoneApp.getInstance().getKeyguardManager().inKeyguardRestrictedInputMode()));
}
//
// In-call menu UI
//
/**
* Override onCreatePanelView(), in order to get complete control
* over the UI that comes up when the user presses MENU.
*
* This callback allows me to return a totally custom View hierarchy
* (with custom layout and custom "item" views) to be shown instead
* of a standard android.view.Menu hierarchy.
*
* This gets called (with featureId == FEATURE_OPTIONS_PANEL) every
* time we need to bring up the menu. (And in cases where we return
* non-null, that means that the "standard" menu callbacks
* onCreateOptionsMenu() and onPrepareOptionsMenu() won't get called
* at all.)
*/
@Override
public View onCreatePanelView(int featureId) {
if (VDBG) log("onCreatePanelView(featureId = " + featureId + ")...");
// We only want this special behavior for the "options panel"
// feature (i.e. the standard menu triggered by the MENU button.)
if (featureId != Window.FEATURE_OPTIONS_PANEL) {
return null;
}
// For now, totally disable the in-call menu on devices where we
// use onscreen touchable buttons instead.
// TODO: even on "full touch" devices we may still ultimately need
// a regular menu in some states. Need UI spec.
if (isTouchUiEnabled()) {
return null;
}
// TODO: May need to revisit the wake state here if this needs to be
// tweaked.
// Make sure there are no pending messages to *dismiss* the menu.
mHandler.removeMessages(DISMISS_MENU);
if (mInCallMenu == null) {
if (VDBG) log("onCreatePanelView: creating mInCallMenu (first time)...");
mInCallMenu = new InCallMenu(this);
mInCallMenu.initMenu();
}
boolean okToShowMenu = mInCallMenu.updateItems(mPhone);
return okToShowMenu ? mInCallMenu.getView() : null;
}
/**
* Dismisses the menu panel (see onCreatePanelView().)
*
* @param dismissImmediate If true, hide the panel immediately.
* If false, leave the menu visible onscreen for
* a brief interval before dismissing it (so the
* user can see the state change resulting from
* his original click.)
*/
/* package */ void dismissMenu(boolean dismissImmediate) {
if (VDBG) log("dismissMenu(immediate = " + dismissImmediate + ")...");
if (dismissImmediate) {
closeOptionsMenu();
} else {
mHandler.removeMessages(DISMISS_MENU);
mHandler.sendEmptyMessageDelayed(DISMISS_MENU, MENU_DISMISS_DELAY);
// This will result in a dismissMenu(true) call shortly.
}
}
/**
* Override onPanelClosed() to capture the panel closing event,
* allowing us to set the poke lock correctly whenever the option
* menu panel goes away.
*/
@Override
public void onPanelClosed(int featureId, Menu menu) {
if (VDBG) log("onPanelClosed(featureId = " + featureId + ")...");
// We only want this special behavior for the "options panel"
// feature (i.e. the standard menu triggered by the MENU button.)
if (featureId == Window.FEATURE_OPTIONS_PANEL) {
// TODO: May need to return to the original wake state here
// if onCreatePanelView ends up changing the wake state.
}
super.onPanelClosed(featureId, menu);
}
//
// 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.
//
// - BluetoothHandsfree is the API to control the audio connection to
// a bluetooth headset. We use this API to switch the headset on and
// off when the user presses the "Bluetooth" button.
// Our BluetoothHandsfree instance (mBluetoothHandsfree) is created
// by the PhoneApp and will be null if the device is not BT capable.
//
/**
* @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()...");
if (mBluetoothHandsfree == null) {
// Device is not BT capable.
if (VDBG) log(" ==> FALSE (not BT capable)");
return false;
}
// 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) {
if (VDBG) log(" - headset state = " + mBluetoothHeadset.getState());
BluetoothDevice headset = mBluetoothHeadset.getCurrentHeadset();
if (VDBG) log(" - headset address: " + headset);
if (headset != null) {
isConnected = mBluetoothHeadset.isConnected(headset);
if (VDBG) log(" - isConnected: " + isConnected);
}
}
if (VDBG) log(" ==> " + isConnected);
return isConnected;
}
/**
* @return true if a BT device is available, and its audio is currently connected.
*/
/* package */ boolean isBluetoothAudioConnected() {
if (mBluetoothHandsfree == null) {
if (VDBG) log("isBluetoothAudioConnected: ==> FALSE (null mBluetoothHandsfree)");
return false;
}
boolean isAudioOn = mBluetoothHandsfree.isAudioOn();
if (VDBG) log("isBluetoothAudioConnected: ==> isAudioOn = " + isAudioOn);
return isAudioOn;
}
/**
* Helper method used to control the state of the green LED in the
* "Bluetooth" menu item.
*
* @return true if a BT device is available and its audio is currently connected,
* <b>or</b> if we issued a BluetoothHandsfree.userWantsAudioOn()
* 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 userWantsAudioOn() call "recently enough", even
// if BT isn't actually connected yet, let's still pretend BT is
// on. This is how we make the green LED in the menu item turn on
// right away.
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: "
+ PhoneApp.getInstance().showBluetoothIndication());
log("=");
if (mBluetoothHandsfree != null) {
log("= BluetoothHandsfree.isAudioOn: " + mBluetoothHandsfree.isAudioOn());
if (mBluetoothHeadset != null) {
BluetoothDevice headset = mBluetoothHeadset.getCurrentHeadset();
log("= BluetoothHeadset.getCurrentHeadset: " + headset);
if (headset != null) {
log("= BluetoothHeadset.isConnected: "
+ mBluetoothHeadset.isConnected(headset));
}
} else {
log("= mBluetoothHeadset is null");
}
} else {
log("= mBluetoothHandsfree is null; device is not BT capable");
}
}
/* package */ void connectBluetoothAudio() {
if (VDBG) log("connectBluetoothAudio()...");
if (mBluetoothHandsfree != null) {
mBluetoothHandsfree.userWantsAudioOn();
}
// Watch out: The bluetooth connection doesn't happen instantly;
// the userWantsAudioOn() call returns instantly but does its real
// work in another thread. Also, in practice the BT connection
// takes longer than MENU_DISMISS_DELAY to complete(!) so we need
// a little trickery here to make the menu item's green LED update
// instantly.
// (See isBluetoothAudioConnectedOrPending() above.)
mBluetoothConnectionPending = true;
mBluetoothConnectionRequestTime = SystemClock.elapsedRealtime();
}
/* package */ void disconnectBluetoothAudio() {
if (VDBG) log("disconnectBluetoothAudio()...");
if (mBluetoothHandsfree != null) {
mBluetoothHandsfree.userWantsAudioOff();
}
mBluetoothConnectionPending = false;
}
//
// "Touch lock" UI.
//
// When the DTMF dialpad is up, after a certain amount of idle time we
// display an overlay graphic on top of the dialpad and "lock" the
// touch UI. (UI Rationale: We need *some* sort of screen lock, with
// a fairly short timeout, to avoid false touches from the user's face
// while in-call. But we *don't* want to do this by turning off the
// screen completely, since that's confusing (the user can't tell
// what's going on) *and* it's fairly cumbersome to have to hit MENU
// to bring the screen back, rather than using some gesture on the
// touch screen.)
//
// The user can dismiss the touch lock overlay by double-tapping on
// the central "lock" icon. Also, the touch lock overlay will go away
// by itself if the DTMF dialpad is dismissed for any reason, such as
// the current call getting disconnected (see onDialerClose()).
//
// This entire feature is disabled on devices which use a proximity
// sensor to turn the screen off while in-call.
//
/**
* Initializes the "touch lock" UI widgets. We do this lazily
* to avoid slowing down the initial launch of the InCallScreen.
*/
private void initTouchLock() {
if (VDBG) log("initTouchLock()...");
if (mTouchLockOverlay != null) {
Log.w(LOG_TAG, "initTouchLock: already initialized!");
return;
}
if (!mUseTouchLockOverlay) {
Log.w(LOG_TAG, "initTouchLock: touch lock isn't used on this device!");
return;
}
mTouchLockOverlay = (View) findViewById(R.id.touchLockOverlay);
// Note mTouchLockOverlay's visibility is initially GONE.
mTouchLockIcon = (View) findViewById(R.id.touchLockIcon);
// Handle touch events. (Basically mTouchLockOverlay consumes and
// discards any touch events it sees, and mTouchLockIcon listens
// for the "double-tap to unlock" gesture.)
mTouchLockOverlay.setOnTouchListener(this);
mTouchLockIcon.setOnTouchListener(this);
mTouchLockFadeIn = AnimationUtils.loadAnimation(this, R.anim.touch_lock_fade_in);
}
private boolean isTouchLocked() {
return mUseTouchLockOverlay
&& (mTouchLockOverlay != null)
&& (mTouchLockOverlay.getVisibility() == View.VISIBLE);
}
/**
* Enables or disables the "touch lock" overlay on top of the DTMF dialpad.
*
* If enable=true, bring up the overlay immediately using an animated
* fade-in effect. (Or do nothing if the overlay isn't appropriate
* right now, like if the dialpad isn't up, or the speaker is on.)
*
* If enable=false, immediately take down the overlay. (Or do nothing
* if the overlay isn't actually up right now.)
*
* Note that with enable=false this method will *not* automatically
* start the touch lock timer. (So when taking down the overlay while
* the dialer is still up, the caller is also responsible for calling
* resetTouchLockTimer(), to make sure the overlay will get
* (re-)enabled later.)
*
*/
private void enableTouchLock(boolean enable) {
if (VDBG) log("enableTouchLock(" + enable + ")...");
if (enable) {
// We shouldn't have even gotten here if we don't use the
// touch lock overlay feature at all on this device.
if (!mUseTouchLockOverlay) {
Log.w(LOG_TAG, "enableTouchLock: touch lock isn't used on this device!");
return;
}
// The "touch lock" overlay is only ever used on top of the
// DTMF dialpad.
if (!mDialer.isOpened()) {
if (VDBG) log("enableTouchLock: dialpad isn't up, no need to lock screen.");
return;
}
// Also, the "touch lock" overlay NEVER appears if the speaker is in use.
if (PhoneUtils.isSpeakerOn(this)) {
if (VDBG) log("enableTouchLock: speaker is on, no need to lock screen.");
return;
}
// Initialize the UI elements if necessary.
if (mTouchLockOverlay == null) {
initTouchLock();
}
// First take down the menu if it's up (since it's confusing
// to see a touchable menu *above* the touch lock overlay.)
// Note dismissMenu() has no effect if the menu is already closed.
dismissMenu(true); // dismissImmediate = true
// Bring up the touch lock overlay (with an animated fade)
mTouchLockOverlay.setVisibility(View.VISIBLE);
mTouchLockOverlay.startAnimation(mTouchLockFadeIn);
} else {
// TODO: it might be nice to immediately kill the animation if
// we're in the middle of fading-in:
// if (mTouchLockFadeIn.hasStarted() && !mTouchLockFadeIn.hasEnded()) {
// mTouchLockOverlay.clearAnimation();
// }
// but the fade-in is so quick that this probably isn't necessary.
// Take down the touch lock overlay (immediately)
if (mTouchLockOverlay != null) mTouchLockOverlay.setVisibility(View.GONE);
}
}
/**
* Schedule the "touch lock" overlay to begin fading in after a short
* delay, but only if the DTMF dialpad is currently visible.
*
* (This is designed to be triggered on any user activity
* while the dialpad is up but not locked, and also
* whenever the user "unlocks" the touch lock overlay.)
*
* Calling this method supersedes any previous resetTouchLockTimer()
* calls (i.e. we first clear any pending TOUCH_LOCK_TIMER messages.)
*/
private void resetTouchLockTimer() {
if (VDBG) log("resetTouchLockTimer()...");
// This is a no-op if this device doesn't use the touch lock
// overlay feature at all.
if (!mUseTouchLockOverlay) return;
mHandler.removeMessages(TOUCH_LOCK_TIMER);
if (mDialer.isOpened() && !isTouchLocked()) {
// The touch lock delay value comes from Gservices; we use
// the same value that's used for the PowerManager's
// POKE_LOCK_SHORT_TIMEOUT flag (i.e. the fastest possible
// screen timeout behavior.)
// Do a fresh lookup each time, since settings values can
// change on the fly. (The Settings.Secure helper class
// caches these values so this call is usually cheap.)
int touchLockDelay = Settings.Secure.getInt(
getContentResolver(),
Settings.Secure.SHORT_KEYLIGHT_DELAY_MS,
TOUCH_LOCK_DELAY_DEFAULT);
mHandler.sendEmptyMessageDelayed(TOUCH_LOCK_TIMER, touchLockDelay);
}
}
/**
* Handles the TOUCH_LOCK_TIMER event.
* @see resetTouchLockTimer
*/
private void touchLockTimerExpired() {
// Ok, it's been long enough since we had any user activity with
// the DTMF dialpad up. If the dialpad is still up, start fading
// in the "touch lock" overlay.
enableTouchLock(true);
}
// View.OnTouchListener implementation
public boolean onTouch(View v, MotionEvent event) {
if (VDBG) log ("onTouch(View " + v + ")...");
// Handle touch events on the "touch lock" overlay.
if ((v == mTouchLockIcon) || (v == mTouchLockOverlay)) {
// TODO: move this big hunk of code to a helper function, or
// even better out to a separate helper class containing all
// the touch lock overlay code.
// We only care about these touches while the touch lock UI is
// visible (including the time during the fade-in animation.)
if (!isTouchLocked()) {
// Got an event from the touch lock UI, but we're not locked!
// (This was probably a touch-UP right after we unlocked.
// Ignore it.)
return false;
}
// (v == mTouchLockIcon) means the user hit the lock icon in the
// middle of the screen, and (v == mTouchLockOverlay) is a touch
// anywhere else on the overlay.
if (v == mTouchLockIcon) {
// Direct hit on the "lock" icon. Handle the double-tap gesture.
if (event.getAction() == MotionEvent.ACTION_DOWN) {
long now = SystemClock.uptimeMillis();
if (VDBG) log("- touch lock icon: handling a DOWN event, t = " + now);
// Look for the double-tap gesture:
if (now < mTouchLockLastTouchTime + ViewConfiguration.getDoubleTapTimeout()) {
if (VDBG) log("==> touch lock icon: DOUBLE-TAP!");
// This was the 2nd tap of a double-tap gesture.
// Take down the touch lock overlay, but post a
// message in the future to bring it back later.
enableTouchLock(false);
resetTouchLockTimer();
// This counts as explicit "user activity".
PhoneApp.getInstance().pokeUserActivity();
}
} else if (event.getAction() == MotionEvent.ACTION_UP) {
// Stash away the current time in case this is the first
// tap of a double-tap gesture. (We measure the time from
// the first tap's UP to the second tap's DOWN.)
mTouchLockLastTouchTime = SystemClock.uptimeMillis();
}
// And regardless of what just happened, we *always* consume
// touch events while the touch lock UI is (or was) visible.
return true;
} else { // (v == mTouchLockOverlay)
// User touched the "background" area of the touch lock overlay.
// TODO: If we're in the middle of the fade-in animation,
// consider making a touch *anywhere* immediately unlock the
// UI. This could be risky, though, if the user tries to
// *double-tap* during the fade-in (in which case the 2nd tap
// might 't become a false touch on the dialpad!)
//
//if (event.getAction() == MotionEvent.ACTION_DOWN) {
// if (DBG) log("- touch lock overlay background: handling a DOWN event.");
//
// if (mTouchLockFadeIn.hasStarted() && !mTouchLockFadeIn.hasEnded()) {
// // If we're still fading-in, a touch *anywhere* onscreen
// // immediately unlocks.
// if (DBG) log("==> touch lock: tap during fade-in!");
//
// mTouchLockOverlay.clearAnimation();
// enableTouchLock(false);
// // ...but post a message in the future to bring it
// // back later.
// resetTouchLockTimer();
// }
//}
// And regardless of what just happened, we *always* consume
// touch events while the touch lock UI is (or was) visible.
return true;
}
} else {
Log.w(LOG_TAG, "onTouch: event from unexpected View: " + v);
return false;
}
}
// Any user activity while the dialpad is up, but not locked, should
// reset the touch lock timer back to the full delay amount.
@Override
public void onUserInteraction() {
if (mDialer.isOpened() && !isTouchLocked()) {
resetTouchLockTimer();
}
}
/**
* 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() {
final PhoneApp app = PhoneApp.getInstance();
if ((mInCallScreenMode == InCallScreenMode.OTA_NORMAL)
|| ((app.cdmaOtaScreenState != null)
&& (app.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() {
final PhoneApp app = PhoneApp.getInstance();
if (DBG) log("handleOtaCallEnd entering");
if (((mInCallScreenMode == InCallScreenMode.OTA_NORMAL)
|| ((app.cdmaOtaScreenState != null)
&& (app.cdmaOtaScreenState.otaScreenState !=
CdmaOtaScreenState.OtaScreenState.OTA_STATUS_UNDEFINED)))
&& ((app.cdmaOtaProvisionData != null)
&& (!app.cdmaOtaProvisionData.inOtaSpcState))) {
if (DBG) log("handleOtaCallEnd - Set OTA Call End stater");
setInCallScreenMode(InCallScreenMode.OTA_ENDED);
updateScreen();
}
}
public boolean isOtaCallInEndState() {
return (mInCallScreenMode == InCallScreenMode.OTA_ENDED);
}
/**
* Checks to see if the current call is a CDMA OTA Call, based on the
* action of the specified intent and OTA Screen state information.
*
* The OTA call is a CDMA-specific concept, so this method will
* always return false on a GSM phone.
*/
private boolean checkIsOtaCall(Intent intent) {
if (VDBG) log("checkIsOtaCall...");
if (intent == null || intent.getAction() == null) {
return false;
}
if (mPhone.getPhoneType() != Phone.PHONE_TYPE_CDMA) {
return false;
}
final PhoneApp app = PhoneApp.getInstance();
if ((app.cdmaOtaScreenState == null)
|| (app.cdmaOtaProvisionData == null)) {
if (DBG) log("checkIsOtaCall: OtaUtils.CdmaOtaScreenState not initialized");
return false;
}
String action = intent.getAction();
boolean isOtaCall = false;
if (action.equals(ACTION_SHOW_ACTIVATION)) {
if (DBG) log("checkIsOtaCall action = ACTION_SHOW_ACTIVATION");
if (!app.cdmaOtaProvisionData.isOtaCallIntentProcessed) {
if (DBG) log("checkIsOtaCall: ACTION_SHOW_ACTIVATION is not handled before");
app.cdmaOtaProvisionData.isOtaCallIntentProcessed = true;
app.cdmaOtaScreenState.otaScreenState =
CdmaOtaScreenState.OtaScreenState.OTA_STATUS_ACTIVATION;
}
isOtaCall = true;
} else if (action.equals(Intent.ACTION_CALL)
|| action.equals(Intent.ACTION_CALL_EMERGENCY)) {
String number;
try {
number = getInitialNumber(intent);
} catch (PhoneUtils.VoiceMailNumberMissingException ex) {
if (DBG) log("Error retrieving number using the api getInitialNumber()");
return false;
}
if (mPhone.isOtaSpNumber(number)) {
if (DBG) log("checkIsOtaCall action ACTION_CALL, it is valid OTA number");
isOtaCall = true;
}
} else if (action.equals(intent.ACTION_MAIN)) {
if (DBG) log("checkIsOtaCall action ACTION_MAIN");
boolean isRingingCall = !mRingingCall.isIdle();
if (isRingingCall) {
if (DBG) log("checkIsOtaCall isRingingCall: " + isRingingCall);
return false;
} else if ((app.cdmaOtaInCallScreenUiState.state
== CdmaOtaInCallScreenUiState.State.NORMAL)
|| (app.cdmaOtaInCallScreenUiState.state
== CdmaOtaInCallScreenUiState.State.ENDED)) {
if (DBG) log("checkIsOtaCall action ACTION_MAIN, OTA call already in progress");
isOtaCall = true;
} else {
if (app.cdmaOtaScreenState.otaScreenState !=
CdmaOtaScreenState.OtaScreenState.OTA_STATUS_UNDEFINED) {
if (DBG) log("checkIsOtaCall action ACTION_MAIN, "
+ "OTA call in progress with UNDEFINED");
isOtaCall = true;
}
}
}
if (DBG) log("checkIsOtaCall: isOtaCall =" + isOtaCall);
if (isOtaCall && (otaUtils == null)) {
if (DBG) log("checkIsOtaCall: creating OtaUtils...");
otaUtils = new OtaUtils(getApplicationContext(),
this, mInCallPanel, mCallCard, mDialer);
}
return isOtaCall;
}
/**
* Initialize the OTA State and UI.
*
* On Resume, this function is called to check if current call is
* OTA Call and if it is OTA Call, create OtaUtil object and set
* InCallScreenMode to OTA Call mode (OTA_NORMAL or OTA_ENDED).
* As part of initialization, OTA Call Card is inflated.
* OtaUtil object provides utility apis that InCallScreen calls for OTA Call UI
* rendering, handling of touck/key events on OTA Screens and handling of
* Framework events that result in OTA State change
*
* @return: true if we are in an OtaCall
*/
private boolean initOtaState() {
boolean inOtaCall = false;
if (mPhone.getPhoneType() == Phone.PHONE_TYPE_CDMA) {
final PhoneApp app = PhoneApp.getInstance();
if ((app.cdmaOtaScreenState == null) || (app.cdmaOtaProvisionData == null)) {
if (DBG) log("initOtaState func - All CdmaOTA utility classes not initialized");
return false;
}
inOtaCall = checkIsOtaCall(getIntent());
if (inOtaCall) {
OtaUtils.CdmaOtaInCallScreenUiState.State cdmaOtaInCallScreenState =
otaUtils.getCdmaOtaInCallScreenUiState();
if (cdmaOtaInCallScreenState == OtaUtils.CdmaOtaInCallScreenUiState.State.NORMAL) {
if (DBG) log("initOtaState - in OTA Normal mode");
setInCallScreenMode(InCallScreenMode.OTA_NORMAL);
} else if (cdmaOtaInCallScreenState ==
OtaUtils.CdmaOtaInCallScreenUiState.State.ENDED) {
if (DBG) log("initOtaState - in OTA END mode");
setInCallScreenMode(InCallScreenMode.OTA_ENDED);
} else if (app.cdmaOtaScreenState.otaScreenState ==
CdmaOtaScreenState.OtaScreenState.OTA_STATUS_SUCCESS_FAILURE_DLG) {
if (DBG) log("initOtaState - set OTA END Mode");
setInCallScreenMode(InCallScreenMode.OTA_ENDED);
} else {
if (DBG) log("initOtaState - Set OTA NORMAL Mode");
setInCallScreenMode(InCallScreenMode.OTA_NORMAL);
}
} else {
if (otaUtils != null) {
otaUtils.cleanOtaScreen(false);
}
}
}
return inOtaCall;
}
public void updateMenuItems() {
if (mInCallMenu != null) {
boolean okToShowMenu = mInCallMenu.updateItems(PhoneApp.getInstance().phone);
if (!okToShowMenu) {
dismissMenu(true);
}
}
}
/**
* Updates and returns the InCallControlState instance.
*/
public InCallControlState getUpdatedInCallControlState() {
mInCallControlState.update();
return mInCallControlState;
}
/**
* Updates the background of the InCallScreen to indicate the state of
* the current call(s).
*/
private void updateInCallBackground() {
final boolean hasRingingCall = !mRingingCall.isIdle();
final boolean hasActiveCall = !mForegroundCall.isIdle();
final boolean hasHoldingCall = !mPhone.getBackgroundCall().isIdle();
final PhoneApp app = PhoneApp.getInstance();
final boolean bluetoothActive = app.showBluetoothIndication();
int backgroundResId = R.drawable.bg_in_call_gradient_unidentified;
// Possible states of the background are:
// - bg_in_call_gradient_bluetooth.9.png // blue
// - bg_in_call_gradient_connected.9.png // green
// - bg_in_call_gradient_ended.9.png // red
// - bg_in_call_gradient_on_hold.9.png // orange
// - bg_in_call_gradient_unidentified.9.png // gray
if (hasRingingCall) {
// There's an INCOMING (or WAITING) call.
if (bluetoothActive) {
backgroundResId = R.drawable.bg_in_call_gradient_bluetooth;
} else {
backgroundResId = R.drawable.bg_in_call_gradient_unidentified;
}
} else if (hasHoldingCall && !hasActiveCall) {
// No foreground call, but there is a call on hold.
backgroundResId = R.drawable.bg_in_call_gradient_on_hold;
} else {
// In all cases other than "ringing" and "on hold", the state
// of the foreground call determines the background.
final Call.State fgState = mForegroundCall.getState();
switch (fgState) {
case ACTIVE:
case DISCONNECTING: // Call will disconnect soon, but keep showing
// the normal "connected" background for now.
if (bluetoothActive) {
backgroundResId = R.drawable.bg_in_call_gradient_bluetooth;
} else {
backgroundResId = R.drawable.bg_in_call_gradient_connected;
}
break;
case DISCONNECTED:
backgroundResId = R.drawable.bg_in_call_gradient_ended;
break;
case DIALING:
case ALERTING:
if (bluetoothActive) {
backgroundResId = R.drawable.bg_in_call_gradient_bluetooth;
} else {
backgroundResId = R.drawable.bg_in_call_gradient_unidentified;
}
break;
default:
// Foreground call is (presumably) IDLE.
// We're not usually here at all in this state, but
// this *does* happen in some unusual cases (like
// while displaying an MMI result).
// Use the most generic background.
backgroundResId = R.drawable.bg_in_call_gradient_unidentified;
break;
}
}
mMainFrame.setBackgroundResource(backgroundResId);
}
public void resetInCallScreenMode() {
if (DBG) log("resetInCallScreenMode - InCallScreenMode set to UNDEFINED");
setInCallScreenMode(InCallScreenMode.UNDEFINED);
}
/**
* Clear all the fields related to the provider support.
*/
private void clearProvider() {
mProviderOverlayVisible = false;
mProviderLabel = null;
mProviderIcon = null;
mProviderGatewayUri = null;
mProviderAddress = null;
}
/**
* 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 updateSlidingTabHint(int hintTextResId, int hintColorResId) {
if (VDBG) log("updateRotarySelectorHint(" + hintTextResId + ")...");
if (mCallCard != null) {
mCallCard.setRotarySelectorHint(hintTextResId, hintColorResId);
mCallCard.updateState(mPhone);
// 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...)
}
}
@Override
public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) {
super.dispatchPopulateAccessibilityEvent(event);
mCallCard.dispatchPopulateAccessibilityEvent(event);
return true;
}
private void log(String msg) {
Log.d(LOG_TAG, msg);
}
}