CallNotifier.javaAPI DocAndroid 1.5 API39099Wed May 06 22:42:46 BST


public class CallNotifier extends android.os.Handler implements CallerInfoAsyncQuery.OnQueryCompleteListener
Stub which listens for phone state changes and decides whether it is worth telling the user what just happened.

Fields Summary
private static final String
private static final boolean
private static final boolean
private static final String
private static final String
private static final int
private static final int
private static final int
private int
private Object
private static final int
private static final int
private static final int
private static final int
private static final int
private static final int
private static final int
private static final int
private PhoneApp
private Ringer
private BluetoothHandsfree
private boolean
Constructors Summary
public CallNotifier(PhoneApp app, phone, Ringer ringer, BluetoothHandsfree btMgr)

        mApplication = app;

        mPhone = phone;
        mPhone.registerForNewRingingConnection(this, PHONE_NEW_RINGING_CONNECTION, null);
        mPhone.registerForPhoneStateChanged(this, PHONE_STATE_CHANGED, null);
        mPhone.registerForDisconnect(this, PHONE_DISCONNECT, null);
        mPhone.registerForUnknownConnection(this, PHONE_UNKNOWN_CONNECTION_APPEARED, null);
        mPhone.registerForIncomingRing(this, PHONE_INCOMING_RING, null);
        mRinger = ringer;
        mBluetoothHandsfree = btMgr;

        TelephonyManager telephonyManager = (TelephonyManager)app.getSystemService(
                | PhoneStateListener.LISTEN_CALL_FORWARDING_INDICATOR);
Methods Summary
public voidhandleMessage(android.os.Message msg)

        switch (msg.what) {
                if (DBG) log("RINGING... (new)");
                onNewRingingConnection((AsyncResult) msg.obj);
                mSilentRingerRequested = false;

            case 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 &&
                        ((GSMPhone)((AsyncResult) msg.obj).result).getState() == Phone.State.RINGING
                        && mSilentRingerRequested == false) {
                    if (DBG) log("RINGING... (PHONE_INCOMING_RING event)");
                } else {
                    if (DBG) log("RING before NEW_RING, skipping");

            case PHONE_STATE_CHANGED:
                onPhoneStateChanged((AsyncResult) msg.obj);

            case PHONE_DISCONNECT:
                if (DBG) log("DISCONNECT");
                onDisconnect((AsyncResult) msg.obj);

                onUnknownConnectionAppeared((AsyncResult) msg.obj);

                // CallerInfo query is taking too long!  But we can't wait
                // any more, so start ringing NOW even if it means we won't
                // use the correct custom ringtone.
                Log.w(LOG_TAG, "CallerInfo query took too long; manually starting ringer");

                // 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()).

            case PHONE_MWI_CHANGED:

            case PHONE_BATTERY_LOW:

                // super.handleMessage(msg);
Indicates whether or not this ringer is ringing.

        return mRinger.isRinging();
private voidlog(java.lang.String msg)

        Log.d(LOG_TAG, msg);
private voidonBatteryLow()

        if (DBG) log("onBatteryLow()...");

        // Play the "low battery" warning tone, only if the user is
        // in-call.  (The test here is exactly the opposite of the test in
        // StatusBarPolicy.updateBattery(), where we bring up the "low
        // battery warning" dialog only if the user is NOT in-call.)
        if (mPhone.getState() != Phone.State.IDLE) {
            new InCallTonePlayer(InCallTonePlayer.TONE_BATTERY_LOW).start();
private voidonCfiChanged(boolean visible)

        if (VDBG) log("onCfiChanged(): " + visible);
private voidonCustomRingQueryComplete()
Performs the final steps of the onNewRingingConnection sequence: starts the ringer, and launches the InCallScreen to show 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.)

        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");

        // 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 (mPhone.getState() != Phone.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.

        // Ring, either with the queried ringtone or default one.
        if (VDBG) log("RINGING... (onCustomRingQueryComplete)");

        // ...and show the InCallScreen.
private voidonDisconnect(android.os.AsyncResult r)

        if (VDBG) log("onDisconnect()...  phone state: " + mPhone.getState());
        if (mPhone.getState() == Phone.State.IDLE) {

        Connection c = (Connection) r.result;
        if (DBG && c != null) {
            log("- onDisconnect: cause = " + c.getDisconnectCause()
                + ", incoming = " + c.isIncoming()
                + ", date = " + c.getCreateTime());

        // 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.
        if (DBG) log("stopRing()... (onDisconnect)");

        // 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;

        // 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)
            && (mPhone.getState() == Phone.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;

        if (mPhone.getState() == Phone.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) {


            // If the InCallScreen is *not* in the foreground, forcibly
            // dismiss it to make sure it won't still be in the activity
            // history.  (But if it *is* in the foreground, don't mess
            // with it; it needs to be visible, displaying the "Call
            // ended" state.)
            if (!mApplication.isShowingCallScreen()) {
                if (VDBG) log("onDisconnect: force InCallScreen to finish()");

        if (c != null) {
            final String number = c.getAddress();
            final int presentation = c.getNumberPresentation();
            final long date = c.getCreateTime();
            final long duration = c.getDurationMillis();
            final Connection.DisconnectCause cause = c.getDisconnectCause();

            // Set the "type" to be displayed in the call log (see constants in CallLog.Calls)
            final int callLogType;
            if (c.isIncoming()) {
                callLogType = (cause == Connection.DisconnectCause.INCOMING_MISSED ?
                               CallLog.Calls.MISSED_TYPE :
            } else {
                callLogType = CallLog.Calls.OUTGOING_TYPE;
            if (VDBG) log("- callLogType: " + callLogType + ", UserData: " + c.getUserData());

            // Get the CallerInfo object and then log the call with it.
                Object o = c.getUserData();
                final CallerInfo ci;
                if ((o == null) || (o instanceof CallerInfo)){
                    ci = (CallerInfo) o;
                } else {
                    ci = ((PhoneUtils.CallerInfoToken) o).currentInfo;

                // Watch out: Calls.addCall() hits the Contacts database,
                // so we shouldn't call it from the main thread.
                Thread t = new Thread() {
                        public void run() {
                            Calls.addCall(ci, mApplication, number, presentation,
                                          callLogType, date, (int) duration / 1000);
                            // if (DBG) log("onDisconnect helper thread: Calls.addCall() done.");

            if (callLogType == CallLog.Calls.MISSED_TYPE) {
                // Show the "Missed call" notification.
                // (Note we *don't* do this if this was an incoming call that
                // the user deliberately rejected.)

                PhoneUtils.CallerInfoToken info =
                    PhoneUtils.startGetCallerInfo(mApplication, c, this, new Long(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("onDisconnect: 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;
                        NotificationMgr.getDefault().notifyMissedCall(, ci.phoneNumber,
                                ci.phoneLabel, 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, "onDisconnect: got null CallerInfo for Connection " + c);

            // 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();
                // The InCallTonePlayer will automatically stop playing (and
                // clean itself up) after a few seconds.

                // 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 (mPhone.getState() == Phone.State.IDLE) {
                // Release screen wake locks if the in-call screen is not
                // showing. Otherwise, let the in-call screen handle this because
                // it needs to show the call ended screen for a couple of
                // seconds.
                if (!mApplication.isShowingCallScreen()) {
                    if (VDBG) log("- NOT showing in-call screen; releasing wake locks!");
                } else {
                    if (VDBG) log("- still showing in-call screen; not releasing wake locks.");
            } else {
                if (VDBG) log("- phone still in use; not releasing wake locks.");
private voidonMwiChanged(boolean visible)

        if (VDBG) log("onMwiChanged(): " + visible);
private voidonNewRingingConnection(android.os.AsyncResult r)

        Connection c = (Connection) r.result;
        if (DBG) log("onNewRingingConnection(): " + c);
        PhoneApp app = PhoneApp.getInstance();

        // Incoming calls are totally ignored if the device isn't provisioned yet
        boolean provisioned = Settings.Secure.getInt(mPhone.getContext().getContentResolver(),
            Settings.Secure.DEVICE_PROVISIONED, 0) != 0;
        if (!provisioned) {
            Log.i(LOG_TAG, "CallNotifier: rejecting incoming call: device isn't provisioned");
            // Send the caller straight to voicemail, just like
            // "rejecting" an incoming call.

        if (c != null && c.isRinging()) {
            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!)
            // by firing off an ACTION_ANSWER intent.
            // We'd need to protect this with a new "intercept incoming calls"
            // system permission.

            // - don't ring for call waiting connections
            // - do this before showing the incoming call panel
            if (state == Call.State.INCOMING) {
            } else {
                if (VDBG) log("- starting call waiting tone...");
                new InCallTonePlayer(InCallTonePlayer.TONE_CALL_WAITING).start();
                // The InCallTonePlayer will automatically stop playing (and
                // clean itself up) after playing the tone.

                // TODO: alternatively, consider starting an
                // InCallTonePlayer with an "unlimited" tone length, and
                // manually stop it later when the ringing call either (a)
                // gets answered, or (b) gets disconnected.

                // in this case, just fall through like before, and call
                // PhoneUtils.showIncomingCallUi

        // 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
        // PhoneUtils.showIncomingCallUi().)
        if (VDBG) log("Holding wake lock on new incoming connection.");

        if (VDBG) log("- onNewRingingConnection() done.");
private voidonPhoneStateChanged(android.os.AsyncResult r)

        Phone.State state = mPhone.getState();

        // 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.
                .enableNotificationAlerts(state == Phone.State.IDLE);

        // 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.

        if (state == Phone.State.OFFHOOK) {
            if (VDBG) log("onPhoneStateChanged: OFF HOOK");

            // if the call screen is showing, let it handle the event,
            // otherwise handle it here.
            if (!mApplication.isShowingCallScreen()) {

            // 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)");

            // put a icon in the status bar
public voidonQueryComplete(int token, java.lang.Object cookie, ci)
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.

        if (cookie instanceof Long) {
            if (VDBG) log("CallerInfo query complete, posting missed call notification");

            NotificationMgr.getDefault().notifyMissedCall(, ci.phoneNumber,
                    ci.phoneLabel, ((Long) cookie).longValue());
        } else if (cookie instanceof CallNotifier){
            if (VDBG) log("CallerInfo query complete, updating data");

            // get rid of the timeout messages

            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.");

                // 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;
                // ring, and other post-ring actions.
private voidonUnknownConnectionAppeared(android.os.AsyncResult r)

        Phone.State state = mPhone.getState();

        if (state == Phone.State.OFFHOOK) {
            // basically do onPhoneStateChanged + displayCallScreen
private voidresetAudioStateAfterDisconnect()
Resets the audio mode and speaker state when a call ends.

        if (VDBG) log("resetAudioStateAfterDisconnect()...");

        if (mBluetoothHandsfree != null) {

        if (PhoneUtils.isSpeakerOn(mPhone.getContext())) {
            PhoneUtils.turnOnSpeaker(mPhone.getContext(), false);

        PhoneUtils.setAudioMode(mPhone.getContext(), AudioManager.MODE_NORMAL);
Posts a PHONE_BATTERY_LOW event, causing us to play a warning tone if the user is in-call.

        Message message = Message.obtain(this, PHONE_BATTERY_LOW);
voidsendMwiChangedDelayed(long delayMillis)
Posts a delayed PHONE_MWI_CHANGED event, to schedule a "retry" for a failed NotificationMgr.updateMwi() call.

        Message message = Message.obtain(this, PHONE_MWI_CHANGED);
        sendMessageDelayed(message, delayMillis);
Stops the current ring, and tells the notifier that future ring requests should be ignored.

        mSilentRingerRequested = true;
        if (DBG) log("stopRing()... (silenceRinger)");
private voidstartIncomingCallQuery( c)
Helper method to manage the start of incoming call queries

        // 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) {
            // create a custom ringer using the default ringer first

            // query the callerinfo to try to get the ringer.
            PhoneUtils.CallerInfoToken cit = PhoneUtils.startGetCallerInfo(
                    mPhone.getContext(), 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.");
            // calls to PhoneUtils.showIncomingCallUi will come after the
            // queries are complete (or timeout).
        } 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.

            // In this case, just log the request and ring.
            if (VDBG) log("RINGING... (request to ring arrived while query is running)");

            // in this case, just fall through like before, and call
            // PhoneUtils.showIncomingCallUi