Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Detect when the outgoing call starts playing ringback tone

I'm trying to detect the state of an outgoing call when it starts playing ringback tone. I have tried various approaches for detecting this state. Here are some of them:

1. Using PhoneStateListener:

(Cannot detect when outgoing call is answered in Android)

import android.telephony.PhoneStateListener;
import android.telephony.TelephonyManager;
import android.util.Log;

public class CustomPhoneStateListener extends PhoneStateListener {

    public void onCallStateChanged(int state, String num) {
        Log.d(CallStatusPlugin.TAG, ">>>state changed" + state);
    }
}

But states like TelephonyManager.CALL_STATE_IDLE, TelephonyManager.CALL_STATE_OFFHOOK doesn't give us those states.

2. Using READ_PRECISE_PHONE_STATE:

Adding the method to the above same phone state listener

public void onPreciseCallStateChanged() {
    Log.d(CallStatusPlugin.TAG, "onPreciseCallStateChanged");
}

But as per my research, reading the precise state requires the app must be a system app.

3. Using NotificationListener:

public class CustomNotificationListener extends NotificationListenerService {

    public static final String TAG = "CallStatusPlugin";

    public CustomNotificationListener() {
        Log.v(TAG, ">>> CustomNotificationListener");
    }

    public void onNotificationPosted(StatusBarNotification sbn) {
        Log.i(TAG, "New Notification");

        Bundle extras = sbn.getNotification().extras;

        if ("Ongoing call".equals(extras.getString(Notification.EXTRA_TEXT))) {
            Log.v(TAG, "outgoing call");
        } else if ("Dialing".equals(extras.getString(Notification.EXTRA_TEXT))) {
            Log.v(TAG, "dialling call");
        }
    }
}

But this doesn't help because the OS doesn't change the notification when the ringback tone starts playing for an outgoing call.

4. Using BroadcastReceiver:

public class CallBroadcastReceiver extends BroadcastReceiver {

    public static final String TAG = "CallStatusPlugin";

    public void onReceive(Context context, Intent intent) {
        String state = intent.getStringExtra(TelephonyManager.EXTRA_STATE);

        Log.i(TAG, "CallBroadcastReceiver state: " + state);

        // TelephonyManager.EXTRA_FOREGROUND_CALL_STATE = "foreground_state"
        Log.d(TAG, "new state >>>>" + intent.getIntExtra("foreground_state", -2));
    }
}

But didn't help either.

5. Using reflections:

I also tried getting the instance of default dialer using reflection without success:

    //com.samsung.android.incallui
    Reflections reflections = new Reflections("com.samsung.android.contacts", new SubTypesScanner(false));

    final ClassLoader classLoader = this.getClass().getClassLoader();
    ClassLoader[] loaders = { classLoader };

    ConfigurationBuilder configurationBuilder = (ConfigurationBuilder) reflections.getConfiguration();
    configurationBuilder.setClassLoaders(loaders);

    Log.d(TAG, "cl" + classLoader.toString());
    Set<Class<? extends Object>> allClasses = reflections.getSubTypesOf(Object.class);
    Log.d(TAG, "allclasses" + allClasses.toString());

I could not get any class (maybe I'm not using the reflection properly)

6. Using InCallService:

By replacing a default dialer (which I don't want to use) with the custom dialer to get the Call states.

import android.os.Bundle;
import android.telecom.Call;
import android.telecom.InCallService;
import android.util.Log;

public class CustomInCallService extends InCallService {

    public static final String TAG = "CallStatusPlugin";

    @Override
    public void onCallAdded(Call call) {
        Log.d(TAG, "onCallAdded: " + call.getState());

        call.registerCallback(
                new Call.Callback() {
                    @Override
                    public void onConnectionEvent (Call call, String event, Bundle extras) {
                        Log.d(TAG, "Call.Callback.onConnectionEvent: " + event + ", " + call.getState());
                    }

                    @Override
                    public void onStateChanged (Call call, int state) {
                        Log.d(TAG, "Call.Callback.onStateChanged: " + state + ", " + call.getState());
                    }
                }
        );
    }
}

The onStateChanged finally gives me something as:

Call State -> 9 (STATE_CONNECTING) -> 1 (STATE_DIALING) -> 4 (STATE_ACTIVE)(when answered) -> 7 (STATE_DISCONNECTING)

But the call state is also changed to STATE_DIALING when there is some problem with the outgoing call like the dialed number is unreachable or the phone is switched off. So this means we can't say DIALING state is the state where the outgoing call starts playing the ringback tone.

7. Using CallManager from reflection:

(Added 3rd Aug 2018)

public class OutCallLogger extends BroadcastReceiver {

    public static final String TAG = "CallStatusPlugin";

    public OutCallLogger() {
        Log.e(TAG, "\n\n\nOutCallLogger Instance Created\n\n\n");
    }

    @Override
    public void onReceive(Context context, Intent intent) {
        String state = intent.getStringExtra(TelephonyManager.EXTRA_STATE);
        Log.i(TAG, "OutCallLogger state: " + state);

        String number = intent.getStringExtra(Intent.EXTRA_PHONE_NUMBER);
        Log.i(TAG, "Outgoing Number: " + number);

        // TelephonyManager.EXTRA_FOREGROUND_CALL_STATE = "foreground_state"
        Log.d(TAG, "new state >>>>" + intent.getIntExtra("foreground_state", -2));
        Log.d(TAG, "new state >>>>" + intent.getIntExtra("ringing_state", -2));
        Log.d(TAG, "new state >>>>" + intent.getIntExtra("background_state", -2));
        Log.d(TAG, "new state >>>>" + intent.getIntExtra("disconnect_cause", -2));

        final ClassLoader classLoader = this.getClass().getClassLoader();

        try {
            Class<?> callManagerClass = classLoader.loadClass("com.android.internal.telephony.CallManager");
            Log.e(TAG, "CallManager: Class loaded " + callManagerClass.toString());

            Method[] methods = callManagerClass.getDeclaredMethods();
            for (Method m : methods) {
                Log.e(TAG, "Methods: " + m.getName());
            }

            Method getInstanceMethod = callManagerClass.getDeclaredMethod("getInstance");
            getInstanceMethod.setAccessible(true);
            Log.e(TAG, "CallManager: Method loaded " + getInstanceMethod.getName());

            Object callManagerObject = getInstanceMethod.invoke(null);
            Log.e(TAG, "CallManager: Object loaded " + callManagerObject.getClass().getName());

            Method getAllPhonesMethod = callManagerClass.getDeclaredMethod("getAllPhones");
            Log.e(TAG, "CallManager: Method loaded " + getAllPhonesMethod.getName());

            Method getForegroundCallsMethod = callManagerClass.getDeclaredMethod("getForegroundCalls");
            Log.e(TAG, "CallManager: Method loaded " + getForegroundCallsMethod.getName());
            List foregroundCalls = (List) getForegroundCallsMethod.invoke(callManagerObject);
            Log.e(TAG, "Foreground calls: " + foregroundCalls + ", " + foregroundCalls.size());

            Method getBackgroundCallsMethod = callManagerClass.getDeclaredMethod("getBackgroundCalls");
            Log.e(TAG, "CallManager: Method loaded " + getForegroundCallsMethod.getName());
            List backgroundCalls = (List) getBackgroundCallsMethod.invoke(callManagerObject);
            Log.e(TAG, "Background calls: " + backgroundCalls + ", " + backgroundCalls.size());

            Timer timer = new Timer();

            // keep printing all the for 20 seconds to check if we got one
            TimerTask doAsynchronousTask = new TimerTask() {
                long t0 = System.currentTimeMillis();

                @Override
                public void run() {
                    // cancel the timer after 20 seconds
                    if (System.currentTimeMillis() - t0 > 20 * 1000) {
                        cancel();
                        return;
                    }

                    try {
                        List phonesObject = (List) getAllPhonesMethod.invoke(callManagerObject);
                        Log.e(TAG, "All phones " + phonesObject + ", " + phonesObject.size());

                        List foregroundCalls = (List) getForegroundCallsMethod.invoke(callManagerObject);
                        Log.e(TAG, "Foreground calls: " + foregroundCalls + ", " + foregroundCalls.size());

                        Object backgroundCalls = getBackgroundCallsMethod.invoke(callManagerObject);
                        Log.e(TAG, "Background calls: " + backgroundCalls);

                        String state = intent.getStringExtra(TelephonyManager.EXTRA_STATE);
                        Log.i(TAG, "New state: " + state);
                    } catch (Exception e) {
                        Log.e(TAG, ">>>1. " + e.getMessage());
                    }
                }
            };

            timer.schedule(doAsynchronousTask, 0, 1000); //execute in every 1000 ms
        } catch (ClassNotFoundException e) {
            Log.e(TAG, ">>>2. " + e.getMessage());
        } catch (NoSuchMethodException e) {
            Log.e(TAG, ">>>3. " + e.getMessage());
        } catch (InvocationTargetException e) {
            Log.e(TAG, ">>>4. " + e.getMessage());
        } catch (IllegalAccessException e) {
            Log.e(TAG, ">>>5. " + e.getMessage());
        }
    }
}

But I'm getting all the background/foreground calls & phones as emty results:

08-03 15:19:22.638  2586  4636 E CallStatusPlugin: All phones [], 0
08-03 15:19:22.639  2586  4636 E CallStatusPlugin: Foreground calls: [], 0
08-03 15:19:22.639  2586  4636 E CallStatusPlugin: Background calls: []

Not sure if OS is using the CallManager or not.

After 2-3 weeks of research, I learned:

  1. Hidden API's are something which provides us the deeper integration with OS (but Google may remove access to hidden API in Android P)
  2. Ringing sound of an outgoing call == Ringback tone
  3. State dialing !== State of playing ringback tone
  4. But I didn't reach a conclusion where I can say that this is the way where we can get the state on which an outgoing call starts playing ringback tone

I kept debugging by reading all the OS logs by adb logcat '*:V' and saw that a log is being printed at the time when I hear the ringback tone (adb logcat -b system '*:V'):

07-31 13:34:13.487  3738 29960 I Telephony: AsyncConnectTonePlayer : play
07-31 13:34:13.784  3273  7999 D SSRM:p  : SIOP:: AP = 330, PST = 313 (W:26), BAT = 294, USB = 0, CHG = 0
07-31 13:34:13.902  3738 29960 I Telephony: AsyncConnectTonePlayer : onCompletion
07-31 13:34:14.304  3273 15438 D CustomFrequencyManagerService: releaseDVFSLockLocked : Getting Lock type frm List : DVFS_MIN_LIMIT  frequency : 1352000  uid : 1000  pid : 3273  tag : com.samsung.android.incallui@2
07-31 13:34:14.639  3273  7999 D AudioService: getStreamVolume 0 index 10
07-31 13:34:16.449  4175  4175 D io_stats: !@ 179,0 r 264349 9232882 w 465999 10111332 d 42634 3418440 f 235485 235449 iot 468730 459107 th 51200 0 0 pt 0 inp 0 0 104823.814

Then I started googling for something related to io stats events android & gsm connection events but I'm not able to figure out any way to get that particular state of an outgoing call where the call starts playing ringback tone.

I looked at various Android codes to find out some hints:

  1. TelephonyConnection
  2. GsmConnection

Any hints or direction where I should look to achieve this? Like GSM connection, ConnectionService, ConnectivityManager, ConnectionService or ConnectionRequest anything?

Edit 1:

Reading more logs led me to SipManager and I read about SIP Basic Call Flow Examples which I think, Android uses for making or receiving the calls.

07-31 16:29:55.335  4076  5081 I reSIProcate: INFO | RESIP:TRANSACTION | TuSelector.cxx:131 | Start add(alm)
07-31 16:29:55.335  4076  5081 I reSIProcate: INFO | RESIP:TRANSACTION | TuSelector.cxx:138 | AlarmMsg is not null
07-31 16:29:55.335  4076  5081 I reSIProcate: INFO | RESIP:TRANSACTION | TuSelector.cxx:140 | Sending AlarmMessage 0 to TUs
07-31 16:29:55.335  4076  5081 I reSIProcate: INFO | RESIP:TRANSACTION | TuSelector.cxx:145 | End add(alm)
07-31 16:29:55.335  4076  5081 I reSIProcate: INFO | RESIP:DNS | DnsResult.cxx:240 | Whitelisting 2405:200:380:1581::42(28): 2405:200:380:1581::42
07-31 16:29:55.335  4076  5081 I reSIProcate: INFO | RESIP:DNS | RRVip.cxx:128 | updating an existing vip: 2405:200:380:1581::42 with 2405:200:380:1581::42
07-31 16:29:55.335  4076  5081 I reSIProcate: INFO | RESIP:TRANSACTION | TuSelector.cxx:71 | Send to TU: TU: CALL-SESSION(8) size=0 
07-31 16:29:55.335  4076  5081 I reSIProcate: 
07-31 16:29:55.335  4076  5081 I reSIProcate: SipResp: 180 tid=935e2afa889bdcad cseq=1 INVITE [email protected]:5070 / 1 from(wire)
07-31 16:29:55.336  4076  5081 D StackIF : readMessage: messageType 2 tid 0 pduLength 920
07-31 16:29:55.336  4076  5081 D SECIMSJ[0]: [UNSL]< NOTIFY_SIP_MESSAGE
07-31 16:29:55.336  4076  5081 D StackIF[0]: processNotify: id NOTIFY_SIP_MESSAGE
07-31 16:29:55.340  4076  5081 D SIPMSG[0]: [<--] SIP/2.0 180 Ringing [CSeq: 1 INVITE]
07-31 16:29:55.340  4076  4896 D ResipRawSipHandler: handleMessage: event: 100
07-31 16:29:55.340  4076  5082 D OpenApiServiceModule: handleMessage: what 100
07-31 16:29:55.341  4076  4896 D ResipVolteHandler: handleMessage: evt 114
07-31 16:29:55.341  4076  5081 D CpAudioEngineClient: SAE_NotiInfo: SAE_NotiInfo string: 16:29:55.341<180 Ringing:1 INVITE
07-31 16:29:55.341  4076  5081 D VoIpEngineProxy: NotiInfo:Sending IPC_IMS_INFO Noti

Will that be of use?

like image 657
Shashank Agrawal Avatar asked Jul 31 '18 10:07

Shashank Agrawal


People also ask

Why does my phone not ring on outgoing calls?

There are various reasons why your Android phone won't ring when someone calls, including: Your ringer volume is turned down. Your phone is on Do Not Disturb or Airplane mode. You turned on call forwarding.

Why can't I hear my Iphone ring on outgoing calls?

Go to Settings > Sounds (or Settings > Sounds & Haptics), and drag the Ringer and Alerts slider back and forth a few times. If you don't hear any sound, or if your speaker button on the Ringer and Alerts slider is dimmed, your speaker might need service.


1 Answers

Now in Android R, we have the permission for public api.

READ_PRECISE_PHONE_STATE

Allows read only access to precise phone state. Allows reading of detailed information about phone state for special-use applications such as dialers, carrier applications, or ims applications.

Maybe you can use it right now for the precise phone call state,I have got the precise phone call state in custom Android system.

like image 122
ifeegoo Avatar answered Sep 20 '22 08:09

ifeegoo