Monday, June 2, 2014

How to create PhoneStateReceiver get CLI and call events

During first 5 years of my career I luckily worked at software houses that created computer telephony related software, I had used Microsoft TAPI and Dialogic APIs to create IVR and Operator Console and Call Recorder software. When I first started Android programming in 2010, I was very curious about phone states and CLI. 





I later figured it out and came to know that a BroadcastReceiver will be the right thing to use in
this case. BroadcastReceiver is a specially designed component offered by Android OS, and
it is used to handle various broadcasts sent by apps or OS components.
The broadcasts related to telephone call states are sent by TelephonyManager which is a
component of Android OS.
The phone state receiver given below can be used in scenarios like call blocking, call tracking,
and sending information to http or cloud based servers.
This salient features of my phone state receiver are given below
  1. Get the CLI from an incoming call
  2. Get the dialled number from an outgoing call
  3. When an inbound call is connected, get informed
  4. When a call is disconnected, get notified









public class PhoneStateReceiver extends BroadcastReceiver {
public enum CallType {INCOMING, OUTGOING, NA}
private static String lastNumber = "";
private static int lastCallState = 0;
/**
* This code is provided by Naeem Akram, 
* without any warranty guarantee or liability
* This code is meant for the good of developer community
* Please don't delete this comment when reusing 
* this code, and if possible give proper
* credit to the developer.
* In case you want to hire Naeem, go to following URL:
* https://www.odesk.com/users/~012d73aa92fad47188
*/
@Override
public void onReceive(final Context context, Intent intent) {

// Check phone state
String strPhoneState = "", strNumber = "";
CallType ctVal = CallType.NA;
int callState = 0;  
TelephonyManager tm = null;

try{
tm = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
strPhoneState = intent
.getStringExtra(TelephonyManager.EXTRA_STATE);
callState = tm.getCallState();

Log.d(Keys.LOGTAG, "CS: " + callState + 
" - Last CS: " + lastCallState +" - Phone State: " + strPhoneState);

if(strPhoneState != null)
{
if(!strPhoneState.equals(TelephonyManager.EXTRA_STATE_IDLE))
// don't post here if phone state is idle, it will get posted later.
{
postPhoneStateEvent(context, strPhoneState);
}
}else{
strPhoneState = "";
}

Log.d(Keys.LOGTAG, "INF: Broadcast received; " + strPhoneState);

if(callState == TelephonyManager.CALL_STATE_IDLE){
// the call just ended, it might be incoming or outgoing
Log.d(Keys.LOGTAG, "CALL STATE: " + callState);    
if(intent.hasExtra(TelephonyManager.EXTRA_INCOMING_NUMBER))
{// this is an incoming call
ctVal = CallType.INCOMING;
strNumber = intent.getStringExtra(
 TelephonyManager.EXTRA_INCOMING_NUMBER).trim();
}
else if(intent.hasExtra(Intent.EXTRA_PHONE_NUMBER))
{// this is an outgoing call
ctVal = CallType.OUTGOING;
strNumber = intent.getStringExtra(Intent.EXTRA_PHONE_NUMBER);
}
else if(lastNumber.length() > 0)
{
ctVal = CallType.INCOMING;
strNumber = lastNumber;
lastNumber = "";
}

if(strPhoneState != null){
// maybe save phone state to DB, or send it to a server 
postPhoneStateEvent(context, strPhoneState);
}

if(ctVal == CallType.OUTGOING){
// maybe save the number of called party, 
// or send the info to a server over http
postCLIEvent(context, ctVal, strNumber);
lastNumber = "";
strNumber = "";
}

if(lastCallState == CALL_STATE_CONNECTED || 
 lastCallState == TelephonyManager.CALL_STATE_OFFHOOK){
 postPhoneStateEvent(context, "DISCONNECTED");
}
if(lastCallState == TelephonyManager.CALL_STATE_RINGING){
postPhoneStateEvent(context, "MISSED");
}
}// if callState == CALL_STATE_IDLE
else if(
callState == TelephonyManager.CALL_STATE_OFFHOOK)
{// until so far we can only figure out 
// when an incoming call gets connected
if(lastCallState == TelephonyManager.CALL_STATE_RINGING){
// offhook right after ringing means we 
// attended the call
Log.d(Keys.LOGTAG, "INCOMING CALL CONNECTED");
postPhoneStateEvent(context, "CONNECTED " + lastNumber);
// tell a server that a call is
// connected over http or socket
}
}
else{
// callstate != 0 zero means call state is idle
if(intent.hasExtra
(TelephonyManager.EXTRA_INCOMING_NUMBER))
{// this is an incoming call     
lastNumber = intent.getStringExtra
 (TelephonyManager.EXTRA_INCOMING_NUMBER).trim();
Log.d(Keys.LOGTAG, "Ringing INCOMING event.");
postPhoneStateEvent(context, CallType.INCOMING + " : " + lastNumber);
}
else if(intent.hasExtra
(Intent.EXTRA_PHONE_NUMBER)){
// this is an outgoing call
lastNumber = intent.getStringExtra(Intent.EXTRA_PHONE_NUMBER);
postCLIEvent(context, CallType.OUTGOING, lastNumber);
}    
Log.d(Keys.LOGTAG, "Last Number: " + lastNumber);
}
}catch(Exception excp){
excp.printStackTrace();
} 
lastCallState = callState;
// local callState is OFFHOOK our custom 
CONNECTED val gets messed
}
private void postPhoneStateEvent(final Context context, 
 String strPhoneState) {
if(strPhoneState != null){
// POST THE PHONE STATE SOMEWHERE SOMEHOW
}
}
private void postCLIEvent(Context _context, 
 CallType _callType, String _cli){
// POST THE CLI SOMEWHERE SOMEHOW
}
}

You will need following permissions in order to sue the phone state receiver
:
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
<uses-permission android:name="android.permission.PROCESS_OUTGOING_CALLS" />

We must add every app component to our Manifest.xml file, and our phone state broadcast receiver will be added like this:

<receiver
 android:name="com.usra.ca.PhoneStateReceiver"
    android:enabled="true" >
    <intent-filter android:priority="1" >
    <action android:name="android.intent.action.PHONE_STATE"
    <action android:name="android.intent.action.NEW_OUTGOING_CALL"/>
    <action android:name="android.intent.action.ANSWER"/>
    </intent-filter>
</receiver>

Android has got only three phone states
  • IDLE
  • RINGING
  • OFFHOOK

Please feel free to comment on the post, and share ideas in general.

I'm working as a full time freelance programmer through ODesk.com, I have a strong profile over there. Feel free to ping me if you need an Android app. Elance widget is given on top right of the page, for ODesk follow the link given below:


https://www.odesk.com/users/~012d73aa92fad47188

8 comments:

  1. Thanks for this code , it works fine.. But How can i detect radio link failure during comm unication , i mean how to detect call drop event in my app?

    ReplyDelete
  2. You are welcome, I am not sure how that will happen. If the call is fully disconnected/dropped then an IDLE event will be shown in phone state receiver. If you will be keeping track of previous phone states, you can check whether previous phone state was OFFHOOK or RINGING in which cases you will know that the call was dropped after either an outgoing call or on incoming call.
    It gets complicated I know, but that's the way it is... Google does not want people to mess with its operating system :)

    ReplyDelete
  3. Hey Naeem, the statement "Log.d(Keys.LOGTAG, "Ringing INCOMING event.");" shows an error: "cannot resolve symbol Keys". Is it a local variable you created?

    ReplyDelete
  4. "LOGTAG" is a public constant defined in class "Keys". I try to use a separate class for frequently used strings and variables,

    ReplyDelete
    Replies
    1. I'm still learning and get "Multiple markers at this line
      - Log cannot be resolved
      - Keys cannot be resolved to a
      variable"
      How do I define Log, Keys and LOGTAG in this program?

      Delete
    2. I also get "Executors cannot be resolved" and "Executor cannot be resolved
      to a type" - how do I get this working?

      Delete
    3. I found "import android.util.Log;" solved the Log error

      Delete
  5. Keys is a separate class it contains a static final string named LOGTAG, you can set value to anything you like.
    You will need to import package "java.util.concurrent.Executors" in order to resolve the executors error.

    ReplyDelete

Feel free to talk back...