Call recording/processing service! - Android

Piotr Sobecki picture Piotr Sobecki · Nov 28, 2011 · Viewed 12.8k times · Source

Hello,
Im working on a solution for Android that will record calls (both out and incomming) and will further process recorded data (at end point of my application, no audio-file data will be hold on phone memory). I have implemented BroadcastReceiver with PhoneStateListener.LISTEN_CALL_STATE that starts a recording service if state is CALL_STATE_OFFHOOK. Then in service I start a new thread that attempts recording a call, and another BroadcastReceiver with PhoneStateListener.LISTEN_CALL_STATE, that calls a method to stop recording if phone state is changed to TelephonyManager.CALL_STATE_IDLE.

The audio file created is empty. Exception is thrown when recorder.stop() method in my service is called. Where is an error? What could i do better?

First (standalone) BroadcastReceiver:

    package com.piotr.callerrecogniser;

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

public class CallReceiver extends BroadcastReceiver {
    @Override
    public void onReceive(Context context, Intent intent) {
        MyPhoneStateListener phoneListener = new MyPhoneStateListener(context);
        TelephonyManager telephony = (TelephonyManager) context
                .getSystemService(Context.TELEPHONY_SERVICE);
        telephony.listen(phoneListener, PhoneStateListener.LISTEN_CALL_STATE);
    }
    class MyPhoneStateListener extends PhoneStateListener {
        public static final String tag = "CallerRecogniser - CallReceiver";
        private Context context;
        MyPhoneStateListener(Context c) {
            super();
            context = c;
        }
        public static final int NOTIFICATION_ID_RECEIVED = 0x1221;
        @Override
        public void onCallStateChanged(int state, String incomingNumber) {
            SharedPreferences preferences = context.getSharedPreferences("CallReceiver", Context.MODE_PRIVATE);
            switch (state) {
            case TelephonyManager.CALL_STATE_IDLE:
                break;
                //If call is answered, run recording service. Also pass "phone_number" variable with incomingNumber to shared prefs, so service will be able to access that via shared prefs.
            case TelephonyManager.CALL_STATE_OFFHOOK: // The call is answered
                String phone_number = preferences.getString("phone_number",null);
                 Intent serv = new Intent(context,
                 CallRecordingService.class);
                 serv.putExtra("number", phone_number);
                 context.startService(serv);
                break;          
                //If phone is ringing, save phone_number. This is done because incomingNumber is not saved on CALL_STATE_OFFHOOK
            case TelephonyManager.CALL_STATE_RINGING:
                SharedPreferences.Editor editor = preferences.edit();
                editor.putString("phone_number", incomingNumber);
                editor.commit();
                break;
            }
        }

    }
}

Service:

package com.piotr.callerrecogniser;

import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.media.MediaRecorder;
import android.os.Environment;
import android.os.Handler;
import android.os.IBinder;
import android.os.Looper;
import android.telephony.PhoneStateListener;
import android.telephony.TelephonyManager;

public class CallRecordingService extends Service implements Runnable {
    CallerRecogniserDB database = new CallerRecogniserDB(this);

    String phoneNumber;
    MediaRecorder recorder;
    private final Handler mHandler = new Handler();
    private final BroadcastReceiver myCallStateReceiver = new BroadcastReceiver() {
        @Override
        public void onReceive(Context context, Intent intent) {
            MyPhoneStateListener phoneListener = new MyPhoneStateListener(
                    context);
            TelephonyManager telephony = (TelephonyManager) context
                    .getSystemService(Context.TELEPHONY_SERVICE);
            telephony.listen(phoneListener,
                    PhoneStateListener.LISTEN_CALL_STATE);
        }

        class MyPhoneStateListener extends PhoneStateListener {
            private Context context;

            MyPhoneStateListener(Context c) {
                super();
                context = c;
            }

            @Override
            public void onCallStateChanged(int state, String incomingNumber) {
                switch (state) {
                case TelephonyManager.CALL_STATE_IDLE:
                    if (recording) {
                        NotificationManager mNotificationManager = (NotificationManager) context
                                .getSystemService(Context.NOTIFICATION_SERVICE);
                        Notification not;

                        not = new Notification(R.drawable.ic_launcher,
                                "Phone call being processed",
                                System.currentTimeMillis());
                        Intent notIntent = new Intent();
                        PendingIntent contentIntent = PendingIntent
                                .getActivity(context, 0, notIntent, 0);
                        not.setLatestEventInfo(context, "CallRecordingService",
                                "Notification from idle",
                                contentIntent);
                        mNotificationManager.notify(NOTIFICATION_ID_RECEIVED,
                                not);

                        stopRecording();
                    }
                    break;
                }
            }

        }
    };

    private static boolean recording = false;

    private String INCOMING_CALL_ACTION = "android.intent.action.PHONE_STATE";

    @Override
    public void onCreate() {
        // TODO Auto-generated method stub
        super.onCreate();

        IntentFilter intentToReceiveFilter = new IntentFilter();
        intentToReceiveFilter.addAction(INCOMING_CALL_ACTION);
        this.registerReceiver(myCallStateReceiver, intentToReceiveFilter, null,
                mHandler);

        Thread aThread = new Thread(this);
        aThread.start();

    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        // TODO Auto-generated method stub
        super.onStart(intent, startId);
        phoneNumber = intent.getExtras().getString("number");

        return START_STICKY;
    }

    @Override
    public IBinder onBind(Intent arg0) {
        // TODO Auto-generated method stub
        return null;
    }

    public static final int NOTIFICATION_ID_RECEIVED = 0x1221;

    @Override
    public void run() {
        Looper.myLooper();
        Looper.prepare();
        // TODO Auto-generated method stub


        database.open();
        String[] numbers = database.getData(CallerRecogniserDB.KEY_NUMBER);
        int index = -1;

        outside_for: for (int i = 0; i < numbers.length; i++) {
            String[] splitted = numbers[i].split(",");
            for (String nu : splitted) {
                if (nu.equals(phoneNumber)) {
                    index = i;
                    break outside_for;
                }
            }
        }

        database.close();

        if (index >= 0) { // Phone number is in a database and it's state==0 =>
                            // it has no data recorded
            // Notification that call is recorded

            recorder = new MediaRecorder();
            recorder.setAudioSource(MediaRecorder.AudioSource.VOICE_CALL);
            recorder.setOutputFormat(MediaRecorder.OutputFormat.THREE_GPP);
            recorder.setAudioEncoder(MediaRecorder.AudioEncoder.AMR_NB);

            recorder.setOutputFile(Environment.getExternalStorageDirectory()
                    .getAbsolutePath() + "/" + phoneNumber + ".3gp");
            try {
                recorder.prepare();
            } catch (Exception e) {
                e.printStackTrace();
            }
            recording = true;
            recorder.start();


        } 
    }

    void stopRecording() {

        // Then clean up with when it hangs up:
        recorder.stop();
        recorder.release();
        recording=false;
    }
}

Permissions:

<uses-permission android:name="android.permission.READ_CONTACTS" />
    <uses-permission android:name="android.permission.READ_PHONE_STATE" />
    <uses-permission android:name="android.permission.RECORD_AUDIO"/>
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />

Exception output:

11-28 14:28:44.385: E/MediaRecorder(22438): stop called in an invalid state: 8
11-28 14:28:44.400: D/AndroidRuntime(22438): Shutting down VM
11-28 14:28:44.425: W/dalvikvm(22438): threadid=1: thread exiting with uncaught exception (group=0x4001e578)
11-28 14:28:44.435: E/AndroidRuntime(22438): FATAL EXCEPTION: main
11-28 14:28:44.435: E/AndroidRuntime(22438): java.lang.IllegalStateException
11-28 14:28:44.435: E/AndroidRuntime(22438):    at android.media.MediaRecorder.stop(Native Method)
11-28 14:28:44.435: E/AndroidRuntime(22438):    at com.piotr.callerrecogniser.CallRecordingService.stopRecording(CallRecordingService.java:159)
11-28 14:28:44.435: E/AndroidRuntime(22438):    at com.piotr.callerrecogniser.CallRecordingService$1$MyPhoneStateListener.onCallStateChanged(CallRecordingService.java:65)
11-28 14:28:44.435: E/AndroidRuntime(22438):    at android.telephony.PhoneStateListener$2.handleMessage(PhoneStateListener.java:369)
11-28 14:28:44.435: E/AndroidRuntime(22438):    at android.os.Handler.dispatchMessage(Handler.java:99)
11-28 14:28:44.435: E/AndroidRuntime(22438):    at android.os.Looper.loop(Looper.java:123)
11-28 14:28:44.435: E/AndroidRuntime(22438):    at android.app.ActivityThread.main(ActivityThread.java:3691)
11-28 14:28:44.435: E/AndroidRuntime(22438):    at java.lang.reflect.Method.invokeNative(Native Method)
11-28 14:28:44.435: E/AndroidRuntime(22438):    at java.lang.reflect.Method.invoke(Method.java:507)
11-28 14:28:44.435: E/AndroidRuntime(22438):    at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:847)
11-28 14:28:44.435: E/AndroidRuntime(22438):    at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:605)
11-28 14:28:44.435: E/AndroidRuntime(22438):    at dalvik.system.NativeStart.main(Native Method)

Answer

Eugene picture Eugene · Jan 11, 2012

I've test your code with small modifications on my Samsung Galaxy S2. It woks pretty good without exceptions. Call was recorded successfully using the service. The one issue that i found is that service was started twice (Broadcast Receiver was started twice and TelephonyManager.PhoneStateListner called twice with state OFFHOOK) I have to change constant in the method setAudioSource from VOICE_CALL to VOICE_UPLINK. It looks like Java enum definition and enum in C header are different. Call was recorded for both sides (UPLINK and DOWNLINK)