Is the phone ringing?

About 2 years ago I was writing an app that needed to know when a call was happening.  More specifically, I needed to be able to detect when a call ended, its duration, and who it was made to.  The app was never released but it was a small app to categorize my calls so I could account for them and accurately charge for my consulting work (also so I could accurately deduct my business ratio of calls from my taxes).

This ended up being surprisingly annoying on Android.  Android’s method of sending events to activities that aren’t running (BroadcastReceivers) aren’t too bad, but they aren’t always convenient to work with.  Instead of getting specific functions called on an object somewhere, you get generic calls on an object with no context and no saved state.  In addition, Android doesn’t have call starting and call end broadcasts.  It has an outgoing call started broadcast, but call ends can only be detected by checking the ringing state changing and duration has to be calculated on your own.  Detection of outgoing calls needs to be detected in a similar way.

I’m ok with writing code like this once.  I pretty much have no choice.  But I’m not going to do it twice.  There’s better ways.

First off, the interface.  Due to the design of Android, it will have to be a BroadcastReceiver.  It can’t be an interface registered with one because the receiver may be run without our app being run, so we’d have no change to register it.  But I want the heavy lifting to be done by a library class- I just want to derive from something and override a few functions.  So ideally we have something like:

public abstract class PhonecallReceiver extends BroadcastReceiver {
protected void onIncomingCallStarted(Context ctx, String number, Date start);
protected void onOutgoingCallStarted(Context ctx, String number, Date start);
protected void onIncomingCallEnded(Context ctx, String number, Date start, Date end);
protected void onOutgoingCallEnded(Context ctx, String number, Date start, Date end);
protected void onMissedCall(Context ctx, String number, Date start);
}

This gives us specific function calls for each event.  We get a Context object so we can start a Service or Activity if needed.  We get the number of the other end (which can be looked up in the Contacts db if needed).  We get the start and end times of each call, duration can be easily calculated if needed.  We get notification for start and end pf call, incoming and outgoing.  We even get missed calls.  We can override the ones we care about and ignore the rest.

Now to explain the way Android calls work.  To do anything, we need to handle android.intent.action.PHONE_STATE broadcasts.  This calls our receiver whenever the state changes.  The state can be IDLE, RINGING, or OFFHOOK.  IDLE is when no calls are going on.  RINGING means the phone is actually ringing.  OFFHOOK means that we’re actually talking.

Now think about how an incoming call goes.  You start off in IDLE state.  If someone calls you, it goes to RINGING.  When you answer, it goes to OFFHOOK.  If you don’t answer, it goes to IDLE.  Sounds like a simple state machine.  Lets write it:

       
public void onCallStateChanged(Context context, int state, String incomingNumber) {
    switch (state) {
        case TelephonyManager.CALL_STATE_RINGING:
            isIncoming = true;
            callStartTime = new Date();
            onIncomingCallStarted(context, number, callStartTime);
            break;
        case TelephonyManager.CALL_STATE_IDLE:
            if(lastState == TelephonyManager.CALL_STATE_RINGING){
                //Ring but no pickup-  a miss
                onMissedCall(context, number, callStartTime);
            }
            else {
                onIncomingCallEnded(context, number, callStartTime, new Date());
            }
            break;
    }
    lastState = state;

}
We also have to set up a receiver to call this code:

public void onReceive(Context context, Intent intent) {
		
	//We listen to two intents.  The new outgoing call only tells us of an outgoing call.  We use it to get the number.
		String stateStr = intent.getExtras().getString(TelephonyManager.EXTRA_STATE);
		String number = intent.getExtras().getString(TelephonyManager.EXTRA_INCOMING_NUMBER);
		int state = 0;
		if(stateStr.equals(TelephonyManager.EXTRA_STATE_IDLE)){
			state = TelephonyManager.CALL_STATE_IDLE;
		}
		else if(stateStr.equals(TelephonyManager.EXTRA_STATE_OFFHOOK)){
			state = TelephonyManager.CALL_STATE_OFFHOOK;
		}
		else if(stateStr.equals(TelephonyManager.EXTRA_STATE_RINGING)){
			state = TelephonyManager.CALL_STATE_RINGING;
		}
			
			
		onCallStateChanged(context, state, number);
	}
}

We’re going to quickly notice a few things. First, for some reason Android sends a string state rather than the integer codes. We need to convert. Second, we notice that any data we save in variables in the receiver are lost. That’s because each broadcast builds a new receiver. So we need to hold them all in static variables. Ugly, but not too harsh.

The third issue you’ll see is that this isn’t going to work- the number is always right in the ringing state, but isn’t necessarily in other states- especially not in idle. So we need to save the phone number. That gives us:

public void onCallStateChanged(Context context, int state, String number) {
	switch (state) {
		case TelephonyManager.CALL_STATE_RINGING:
			callStartTime = new Date();
			savedNumber = number;
			onIncomingCallStarted(context, number, callStartTime);
			break;
		case TelephonyManager.CALL_STATE_IDLE:
			if(lastState == TelephonyManager.CALL_STATE_RINGING){
				//Ring but no pickup-  a miss
				onMissedCall(context, savedNumber, callStartTime);
		    	}
		    	else{
				onIncomingCallEnded(context, savedNumber, callStartTime, new Date());			    		
			}
			break;
	}
	lastState = state;
}

Now lets add in outgoing calls. In an outgoing call, you go from IDLE directly to OFFHOOK without going through ringing. So we’ll need to add a check for the OFFHOOK state, and look at the last state to see if we were in IDLE or RINGING to tell if we were outgoing or incoming. We’ll also need to keep that knowledge around in a variable so we can use it at call end. That gives us the final version of our state machine:

public void onCallStateChanged(Context context, int state, String number) {
	switch (state) {
		case TelephonyManager.CALL_STATE_RINGING:
			callStartTime = new Date();
			savedNumber = number;
			onIncomingCallStarted(context, number, callStartTime);
			break;
		case TelephonyManager.CALL_STATE_OFFHOOK:
		    	//Transition of ringing->offhook are pickups of incoming calls.  
			if(lastState != TelephonyManager.CALL_STATE_RINGING){
				isIncoming = false;
				callStartTime = new Date();
				onOutgoingCallStarted(context, savedNumber, callStartTime);			    		
		    	}
		        break;
		case TelephonyManager.CALL_STATE_IDLE:
			if(lastState == TelephonyManager.CALL_STATE_RINGING){
				//Ring but no pickup-  a miss
				onMissedCall(context, savedNumber, callStartTime);
			}
			else if(isIncoming){
				onIncomingCallEnded(context, savedNumber, callStartTime, new Date());			    		
			}
			else{
				onOutgoingCallEnded(context, savedNumber, callStartTime, new Date());			    					    		
			}
			break;
	}
	lastState = state;
}

One last hurdle- remember that OFFHOOK doesn’t give you a correct phone number? That means we don’t have a valid phone number for outgoing calls. Luckily there’s another broadcast we can hook for that- the android.intent.action.NEW_OUTGOING_CALL broadcast. We add this to our receiver for our final receiver:

public void onReceive(Context context, Intent intent) {
		
	//We listen to two intents.  The new outgoing call only tells us of an outgoing call.  We use it to get the number.
	if (intent.getAction().equals("android.intent.action.NEW_OUTGOING_CALL")) {
		savedNumber = intent.getExtras().getString("android.intent.extra.PHONE_NUMBER");
	}
	else{
		String stateStr = intent.getExtras().getString(TelephonyManager.EXTRA_STATE);
		String number = intent.getExtras().getString(TelephonyManager.EXTRA_INCOMING_NUMBER);
		int state = 0;
		if(stateStr.equals(TelephonyManager.EXTRA_STATE_IDLE)){
			state = TelephonyManager.CALL_STATE_IDLE;
		}
		else if(stateStr.equals(TelephonyManager.EXTRA_STATE_OFFHOOK)){
			state = TelephonyManager.CALL_STATE_OFFHOOK;
		}
		else if(stateStr.equals(TelephonyManager.EXTRA_STATE_RINGING)){
			state = TelephonyManager.CALL_STATE_RINGING;
		}
						
		onCallStateChanged(context, state, number);
	}
}

This being android, you’ll need some listings in your manifest as well- a few broadcast and registering for the two broadcasts:

	
	
	
	
        	
	        	
	    	
        	
	        	
	    	
	

Here’s the final version of the class:

package com.gabesechan.android.reusable.receivers;

import java.util.Date;

import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.telephony.TelephonyManager;

public abstract class PhonecallReceiver extends BroadcastReceiver {
	
	//The receiver will be recreated whenever android feels like it.  We need a static variable to remember data between instantiations
	
	private static int lastState = TelephonyManager.CALL_STATE_IDLE;
	private static Date callStartTime;
	private static boolean isIncoming;
	private static String savedNumber;  //because the passed incoming is only valid in ringing

	
	@Override
	public void onReceive(Context context, Intent intent) {
		
		//We listen to two intents.  The new outgoing call only tells us of an outgoing call.  We use it to get the number.
		if (intent.getAction().equals("android.intent.action.NEW_OUTGOING_CALL")) {
			savedNumber = intent.getExtras().getString("android.intent.extra.PHONE_NUMBER");
	    }
		else{
			String stateStr = intent.getExtras().getString(TelephonyManager.EXTRA_STATE);
			String number = intent.getExtras().getString(TelephonyManager.EXTRA_INCOMING_NUMBER);
			int state = 0;
			if(stateStr.equals(TelephonyManager.EXTRA_STATE_IDLE)){
				state = TelephonyManager.CALL_STATE_IDLE;
			}
			else if(stateStr.equals(TelephonyManager.EXTRA_STATE_OFFHOOK)){
				state = TelephonyManager.CALL_STATE_OFFHOOK;
			}
			else if(stateStr.equals(TelephonyManager.EXTRA_STATE_RINGING)){
				state = TelephonyManager.CALL_STATE_RINGING;
			}
			
			
			onCallStateChanged(context, state, number);
		}
	}
	
	//Derived classes should override these to respond to specific events of interest
	protected void onIncomingCallStarted(Context ctx, String number, Date start){}
	protected void onOutgoingCallStarted(Context ctx, String number, Date start){}
	protected void onIncomingCallEnded(Context ctx, String number, Date start, Date end){}
	protected void onOutgoingCallEnded(Context ctx, String number, Date start, Date end){}
	protected void onMissedCall(Context ctx, String number, Date start){}

	//Deals with actual events
		
	//Incoming call-  goes from IDLE to RINGING when it rings, to OFFHOOK when it's answered, to IDLE when its hung up
	//Outgoing call-  goes from IDLE to OFFHOOK when it dials out, to IDLE when hung up
	public void onCallStateChanged(Context context, int state, String number) {
	    if(lastState == state){
	    	//No change, debounce extras
	    	return;
	    }
	    switch (state) {
		    case TelephonyManager.CALL_STATE_RINGING:
		    	isIncoming = true;
		    	callStartTime = new Date();
		    	savedNumber = number;
		    	onIncomingCallStarted(context, number, callStartTime);
		        break;
		    case TelephonyManager.CALL_STATE_OFFHOOK:
		    	//Transition of ringing->offhook are pickups of incoming calls.  Nothing done on them
		    	if(lastState != TelephonyManager.CALL_STATE_RINGING){
	                isIncoming = false;
			    	callStartTime = new Date();
			    	onOutgoingCallStarted(context, savedNumber, callStartTime);			    		
		    	}
		        break;
		    case TelephonyManager.CALL_STATE_IDLE:
		    	//Went to idle-  this is the end of a call.  What type depends on previous state(s)
		    	if(lastState == TelephonyManager.CALL_STATE_RINGING){
		    		//Ring but no pickup-  a miss
		    		onMissedCall(context, savedNumber, callStartTime);
		    	}
		    	else if(isIncoming){
		    		onIncomingCallEnded(context, savedNumber, callStartTime, new Date());			    		
		    	}
		    	else{
		    		onOutgoingCallEnded(context, savedNumber, callStartTime, new Date());			    					    		
		    	}
		        break;
	    }
	    lastState = state;
	}
}

This Post Has Been Viewed 9,650 Times