diff options
Diffstat (limited to 'src/com/android/phone/CallNotifier.java')
-rwxr-xr-x | src/com/android/phone/CallNotifier.java | 1907 |
1 files changed, 1907 insertions, 0 deletions
diff --git a/src/com/android/phone/CallNotifier.java b/src/com/android/phone/CallNotifier.java new file mode 100755 index 00000000..39feb255 --- /dev/null +++ b/src/com/android/phone/CallNotifier.java @@ -0,0 +1,1907 @@ +/* + * 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 com.android.internal.telephony.Call; +import com.android.internal.telephony.CallManager; +import com.android.internal.telephony.CallerInfo; +import com.android.internal.telephony.CallerInfoAsyncQuery; +import com.android.internal.telephony.Connection; +import com.android.internal.telephony.Phone; +import com.android.internal.telephony.PhoneConstants; +import com.android.internal.telephony.PhoneBase; +import com.android.internal.telephony.TelephonyCapabilities; +import com.android.internal.telephony.cdma.CdmaCallWaitingNotification; +import com.android.internal.telephony.cdma.CdmaInformationRecords.CdmaDisplayInfoRec; +import com.android.internal.telephony.cdma.CdmaInformationRecords.CdmaSignalInfoRec; +import com.android.internal.telephony.cdma.SignalToneUtil; + +import android.app.ActivityManagerNative; +import android.bluetooth.BluetoothAdapter; +import android.bluetooth.BluetoothHeadset; +import android.bluetooth.BluetoothProfile; +import android.content.Context; +import android.media.AudioManager; +import android.media.ToneGenerator; +import android.net.Uri; +import android.os.AsyncResult; +import android.os.Handler; +import android.os.Message; +import android.os.RemoteException; +import android.os.SystemProperties; +import android.os.SystemVibrator; +import android.os.Vibrator; +import android.provider.CallLog.Calls; +import android.provider.Settings; +import android.telephony.PhoneNumberUtils; +import android.telephony.PhoneStateListener; +import android.telephony.TelephonyManager; +import android.text.TextUtils; +import android.util.EventLog; +import android.util.Log; + +/** + * Phone app module that listens for phone state changes and various other + * events from the telephony layer, and triggers any resulting UI behavior + * (like starting the Ringer and Incoming Call UI, playing in-call tones, + * updating notifications, writing call log entries, etc.) + */ +public class CallNotifier extends Handler + implements CallerInfoAsyncQuery.OnQueryCompleteListener { + private static final String LOG_TAG = "CallNotifier"; + private static final boolean DBG = + (PhoneGlobals.DBG_LEVEL >= 1) && (SystemProperties.getInt("ro.debuggable", 0) == 1); + private static final boolean VDBG = (PhoneGlobals.DBG_LEVEL >= 2); + + // Maximum time we allow the CallerInfo query to run, + // before giving up and falling back to the default ringtone. + private static final int RINGTONE_QUERY_WAIT_TIME = 500; // msec + + // Timers related to CDMA Call Waiting + // 1) For displaying Caller Info + // 2) For disabling "Add Call" menu option once User selects Ignore or CW Timeout occures + private static final int CALLWAITING_CALLERINFO_DISPLAY_TIME = 20000; // msec + private static final int CALLWAITING_ADDCALL_DISABLE_TIME = 30000; // msec + + // Time to display the DisplayInfo Record sent by CDMA network + private static final int DISPLAYINFO_NOTIFICATION_TIME = 2000; // msec + + /** The singleton instance. */ + private static CallNotifier sInstance; + + // Boolean to keep track of whether or not a CDMA Call Waiting call timed out. + // + // This is CDMA-specific, because with CDMA we *don't* get explicit + // notification from the telephony layer that a call-waiting call has + // stopped ringing. Instead, when a call-waiting call first comes in we + // start a 20-second timer (see CALLWAITING_CALLERINFO_DISPLAY_DONE), and + // if the timer expires we clean up the call and treat it as a missed call. + // + // If this field is true, that means that the current Call Waiting call + // "timed out" and should be logged in Call Log as a missed call. If it's + // false when we reach onCdmaCallWaitingReject(), we can assume the user + // explicitly rejected this call-waiting call. + // + // This field is reset to false any time a call-waiting call first comes + // in, and after cleaning up a missed call-waiting call. It's only ever + // set to true when the CALLWAITING_CALLERINFO_DISPLAY_DONE timer fires. + // + // TODO: do we really need a member variable for this? Don't we always + // know at the moment we call onCdmaCallWaitingReject() whether this is an + // explicit rejection or not? + // (Specifically: when we call onCdmaCallWaitingReject() from + // PhoneUtils.hangupRingingCall() that means the user deliberately rejected + // the call, and if we call onCdmaCallWaitingReject() because of a + // CALLWAITING_CALLERINFO_DISPLAY_DONE event that means that it timed + // out...) + private boolean mCallWaitingTimeOut = false; + + // values used to track the query state + private static final int CALLERINFO_QUERY_READY = 0; + private static final int CALLERINFO_QUERYING = -1; + + // the state of the CallerInfo Query. + private int mCallerInfoQueryState; + + // object used to synchronize access to mCallerInfoQueryState + private Object mCallerInfoQueryStateGuard = new Object(); + + // Event used to indicate a query timeout. + private static final int RINGER_CUSTOM_RINGTONE_QUERY_TIMEOUT = 100; + + // Events generated internally: + private static final int PHONE_MWI_CHANGED = 21; + private static final int CALLWAITING_CALLERINFO_DISPLAY_DONE = 22; + private static final int CALLWAITING_ADDCALL_DISABLE_TIMEOUT = 23; + private static final int DISPLAYINFO_NOTIFICATION_DONE = 24; + private static final int CDMA_CALL_WAITING_REJECT = 26; + private static final int UPDATE_IN_CALL_NOTIFICATION = 27; + + // Emergency call related defines: + private static final int EMERGENCY_TONE_OFF = 0; + private static final int EMERGENCY_TONE_ALERT = 1; + private static final int EMERGENCY_TONE_VIBRATE = 2; + + private PhoneGlobals mApplication; + private CallManager mCM; + private CallStateMonitor mCallStateMonitor; + private Ringer mRinger; + private BluetoothHeadset mBluetoothHeadset; + private CallLogger mCallLogger; + private boolean mSilentRingerRequested; + + // ToneGenerator instance for playing SignalInfo tones + private ToneGenerator mSignalInfoToneGenerator; + + // The tone volume relative to other sounds in the stream SignalInfo + private static final int TONE_RELATIVE_VOLUME_SIGNALINFO = 80; + + private Call.State mPreviousCdmaCallState; + private boolean mVoicePrivacyState = false; + private boolean mIsCdmaRedialCall = false; + + // Emergency call tone and vibrate: + private int mIsEmergencyToneOn; + private int mCurrentEmergencyToneState = EMERGENCY_TONE_OFF; + private EmergencyTonePlayerVibrator mEmergencyTonePlayerVibrator; + + // Ringback tone player + private InCallTonePlayer mInCallRingbackTonePlayer; + + // Call waiting tone player + private InCallTonePlayer mCallWaitingTonePlayer; + + // Cached AudioManager + private AudioManager mAudioManager; + + /** + * Initialize the singleton CallNotifier instance. + * This is only done once, at startup, from PhoneApp.onCreate(). + */ + /* package */ static CallNotifier init(PhoneGlobals app, Phone phone, Ringer ringer, + CallLogger callLogger, CallStateMonitor callStateMonitor) { + synchronized (CallNotifier.class) { + if (sInstance == null) { + sInstance = new CallNotifier(app, phone, ringer, callLogger, callStateMonitor); + } else { + Log.wtf(LOG_TAG, "init() called multiple times! sInstance = " + sInstance); + } + return sInstance; + } + } + + /** Private constructor; @see init() */ + private CallNotifier(PhoneGlobals app, Phone phone, Ringer ringer, CallLogger callLogger, + CallStateMonitor callStateMonitor) { + mApplication = app; + mCM = app.mCM; + mCallLogger = callLogger; + mCallStateMonitor = callStateMonitor; + + mAudioManager = (AudioManager) mApplication.getSystemService(Context.AUDIO_SERVICE); + + callStateMonitor.addListener(this); + + createSignalInfoToneGenerator(); + + mRinger = ringer; + BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter(); + if (adapter != null) { + adapter.getProfileProxy(mApplication.getApplicationContext(), + mBluetoothProfileServiceListener, + BluetoothProfile.HEADSET); + } + + TelephonyManager telephonyManager = (TelephonyManager)app.getSystemService( + Context.TELEPHONY_SERVICE); + telephonyManager.listen(mPhoneStateListener, + PhoneStateListener.LISTEN_MESSAGE_WAITING_INDICATOR + | PhoneStateListener.LISTEN_CALL_FORWARDING_INDICATOR); + } + + private void createSignalInfoToneGenerator() { + // Instantiate the ToneGenerator for SignalInfo and CallWaiting + // TODO: We probably don't need the mSignalInfoToneGenerator instance + // around forever. Need to change it so as to create a ToneGenerator instance only + // when a tone is being played and releases it after its done playing. + if (mSignalInfoToneGenerator == null) { + try { + mSignalInfoToneGenerator = new ToneGenerator(AudioManager.STREAM_VOICE_CALL, + TONE_RELATIVE_VOLUME_SIGNALINFO); + Log.d(LOG_TAG, "CallNotifier: mSignalInfoToneGenerator created when toneplay"); + } catch (RuntimeException e) { + Log.w(LOG_TAG, "CallNotifier: Exception caught while creating " + + "mSignalInfoToneGenerator: " + e); + mSignalInfoToneGenerator = null; + } + } else { + Log.d(LOG_TAG, "mSignalInfoToneGenerator created already, hence skipping"); + } + } + + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case CallStateMonitor.PHONE_NEW_RINGING_CONNECTION: + log("RINGING... (new)"); + onNewRingingConnection((AsyncResult) msg.obj); + mSilentRingerRequested = false; + break; + + case CallStateMonitor.PHONE_INCOMING_RING: + // repeat the ring when requested by the RIL, and when the user has NOT + // specifically requested silence. + if (msg.obj != null && ((AsyncResult) msg.obj).result != null) { + PhoneBase pb = (PhoneBase)((AsyncResult)msg.obj).result; + + if ((pb.getState() == PhoneConstants.State.RINGING) + && (mSilentRingerRequested == false)) { + if (DBG) log("RINGING... (PHONE_INCOMING_RING event)"); + mRinger.ring(); + } else { + if (DBG) log("RING before NEW_RING, skipping"); + } + } + break; + + case CallStateMonitor.PHONE_STATE_CHANGED: + onPhoneStateChanged((AsyncResult) msg.obj); + break; + + case CallStateMonitor.PHONE_DISCONNECT: + if (DBG) log("DISCONNECT"); + onDisconnect((AsyncResult) msg.obj); + break; + + case CallStateMonitor.PHONE_UNKNOWN_CONNECTION_APPEARED: + onUnknownConnectionAppeared((AsyncResult) msg.obj); + break; + + case RINGER_CUSTOM_RINGTONE_QUERY_TIMEOUT: + onCustomRingtoneQueryTimeout((String) msg.obj); + break; + + case PHONE_MWI_CHANGED: + onMwiChanged(mApplication.phone.getMessageWaitingIndicator()); + break; + + case CallStateMonitor.PHONE_CDMA_CALL_WAITING: + if (DBG) log("Received PHONE_CDMA_CALL_WAITING event"); + onCdmaCallWaiting((AsyncResult) msg.obj); + break; + + case CDMA_CALL_WAITING_REJECT: + Log.i(LOG_TAG, "Received CDMA_CALL_WAITING_REJECT event"); + onCdmaCallWaitingReject(); + break; + + case CALLWAITING_CALLERINFO_DISPLAY_DONE: + Log.i(LOG_TAG, "Received CALLWAITING_CALLERINFO_DISPLAY_DONE event"); + mCallWaitingTimeOut = true; + onCdmaCallWaitingReject(); + break; + + case CALLWAITING_ADDCALL_DISABLE_TIMEOUT: + if (DBG) log("Received CALLWAITING_ADDCALL_DISABLE_TIMEOUT event ..."); + // Set the mAddCallMenuStateAfterCW state to true + mApplication.cdmaPhoneCallState.setAddCallMenuStateAfterCallWaiting(true); + mApplication.updateInCallScreen(); + break; + + case CallStateMonitor.PHONE_STATE_DISPLAYINFO: + if (DBG) log("Received PHONE_STATE_DISPLAYINFO event"); + onDisplayInfo((AsyncResult) msg.obj); + break; + + case CallStateMonitor.PHONE_STATE_SIGNALINFO: + if (DBG) log("Received PHONE_STATE_SIGNALINFO event"); + onSignalInfo((AsyncResult) msg.obj); + break; + + case DISPLAYINFO_NOTIFICATION_DONE: + if (DBG) log("Received Display Info notification done event ..."); + CdmaDisplayInfo.dismissDisplayInfoRecord(); + break; + + case CallStateMonitor.EVENT_OTA_PROVISION_CHANGE: + if (DBG) log("EVENT_OTA_PROVISION_CHANGE..."); + mApplication.handleOtaspEvent(msg); + break; + + case CallStateMonitor.PHONE_ENHANCED_VP_ON: + if (DBG) log("PHONE_ENHANCED_VP_ON..."); + if (!mVoicePrivacyState) { + int toneToPlay = InCallTonePlayer.TONE_VOICE_PRIVACY; + new InCallTonePlayer(toneToPlay).start(); + mVoicePrivacyState = true; + // Update the VP icon: + if (DBG) log("- updating notification for VP state..."); + mApplication.notificationMgr.updateInCallNotification(); + } + break; + + case CallStateMonitor.PHONE_ENHANCED_VP_OFF: + if (DBG) log("PHONE_ENHANCED_VP_OFF..."); + if (mVoicePrivacyState) { + int toneToPlay = InCallTonePlayer.TONE_VOICE_PRIVACY; + new InCallTonePlayer(toneToPlay).start(); + mVoicePrivacyState = false; + // Update the VP icon: + if (DBG) log("- updating notification for VP state..."); + mApplication.notificationMgr.updateInCallNotification(); + } + break; + + case CallStateMonitor.PHONE_RINGBACK_TONE: + onRingbackTone((AsyncResult) msg.obj); + break; + + case CallStateMonitor.PHONE_RESEND_MUTE: + onResendMute(); + break; + + case UPDATE_IN_CALL_NOTIFICATION: + mApplication.notificationMgr.updateInCallNotification(); + break; + + default: + // super.handleMessage(msg); + } + } + + PhoneStateListener mPhoneStateListener = new PhoneStateListener() { + @Override + public void onMessageWaitingIndicatorChanged(boolean mwi) { + onMwiChanged(mwi); + } + + @Override + public void onCallForwardingIndicatorChanged(boolean cfi) { + onCfiChanged(cfi); + } + }; + + /** + * Handles a "new ringing connection" event from the telephony layer. + */ + private void onNewRingingConnection(AsyncResult r) { + Connection c = (Connection) r.result; + log("onNewRingingConnection(): state = " + mCM.getState() + ", conn = { " + c + " }"); + Call ringing = c.getCall(); + Phone phone = ringing.getPhone(); + + // Check for a few cases where we totally ignore incoming calls. + if (ignoreAllIncomingCalls(phone)) { + // Immediately reject the call, without even indicating to the user + // that an incoming call occurred. (This will generally send the + // caller straight to voicemail, just as if we *had* shown the + // incoming-call UI and the user had declined the call.) + PhoneUtils.hangupRingingCall(ringing); + return; + } + + if (!c.isRinging()) { + Log.i(LOG_TAG, "CallNotifier.onNewRingingConnection(): connection not ringing!"); + // This is a very strange case: an incoming call that stopped + // ringing almost instantly after the onNewRingingConnection() + // event. There's nothing we can do here, so just bail out + // without doing anything. (But presumably we'll log it in + // the call log when the disconnect event comes in...) + return; + } + + // Stop any signalInfo tone being played on receiving a Call + stopSignalInfoTone(); + + Call.State state = c.getState(); + // State will be either INCOMING or WAITING. + if (VDBG) log("- connection is ringing! state = " + state); + // if (DBG) PhoneUtils.dumpCallState(mPhone); + + // No need to do any service state checks here (like for + // "emergency mode"), since in those states the SIM won't let + // us get incoming connections in the first place. + + // TODO: Consider sending out a serialized broadcast Intent here + // (maybe "ACTION_NEW_INCOMING_CALL"), *before* starting the + // ringer and going to the in-call UI. The intent should contain + // the caller-id info for the current connection, and say whether + // it would be a "call waiting" call or a regular ringing call. + // If anybody consumed the broadcast, we'd bail out without + // ringing or bringing up the in-call UI. + // + // This would give 3rd party apps a chance to listen for (and + // intercept) new ringing connections. An app could reject the + // incoming call by consuming the broadcast and doing nothing, or + // it could "pick up" the call (without any action by the user!) + // via some future TelephonyManager API. + // + // See bug 1312336 for more details. + // We'd need to protect this with a new "intercept incoming calls" + // system permission. + + // Obtain a partial wake lock to make sure the CPU doesn't go to + // sleep before we finish bringing up the InCallScreen. + // (This will be upgraded soon to a full wake lock; see + // showIncomingCall().) + if (VDBG) log("Holding wake lock on new incoming connection."); + mApplication.requestWakeState(PhoneGlobals.WakeState.PARTIAL); + + // - don't ring for call waiting connections + // - do this before showing the incoming call panel + if (PhoneUtils.isRealIncomingCall(state)) { + startIncomingCallQuery(c); + } else { + if (VDBG) log("- starting call waiting tone..."); + if (mCallWaitingTonePlayer == null) { + mCallWaitingTonePlayer = new InCallTonePlayer(InCallTonePlayer.TONE_CALL_WAITING); + mCallWaitingTonePlayer.start(); + } + // in this case, just fall through like before, and call + // showIncomingCall(). + if (DBG) log("- showing incoming call (this is a WAITING call)..."); + showIncomingCall(); + } + + // Note we *don't* post a status bar notification here, since + // we're not necessarily ready to actually show the incoming call + // to the user. (For calls in the INCOMING state, at least, we + // still need to run a caller-id query, and we may not even ring + // at all if the "send directly to voicemail" flag is set.) + // + // Instead, we update the notification (and potentially launch the + // InCallScreen) from the showIncomingCall() method, which runs + // when the caller-id query completes or times out. + + if (VDBG) log("- onNewRingingConnection() done."); + } + + /** + * Determines whether or not we're allowed to present incoming calls to the + * user, based on the capabilities and/or current state of the device. + * + * If this method returns true, that means we should immediately reject the + * current incoming call, without even indicating to the user that an + * incoming call occurred. + * + * (We only reject incoming calls in a few cases, like during an OTASP call + * when we can't interrupt the user, or if the device hasn't completed the + * SetupWizard yet. We also don't allow incoming calls on non-voice-capable + * devices. But note that we *always* allow incoming calls while in ECM.) + * + * @return true if we're *not* allowed to present an incoming call to + * the user. + */ + private boolean ignoreAllIncomingCalls(Phone phone) { + // Incoming calls are totally ignored on non-voice-capable devices. + if (!PhoneGlobals.sVoiceCapable) { + // ...but still log a warning, since we shouldn't have gotten this + // event in the first place! (Incoming calls *should* be blocked at + // the telephony layer on non-voice-capable capable devices.) + Log.w(LOG_TAG, "Got onNewRingingConnection() on non-voice-capable device! Ignoring..."); + return true; + } + + // In ECM (emergency callback mode), we ALWAYS allow incoming calls + // to get through to the user. (Note that ECM is applicable only to + // voice-capable CDMA devices). + if (PhoneUtils.isPhoneInEcm(phone)) { + if (DBG) log("Incoming call while in ECM: always allow..."); + return false; + } + + // Incoming calls are totally ignored if the device isn't provisioned yet. + boolean provisioned = Settings.Global.getInt(mApplication.getContentResolver(), + Settings.Global.DEVICE_PROVISIONED, 0) != 0; + if (!provisioned) { + Log.i(LOG_TAG, "Ignoring incoming call: not provisioned"); + return true; + } + + // Incoming calls are totally ignored if an OTASP call is active. + if (TelephonyCapabilities.supportsOtasp(phone)) { + boolean activateState = (mApplication.cdmaOtaScreenState.otaScreenState + == OtaUtils.CdmaOtaScreenState.OtaScreenState.OTA_STATUS_ACTIVATION); + boolean dialogState = (mApplication.cdmaOtaScreenState.otaScreenState + == OtaUtils.CdmaOtaScreenState.OtaScreenState.OTA_STATUS_SUCCESS_FAILURE_DLG); + boolean spcState = mApplication.cdmaOtaProvisionData.inOtaSpcState; + + if (spcState) { + Log.i(LOG_TAG, "Ignoring incoming call: OTA call is active"); + return true; + } else if (activateState || dialogState) { + // We *are* allowed to receive incoming calls at this point. + // But clear out any residual OTASP UI first. + // TODO: It's an MVC violation to twiddle the OTA UI state here; + // we should instead provide a higher-level API via OtaUtils. + if (dialogState) mApplication.dismissOtaDialogs(); + mApplication.clearOtaState(); + mApplication.clearInCallScreenMode(); + return false; + } + } + + // Normal case: allow this call to be presented to the user. + return false; + } + + /** + * Helper method to manage the start of incoming call queries + */ + private void startIncomingCallQuery(Connection c) { + // TODO: cache the custom ringer object so that subsequent + // calls will not need to do this query work. We can keep + // the MRU ringtones in memory. We'll still need to hit + // the database to get the callerinfo to act as a key, + // but at least we can save the time required for the + // Media player setup. The only issue with this is that + // we may need to keep an eye on the resources the Media + // player uses to keep these ringtones around. + + // make sure we're in a state where we can be ready to + // query a ringtone uri. + boolean shouldStartQuery = false; + synchronized (mCallerInfoQueryStateGuard) { + if (mCallerInfoQueryState == CALLERINFO_QUERY_READY) { + mCallerInfoQueryState = CALLERINFO_QUERYING; + shouldStartQuery = true; + } + } + if (shouldStartQuery) { + // Reset the ringtone to the default first. + mRinger.setCustomRingtoneUri(Settings.System.DEFAULT_RINGTONE_URI); + + // query the callerinfo to try to get the ringer. + PhoneUtils.CallerInfoToken cit = PhoneUtils.startGetCallerInfo( + mApplication, c, this, this); + + // if this has already been queried then just ring, otherwise + // we wait for the alloted time before ringing. + if (cit.isFinal) { + if (VDBG) log("- CallerInfo already up to date, using available data"); + onQueryComplete(0, this, cit.currentInfo); + } else { + if (VDBG) log("- Starting query, posting timeout message."); + + // Phone number (via getAddress()) is stored in the message to remember which + // number is actually used for the look up. + sendMessageDelayed( + Message.obtain(this, RINGER_CUSTOM_RINGTONE_QUERY_TIMEOUT, c.getAddress()), + RINGTONE_QUERY_WAIT_TIME); + } + // The call to showIncomingCall() will happen after the + // queries are complete (or time out). + } else { + // This should never happen; its the case where an incoming call + // arrives at the same time that the query is still being run, + // and before the timeout window has closed. + EventLog.writeEvent(EventLogTags.PHONE_UI_MULTIPLE_QUERY); + + // In this case, just log the request and ring. + if (VDBG) log("RINGING... (request to ring arrived while query is running)"); + mRinger.ring(); + + // in this case, just fall through like before, and call + // showIncomingCall(). + if (DBG) log("- showing incoming call (couldn't start query)..."); + showIncomingCall(); + } + } + + /** + * Performs the final steps of the onNewRingingConnection sequence: + * starts the ringer, and brings up the "incoming call" UI. + * + * Normally, this is called when the CallerInfo query completes (see + * onQueryComplete()). In this case, onQueryComplete() has already + * configured the Ringer object to use the custom ringtone (if there + * is one) for this caller. So we just tell the Ringer to start, and + * proceed to the InCallScreen. + * + * But this method can *also* be called if the + * RINGTONE_QUERY_WAIT_TIME timeout expires, which means that the + * CallerInfo query is taking too long. In that case, we log a + * warning but otherwise we behave the same as in the normal case. + * (We still tell the Ringer to start, but it's going to use the + * default ringtone.) + */ + private void onCustomRingQueryComplete() { + boolean isQueryExecutionTimeExpired = false; + synchronized (mCallerInfoQueryStateGuard) { + if (mCallerInfoQueryState == CALLERINFO_QUERYING) { + mCallerInfoQueryState = CALLERINFO_QUERY_READY; + isQueryExecutionTimeExpired = true; + } + } + if (isQueryExecutionTimeExpired) { + // There may be a problem with the query here, since the + // default ringtone is playing instead of the custom one. + Log.w(LOG_TAG, "CallerInfo query took too long; falling back to default ringtone"); + EventLog.writeEvent(EventLogTags.PHONE_UI_RINGER_QUERY_ELAPSED); + } + + // Make sure we still have an incoming call! + // + // (It's possible for the incoming call to have been disconnected + // while we were running the query. In that case we better not + // start the ringer here, since there won't be any future + // DISCONNECT event to stop it!) + // + // Note we don't have to worry about the incoming call going away + // *after* this check but before we call mRinger.ring() below, + // since in that case we *will* still get a DISCONNECT message sent + // to our handler. (And we will correctly stop the ringer when we + // process that event.) + if (mCM.getState() != PhoneConstants.State.RINGING) { + Log.i(LOG_TAG, "onCustomRingQueryComplete: No incoming call! Bailing out..."); + // Don't start the ringer *or* bring up the "incoming call" UI. + // Just bail out. + return; + } + + // Ring, either with the queried ringtone or default one. + if (VDBG) log("RINGING... (onCustomRingQueryComplete)"); + mRinger.ring(); + + // ...and display the incoming call to the user: + if (DBG) log("- showing incoming call (custom ring query complete)..."); + showIncomingCall(); + } + + private void onUnknownConnectionAppeared(AsyncResult r) { + PhoneConstants.State state = mCM.getState(); + + if (state == PhoneConstants.State.OFFHOOK) { + // basically do onPhoneStateChanged + display the incoming call UI + onPhoneStateChanged(r); + if (DBG) log("- showing incoming call (unknown connection appeared)..."); + showIncomingCall(); + } + } + + /** + * Informs the user about a new incoming call. + * + * In most cases this means "bring up the full-screen incoming call + * UI". However, if an immersive activity is running, the system + * NotificationManager will instead pop up a small notification window + * on top of the activity. + * + * Watch out: be sure to call this method only once per incoming call, + * or otherwise we may end up launching the InCallScreen multiple + * times (which can lead to slow responsiveness and/or visible + * glitches.) + * + * Note this method handles only the onscreen UI for incoming calls; + * the ringer and/or vibrator are started separately (see the various + * calls to Ringer.ring() in this class.) + * + * @see NotificationMgr#updateNotificationAndLaunchIncomingCallUi() + */ + private void showIncomingCall() { + log("showIncomingCall()... phone state = " + mCM.getState()); + + // Before bringing up the "incoming call" UI, force any system + // dialogs (like "recent tasks" or the power dialog) to close first. + try { + ActivityManagerNative.getDefault().closeSystemDialogs("call"); + } catch (RemoteException e) { + } + + // Go directly to the in-call screen. + // (No need to do anything special if we're already on the in-call + // screen; it'll notice the phone state change and update itself.) + mApplication.requestWakeState(PhoneGlobals.WakeState.FULL); + + // Post the "incoming call" notification *and* include the + // fullScreenIntent that'll launch the incoming-call UI. + // (This will usually take us straight to the incoming call + // screen, but if an immersive activity is running it'll just + // appear as a notification.) + if (DBG) log("- updating notification from showIncomingCall()..."); + mApplication.notificationMgr.updateNotificationAndLaunchIncomingCallUi(); + } + + /** + * Updates the phone UI in response to phone state changes. + * + * Watch out: certain state changes are actually handled by their own + * specific methods: + * - see onNewRingingConnection() for new incoming calls + * - see onDisconnect() for calls being hung up or disconnected + */ + private void onPhoneStateChanged(AsyncResult r) { + PhoneConstants.State state = mCM.getState(); + if (VDBG) log("onPhoneStateChanged: state = " + state); + + // Turn status bar notifications on or off depending upon the state + // of the phone. Notification Alerts (audible or vibrating) should + // be on if and only if the phone is IDLE. + mApplication.notificationMgr.statusBarHelper + .enableNotificationAlerts(state == PhoneConstants.State.IDLE); + + Phone fgPhone = mCM.getFgPhone(); + if (fgPhone.getPhoneType() == PhoneConstants.PHONE_TYPE_CDMA) { + if ((fgPhone.getForegroundCall().getState() == Call.State.ACTIVE) + && ((mPreviousCdmaCallState == Call.State.DIALING) + || (mPreviousCdmaCallState == Call.State.ALERTING))) { + if (mIsCdmaRedialCall) { + int toneToPlay = InCallTonePlayer.TONE_REDIAL; + new InCallTonePlayer(toneToPlay).start(); + } + // Stop any signal info tone when call moves to ACTIVE state + stopSignalInfoTone(); + } + mPreviousCdmaCallState = fgPhone.getForegroundCall().getState(); + } + + // Have the PhoneApp recompute its mShowBluetoothIndication + // flag based on the (new) telephony state. + // There's no need to force a UI update since we update the + // in-call notification ourselves (below), and the InCallScreen + // listens for phone state changes itself. + mApplication.updateBluetoothIndication(false); + + + // Update the phone state and other sensor/lock. + mApplication.updatePhoneState(state); + + if (state == PhoneConstants.State.OFFHOOK) { + // stop call waiting tone if needed when answering + if (mCallWaitingTonePlayer != null) { + mCallWaitingTonePlayer.stopTone(); + mCallWaitingTonePlayer = null; + } + + if (VDBG) log("onPhoneStateChanged: OFF HOOK"); + // make sure audio is in in-call mode now + PhoneUtils.setAudioMode(mCM); + + // if the call screen is showing, let it handle the event, + // otherwise handle it here. + if (!mApplication.isShowingCallScreen()) { + mApplication.requestWakeState(PhoneGlobals.WakeState.SLEEP); + } + + // Since we're now in-call, the Ringer should definitely *not* + // be ringing any more. (This is just a sanity-check; we + // already stopped the ringer explicitly back in + // PhoneUtils.answerCall(), before the call to phone.acceptCall().) + // TODO: Confirm that this call really *is* unnecessary, and if so, + // remove it! + if (DBG) log("stopRing()... (OFFHOOK state)"); + mRinger.stopRing(); + + // Post a request to update the "in-call" status bar icon. + // + // We don't call NotificationMgr.updateInCallNotification() + // directly here, for two reasons: + // (1) a single phone state change might actually trigger multiple + // onPhoneStateChanged() callbacks, so this prevents redundant + // updates of the notification. + // (2) we suppress the status bar icon while the in-call UI is + // visible (see updateInCallNotification()). But when launching + // an outgoing call the phone actually goes OFFHOOK slightly + // *before* the InCallScreen comes up, so the delay here avoids a + // brief flicker of the icon at that point. + + if (DBG) log("- posting UPDATE_IN_CALL_NOTIFICATION request..."); + // Remove any previous requests in the queue + removeMessages(UPDATE_IN_CALL_NOTIFICATION); + final int IN_CALL_NOTIFICATION_UPDATE_DELAY = 1000; // msec + sendEmptyMessageDelayed(UPDATE_IN_CALL_NOTIFICATION, + IN_CALL_NOTIFICATION_UPDATE_DELAY); + } + + if (fgPhone.getPhoneType() == PhoneConstants.PHONE_TYPE_CDMA) { + Connection c = fgPhone.getForegroundCall().getLatestConnection(); + if ((c != null) && (PhoneNumberUtils.isLocalEmergencyNumber(c.getAddress(), + mApplication))) { + if (VDBG) log("onPhoneStateChanged: it is an emergency call."); + Call.State callState = fgPhone.getForegroundCall().getState(); + if (mEmergencyTonePlayerVibrator == null) { + mEmergencyTonePlayerVibrator = new EmergencyTonePlayerVibrator(); + } + + if (callState == Call.State.DIALING || callState == Call.State.ALERTING) { + mIsEmergencyToneOn = Settings.Global.getInt( + mApplication.getContentResolver(), + Settings.Global.EMERGENCY_TONE, EMERGENCY_TONE_OFF); + if (mIsEmergencyToneOn != EMERGENCY_TONE_OFF && + mCurrentEmergencyToneState == EMERGENCY_TONE_OFF) { + if (mEmergencyTonePlayerVibrator != null) { + mEmergencyTonePlayerVibrator.start(); + } + } + } else if (callState == Call.State.ACTIVE) { + if (mCurrentEmergencyToneState != EMERGENCY_TONE_OFF) { + if (mEmergencyTonePlayerVibrator != null) { + mEmergencyTonePlayerVibrator.stop(); + } + } + } + } + } + + if ((fgPhone.getPhoneType() == PhoneConstants.PHONE_TYPE_GSM) + || (fgPhone.getPhoneType() == PhoneConstants.PHONE_TYPE_SIP)) { + Call.State callState = mCM.getActiveFgCallState(); + if (!callState.isDialing()) { + // If call get activated or disconnected before the ringback + // tone stops, we have to stop it to prevent disturbing. + if (mInCallRingbackTonePlayer != null) { + mInCallRingbackTonePlayer.stopTone(); + mInCallRingbackTonePlayer = null; + } + } + } + } + + void updateCallNotifierRegistrationsAfterRadioTechnologyChange() { + if (DBG) Log.d(LOG_TAG, "updateCallNotifierRegistrationsAfterRadioTechnologyChange..."); + + // Clear ringback tone player + mInCallRingbackTonePlayer = null; + + // Clear call waiting tone player + mCallWaitingTonePlayer = null; + + // Instantiate mSignalInfoToneGenerator + createSignalInfoToneGenerator(); + } + + /** + * Implemented for CallerInfoAsyncQuery.OnQueryCompleteListener interface. + * refreshes the CallCard data when it called. If called with this + * class itself, it is assumed that we have been waiting for the ringtone + * and direct to voicemail settings to update. + */ + @Override + public void onQueryComplete(int token, Object cookie, CallerInfo ci) { + if (cookie instanceof Long) { + if (VDBG) log("CallerInfo query complete, posting missed call notification"); + + mApplication.notificationMgr.notifyMissedCall(ci.name, ci.phoneNumber, + ci.phoneLabel, ci.cachedPhoto, ci.cachedPhotoIcon, + ((Long) cookie).longValue()); + } else if (cookie instanceof CallNotifier) { + if (VDBG) log("CallerInfo query complete (for CallNotifier), " + + "updating state for incoming call.."); + + // get rid of the timeout messages + removeMessages(RINGER_CUSTOM_RINGTONE_QUERY_TIMEOUT); + + boolean isQueryExecutionTimeOK = false; + synchronized (mCallerInfoQueryStateGuard) { + if (mCallerInfoQueryState == CALLERINFO_QUERYING) { + mCallerInfoQueryState = CALLERINFO_QUERY_READY; + isQueryExecutionTimeOK = true; + } + } + //if we're in the right state + if (isQueryExecutionTimeOK) { + + // send directly to voicemail. + if (ci.shouldSendToVoicemail) { + if (DBG) log("send to voicemail flag detected. hanging up."); + PhoneUtils.hangupRingingCall(mCM.getFirstActiveRingingCall()); + return; + } + + // set the ringtone uri to prepare for the ring. + if (ci.contactRingtoneUri != null) { + if (DBG) log("custom ringtone found, setting up ringer."); + Ringer r = ((CallNotifier) cookie).mRinger; + r.setCustomRingtoneUri(ci.contactRingtoneUri); + } + // ring, and other post-ring actions. + onCustomRingQueryComplete(); + } + } + } + + /** + * Called when asynchronous CallerInfo query is taking too long (more than + * {@link #RINGTONE_QUERY_WAIT_TIME} msec), but we cannot wait any more. + * + * This looks up in-memory fallback cache and use it when available. If not, it just calls + * {@link #onCustomRingQueryComplete()} with default ringtone ("Send to voicemail" flag will + * be just ignored). + * + * @param number The phone number used for the async query. This method will take care of + * formatting or normalization of the number. + */ + private void onCustomRingtoneQueryTimeout(String number) { + // First of all, this case itself should be rare enough, though we cannot avoid it in + // some situations (e.g. IPC is slow due to system overload, database is in sync, etc.) + Log.w(LOG_TAG, "CallerInfo query took too long; look up local fallback cache."); + + // This method is intentionally verbose for now to detect possible bad side-effect for it. + // TODO: Remove the verbose log when it looks stable and reliable enough. + + final CallerInfoCache.CacheEntry entry = + mApplication.callerInfoCache.getCacheEntry(number); + if (entry != null) { + if (entry.sendToVoicemail) { + log("send to voicemail flag detected (in fallback cache). hanging up."); + PhoneUtils.hangupRingingCall(mCM.getFirstActiveRingingCall()); + return; + } + + if (entry.customRingtone != null) { + log("custom ringtone found (in fallback cache), setting up ringer: " + + entry.customRingtone); + this.mRinger.setCustomRingtoneUri(Uri.parse(entry.customRingtone)); + } + } else { + // In this case we call onCustomRingQueryComplete(), just + // like if the query had completed normally. (But we're + // going to get the default ringtone, since we never got + // the chance to call Ringer.setCustomRingtoneUri()). + log("Failed to find fallback cache. Use default ringer tone."); + } + + onCustomRingQueryComplete(); + } + + private void onDisconnect(AsyncResult r) { + if (VDBG) log("onDisconnect()... CallManager state: " + mCM.getState()); + + mVoicePrivacyState = false; + Connection c = (Connection) r.result; + if (c != null) { + log("onDisconnect: cause = " + c.getDisconnectCause() + + ", incoming = " + c.isIncoming() + + ", date = " + c.getCreateTime()); + } else { + Log.w(LOG_TAG, "onDisconnect: null connection"); + } + + int autoretrySetting = 0; + if ((c != null) && (c.getCall().getPhone().getPhoneType() == PhoneConstants.PHONE_TYPE_CDMA)) { + autoretrySetting = android.provider.Settings.Global.getInt(mApplication. + getContentResolver(),android.provider.Settings.Global.CALL_AUTO_RETRY, 0); + } + + // Stop any signalInfo tone being played when a call gets ended + stopSignalInfoTone(); + + if ((c != null) && (c.getCall().getPhone().getPhoneType() == PhoneConstants.PHONE_TYPE_CDMA)) { + // Resetting the CdmaPhoneCallState members + mApplication.cdmaPhoneCallState.resetCdmaPhoneCallState(); + + // Remove Call waiting timers + removeMessages(CALLWAITING_CALLERINFO_DISPLAY_DONE); + removeMessages(CALLWAITING_ADDCALL_DISABLE_TIMEOUT); + } + + // Stop the ringer if it was ringing (for an incoming call that + // either disconnected by itself, or was rejected by the user.) + // + // TODO: We technically *shouldn't* stop the ringer if the + // foreground or background call disconnects while an incoming call + // is still ringing, but that's a really rare corner case. + // It's safest to just unconditionally stop the ringer here. + + // CDMA: 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* be stopping the ringer being played for + // the Incoming Call + Call ringingCall = mCM.getFirstActiveRingingCall(); + if (ringingCall.getPhone().getPhoneType() == PhoneConstants.PHONE_TYPE_CDMA) { + if (PhoneUtils.isRealIncomingCall(ringingCall.getState())) { + // Also we need to take off the "In Call" icon from the Notification + // area as the Out going Call never got connected + if (DBG) log("cancelCallInProgressNotifications()... (onDisconnect)"); + mApplication.notificationMgr.cancelCallInProgressNotifications(); + } else { + if (DBG) log("stopRing()... (onDisconnect)"); + mRinger.stopRing(); + } + } else { // GSM + if (DBG) log("stopRing()... (onDisconnect)"); + mRinger.stopRing(); + } + + // stop call waiting tone if needed when disconnecting + if (mCallWaitingTonePlayer != null) { + mCallWaitingTonePlayer.stopTone(); + mCallWaitingTonePlayer = null; + } + + // If this is the end of an OTASP call, pass it on to the PhoneApp. + if (c != null && TelephonyCapabilities.supportsOtasp(c.getCall().getPhone())) { + final String number = c.getAddress(); + if (c.getCall().getPhone().isOtaSpNumber(number)) { + if (DBG) log("onDisconnect: this was an OTASP call!"); + mApplication.handleOtaspDisconnect(); + } + } + + // Check for the various tones we might need to play (thru the + // earpiece) after a call disconnects. + int toneToPlay = InCallTonePlayer.TONE_NONE; + + // The "Busy" or "Congestion" tone is the highest priority: + if (c != null) { + Connection.DisconnectCause cause = c.getDisconnectCause(); + if (cause == Connection.DisconnectCause.BUSY) { + if (DBG) log("- need to play BUSY tone!"); + toneToPlay = InCallTonePlayer.TONE_BUSY; + } else if (cause == Connection.DisconnectCause.CONGESTION) { + if (DBG) log("- need to play CONGESTION tone!"); + toneToPlay = InCallTonePlayer.TONE_CONGESTION; + } else if (((cause == Connection.DisconnectCause.NORMAL) + || (cause == Connection.DisconnectCause.LOCAL)) + && (mApplication.isOtaCallInActiveState())) { + if (DBG) log("- need to play OTA_CALL_END tone!"); + toneToPlay = InCallTonePlayer.TONE_OTA_CALL_END; + } else if (cause == Connection.DisconnectCause.CDMA_REORDER) { + if (DBG) log("- need to play CDMA_REORDER tone!"); + toneToPlay = InCallTonePlayer.TONE_REORDER; + } else if (cause == Connection.DisconnectCause.CDMA_INTERCEPT) { + if (DBG) log("- need to play CDMA_INTERCEPT tone!"); + toneToPlay = InCallTonePlayer.TONE_INTERCEPT; + } else if (cause == Connection.DisconnectCause.CDMA_DROP) { + if (DBG) log("- need to play CDMA_DROP tone!"); + toneToPlay = InCallTonePlayer.TONE_CDMA_DROP; + } else if (cause == Connection.DisconnectCause.OUT_OF_SERVICE) { + if (DBG) log("- need to play OUT OF SERVICE tone!"); + toneToPlay = InCallTonePlayer.TONE_OUT_OF_SERVICE; + } else if (cause == Connection.DisconnectCause.UNOBTAINABLE_NUMBER) { + if (DBG) log("- need to play TONE_UNOBTAINABLE_NUMBER tone!"); + toneToPlay = InCallTonePlayer.TONE_UNOBTAINABLE_NUMBER; + } else if (cause == Connection.DisconnectCause.ERROR_UNSPECIFIED) { + if (DBG) log("- DisconnectCause is ERROR_UNSPECIFIED: play TONE_CALL_ENDED!"); + toneToPlay = InCallTonePlayer.TONE_CALL_ENDED; + } + } + + // If we don't need to play BUSY or CONGESTION, then play the + // "call ended" tone if this was a "regular disconnect" (i.e. a + // normal call where one end or the other hung up) *and* this + // disconnect event caused the phone to become idle. (In other + // words, we *don't* play the sound if one call hangs up but + // there's still an active call on the other line.) + // TODO: We may eventually want to disable this via a preference. + if ((toneToPlay == InCallTonePlayer.TONE_NONE) + && (mCM.getState() == PhoneConstants.State.IDLE) + && (c != null)) { + Connection.DisconnectCause cause = c.getDisconnectCause(); + if ((cause == Connection.DisconnectCause.NORMAL) // remote hangup + || (cause == Connection.DisconnectCause.LOCAL)) { // local hangup + if (VDBG) log("- need to play CALL_ENDED tone!"); + toneToPlay = InCallTonePlayer.TONE_CALL_ENDED; + mIsCdmaRedialCall = false; + } + } + + // All phone calls are disconnected. + if (mCM.getState() == PhoneConstants.State.IDLE) { + // Don't reset the audio mode or bluetooth/speakerphone state + // if we still need to let the user hear a tone through the earpiece. + if (toneToPlay == InCallTonePlayer.TONE_NONE) { + resetAudioStateAfterDisconnect(); + } + + mApplication.notificationMgr.cancelCallInProgressNotifications(); + } + + if (c != null) { + mCallLogger.logCall(c); + + final String number = c.getAddress(); + final Phone phone = c.getCall().getPhone(); + final boolean isEmergencyNumber = + PhoneNumberUtils.isLocalEmergencyNumber(number, mApplication); + + if (phone.getPhoneType() == PhoneConstants.PHONE_TYPE_CDMA) { + if ((isEmergencyNumber) + && (mCurrentEmergencyToneState != EMERGENCY_TONE_OFF)) { + if (mEmergencyTonePlayerVibrator != null) { + mEmergencyTonePlayerVibrator.stop(); + } + } + } + + final long date = c.getCreateTime(); + final Connection.DisconnectCause cause = c.getDisconnectCause(); + final boolean missedCall = c.isIncoming() && + (cause == Connection.DisconnectCause.INCOMING_MISSED); + if (missedCall) { + // Show the "Missed call" notification. + // (Note we *don't* do this if this was an incoming call that + // the user deliberately rejected.) + showMissedCallNotification(c, date); + } + + // Possibly play a "post-disconnect tone" thru the earpiece. + // We do this here, rather than from the InCallScreen + // activity, since we need to do this even if you're not in + // the Phone UI at the moment the connection ends. + if (toneToPlay != InCallTonePlayer.TONE_NONE) { + if (VDBG) log("- starting post-disconnect tone (" + toneToPlay + ")..."); + new InCallTonePlayer(toneToPlay).start(); + + // TODO: alternatively, we could start an InCallTonePlayer + // here with an "unlimited" tone length, + // and manually stop it later when this connection truly goes + // away. (The real connection over the network was closed as soon + // as we got the BUSY message. But our telephony layer keeps the + // connection open for a few extra seconds so we can show the + // "busy" indication to the user. We could stop the busy tone + // when *that* connection's "disconnect" event comes in.) + } + + if (((mPreviousCdmaCallState == Call.State.DIALING) + || (mPreviousCdmaCallState == Call.State.ALERTING)) + && (!isEmergencyNumber) + && (cause != Connection.DisconnectCause.INCOMING_MISSED ) + && (cause != Connection.DisconnectCause.NORMAL) + && (cause != Connection.DisconnectCause.LOCAL) + && (cause != Connection.DisconnectCause.INCOMING_REJECTED)) { + if (!mIsCdmaRedialCall) { + if (autoretrySetting == InCallScreen.AUTO_RETRY_ON) { + // TODO: (Moto): The contact reference data may need to be stored and use + // here when redialing a call. For now, pass in NULL as the URI parameter. + PhoneUtils.placeCall(mApplication, phone, number, null, false, null); + mIsCdmaRedialCall = true; + } else { + mIsCdmaRedialCall = false; + } + } else { + mIsCdmaRedialCall = false; + } + } + } + } + + /** + * Resets the audio mode and speaker state when a call ends. + */ + private void resetAudioStateAfterDisconnect() { + if (VDBG) log("resetAudioStateAfterDisconnect()..."); + + if (mBluetoothHeadset != null) { + mBluetoothHeadset.disconnectAudio(); + } + + // call turnOnSpeaker() with state=false and store=true even if speaker + // is already off to reset user requested speaker state. + PhoneUtils.turnOnSpeaker(mApplication, false, true); + + PhoneUtils.setAudioMode(mCM); + } + + private void onMwiChanged(boolean visible) { + if (VDBG) log("onMwiChanged(): " + visible); + + // "Voicemail" is meaningless on non-voice-capable devices, + // so ignore MWI events. + if (!PhoneGlobals.sVoiceCapable) { + // ...but still log a warning, since we shouldn't have gotten this + // event in the first place! + // (PhoneStateListener.LISTEN_MESSAGE_WAITING_INDICATOR events + // *should* be blocked at the telephony layer on non-voice-capable + // capable devices.) + Log.w(LOG_TAG, "Got onMwiChanged() on non-voice-capable device! Ignoring..."); + return; + } + + mApplication.notificationMgr.updateMwi(visible); + } + + /** + * Posts a delayed PHONE_MWI_CHANGED event, to schedule a "retry" for a + * failed NotificationMgr.updateMwi() call. + */ + /* package */ void sendMwiChangedDelayed(long delayMillis) { + Message message = Message.obtain(this, PHONE_MWI_CHANGED); + sendMessageDelayed(message, delayMillis); + } + + private void onCfiChanged(boolean visible) { + if (VDBG) log("onCfiChanged(): " + visible); + mApplication.notificationMgr.updateCfi(visible); + } + + /** + * Indicates whether or not this ringer is ringing. + */ + boolean isRinging() { + return mRinger.isRinging(); + } + + /** + * Stops the current ring, and tells the notifier that future + * ring requests should be ignored. + */ + void silenceRinger() { + mSilentRingerRequested = true; + if (DBG) log("stopRing()... (silenceRinger)"); + mRinger.stopRing(); + } + + /** + * Restarts the ringer after having previously silenced it. + * + * (This is a no-op if the ringer is actually still ringing, or if the + * incoming ringing call no longer exists.) + */ + /* package */ void restartRinger() { + if (DBG) log("restartRinger()..."); + // Already ringing or Silent requested; no need to restart. + if (isRinging() || mSilentRingerRequested) return; + + final Call ringingCall = mCM.getFirstActiveRingingCall(); + // Don't check ringingCall.isRinging() here, since that'll be true + // for the WAITING state also. We only allow the ringer for + // regular INCOMING calls. + if (DBG) log("- ringingCall state: " + ringingCall.getState()); + if (ringingCall.getState() == Call.State.INCOMING) { + mRinger.ring(); + } + } + + /** + * Helper class to play tones through the earpiece (or speaker / BT) + * during a call, using the ToneGenerator. + * + * To use, just instantiate a new InCallTonePlayer + * (passing in the TONE_* constant for the tone you want) + * and start() it. + * + * When we're done playing the tone, if the phone is idle at that + * point, we'll reset the audio routing and speaker state. + * (That means that for tones that get played *after* a call + * disconnects, like "busy" or "congestion" or "call ended", you + * should NOT call resetAudioStateAfterDisconnect() yourself. + * Instead, just start the InCallTonePlayer, which will automatically + * defer the resetAudioStateAfterDisconnect() call until the tone + * finishes playing.) + */ + private class InCallTonePlayer extends Thread { + private int mToneId; + private int mState; + // The possible tones we can play. + public static final int TONE_NONE = 0; + public static final int TONE_CALL_WAITING = 1; + public static final int TONE_BUSY = 2; + public static final int TONE_CONGESTION = 3; + public static final int TONE_CALL_ENDED = 4; + public static final int TONE_VOICE_PRIVACY = 5; + public static final int TONE_REORDER = 6; + public static final int TONE_INTERCEPT = 7; + public static final int TONE_CDMA_DROP = 8; + public static final int TONE_OUT_OF_SERVICE = 9; + public static final int TONE_REDIAL = 10; + public static final int TONE_OTA_CALL_END = 11; + public static final int TONE_RING_BACK = 12; + public static final int TONE_UNOBTAINABLE_NUMBER = 13; + + // The tone volume relative to other sounds in the stream + static final int TONE_RELATIVE_VOLUME_EMERGENCY = 100; + static final int TONE_RELATIVE_VOLUME_HIPRI = 80; + static final int TONE_RELATIVE_VOLUME_LOPRI = 50; + + // Buffer time (in msec) to add on to tone timeout value. + // Needed mainly when the timeout value for a tone is the + // exact duration of the tone itself. + static final int TONE_TIMEOUT_BUFFER = 20; + + // The tone state + static final int TONE_OFF = 0; + static final int TONE_ON = 1; + static final int TONE_STOPPED = 2; + + InCallTonePlayer(int toneId) { + super(); + mToneId = toneId; + mState = TONE_OFF; + } + + @Override + public void run() { + log("InCallTonePlayer.run(toneId = " + mToneId + ")..."); + + int toneType = 0; // passed to ToneGenerator.startTone() + int toneVolume; // passed to the ToneGenerator constructor + int toneLengthMillis; + int phoneType = mCM.getFgPhone().getPhoneType(); + + switch (mToneId) { + case TONE_CALL_WAITING: + toneType = ToneGenerator.TONE_SUP_CALL_WAITING; + toneVolume = TONE_RELATIVE_VOLUME_HIPRI; + // Call waiting tone is stopped by stopTone() method + toneLengthMillis = Integer.MAX_VALUE - TONE_TIMEOUT_BUFFER; + break; + case TONE_BUSY: + if (phoneType == PhoneConstants.PHONE_TYPE_CDMA) { + toneType = ToneGenerator.TONE_CDMA_NETWORK_BUSY_ONE_SHOT; + toneVolume = TONE_RELATIVE_VOLUME_LOPRI; + toneLengthMillis = 1000; + } else if ((phoneType == PhoneConstants.PHONE_TYPE_GSM) + || (phoneType == PhoneConstants.PHONE_TYPE_SIP)) { + toneType = ToneGenerator.TONE_SUP_BUSY; + toneVolume = TONE_RELATIVE_VOLUME_HIPRI; + toneLengthMillis = 4000; + } else { + throw new IllegalStateException("Unexpected phone type: " + phoneType); + } + break; + case TONE_CONGESTION: + toneType = ToneGenerator.TONE_SUP_CONGESTION; + toneVolume = TONE_RELATIVE_VOLUME_HIPRI; + toneLengthMillis = 4000; + break; + + case TONE_CALL_ENDED: + toneType = ToneGenerator.TONE_PROP_PROMPT; + toneVolume = TONE_RELATIVE_VOLUME_HIPRI; + toneLengthMillis = 200; + break; + case TONE_OTA_CALL_END: + if (mApplication.cdmaOtaConfigData.otaPlaySuccessFailureTone == + OtaUtils.OTA_PLAY_SUCCESS_FAILURE_TONE_ON) { + toneType = ToneGenerator.TONE_CDMA_ALERT_CALL_GUARD; + toneVolume = TONE_RELATIVE_VOLUME_HIPRI; + toneLengthMillis = 750; + } else { + toneType = ToneGenerator.TONE_PROP_PROMPT; + toneVolume = TONE_RELATIVE_VOLUME_HIPRI; + toneLengthMillis = 200; + } + break; + case TONE_VOICE_PRIVACY: + toneType = ToneGenerator.TONE_CDMA_ALERT_NETWORK_LITE; + toneVolume = TONE_RELATIVE_VOLUME_HIPRI; + toneLengthMillis = 5000; + break; + case TONE_REORDER: + toneType = ToneGenerator.TONE_CDMA_REORDER; + toneVolume = TONE_RELATIVE_VOLUME_HIPRI; + toneLengthMillis = 4000; + break; + case TONE_INTERCEPT: + toneType = ToneGenerator.TONE_CDMA_ABBR_INTERCEPT; + toneVolume = TONE_RELATIVE_VOLUME_LOPRI; + toneLengthMillis = 500; + break; + case TONE_CDMA_DROP: + case TONE_OUT_OF_SERVICE: + toneType = ToneGenerator.TONE_CDMA_CALLDROP_LITE; + toneVolume = TONE_RELATIVE_VOLUME_LOPRI; + toneLengthMillis = 375; + break; + case TONE_REDIAL: + toneType = ToneGenerator.TONE_CDMA_ALERT_AUTOREDIAL_LITE; + toneVolume = TONE_RELATIVE_VOLUME_LOPRI; + toneLengthMillis = 5000; + break; + case TONE_RING_BACK: + toneType = ToneGenerator.TONE_SUP_RINGTONE; + toneVolume = TONE_RELATIVE_VOLUME_HIPRI; + // Call ring back tone is stopped by stopTone() method + toneLengthMillis = Integer.MAX_VALUE - TONE_TIMEOUT_BUFFER; + break; + case TONE_UNOBTAINABLE_NUMBER: + toneType = ToneGenerator.TONE_SUP_ERROR; + toneVolume = TONE_RELATIVE_VOLUME_HIPRI; + toneLengthMillis = 4000; + break; + default: + throw new IllegalArgumentException("Bad toneId: " + mToneId); + } + + // If the mToneGenerator creation fails, just continue without it. It is + // a local audio signal, and is not as important. + ToneGenerator toneGenerator; + try { + int stream; + if (mBluetoothHeadset != null) { + stream = mBluetoothHeadset.isAudioOn() ? AudioManager.STREAM_BLUETOOTH_SCO: + AudioManager.STREAM_VOICE_CALL; + } else { + stream = AudioManager.STREAM_VOICE_CALL; + } + toneGenerator = new ToneGenerator(stream, toneVolume); + // if (DBG) log("- created toneGenerator: " + toneGenerator); + } catch (RuntimeException e) { + Log.w(LOG_TAG, + "InCallTonePlayer: Exception caught while creating ToneGenerator: " + e); + toneGenerator = null; + } + + // Using the ToneGenerator (with the CALL_WAITING / BUSY / + // CONGESTION tones at least), the ToneGenerator itself knows + // the right pattern of tones to play; we do NOT need to + // manually start/stop each individual tone, or manually + // insert the correct delay between tones. (We just start it + // and let it run for however long we want the tone pattern to + // continue.) + // + // TODO: When we stop the ToneGenerator in the middle of a + // "tone pattern", it sounds bad if we cut if off while the + // tone is actually playing. Consider adding API to the + // ToneGenerator to say "stop at the next silent part of the + // pattern", or simply "play the pattern N times and then + // stop." + boolean needToStopTone = true; + boolean okToPlayTone = false; + + if (toneGenerator != null) { + int ringerMode = mAudioManager.getRingerMode(); + if (phoneType == PhoneConstants.PHONE_TYPE_CDMA) { + if (toneType == ToneGenerator.TONE_CDMA_ALERT_CALL_GUARD) { + if ((ringerMode != AudioManager.RINGER_MODE_SILENT) && + (ringerMode != AudioManager.RINGER_MODE_VIBRATE)) { + if (DBG) log("- InCallTonePlayer: start playing call tone=" + toneType); + okToPlayTone = true; + needToStopTone = false; + } + } else if ((toneType == ToneGenerator.TONE_CDMA_NETWORK_BUSY_ONE_SHOT) || + (toneType == ToneGenerator.TONE_CDMA_REORDER) || + (toneType == ToneGenerator.TONE_CDMA_ABBR_REORDER) || + (toneType == ToneGenerator.TONE_CDMA_ABBR_INTERCEPT) || + (toneType == ToneGenerator.TONE_CDMA_CALLDROP_LITE)) { + if (ringerMode != AudioManager.RINGER_MODE_SILENT) { + if (DBG) log("InCallTonePlayer:playing call fail tone:" + toneType); + okToPlayTone = true; + needToStopTone = false; + } + } else if ((toneType == ToneGenerator.TONE_CDMA_ALERT_AUTOREDIAL_LITE) || + (toneType == ToneGenerator.TONE_CDMA_ALERT_NETWORK_LITE)) { + if ((ringerMode != AudioManager.RINGER_MODE_SILENT) && + (ringerMode != AudioManager.RINGER_MODE_VIBRATE)) { + if (DBG) log("InCallTonePlayer:playing tone for toneType=" + toneType); + okToPlayTone = true; + needToStopTone = false; + } + } else { // For the rest of the tones, always OK to play. + okToPlayTone = true; + } + } else { // Not "CDMA" + okToPlayTone = true; + } + + synchronized (this) { + if (okToPlayTone && mState != TONE_STOPPED) { + mState = TONE_ON; + toneGenerator.startTone(toneType); + try { + wait(toneLengthMillis + TONE_TIMEOUT_BUFFER); + } catch (InterruptedException e) { + Log.w(LOG_TAG, + "InCallTonePlayer stopped: " + e); + } + if (needToStopTone) { + toneGenerator.stopTone(); + } + } + // if (DBG) log("- InCallTonePlayer: done playing."); + toneGenerator.release(); + mState = TONE_OFF; + } + } + + // Finally, do the same cleanup we otherwise would have done + // in onDisconnect(). + // + // (But watch out: do NOT do this if the phone is in use, + // since some of our tones get played *during* a call (like + // CALL_WAITING) and we definitely *don't* + // want to reset the audio mode / speaker / bluetooth after + // playing those! + // This call is really here for use with tones that get played + // *after* a call disconnects, like "busy" or "congestion" or + // "call ended", where the phone has already become idle but + // we need to defer the resetAudioStateAfterDisconnect() call + // till the tone finishes playing.) + if (mCM.getState() == PhoneConstants.State.IDLE) { + resetAudioStateAfterDisconnect(); + } + } + + public void stopTone() { + synchronized (this) { + if (mState == TONE_ON) { + notify(); + } + mState = TONE_STOPPED; + } + } + } + + /** + * Displays a notification when the phone receives a DisplayInfo record. + */ + private void onDisplayInfo(AsyncResult r) { + // Extract the DisplayInfo String from the message + CdmaDisplayInfoRec displayInfoRec = (CdmaDisplayInfoRec)(r.result); + + if (displayInfoRec != null) { + String displayInfo = displayInfoRec.alpha; + if (DBG) log("onDisplayInfo: displayInfo=" + displayInfo); + CdmaDisplayInfo.displayInfoRecord(mApplication, displayInfo); + + // start a 2 second timer + sendEmptyMessageDelayed(DISPLAYINFO_NOTIFICATION_DONE, + DISPLAYINFO_NOTIFICATION_TIME); + } + } + + /** + * Helper class to play SignalInfo tones using the ToneGenerator. + * + * To use, just instantiate a new SignalInfoTonePlayer + * (passing in the ToneID constant for the tone you want) + * and start() it. + */ + private class SignalInfoTonePlayer extends Thread { + private int mToneId; + + SignalInfoTonePlayer(int toneId) { + super(); + mToneId = toneId; + } + + @Override + public void run() { + log("SignalInfoTonePlayer.run(toneId = " + mToneId + ")..."); + + if (mSignalInfoToneGenerator != null) { + //First stop any ongoing SignalInfo tone + mSignalInfoToneGenerator.stopTone(); + + //Start playing the new tone if its a valid tone + mSignalInfoToneGenerator.startTone(mToneId); + } + } + } + + /** + * Plays a tone when the phone receives a SignalInfo record. + */ + private void onSignalInfo(AsyncResult r) { + // Signal Info are totally ignored on non-voice-capable devices. + if (!PhoneGlobals.sVoiceCapable) { + Log.w(LOG_TAG, "Got onSignalInfo() on non-voice-capable device! Ignoring..."); + return; + } + + if (PhoneUtils.isRealIncomingCall(mCM.getFirstActiveRingingCall().getState())) { + // Do not start any new SignalInfo tone when Call state is INCOMING + // and stop any previous SignalInfo tone which is being played + stopSignalInfoTone(); + } else { + // Extract the SignalInfo String from the message + CdmaSignalInfoRec signalInfoRec = (CdmaSignalInfoRec)(r.result); + // Only proceed if a Signal info is present. + if (signalInfoRec != null) { + boolean isPresent = signalInfoRec.isPresent; + if (DBG) log("onSignalInfo: isPresent=" + isPresent); + if (isPresent) {// if tone is valid + int uSignalType = signalInfoRec.signalType; + int uAlertPitch = signalInfoRec.alertPitch; + int uSignal = signalInfoRec.signal; + + if (DBG) log("onSignalInfo: uSignalType=" + uSignalType + ", uAlertPitch=" + + uAlertPitch + ", uSignal=" + uSignal); + //Map the Signal to a ToneGenerator ToneID only if Signal info is present + int toneID = SignalToneUtil.getAudioToneFromSignalInfo + (uSignalType, uAlertPitch, uSignal); + + //Create the SignalInfo tone player and pass the ToneID + new SignalInfoTonePlayer(toneID).start(); + } + } + } + } + + /** + * Stops a SignalInfo tone in the following condition + * 1 - On receiving a New Ringing Call + * 2 - On disconnecting a call + * 3 - On answering a Call Waiting Call + */ + /* package */ void stopSignalInfoTone() { + if (DBG) log("stopSignalInfoTone: Stopping SignalInfo tone player"); + new SignalInfoTonePlayer(ToneGenerator.TONE_CDMA_SIGNAL_OFF).start(); + } + + /** + * Plays a Call waiting tone if it is present in the second incoming call. + */ + private void onCdmaCallWaiting(AsyncResult r) { + // Remove any previous Call waiting timers in the queue + removeMessages(CALLWAITING_CALLERINFO_DISPLAY_DONE); + removeMessages(CALLWAITING_ADDCALL_DISABLE_TIMEOUT); + + // Set the Phone Call State to SINGLE_ACTIVE as there is only one connection + // else we would not have received Call waiting + mApplication.cdmaPhoneCallState.setCurrentCallState( + CdmaPhoneCallState.PhoneCallState.SINGLE_ACTIVE); + + // Display the incoming call to the user if the InCallScreen isn't + // already in the foreground. + if (!mApplication.isShowingCallScreen()) { + if (DBG) log("- showing incoming call (CDMA call waiting)..."); + showIncomingCall(); + } + + // Start timer for CW display + mCallWaitingTimeOut = false; + sendEmptyMessageDelayed(CALLWAITING_CALLERINFO_DISPLAY_DONE, + CALLWAITING_CALLERINFO_DISPLAY_TIME); + + // Set the mAddCallMenuStateAfterCW state to false + mApplication.cdmaPhoneCallState.setAddCallMenuStateAfterCallWaiting(false); + + // Start the timer for disabling "Add Call" menu option + sendEmptyMessageDelayed(CALLWAITING_ADDCALL_DISABLE_TIMEOUT, + CALLWAITING_ADDCALL_DISABLE_TIME); + + // Extract the Call waiting information + CdmaCallWaitingNotification infoCW = (CdmaCallWaitingNotification) r.result; + int isPresent = infoCW.isPresent; + if (DBG) log("onCdmaCallWaiting: isPresent=" + isPresent); + if (isPresent == 1 ) {//'1' if tone is valid + int uSignalType = infoCW.signalType; + int uAlertPitch = infoCW.alertPitch; + int uSignal = infoCW.signal; + if (DBG) log("onCdmaCallWaiting: uSignalType=" + uSignalType + ", uAlertPitch=" + + uAlertPitch + ", uSignal=" + uSignal); + //Map the Signal to a ToneGenerator ToneID only if Signal info is present + int toneID = + SignalToneUtil.getAudioToneFromSignalInfo(uSignalType, uAlertPitch, uSignal); + + //Create the SignalInfo tone player and pass the ToneID + new SignalInfoTonePlayer(toneID).start(); + } + } + + /** + * Posts a event causing us to clean up after rejecting (or timing-out) a + * CDMA call-waiting call. + * + * This method is safe to call from any thread. + * @see #onCdmaCallWaitingReject() + */ + /* package */ void sendCdmaCallWaitingReject() { + sendEmptyMessage(CDMA_CALL_WAITING_REJECT); + } + + /** + * Performs Call logging based on Timeout or Ignore Call Waiting Call for CDMA, + * and finally calls Hangup on the Call Waiting connection. + * + * This method should be called only from the UI thread. + * @see #sendCdmaCallWaitingReject() + */ + private void onCdmaCallWaitingReject() { + final Call ringingCall = mCM.getFirstActiveRingingCall(); + + // Call waiting timeout scenario + if (ringingCall.getState() == Call.State.WAITING) { + // Code for perform Call logging and missed call notification + Connection c = ringingCall.getLatestConnection(); + + if (c != null) { + final int callLogType = mCallWaitingTimeOut ? + Calls.MISSED_TYPE : Calls.INCOMING_TYPE; + + // TODO: This callLogType override is not ideal. Connection should be astracted away + // at a telephony-phone layer that can understand and edit the callTypes within + // the abstraction for CDMA devices. + mCallLogger.logCall(c, callLogType); + + final long date = c.getCreateTime(); + if (callLogType == Calls.MISSED_TYPE) { + // Add missed call notification + showMissedCallNotification(c, date); + } else { + // Remove Call waiting 20 second display timer in the queue + removeMessages(CALLWAITING_CALLERINFO_DISPLAY_DONE); + } + + // Hangup the RingingCall connection for CW + PhoneUtils.hangup(c); + } + + //Reset the mCallWaitingTimeOut boolean + mCallWaitingTimeOut = false; + } + } + + /** + * Return the private variable mPreviousCdmaCallState. + */ + /* package */ Call.State getPreviousCdmaCallState() { + return mPreviousCdmaCallState; + } + + /** + * Return the private variable mVoicePrivacyState. + */ + /* package */ boolean getVoicePrivacyState() { + return mVoicePrivacyState; + } + + /** + * Return the private variable mIsCdmaRedialCall. + */ + /* package */ boolean getIsCdmaRedialCall() { + return mIsCdmaRedialCall; + } + + /** + * Helper function used to show a missed call notification. + */ + private void showMissedCallNotification(Connection c, final long date) { + PhoneUtils.CallerInfoToken info = + PhoneUtils.startGetCallerInfo(mApplication, c, this, Long.valueOf(date)); + if (info != null) { + // at this point, we've requested to start a query, but it makes no + // sense to log this missed call until the query comes back. + if (VDBG) log("showMissedCallNotification: Querying for CallerInfo on missed call..."); + if (info.isFinal) { + // it seems that the query we have actually is up to date. + // send the notification then. + CallerInfo ci = info.currentInfo; + + // Check number presentation value; if we have a non-allowed presentation, + // then display an appropriate presentation string instead as the missed + // call. + String name = ci.name; + String number = ci.phoneNumber; + if (ci.numberPresentation == PhoneConstants.PRESENTATION_RESTRICTED) { + name = mApplication.getString(R.string.private_num); + } else if (ci.numberPresentation != PhoneConstants.PRESENTATION_ALLOWED) { + name = mApplication.getString(R.string.unknown); + } else { + number = PhoneUtils.modifyForSpecialCnapCases(mApplication, + ci, number, ci.numberPresentation); + } + mApplication.notificationMgr.notifyMissedCall(name, number, + ci.phoneLabel, ci.cachedPhoto, ci.cachedPhotoIcon, date); + } + } else { + // getCallerInfo() can return null in rare cases, like if we weren't + // able to get a valid phone number out of the specified Connection. + Log.w(LOG_TAG, "showMissedCallNotification: got null CallerInfo for Connection " + c); + } + } + + /** + * Inner class to handle emergency call tone and vibrator + */ + private class EmergencyTonePlayerVibrator { + private final int EMG_VIBRATE_LENGTH = 1000; // ms. + private final int EMG_VIBRATE_PAUSE = 1000; // ms. + private final long[] mVibratePattern = + new long[] { EMG_VIBRATE_LENGTH, EMG_VIBRATE_PAUSE }; + + private ToneGenerator mToneGenerator; + // We don't rely on getSystemService(Context.VIBRATOR_SERVICE) to make sure this vibrator + // object will be isolated from others. + private Vibrator mEmgVibrator = new SystemVibrator(); + private int mInCallVolume; + + /** + * constructor + */ + public EmergencyTonePlayerVibrator() { + } + + /** + * Start the emergency tone or vibrator. + */ + private void start() { + if (VDBG) log("call startEmergencyToneOrVibrate."); + int ringerMode = mAudioManager.getRingerMode(); + + if ((mIsEmergencyToneOn == EMERGENCY_TONE_ALERT) && + (ringerMode == AudioManager.RINGER_MODE_NORMAL)) { + log("EmergencyTonePlayerVibrator.start(): emergency tone..."); + mToneGenerator = new ToneGenerator (AudioManager.STREAM_VOICE_CALL, + InCallTonePlayer.TONE_RELATIVE_VOLUME_EMERGENCY); + if (mToneGenerator != null) { + mInCallVolume = mAudioManager.getStreamVolume(AudioManager.STREAM_VOICE_CALL); + mAudioManager.setStreamVolume(AudioManager.STREAM_VOICE_CALL, + mAudioManager.getStreamMaxVolume(AudioManager.STREAM_VOICE_CALL), + 0); + mToneGenerator.startTone(ToneGenerator.TONE_CDMA_EMERGENCY_RINGBACK); + mCurrentEmergencyToneState = EMERGENCY_TONE_ALERT; + } + } else if (mIsEmergencyToneOn == EMERGENCY_TONE_VIBRATE) { + log("EmergencyTonePlayerVibrator.start(): emergency vibrate..."); + if (mEmgVibrator != null) { + mEmgVibrator.vibrate(mVibratePattern, 0); + mCurrentEmergencyToneState = EMERGENCY_TONE_VIBRATE; + } + } + } + + /** + * If the emergency tone is active, stop the tone or vibrator accordingly. + */ + private void stop() { + if (VDBG) log("call stopEmergencyToneOrVibrate."); + + if ((mCurrentEmergencyToneState == EMERGENCY_TONE_ALERT) + && (mToneGenerator != null)) { + mToneGenerator.stopTone(); + mToneGenerator.release(); + mAudioManager.setStreamVolume(AudioManager.STREAM_VOICE_CALL, + mInCallVolume, + 0); + } else if ((mCurrentEmergencyToneState == EMERGENCY_TONE_VIBRATE) + && (mEmgVibrator != null)) { + mEmgVibrator.cancel(); + } + mCurrentEmergencyToneState = EMERGENCY_TONE_OFF; + } + } + + private BluetoothProfile.ServiceListener mBluetoothProfileServiceListener = + new BluetoothProfile.ServiceListener() { + public void onServiceConnected(int profile, BluetoothProfile proxy) { + mBluetoothHeadset = (BluetoothHeadset) proxy; + if (VDBG) log("- Got BluetoothHeadset: " + mBluetoothHeadset); + } + + public void onServiceDisconnected(int profile) { + mBluetoothHeadset = null; + } + }; + + private void onRingbackTone(AsyncResult r) { + boolean playTone = (Boolean)(r.result); + + if (playTone == true) { + // Only play when foreground call is in DIALING or ALERTING. + // to prevent a late coming playtone after ALERTING. + // Don't play ringback tone if it is in play, otherwise it will cut + // the current tone and replay it + if (mCM.getActiveFgCallState().isDialing() && + mInCallRingbackTonePlayer == null) { + mInCallRingbackTonePlayer = new InCallTonePlayer(InCallTonePlayer.TONE_RING_BACK); + mInCallRingbackTonePlayer.start(); + } + } else { + if (mInCallRingbackTonePlayer != null) { + mInCallRingbackTonePlayer.stopTone(); + mInCallRingbackTonePlayer = null; + } + } + } + + /** + * Toggle mute and unmute requests while keeping the same mute state + */ + private void onResendMute() { + boolean muteState = PhoneUtils.getMute(); + PhoneUtils.setMute(!muteState); + PhoneUtils.setMute(muteState); + } + + private void log(String msg) { + Log.d(LOG_TAG, msg); + } +} |