Mobile
Article
By Rodrigo Cericatto

Phone callbacks in Android Using TelephonyManager

By Rodrigo Cericatto

If you are an Android Developer and you need to detect if your phone is in idle state, off-hook (receiving state) or ringing state, this tutorial was created for you. The objective here is to show you how to implement phone callbacks in Android, allowing detection of calls to your phone.

About Telephony Manager

In this topic, the most important class that will be talked about is the TelephonyManager. This class uses a listener called PhoneStateListener to listen for telephony service updates.

An object of type TelephonyManager will be instantiated, and it will listen for updates of Context.TELEPHONY_SERVICE. In order to monitor updates of telephony states like PhoneStateListener.LISTEN_DATA_CONNECTION_STATE, PhoneStateListener.LISTEN_CELL_INFO and others, a class called PhoneCallback will be created, extending from PhoneStateListener.

With TelephonyManager the telephony services of a device can be accessed and with the method TelephonyManager.listen(PhoneStateListener listener, int events) all information the telephone is receiving can be monitored.

The class PhoneStateListener receives callbacks when some event of TELEPHONY_SERVICE is observed. In this tutorial, the flag PhoneStateListener.LISTEN_CALL_STATE will be used. It will monitor and listen to the call state of the phone.

As can be read in the PhoneStateListener documentation (see section “Further Reading” below), there are other constants that can be set into the argument int events of the method TelephonyManager.listen. For instance: LISTEN_SIGNAL_STRENGTHS listens for changes to the network signal strengths (cellular). But in this tutorial, the flag LISTEN_CALL_STATE will be used, which listens for changes to the device call state.

Source code!

To show how these callbacks work an Android Application will be created. It will have a main Activity, a custom class to encapsulate the PhoneStateListener callback, and a class to manage the runtime permissions needed to be declared in Android version above 6.0 (Marshmallow).

First of all, it is necessary to declare the permissions to access the telephone state. Which ones will be used by this application? To know this, it is necessary to look at the class PhoneStateListener. The source code of this class is shown below (for API 25).

package android.telephony; 

import android.telephony.CellInfo; 
import android.telephony.CellLocation;
import android.telephony.ServiceState;
import android.telephony.SignalStrength;
import java.util.List;

public class PhoneStateListener {
    public static final int LISTEN_CALL_FORWARDING_INDICATOR = 8;
    public static final int LISTEN_CALL_STATE = 32;
    public static final int LISTEN_CELL_INFO = 1024;
    public static final int LISTEN_CELL_LOCATION = 16;
    public static final int LISTEN_DATA_ACTIVITY = 128;
    public static final int LISTEN_DATA_CONNECTION_STATE = 64;
    public static final int LISTEN_MESSAGE_WAITING_INDICATOR = 4;
    public static final int LISTEN_NONE = 0;
    public static final int LISTEN_SERVICE_STATE = 1;
    /** @deprecated */
    @Deprecated
    public static final int LISTEN_SIGNAL_STRENGTH = 2;
    public static final int LISTEN_SIGNAL_STRENGTHS = 256;

    public PhoneStateListener() {
        throw new RuntimeException("Stub!");
    }

    public void onServiceStateChanged(ServiceState serviceState) {
        throw new RuntimeException("Stub!");
    }

    /** @deprecated */
    @Deprecated
    public void onSignalStrengthChanged(int asu) {
        throw new RuntimeException("Stub!");
    }

    public void onMessageWaitingIndicatorChanged(boolean mwi) {
        throw new RuntimeException("Stub!");
    }

    public void onCallForwardingIndicatorChanged(boolean cfi) {
        throw new RuntimeException("Stub!");
    }

    public void onCellLocationChanged(CellLocation location) {
        throw new RuntimeException("Stub!");
    }

    public void onCallStateChanged(int state, String incomingNumber) {
        throw new RuntimeException("Stub!");
    }

    public void onDataConnectionStateChanged(int state) {
        throw new RuntimeException("Stub!");
    }

    public void onDataConnectionStateChanged(int state, int networkType) {
        throw new RuntimeException("Stub!");
    }

    public void onDataActivity(int direction) {
        throw new RuntimeException("Stub!");
    }

    public void onSignalStrengthsChanged(SignalStrength signalStrength) {
        throw new RuntimeException("Stub!");
    }

    public void onCellInfoChanged(List<CellInfo> cellInfo) {
        throw new RuntimeException("Stub!");
    }
}

Creating a custom class extending PhoneStateListener, some methods can be overridden. For instance:

  • public void onCallStateChanged(int state, String incomingNumber) -> Needs READ_PHONE_STATE permission.
  • public void onCellLocationChanged(CellLocation location) -> Needs ACCESS_COARSE_LOCATION permission.
  • public void onCallForwardingIndicatorChanged(boolean cfi) -> Needs READ_PHONE_STATE permission.
  • public void onMessageWaitingIndicatorChanged(boolean mwi) -> Needs READ_PHONE_STATE permission.

Permissions

Above 2 permissions can be seen: READ_PHONE_STATE and ACCESS_COARSE_LOCATION. They will be declared in the AndroidManifest.xml file.

Note above the static permissions to be used in the app. But with API 23 (version 6.0 – Marshmallow), a new permissions scheme was created in Android. Now permissions must be checked at runtime. To do that a class will be created to encapsulate the runtime permissions needed. This class will be called PermissionUtils.

This class implements runtime checking of the two permissions declared at the file AndroidManifest.xml, at methods canAccessCoarseLocation() and canReadPhoneState() (using the method hasPermission() inside of them). Also, method alertAndFinish() closes the application if the user refuses to allow permissions for the application to run at runtime.

The image below shows how those permission are checked at runtime.

permission_request

Custom PhoneStateListener class

In this class, all the magic happens. As said before, by extending the class PhoneStateListener, some methods can be overridden.

To show the content of each callback, we will use the class Log.

public class PhoneCallback extends PhoneStateListener {

    //--------------------------------------------------
    // Constants
    //--------------------------------------------------

    public static final String LOG_TAG = "PhoneCallback";

    //--------------------------------------------------
    // Attributes
    //--------------------------------------------------

    private final TextView mTextView;

    //--------------------------------------------------
    // Constructor
    //--------------------------------------------------

    public PhoneCallback(TextView textView) {
        mTextView = textView;
    }

    //--------------------------------------------------
    // Methods
    //--------------------------------------------------

    private String serviceStateToString(int serviceState) {
        switch (serviceState) {
            case ServiceState.STATE_IN_SERVICE:
                return "STATE_IN_SERVICE";
            case ServiceState.STATE_OUT_OF_SERVICE:
                return "STATE_OUT_OF_SERVICE";
            case ServiceState.STATE_EMERGENCY_ONLY:
                return "STATE_EMERGENCY_ONLY";
            case ServiceState.STATE_POWER_OFF:
                return "STATE_POWER_OFF";
            default:
                return "UNKNOWN_STATE";
        }
    }

    private String callStateToString(int state) {
        switch (state) {
            case TelephonyManager.CALL_STATE_IDLE:
                return "\nonCallStateChanged: CALL_STATE_IDLE, ";
            case TelephonyManager.CALL_STATE_RINGING:
                return "\nonCallStateChanged: CALL_STATE_RINGING, ";
            case TelephonyManager.CALL_STATE_OFFHOOK:
                return "\nonCallStateChanged: CALL_STATE_OFFHOOK, ";
            default:
                return "\nUNKNOWN_STATE: " + state + ", ";
        }
    }

    //--------------------------------------------------
    // PhoneStateListener
    //--------------------------------------------------

    @Override
    public void onCellInfoChanged(List<CellInfo> cellInfo) {
        super.onCellInfoChanged(cellInfo);
        Log.i(LOG_TAG, "onCellInfoChanged: " + cellInfo);
    }

    @Override
    public void onDataActivity(int direction) {
        super.onDataActivity(direction);
        switch (direction) {
            case TelephonyManager.DATA_ACTIVITY_NONE:
                Log.i(LOG_TAG, "onDataActivity: DATA_ACTIVITY_NONE");
                break;
            case TelephonyManager.DATA_ACTIVITY_IN:
                Log.i(LOG_TAG, "onDataActivity: DATA_ACTIVITY_IN");
                break;
            case TelephonyManager.DATA_ACTIVITY_OUT:
                Log.i(LOG_TAG, "onDataActivity: DATA_ACTIVITY_OUT");
                break;
            case TelephonyManager.DATA_ACTIVITY_INOUT:
                Log.i(LOG_TAG, "onDataActivity: DATA_ACTIVITY_INOUT");
                break;
            case TelephonyManager.DATA_ACTIVITY_DORMANT:
                Log.i(LOG_TAG, "onDataActivity: DATA_ACTIVITY_DORMANT");
                break;
            default:
                Log.w(LOG_TAG, "onDataActivity: UNKNOWN " + direction);
                break;
        }
    }

    @Override
    public void onServiceStateChanged(ServiceState serviceState) {
        super.onServiceStateChanged(serviceState);
        String message = "onServiceStateChanged: " + serviceState + "\n";
        message += "onServiceStateChanged: getOperatorAlphaLong " + serviceState.getOperatorAlphaLong() + "\n";
        message += "onServiceStateChanged: getOperatorAlphaShort " + serviceState.getOperatorAlphaShort() + "\n";
        message += "onServiceStateChanged: getOperatorNumeric " + serviceState.getOperatorNumeric() + "\n";
        message += "onServiceStateChanged: getIsManualSelection " + serviceState.getIsManualSelection() + "\n";
        message += "onServiceStateChanged: getRoaming " + serviceState.getRoaming() + "\n";
        message += "onServiceStateChanged: " + serviceStateToString(serviceState.getState());
        Log.i(LOG_TAG, message);
    }

    @Override
    public void onCallStateChanged(int state, String incomingNumber) {
        super.onCallStateChanged(state, incomingNumber);
        callStateToString(state);
        String message = callStateToString(state) + "incomingNumber: " + incomingNumber;
        mTextView.setText(message);
    }

    @Override
    public void onCellLocationChanged(CellLocation location) {
        super.onCellLocationChanged(location);
        String message = "";
        if (location instanceof GsmCellLocation) {
            GsmCellLocation gcLoc = (GsmCellLocation) location;
            message += "onCellLocationChanged: GsmCellLocation " + gcLoc + "\n";
            message += "onCellLocationChanged: GsmCellLocation getCid " + gcLoc.getCid() + "\n";
            message += "onCellLocationChanged: GsmCellLocation getLac " + gcLoc.getLac() + "\n";
            message += "onCellLocationChanged: GsmCellLocation getPsc" + gcLoc.getPsc(); // Requires min API 9
            Log.i(LOG_TAG, message);
        } else if (location instanceof CdmaCellLocation) {
            CdmaCellLocation ccLoc = (CdmaCellLocation) location;
            message += "onCellLocationChanged: CdmaCellLocation " + ccLoc + "\n";;
            message += "onCellLocationChanged: CdmaCellLocation getBaseStationId " + ccLoc.getBaseStationId() + "\n";;
            message += "onCellLocationChanged: CdmaCellLocation getBaseStationLatitude " + ccLoc.getBaseStationLatitude() + "\n";;
            message += "onCellLocationChanged: CdmaCellLocation getBaseStationLongitude" + ccLoc.getBaseStationLongitude() + "\n";;
            message += "onCellLocationChanged: CdmaCellLocation getNetworkId " + ccLoc.getNetworkId() + "\n";;
            message += "onCellLocationChanged: CdmaCellLocation getSystemId " + ccLoc.getSystemId();
            Log.i(LOG_TAG, message);
        } else {
            Log.i(LOG_TAG, "onCellLocationChanged: " + location);
        }
    }

    @Override
    public void onCallForwardingIndicatorChanged(boolean changed) {
        super.onCallForwardingIndicatorChanged(changed);
    }

    @Override
    public void onMessageWaitingIndicatorChanged(boolean changed) {
        super.onMessageWaitingIndicatorChanged(changed);
    }
}

The most important method implemented in this class is the method onCallStateChanged(). This method listens to the call state changes. For instance, when the phone starts ringing, call is put on hold or a call is ended. In this method is possible to detect the following states: TelephonyManager.CALL_STATE_IDLE, TelephonyManager.CALL_STATE_RINGING or TelephonyManager.CALL_STATE_OFFHOOK. Those are the states that can provide information about what is happening with the phone state.

MainActivity class

With permissions to read the phone state and with a custom class to listen to the phone states, the source code now just needs to call them together. This will be done at the MainActivity class.

public class MainActivity extends AppCompatActivity {

    //--------------------------------------------------
    // Constants
    //--------------------------------------------------

    private static final String[] PERMISSIONS = { Manifest.permission.READ_PHONE_STATE, Manifest.permission.ACCESS_COARSE_LOCATION };
    private static final int PERMISSION_REQUEST = 100;

    //--------------------------------------------------
    // Attributes
    //--------------------------------------------------

    private TelephonyManager mTelephonyManager;

    //--------------------------------------------------
    // Activity Life Cycle
    //--------------------------------------------------

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        checkPermissions();
    }

    //--------------------------------------------------
    // Permissions
    //--------------------------------------------------

    @Override
    public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
        switch (requestCode) {
            case PERMISSION_REQUEST: {
                isPermissionGranted(grantResults);
                return;
            }
        }
    }

    private void isPermissionGranted(int[] grantResults) {
        if (grantResults.length > 0) {
            Boolean permissionGranted = grantResults[0] == PackageManager.PERMISSION_GRANTED;
            if (permissionGranted) {
                callPhoneManager();
            } else {
                PermissionUtils.alertAndFinish(this);
            }
        }
    }

    private void checkPermissions() {
        // Checks the Android version of the device.
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
            Boolean canWriteExternalStorage = PermissionUtils.canReadPhoneState(this);
            Boolean canReadExternalStorage = PermissionUtils.canAccessCoarseLocation(this);
            if (!canWriteExternalStorage || !canReadExternalStorage) {
                requestPermissions(PERMISSIONS, PERMISSION_REQUEST);
            } else {
                // Permission was granted.
                callPhoneManager();
            }
        } else {
            // Version is below Marshmallow.
            callPhoneManager();
        }
    }

    private void callPhoneManager() {
        TextView textView = (TextView)findViewById(R.id.id_text_view);
        mTelephonyManager = (TelephonyManager) getSystemService(TELEPHONY_SERVICE);
        mTelephonyManager.listen(new PhoneCallback(textView), PhoneStateListener.LISTEN_CALL_STATE
            | PhoneStateListener.LISTEN_CELL_INFO // Requires API 17
            | PhoneStateListener.LISTEN_CELL_LOCATION
            | PhoneStateListener.LISTEN_DATA_ACTIVITY
            | PhoneStateListener.LISTEN_DATA_CONNECTION_STATE
            | PhoneStateListener.LISTEN_SERVICE_STATE
            | PhoneStateListener.LISTEN_SIGNAL_STRENGTHS
            | PhoneStateListener.LISTEN_CALL_FORWARDING_INDICATOR
            | PhoneStateListener.LISTEN_MESSAGE_WAITING_INDICATOR);
    }
}

In the onCreate() method, the method checkPermissions() will be called. In this method the permissions Manifest.permission.ACCESS_COARSE_LOCATION and Manifest.permission.READ_PHONE_STATE will be checked.

In the first execution of the app, if the app doesn’t have access to access those permissions, the method requestPermissions(PERMISSIONS, PERMISSION_REQUEST) will be called. If the app has access to those permissions, then the callPhoneManager() will be called.

The method requestPermissions(PERMISSIONS, PERMISSION_REQUEST) redirects the control flow to the onRequestPermissionsResult() method. This method will show a Dialog to the user, asking them to allow the app to access the requested permissions. If the user allows the permissions, the method callPhoneManager() is called. If not, PermissionUtils.alertAndFinish() will be called. This method shows a message for the user (telling the user that he must accept the permissions) and closes the app.

All this flow happens the first time the app is executed. But what happens the second time? If the user allowed permissions the first time, the permissions check won’t be done in the second time and the method callPhoneManager() will be called. If it wasn’t allowed, then this check won’t be done either, and PermissionUtils.alertAndFinish() will be called. If the user didn’t choose any of those options in the first execution, then the Dialog requesting permissions will be shown again.

Last but not least, if the user has allowed permissions, the method callPhoneManager() is called. This is the method that calls our phone state callback.

private void callPhoneManager() {
        TextView textView = (TextView)findViewById(R.id.id_text_view);
        mTelephonyManager = (TelephonyManager) getSystemService(TELEPHONY_SERVICE);
        mTelephonyManager.listen(new PhoneCallback(textView), PhoneStateListener.LISTEN_CALL_STATE
            | PhoneStateListener.LISTEN_CELL_INFO // Requires API 17
            | PhoneStateListener.LISTEN_CELL_LOCATION
            | PhoneStateListener.LISTEN_DATA_ACTIVITY
            | PhoneStateListener.LISTEN_DATA_CONNECTION_STATE
            | PhoneStateListener.LISTEN_SERVICE_STATE
            | PhoneStateListener.LISTEN_SIGNAL_STRENGTHS
            | PhoneStateListener.LISTEN_CALL_FORWARDING_INDICATOR
            | PhoneStateListener.LISTEN_MESSAGE_WAITING_INDICATOR);
    }

In this method the TelephonyManager is accessed, calling the listen() method. See the constructor of this method:

listen_method

This method receives on its first parameter, a constructor with a TextView (that will be used to print the state of the phone). And on its second parameter, it receives all the events will be monitored by the phone call state.

Performing a call using the emulator

All the source code is ready. But not it is necessary to perform a call for the phone to activate the phone state callback.

This can be done calling the phone or simulating a call through the emulator. Let’s try to perform a call in the emulator:

To perform a call in the emulator:

1) Open the Android Virtual Devices

avd

2) Choose the Emulator

emulators

3) Open the Extended Controls

opening_extended_controls

Below there is an image of the Extended Controls tab opened.

extended_controls

4) Execute the app into the Emulator

executing_app

5) Perform the Call

making_phone_call

Below there are the log messages after the call ends:

call_ended

The log messages show that, by default, the phone state is in IDLE state. Then, when the call is made, the state changes to the RINGING state.

After the call ends, the OFF_HOOK state is active. And in all those states, the incoming number is shown.

Conclusion

This tutorial showed how to create an application to monitor the call state of a phone in Android. The basic concepts about phone call listeners were explained, as well the runtime permissions to monitor those listeners callbacks. The app source code was created part to part, and in the end, we saw how a call simulation can be done.

And that is all!

For more Android articles, take a look at the SitePoint website.

Further Reading

More:
Recommended
Sponsors
Get the latest in Mobile, once a week, for free.