Talking to Mik at Tasktop about what it will take to bring his revolutionary Mylyn-based product to the mac platform, Mik pointed me to an issue with the way the Safari-based SWT browser widget accesses GMail. Though Safari is a supported browser for GMail, the embedded Safari browser in eclipse is not recognized by GMail. This post highlights the ups-and-downs of my investigation into this issue.

Others at Tasktop had discovered that the SWT browser works if gmail is accessed via the http://mail.google.com/mail/?nocheckbrowser URL, which was promising. This lead me to believe that the user-agent header of an embedded Safari was different than that produced by Safari launched from the desktop. A quick test browsing to http://whatsmyuseragent.com proved my suspicions were correct.

Here were the user-agent headers:

Safari Inside Eclipse: Mozilla/5.0 (Macintosh; U; Intel Mac OS X; en) AppleWebKit/523.12.2 (KHTML, like Gecko)
Stand-alone Safari: Mozilla/5.0 (Macintosh; U; Intel Mac OS X; en) AppleWebKit/523.12.2 (KHTML, like Gecko) Version/3.0.4 Safari/523.12.2


The next step then was to see if Safari provides an API for changing the user-agent header. Having already installed XCode on my system, it was easy to search the API documentation. I found a section called 'Spoofing', which is specifically about an API for modifying the 'user-agent' header:
  • setCustomUserAgent
  • setApplicationNameForUserAgent
By the looks of the difference in user-agent headers, the second one was exactly what I was looking for.


Knowing that SWT involves native calls to Cocoa, I was a little wary about the next step. Looking at Safari.java I could see in the create() method other calls to the Safari web view to configure various properties. All of them were performed using JNI native calls, one of the various Cocoa.objc_msgSend variants. Unfortunately, there wasn't one with the signature that I needed.  The one that I needed would look like this:


public static final native int objc_msgSend(int object, int selector, String string);
I added the method to Cocoa.java, and added a call to it in Safari.create() as follows:

// [webView setApplicationNameForUserAgent:];
Cocoa.objc_msgSend(webView, Cocoa.S_setApplicationNameForUserAgent,"Safari/unknown");



 The next step is to provide the native implementation of the method.

Using the eclipse search command, I could see that the other objc_msgSend implementations were in cocoa.c  Never having done JNI before, it was time to do a little looking around.  The SWT FAQ had most of what I needed, indicating how to build the native libraries, etc.  The tricky parts were as follows:
  • how to determine the mangled function name for the native method?
  • now to convert the jstring argument to an NSString?
For the first issue, a little guessing and a look at the JNI spec where it talks about mangling and I got it mostly right.  Stubbing out the method, building it (according to the instructions in the SWT FAQ) and I got a java.lang.UnsatisfiedLinkError. Running it again in the debugger, I was able to see the method name it was looking for and fixed up the name of the method in cocoa.c.  Rebuild, re-run, and so far so good!  My stubbed out method was being called whenever a new SWT Browser control is created. This is what I had so far:


#ifndef NO_objc_1msgSend__IILjava_lang_String_2
JNIEXPORT jint JNICALL Cocoa_NATIVE(objc_1msgSend__IILjava_lang_String_2)
(JNIEnv *env, jclass that, jint arg0, jint arg1, jstring arg3)
{
jint rc = 0;
return rc;
}
#endif


So now on to the method implementation: how to call the right method?  A quick google on jstring to NSString resulted in the following:



#ifndef NO_objc_1msgSend__IILjava_lang_String_2
JNIEXPORT jint JNICALL Cocoa_NATIVE(objc_1msgSend__IILjava_lang_String_2)
(JNIEnv *env, jclass that, jint arg0, jint arg1, jstring arg3)
{
jint rc = 0;

Cocoa_NATIVE_ENTER(env, that, objc_1msgSend__IILjava_lang_String_2I_FUNC);
const char *str = (*env)->GetStringUTFChars(env, arg3, 0);

/* use NSString as follows: [NSString stringWithCString: str] */

(*env)->ReleaseStringUTFChars(env, arg3, str);
Cocoa_NATIVE_EXIT(env, that, objc_1msgSend__IILjava_lang_String_2I_FUNC);
return rc;
}
#endif
Okay, so now all I had to do is actually send the Objective-C message to the object!  Copying from other examples in the same file, here's what it I ended up with:


#ifndef NO_objc_1msgSend__IILjava_lang_String_2
JNIEXPORT jint JNICALL Cocoa_NATIVE(objc_1msgSend__IILjava_lang_String_2)
(JNIEnv *env, jclass that, jint arg0, jint arg1, jstring arg3)
{
jint rc = 0;

Cocoa_NATIVE_ENTER(env, that, objc_1msgSend__IILjava_lang_String_2I_FUNC);
const char *str = (*env)->GetStringUTFChars(env, arg3, 0);

rc = (jint)objc_msgSend((id)arg0, (SEL)arg1, [NSString stringWithCString: str]);
(*env)->ReleaseStringUTFChars(env, arg3, str);
Cocoa_NATIVE_EXIT(env, that, objc_1msgSend__IILjava_lang_String_2I_FUNC);
return rc;
}
#endif


I rebuilt the native libraries, re-ran eclipse, and voila!! It worked!  The SWT Safari browser widget was now sending a user-agent string as follows:

Mozilla/5.0 (Macintosh; U; Intel Mac OS X; en) AppleWebKit/523.12.2 (KHTML, like Gecko) Safari/unknown

I browsed back to GMail with this new patch, and GMail now recognized Safari embedded in eclipse as a supported browser.  For my first foray into hacking SWT, using JNI and Objective-C, it was a great success.... now if only the SWT team will accept my patch ;)

updated Feb 29: the fix was committed by the SWT team: see https://bugs.eclipse.org/bugs/show_bug.cgi?id=220836 for details