I’ve been meaning to put a post about this some time back but have either been too busy or too forgetful to do so. A few months ago I was tasked with creating a somewhat basic mobile app on the Android Platform. Google seems to have a way of making their APIs, platforms, and languages really easy to learn and use. Unfortunately, and understandably, Google did not provide native SOAP support in their Android APIs. Thankfully, interfacing with a SOAP based web service is still possible, albeit a bit frustrating thanks to kSOAP2 for Android.
However, either due to my ignorance or something else using kSOAP2 isn’t exactly a usable, production ready solution out of the box. It is certainly not as easy to use or elegant as using Visual Studio to build applications which consume WCF services. A lot of foot work needs to be done to get everything setup and running (and that’s not even running correctly, I still haven’t gotten marshaling to work properly).
To begin, modern versions of Android require the following line in the manifest in order for the application to have access to the internet. Otherwise, it may seem like everything has been done correctly but the code will still throw exceptions due to the OS denying it access to the internet.
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="org.example.android"
android:versionCode="1"
android:versionName="1.0">
…
<uses-permission android:name="android.permission.INTERNET" />
…
</manifest>
Second, ensure that you know the namespace, url, default action URI, and the version of SOAP your web service is using. In my case I assumed it to be 2.0, then tried 1.2, then tried 1.0, until I looked into the source code for kSOAP2 and examined the SOAP packets coming back from my WCF service until I realized I was using 1.1. If you don’t tell kSOAP2 which version of SOAP it will not work. As for the other values, place them in an config file so code doesn’t need to be modified when they changed. I made an application_config.xml in res/values in my project with the following:
<resources>
<string name="WCFNamespace">urn:http://example.org/myservice/mysendpoint/2009/12/25</string>
<string name="WCFUrl">http://example.org/MyService.svc/EndPoint</string>
<string name="WCFDefaultActionURI">urn:http://example.org/myservice/endpoint/2009/12/25/IEndPoint/</string>
</resources>
I was originally going to make classes for each service call but opted not to. However, I still kept the interface so the next few code snippets comprise more or less the entirety of the “communication layer.”
import org.ksoap2.serialization.SoapObject;
import org.ksoap2.serialization.SoapSerializationEnvelope;
import org.ksoap2.transport.AndroidHttpTransport;
//Interface for objects which communicate with SOAP messages
public interface ISoapCommunicable {
SoapObject getSoapRequest();
SoapSerializationEnvelope getSoapEnvelope();
AndroidHttpTransport getTransport();
boolean makeSoapRequest();
Object getSoapResponse();
}
import java.io.IOException;
import org.ksoap2.SoapEnvelope;
import org.ksoap2.SoapFault;
import org.ksoap2.serialization.SoapObject;
import org.ksoap2.serialization.SoapSerializationEnvelope;
import org.ksoap2.transport.AndroidHttpTransport;
import org.xmlpull.v1.XmlPullParserException;
import android.content.Context;
import org.example.android.R;
//a class that will act as a service proxy to the SOAP service
public class SoapHelper implements ISoapCommunicable {
protected String METHOD_NAME = null;
protected String SOAP_ACTION = null;
protected String NAMESPACE = null;
protected String URL = null;
protected Context context;
protected boolean debug;
SoapObject request = null;
AndroidHttpTransport transport = null;
SoapSerializationEnvelope envelope = null;
public SoapHelper (Context context, boolean debug) {
this.context = context;
this.NAMESPACE = this.context.getString(R.string.WCFNamespace);
this.URL = this.context.getString(R.string.WCFUrl);
this.debug = debug;
}
public SoapHelper (Context context) {
this(context, false);
}
public SoapObject getSoapRequest () {
if (this.request == null) {
this.request = new SoapObject(NAMESPACE, METHOD_NAME);
}
return this.request;
}
public SoapSerializationEnvelope getSoapEnvelope () {
if (this.envelope == null) {
this.envelope = new SoapSerializationEnvelope(SoapEnvelope.VER11);
this.envelope.dotNet = true;
this.envelope.setOutputSoapObject(this.getSoapRequest());
this.addMappingsAndMarshals(this.envelope);
}
return this.envelope;
}
public AndroidHttpTransport getTransport() {
if (this.transport == null) {
this.transport = new AndroidHttpTransport(URL);
}
this.transport.debug = this.debug;
return this.transport;
}
public boolean makeSoapRequest () {
try {
if(SOAP_ACTION == null) {
SOAP_ACTION = this.context.getString(R.string.WCFDefaultActionURI) + METHOD_NAME;
}
this.getTransport().call(SOAP_ACTION, this.getSoapEnvelope());
return true;
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (XmlPullParserException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (Exception e) {
e.printStackTrace();
}
return false;
}
public Object getSoapResponse() {
try {
return this.getSoapEnvelope().getResponse();
} catch (SoapFault e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return null;
}
private void addMappingsAndMarshals(SoapSerializationEnvelope envelope) {
EventDetailsMarshal marshal = new EventDetailsMarshal();
marshal.register(envelope);
}
public void setMethodName (String method) {
this.METHOD_NAME = method;
}
}
import org.ksoap2.serialization.KvmSerializable;
//base class for any webservice object
public abstract class BaseWebserviceObject implements KvmSerializable {
public BaseWebserviceObject () {
super();
}
}
import java.util.Date;
import java.util.Hashtable;
import org.kobjects.isodate.IsoDate;
import org.ksoap2.serialization.MarshalDate;
import org.ksoap2.serialization.PropertyInfo;
//a class to represent events pulled from or pushed to the web service
public class EventDetails extends BaseWebserviceObject {
public static Class CLASS = new EventDetails().getClass();
private int id;
private String name;
private Date startDate;
private Date endDate;
public Object getProperty (int idx) {
Object rtn;
switch (idx) {
case 0 :
rtn = this.endDate;
break;
case 1 :
rtn = this.id;
break;
case 2 :
rtn = this.name;
break;
case 3 :
rtn = this.startDate;
break;
default :
rtn = null;
break;
}
return rtn;
}
public int getPropertyCount () {
return 4;
}
public void getPropertyInfo (int idx, Hashtable properties, PropertyInfo info) {
switch (idx) {
case 0 :
info.type = MarshalDate.DATE_CLASS;
info.name = "EndDate";
break;
case 1 :
info.type = PropertyInfo.INTEGER_CLASS;
info.name = "ID";
break;
case 2 :
info.type = PropertyInfo.STRING_CLASS;
info.name = "Name";
break;
case 3 :
info.type = MarshalDate.DATE_CLASS;
info.name = "StartDate";
break;
default :
break;
}
}
public void setProperty (int idx, Object value) {
switch (idx) {
case 0 :
this.endDate = IsoDate.stringToDate(value.toString(), IsoDate.DATE_TIME);
break;
case 1 :
this.id = Integer.parseInt(value.toString(), 10);
break;
case 2 :
this.name = value.toString();
break;
case 3 :
this.startDate = IsoDate.stringToDate(value.toString(), IsoDate.DATE_TIME);
break;
default :
break;
}
}
public int getId() {
return this.id;
}
public Date getStartDate() {
return new Date(this.startDate.toString());
}
public Date getEndDate() {
return new Date(this.endDate.toString());
}
public String getName() {
return this.name;
}
public void setId(int id) {
this.id = id;
}
public void setStartDate(String date) {
this.startDate = IsoDate.stringToDate(date, IsoDate.DATE_TIME);
}
public void setEndDate(String date) {
this.endDate = IsoDate.stringToDate(date, IsoDate.DATE_TIME);
}
public void setName(String name) {
this.name = name;
}
}
Finally, after all that in my activity I added a method which retrieves all events from the service with specified parameters. The code is as follows:
SoapHelper service = new SoapHelper(this.getApplicationContext());
service.setMethodName("GetEvents");
SoapObject request = service.getSoapRequest();
request.addProperty("userID", userId);
boolean rtn = false;
if (service.makeSoapRequest()) {
try {
SoapObject results = (SoapObject) service.getSoapResponse();
this.events = EventDetailsMarshal.parseEventDetails(results);
} catch (Exception e) {
e.printStackTrace();
}
rtn = true;
}
return rtn;
}
Since I could not get marshaling to work in kSOAP2 I added a parse method in the EventDetailsMarshal class. The interesting bits are:
EventDetails event = new EventDetails();
for (int i = 0; i < response.getPropertyCount(); i++) {
event.setProperty(i, response.getProperty(i));
}
return event;
}
public static Vector<EventDetails> parseEventDetails(SoapObject response) {
Vector<EventDetails> events = new Vector<EventDetails>();
int propertySize = response.getPropertyCount();
for(int i = 0; i < propertySize; i++) {
PropertyInfo pi = new PropertyInfo();
response.getPropertyInfo(i, pi);
String tagName = pi.getName();
if(tagName.equals("EventDetails")) {
Object o = response.getProperty(i);
if(response.getProperty(i) instanceof SoapObject) {
events.add(parseEvent((SoapObject)o));
}
}
}
return events;
}
With this it is now possible to communicate with a SOAP service, pass it arguments, and retrieve results. However, this code is far from production ready. Given more time I would have created something akin to Visual Studio’s code generator when it connects to SOAP services to create the classes for types used by the service and setup marshaling properly. However, this is a stepping stone in the right direction.
If you have to make Android connect to a WCF or other SOAP based service, I hope this helps.