Implementation

For information about the design and functionality of the MIDlet, see section Design.

VoIP settings

Nokia Asha software platform 1.1 provides the actual VoIP client engine implementation and the VoIP API for applications to takes advantage of it. Because of the high level VoIP API, handling the VoIP calls in the MIDlet is simple.

As the platform is taking care most of the actual VoIP functionality, it needs also the specific VoIP settings from the given application and SIP account. The VoIP settings of each VoIP MIDlets are given in a form of an XML file to the VoIP API. The complete voip_settings.xml file is found from the project resource files, but below is a short code snippet to demonstrate in which form the SIP account details are given. In addition to the SIP account details, there are lots of other VoIP settings, like network and audio codec settings, that are not covered in the documentation of this example. In most cases, a major part of the settings can be left to their default values. More information about the VoIP settings XML file format can be found from the VoIP API documentation.

<?xml version="1.0"?>
<!DOCTYPE wap-provisioningdoc PUBLIC "-//WAPFORUM//DTD PROV 1.0//EN"
"http://www.wapforum.org/DTD/prov.dtd">
<wap-provisioningdoc version="1.1">

  ...

  <!-- SIP settings, w9010 -->
<characteristic type="APPLICATION">
	<parm name="APPID" value="w9010"/>
    	<parm name="APPREF" value="Nokia_VoIP_example_settings"/>
    	<parm name="PROVIDER-ID" value="Nokia VoIP example"/>
	<parm name="TO-NAPID" value="INTERNET"/>
	<parm name="PTYPE" value="IETF"/>
	<parm name="PUID" value="sip:<!-- SIP_USERNAME -->@<!-- SIP_REGISTRAR -->"/>
	<parm name="APROTOCOL" value="UDP" />

	<!-- Outbound settings -->
	<characteristic type="APPADDR">
		<parm name="LR" value="true"/>
		<parm name="ADDR" value="<!-- SIP_REGISTRAR -->"/>
		<characteristic type="PORT">
			<parm name="PORTNBR" value="5060" />
		</characteristic>
	</characteristic>   

	<!-- Outbound credentials -->
	<characteristic type="APPAUTH">
		<parm name="AAUTHNAME" value="<!-- SIP_USERNAME -->"/>
		<parm name="AAUTHSECRET" value="<!-- SIP_PASSWORD -->"/>
		<parm name="AAUTHDATA" value=""/>
		<parm name="AAUTHTYPE" value="HTTP-DIGEST" />
	</characteristic>   

	<!-- Registrar location and credentials -->
	<characteristic type="RESOURCE">
		<parm name="URI" value="<!-- SIP_REGISTRAR -->"/>
		<parm name="AAUTHNAME" value="<!-- SIP_USERNAME -->"/>
		<parm name="AAUTHSECRET" value="<!-- SIP_PASSWORD -->"/>
		<parm name="AAUTHDATA" value=""/>
		<parm name="AAUTHTYPE" value="HTTP-DIGEST" />
	</characteristic>
</characteristic>
</wap-provisioningdoc>

As can be seen from the previous code snippet, there are placeholders XML comments for the SIP username, password and registrar server address. These placeholders are replaced with the user SIP account settings by the MIDlet and the given to the VoIP API.

SettingsHelper class is encapsulating the settings handling with the VoIP API in this VoIP Example MIDlet. It creates a VoipSettings object, returned by VoipManager class, and uses writeVoipSettings method to write the complete XML settings string to the VoIP API. Before that, it reads the predefined settings XML file from the resources of the MIDlet and replaces the aforementioned placeholder comments in the XML with the actual user SIP account details, specified in the Settings view, in insertUserSettings helper method.

public class SettingsHelper {

    private VoipSettings voipSettings;
    private SettingsStateListener settingsStateListener;
    private Settings settings;
    private SettingsManager settingsManager;

    public SettingsHelper(SettingsManager settingsManager) {
        this.settingsManager = settingsManager;
        voipSettings = VoipManager.createVoipSettingsInstance();
        settingsStateListener = new SettingsStateListener();
        voipSettings.setVoipSettingsStateListener(settingsStateListener);
    }

    public void writeSettings()
        throws IOException {
        String settingsXml = null;
        settings = settingsManager.getSettings();

        settingsXml = readSettingsFile("/voip_settings.xml");
        settingsXml = insertUserSettings(
            settingsXml,
            settings.getUsername(),
            settings.getPassword(),
            settings.getRegistrar());
        voipSettings.writeVoipSettings(settingsXml);
    }

    ...
}

Handling of VoIP call instances

Call class encapsulates the functionality to handle and monitor VoIP call instances. Call class is actually a simple wrapper class to simplify the VoIP API even further for the needs of this VoIP Example MIDlet. It contains an instance of VoipAudioCall class to control the call instance over the VoIP API and implements VoipAudioCallStateListener interface class to monitor the state of the call. The most of the methods of Call class are simple call through methods to VoipAudioCall class with some additional logic. Call class also provides very simplified callback mechanism for UI views just to monitor the disconnection of the call via CallStateListener interface class.

public class Call 
    implements VoipAudioCallStateListener { 
    private VoipAudioCall call = null; 
    private CallStateListener listener = null; 
    private boolean callGotThrough = false; 
    public void setCallStateListener(CallStateListener listener) { 
        this.listener = listener; 
    } 
    public boolean isActive() { 
        if (call == null || call.getCallState() == VoipCauses.CAUSE_DISCONNECTED) { 
            return false; 
        } 
        return true; 
    } 
    ... 
    public void makeCall(String voipAddress) { 
        // Set up an outgoing call instance. 
        call = VoipManager.createVoipAudioMOCallInstance(voipAddress); 
        call.setVoipAudioCallStateListener(this); 
        boolean showCallerId = VoIPExample.getInstance().getSettingsManager() 
            .getSettings().isShowCallerId(); 
        call.startCall(showCallerId); 
    } 
    public void receiveCall(int callId) { 
        // Set up an incoming call instance. 
        call = VoipManager.createVoipAudioMTCallInstance(callId); 
        call.setVoipAudioCallStateListener(this); 
    } 
    public void answer() { 
        if (call != null) { 
            call.startCall(true); 
        } 
    } 
    public void reject() { 
        if (call != null) { 
            call.endCall(); 
        } 
    } 
    public void end() { 
        if (call != null) { 
            call.endCall(); 
        } 
    } 
    ... 
    public void voipAudioCallUpdated(int state, int cause, int callId) { 
        System.out.println("Call state: " + state + ", cause: " + cause 
            + ", callId:" + callId); 
        if (state == VoipStates.AUDIOCALL_ENDED ||  
           (state == VoipStates.AUDIOCALL_CALL & cause == VoipCauses.CAUSE_DISCONNECTED)) { 
            System.out.println("Call disconnected"); 
            callGotThrough = false; 
            // Create a new thread to tear down the call instance and notify 
            // the call state listener about disconnection after few seconds. 
            new Thread() { 
                public void run() { 
                    try { 
                        if (listener != null) { 
                            listener.onCallDisconnected(); 
                        } 
                        Thread.sleep(3000); 
                        call.setVoipAudioCallStateListener(null); 
                        call = null; 
                    } catch (Exception e) { 
                        e.printStackTrace(); 
                    } 
                } 
            }.start(); 
        } 
    } 
}

Outgoing VoIP calls

Making an outgoing VoIP call is simple. There a method named call in the main MIDlet class to launch an outgoing VoIP call. Based on the given VoIP address, a matching Contact object is searched, if available, and a call log entry is made. Creating a new Call object and calling makeCall method is enough to set up the call instance. After that CallView is opened.

public void call(String voipAddress) {
        System.out.println("Outgoing call to: " + voipAddress);

        // Add a call log entry.
        Contact contact = contactsManager.findContact(voipAddress);
        if (contact == null) {
            contact = new Contact("", voipAddress);
        }
        callLogManager.addLogItem(voipAddress, CallLogItem.TYPE_DIALED);

        call = new Call();
        call.makeCall(voipAddress);

        viewManager.pushView(new CallView(viewManager, contact, call));
    }

Incoming VoIP calls

The MIDlet class of this VoIP Example implements VoipMTCallListener interface class in order to get notified about incoming VoIP calls via a callback method onIncomingCall. In order to achieve that, the MIDlet has to register itself as a listener of that callback to VoipManager class through setVoipMTCallListener method. The registration is done in the constructor of the MIDlet.

public class VoIPExample
    extends MIDlet
    implements VoipMTCallListener {

    ...

    public VoIPExample() {
        if (!VoipManager.setVoipMTCallListener(this)) {
            System.out.println("VoIP API not supported!");
        }
    }

    ...

    public void onIncomingCall(int callId) {
        // Handling of multiple simultaneous calls (one active & others on hold)
        // is not implemented in this example. However, the platform VoIP
        // service supports it, so the functionality can be extended to handle
        // multiple calls accordingly.
        if (call == null || !call.isActive()) {
            incomingCall(callId);
        } else {
            System.out.println("Incoming call ignored due to an active call");
        }
    }

    ...
}

In a case of an incoming VoIP call when the MIDlet is not already running, the platform VoIP service will launch the registered MIDlet. It will pass the invoke reason and the call ID of the incoming call instance as command line arguments to the MIDlet. Based on the given argument values, the MIDlet can show an incoming call view accordingly and set up the call.

public void startApp() {
        ...
        // Check the argument values if the MIDlet was launched
        // by the platform VoIP service due to an incoming call.
        String invokeReason = this.getAppProperty("arg-0");
        if (invokeReason.equals(VoipManager.VOIP_MT_CALL_ALERTING)
            || invokeReason.equals(VoipManager.VOIP_MT_CALL_WAITING)) {
            System.out.println("MIDlet launched due to an incoming call");
            try {
                int callId = Integer.parseInt(this.getAppProperty("arg-1"));
                incomingCall(callId);
            } catch (NumberFormatException e) {
                System.out.println("Invalid call ID!");
            }
        }
        ...
    }

After getting a callback about an incoming VoIP call, the handling of it is very similar with outgoing VoIP calls. A new Call object is created and the call is set up by calling receiveCall method with the call instance ID. Playing the ringtone is started, which will be discussed a bit later, and Contact object is searched from the address book, if available, to show the real name of the caller. After that IncomingCallView is opened.

public void incomingCall(int callId) {
        call = new Call();
        call.receiveCall(callId);
        ringtonePlayer.startRinging();

        // Find the caller from the saved contacts to show the name.
        String voipAddress = call.getAddress();
        Contact contact = contactsManager.findContact(voipAddress);
        if (contact == null) {
            contact = new Contact("", voipAddress);
        }

        System.out.println("Incoming call from: " + voipAddress);

        viewManager.pushView(new IncomingCallView(viewManager, contact, call));
    }

Playing ringtone

For playing a ringtone on incoming VoIP calls, there is RingtonePlayer class. An instance of it is stored by the MIDlet main class. It uses Player class of Mobile Media API to simply play a MP3 file in a loop as long as the playback is requested to stop.

public class RingtonePlayer {
    Player player;

    public void startRinging() {
        try {
            // Play the the default ringtone.
            InputStream stream = getClass()
                .getResourceAsStream("/ringtone.mp3");
            player = Manager.createPlayer(stream, "audio/mp3");
            player.setLoopCount(-1);
            player.start();
        } catch (Exception e) {
            System.out.println("Playing ringtone failed!");
            player = null;
        }
    }

    public void stopRinging() {
        if (player != null) {
            try {
                player.stop();
            } catch (MediaException e) {
                System.out.println("Stopping ringtone failed!");
            }
            player = null;
        }
    }
}

Displaying call duration

The CallCounter class is used to determine when a call is connected to the remote recipient and display the call duration. A TimerTask is used for this purpose, that polls every second the call instance to check the status of the current call. If the call is not connected, only the speaker and the dialer options are displayed. For connected calls, the mute and hold options are also displayed. The TimerTask increases the call duration every second and then the covertDurationToString method is used to convert the call duration from seconds to hh:mm:ss format as shown below.

public void run() { 
        if(call.isActiveCall()) { 
            duration++;   
            durationString = convertDurationToString(duration); 
            callView.setCallDuration(durationString); 
            callView.showMuteHold(true); 
        } 
        else { 
            callView.showMuteHold(false); 
        } 
    } 
    /* 
     * Converts the call duration from seconds to 
     * hh:mm:ss format  
     */ 
    private String convertDurationToString(int duration) { 
        int remainingSeconds = duration % 60; 
        int minutes = (duration - remainingSeconds) / 60; 
        int remainingMinutes = minutes % 60; 
        int remainingHours = (minutes - remainingMinutes) / 60; 
        return formatTime(remainingHours) + ":" +  
               formatTime(remainingMinutes) + ":" +  
               formatTime(remainingSeconds); 
	}