Thunderbird: Autocomplete with Gmail contacts by syncing address book

I've been using IMAP to access my work emails because it's much easier (for me) to read and write, but for over a year I've had to log into Gmail's web interface to copy email addresses for new employees or people I've never had to email before.

This method will automatically sync your Gmail address book (via LDAP) into your Thunderbird address book, including contact shared across groups.

First of all, download and install this addon called gContactSync into your Thunderbird.

I'll be basing these instructions on Thunderbird 24, but it shouldn't vary too much.

  • From the main menu, go to "Tools" > "Addons" and then click on the cog > "Install Add-on from file".
  • Select "gcontactsync-0.3.5-sm+tb.xpi" and install it.
  • Restart Thunderbird

image

You will now be prompted to log into your Google account. This will be the account that it'll sync with.

gContactSync will create a new address book (similar to Personal address book and Collected emails) called "Google Contacts". That's where all your Gmail contacts will go.

You can also choose to synchronise only "My Contacts", which means no groups will be pulled into the address book.

Sources

Android: How to generate proguard.cfg / proguard-project.txt

I had a few old projects from the days of Android 1.6 (API 4) which didn't make use of the proguard files. Luckily enough it was to add them in.

In a terminal:

cd android-sdk\tools
android.bat -v update project -p D:\Android\projects\codepeeker

Then add this line into your "project.properties" file:

proguard.config=${sdk.dir}/tools/proguard/proguard-android.txt:proguard-project.txt

This references the default proguard configuration, and then your own local one defined in "local.properties". You should not commit "local.properties" to source control as it's generated for the local development machine.

Note: be sure to check your release project! Using proguard can badly break your release APK!

Android: Fix title in Webview Javascript confirm() and alert() dialogs

Normally the alert/confirm dialog prompts have a title, but it doesn't reflect the actual title of the webpage you're viewing.

This may be somewhat annoying or confusing to your users, and thankfully it's an easy fix!

image

What you need to do is configure a custom WebChromeClient on your WebView. This allows you to tweak the styling and behaviour of the WebView components.

m_webview.setWebChromeClient(new JsPopupWebViewChrome());

private class JsPopupWebViewChrome extends WebChromeClient {
@Override
public boolean onJsConfirm(WebView view, String url, String message, final JsResult result) {
AlertDialog.Builder b = new AlertDialog.Builder(view.getContext())
.setTitle(view.getTitle())
.setMessage(message)
.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
result.confirm();
}
})
.setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
result.cancel();
}
});

b.show();

// Indicate that we're handling this manually
return true;
}
}

All of the magic happens in JsPopupWebViewChrome's onJsConfirm() method. You can apply a similar method to make this work with alert() prompts by overriding onJsAlert().

image http://24.media.tumblr.com/tumblr_ltcgdakccJ1qanfdoo1_500.jpg

Thunderbird: Disable selection quote when composing reply emails

Installing Thunderbird 24 was a pretty good experience, but some little quirks have been getting on my nerves.

One of them is automatically clipping the response email with whatever you had selected. It doesn't really work for me because I like to highlight as I read, and it becomes very annoying after replying to a handful of emails.

chK0DQv
Chef selecting a handful of umm... emails.

From the main menu:

  • Click "Tools" > "Options"
  • In the Options dialog, go to "Advanced" > "General" and click on the "Config editor"

image

  • If you get a little warning, agree to be careful
  • Start filtering with "mailnews.reply_quoting"
  • Change "mailnews.reply_quoting_selection" to false by double clicking it

image

The changes should take effect immediately, so no need to restart Thunderbird.

Source

Android: WebView.addJavascriptInterface() crash workaround for Gingerbread 2.3.x

For a while I thought this was just bad implementation on my part. How could I get it so wrong? I mean, I'm literally just copy-pasting the example from the documentation and it's STILL not working!

Normally, you'd just call this and expect it to work:

m_webview.getSettings().setJavaScriptEnabled(true);
m_webview.addJavascriptInterface(new JSInterface(), "Android");

And normally you'd be right, except on Gingerbread. When you trigger some JS calls to Android 2.3, you'll get this:

JNI WARNING: jarray 0xb5d256f0 points to non-array object (Ljava/lang/String;)
"WebViewCoreThread" prio=5 tid=8 NATIVE
  | group="main" sCount=0 dsCount=0 obj=0xb5cfc348 self=0x8234e98
  | sysTid=2023 nice=0 sched=0/0 cgrp=[fopen-error:2] handle=136531904
  at android.webkit.WebViewCore.nativeTouchUp(Native Method)
  at android.webkit.WebViewCore.nativeTouchUp(Native Method)
  at android.webkit.WebViewCore.access$3300(WebViewCore.java:53)
  at android.webkit.WebViewCore$EventHub$1.handleMessage(WebViewCore.java:1162)
  at android.os.Handler.dispatchMessage(Handler.java:99)
  at android.os.Looper.loop(Looper.java:130)
  at android.webkit.WebViewCore$WebCoreThread.run(WebViewCore.java:633)
  at java.lang.Thread.run(Thread.java:1019)

VM aborting

I ain't done nothing wrong here! Same code works perfectly on other versions of Android.

i74p01uddwr4yldjdh

Well, it turns out that this bug has existed since December 2010 "Issue 12987 - android - Javascript to Java Bridge Throws Exception", and even after 3 years developers still have to jump through hoops to get JavaScript bridging working on 2.3 devices.

image

Gingerbread still holds up to 30% of all device market share on the Play Store at time of writing.

The solution

I'm not gonna lie, this is only a half-baked solution but AWESOME and it's the best I can find so far. It's a method first discovered (or published) by Jason Shah of PhoneGap, then tweaked by Mr S (StackOverflow) to detect multiple versions of Gingerbread.

Unfortunately, there are a few flaws with his implementation which I've taken upon to fix (red ones are still unresolved)

  • Not synchronous, so we can't return values between JS/Java (without callbacks) Fixed 17/9/13
  • Unable to access interface from iframes
  • Required each methods in JSInterface to tokenize a single String argument to accommodate for lack of bridging support
  • Required you to manually map out all methods in the interface
  • Issues with commas/double quotes in strings breaking JS
  • String separator is cumbersome in case data had break string in it
  • It wasn't clear on how to change the interface name

I then took this code and modified it so most of the issues have been ironed out (to a degree).

I wasn't too keen on subclassing the WebView, so here's the helper function. (Don't worry, all the code will be made available for easy copy-pasting in GitHub along with more detailed comments).

private WebView m_webview = null;
private boolean javascriptInterfaceBroken = false;

/**
* @see http://twigstechtips.blogspot.com.au/2013/09/android-webviewaddjavascriptinterface.html
*/
protected void fixWebViewJSInterface(WebView webview, Object jsInterface, String jsInterfaceName, String jsSignature) {
// Gingerbread specific code
if (Build.VERSION.RELEASE.startsWith("2.3")) {
javascriptInterfaceBroken = true;
}
// Everything else is fine
else {
webview.addJavascriptInterface(jsInterface, jsInterfaceName);
}

webview.setWebViewClient(new GingerbreadWebViewClient(jsInterface, jsInterfaceName, jsSignature));
webview.setWebChromeClient(new GingerbreadWebViewChrome(jsInterface, jsSignature));
}

The function fixWebViewJSInterface() simply initiates the WebView with the right implementation depending on the Android build version it's running on.

So replace your addJavascriptInterface() call:

m_webview.addJavascriptInterface(new JSInterface(), "Android");

With the fix:

fixWebViewJSInterface(webview, new JSInterface(), "Android", "_gbjsfix:");

GingerbreadWebViewClient and GingerbreadWebViewChrome contains pretty much all the workaround logic, so prepare yourself for a lengthy snippet!

How it works

Normally, JavaScript in the WebView is able to call Android functions via the interface name "Android" or whatever name we gave it. For example:

Android.showToast("Hello! Is it me you're looking for?");

At the fixWebViewJSInterface() level, the fix is relatively simple. Anything other than Gingerbread 2.3 will use the normal JavaScript interface implementation addJavascriptInterface().

However anyone on Android 2.3 we'll have to treat a little differently, but be glad to know that the JS code itself is (mostly) the same. We DON'T actually set the interface, but instead mark the interface as broken and let GingerbreadWebViewClient/GingerbreadWebViewChrome handle the rest as it'll:

  • Wait until the page has finished loading
  • Generate JS code based off your JSInterface class methods
  • Inject our own "Android" interface object into the page
  • Call android_init() at the end of it all (even on non-Gingerbread Android)

GingerbreadWebViewClient is responsible for the JS code injection and re-injection into the broken WebView.

Regarding the JS code generated:

  • Declares an "Android" object so we don't have to change the JS code we write in the WebView
  • Replicate the method names into the "Android" interface object
  • All the methods will wrap Android._gbFix()
  • _gbFix() is a function which seals the function arguments (and some meta data) into JSON
  • The JS signature is appended to the generated JSON and passed to GingerbreadWebViewChrome via prompt()
  • prompt() will wait for a response, thus making it a synchronous call

Calls by prompt() from the WebView's JS to the Android interface will trigger these events within GingerbreadWebViewChrome:

  • onJsPrompt() picks up the prompt() call and checks the message for the JS signature
  • Decode the meta data from the JSON bubble-wrap
  • Attempt to map the method back to the JSInterface class we've defined and invoke it

 

Well, I promised you a lengthy snippet. Here it is!

private class GingerbreadWebViewClient extends WebViewClient {
private Object jsInterface;
private String jsInterfaceName;
private String jsSignature;

public GingerbreadWebViewClient(Object jsInterface, String jsInterfaceName, String jsSignature) {
this.jsInterface = jsInterface;
this.jsInterfaceName = jsInterfaceName;
this.jsSignature = jsSignature;
}


@Override
public void onPageFinished(WebView view, String url) {
super.onPageFinished(view, url);

if (javascriptInterfaceBroken) {
StringBuilder gbjs = new StringBuilder();

gbjs.append("javascript: ");
gbjs.append(generateJS());

view.loadUrl(gbjs.toString());
}

// Initialise the page
view.loadUrl("javascript: android_init();");
}


/**
* What this JS wrapper function does is convert all the arguments to strings,
* in JSON format before sending it to Android in the form of a prompt() alert.
*
* JSON data is returned by Android and unwrapped as the result.
*/
public String generateJS() {
StringBuilder gbjs = new StringBuilder();

if (javascriptInterfaceBroken) {
StringBuilder sb;

gbjs.append("var "); gbjs.append(jsInterfaceName); gbjs.append(" = { " +
" _gbFix: function(fxname, xargs) {" +
" var args = new Array();" +
" for (var i = 0; i < xargs.length; i++) {" +
" args.push(xargs[i].toString());" +
" };" +
" var data = { name: fxname, len: args.length, args: args };" +
" var json = JSON.stringify(data);" +
" var res = prompt('"); gbjs.append(jsSignature); gbjs.append("' + json);" +
" return JSON.parse(res)['result'];" +
" }" +
"};");

// Build methods for each method in the JSInterface class.
for (Method m : jsInterface.getClass().getMethods()) {
sb = new StringBuilder();

// Output = "Android.showToast = function() { return this._gbFix('showToast', arguments); };"
sb.append(jsInterfaceName);
sb.append(".");
sb.append(m.getName());
sb.append(" = function() { return this._gbFix('");
sb.append(m.getName());
sb.append("', arguments); };");

gbjs.append(sb);
}
}

return gbjs.toString();
}
}


private class GingerbreadWebViewChrome extends WebChromeClient {
private Object jsInterface;
private String jsSignature;


public GingerbreadWebViewChrome(Object jsInterface, String jsSignature) {
this.jsInterface = jsInterface;
this.jsSignature = jsSignature;
}


@Override
public boolean onJsPrompt(WebView view, String url, String message, String defaultValue, JsPromptResult result) {
if (!javascriptInterfaceBroken || TextUtils.isEmpty(message) || !message.startsWith(jsSignature)) {
return false;
}

// We've hit some code through _gbFix()
JSONObject jsonData;
String functionName;
String encodedData;

try {
encodedData = message.substring(jsSignature.length());
jsonData = new JSONObject(encodedData);
encodedData = null; // no longer needed, clear memory
functionName = jsonData.getString("name");

for (Method m : jsInterface.getClass().getMethods()) {
if (m.getName().equals(functionName)) {
JSONArray jsonArgs = jsonData.getJSONArray("args");
Object[] args = new Object[jsonArgs.length()];

for (int i = 0; i < jsonArgs.length(); i++) {
args[i] = jsonArgs.get(i);
}

Object ret = m.invoke(jsInterface, args);
JSONObject res = new JSONObject();
res.put("result", ret);
result.confirm(res.toString());
return true;
}
}

// No matching method name found, should throw an exception.
throw new RuntimeException("shouldOverrideUrlLoading: Could not find method '" + functionName + "()'.");
}
catch (IllegalArgumentException e) {
Log.e("GingerbreadWebViewClient", "shouldOverrideUrlLoading: Please ensure your JSInterface methods only have String as parameters.");
throw new RuntimeException(e);
}
catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
catch (InvocationTargetException e) {
throw new RuntimeException(e);
}
catch (JSONException e) {
e.printStackTrace();
throw new RuntimeException(e);
}
}
}

If you're using a custom WebViewClient and/or WebChromeClient, simply change the subclasses for your one or merge the code somehow. I'll assume you're capable enough as no support will be given here.

Zmoe2S2
Maybe this guy's a little TOO capable...

Things you should be aware of

android_init()

Due to the delay between page load and JS injection, you have to assume that the JS interface is NOT available to you until android_init() has been called. Because of this, you must always declare android_init(), even if it's not used.

If you have any code in $(document).ready() or onload() which uses the Android interface, you may want to move it into android_init().

I think this is a fairly small trade-off for the convenience this method provides.

JSInterface method parameters

The JavaScript calls can be a mixture of numbers and strings, but all parameters in JSInterface Android class must be of type String.

This could probably be fixed later, but I was short on time and chose to convert all the arguments into strings when invoking the method.

Limitations

  • Not synchronous, so we can't return values between JS/Java (without callbacks) Fixed 17/9/13
  • Unable to access interface from iframes
  • Required each methods in JSInterface to tokenize a single String argument to accommodate for lack of bridging support See android_init() limitation and JSInterface method parameters
  • Required you to manually map out all methods in the interface Now automatic
  • Issues with commas/double quotes in strings breaking JS Fixed with JSON
  • String separator is cumbersome in case data had break string in it Fixed with JSON
  • It wasn't clear on how to change the interface name Now an argument to fixWebViewJSInterface()

Here's the link to the code on GitHub. It includes a few more comments to clarify some points here and there.

That's all folks! I feel like I've jumped through enough hoops for now.

D47Z6

Update

17/9/2013: Rewrote the tutorial so the calls are synchronous! No more messy callbacks, yay!

Sources

Big thanks to Jason Shah for sharing his findings and making this work-around possible.

Python: Print XML element to string

Just a little snippet that I used before but had trouble finding.

from xml.etree import ElementTree

ElementTree.tostring(xml_root_or_element)

Android: Disable WebView zoom controls

This method doesn't require you to subclass the WebView, which makes life a bit easier for everyone involved.

The best part is that it works with all versions from 1.6+.

/**
* Disable zoom buttons for WebView.
*/
public static void disableWebviewZoomControls(final WebView webview) {
webview.getSettings().setSupportZoom(true);
webview.getSettings().setBuiltInZoomControls(true);

// Use the API 11+ calls to disable the controls
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.HONEYCOMB) {
new Runnable() {
@SuppressLint("NewApi")
public void run() {
webview.getSettings().setDisplayZoomControls(false);
}
}.run();
}
else {
try {
ZoomButtonsController zoom_control;
zoom_control = ((ZoomButtonsController) webview.getClass().getMethod("getZoomButtonsController").invoke(webview, (Object[]) null));
zoom_control.getContainer().setVisibility(View.GONE);
}
catch (Exception e) {
e.printStackTrace();
}
}
}

Source

Python/Twitter: Posting tweets (with images)

There are a few services out there that'll post your RSS feed to multiple societ networks, but sometimes you want that little extra configurability which just won't happen unless you do it yourself.

There are a few ways of skinning this cat, but today I'll only be writing up about the easiest one.

What you'll need

  • Twython (v3.0.0 at time of writing)
  • A Twitter account (API v1.1 at time of writing)

Download and install Twython from the site or via "pip install twython".

Setting up twitter

What you'll need for this to work is a twitter "app" associated with your account. For the purpose of this tutorial, I'll show you how to set up your own account with your own app for testing purposes.

  • Hit up https://dev.twitter.com and sign in with your twitter account.
  • Go to "My Applications" (via top/right dropdown menu where your account image is).
  • Create a new app by entering the name, description, website
  • Go to "Settings" and see "Application Type"
  • Change it to "read and write" and save.
  • Back in the "Details" tab, click on "Create my access token" (it may take a few minutes so periodically refresh the page and until your access token appears)
  • Click on the "OAuth tool" tab to make sure it's all filled in
  • Copy all 4 values (Consumer and Access token key/secret) somewhere because you'll be needing them soon.

Looking at your normal twitter page, check out https://twitter.com/settings/applications and it should list your app with "Permissions: read and write".

Now that's twitter done.

Python setup

Now for the fun bits, coding!

Just a tweet:

import twython

twitter = twython.Twython(
"CONSUMER_KEY",
"CONSUMER_SECRET",
"ACCESS_TOKEN",
"ACCESS_TOKEN_SECRET"
)

# Tweeting textually
twitter.update_status(status = "Testing tweet from Twig's Tech Tips")

And a tweet containing images:

# Update with image
# f = open("weather_sunny.png", 'rb')
# or
# f = urlopen("http://www.weather.com/sunny.jpg")

twitter.update_status_with_media(
status = "Testing tweet from Twig's Tech Tips... with imagery!",
media = f
)

There you have it, consider your cat is now skinned.

FUR-ITS-MURDER-e1297066919608

This setup can also be used for stuff like fetching/searching tweets using the twitter API, but just read the docs to find examples of that. Just be aware of the limitations which the twitter API impose before conjuring anything on a grand scale.

Sources

Python: Setting up easy_install on Windows

This was much easier than expected, especially without the hassle of installing Cygwin!

First of all, grab ez_setup.py from the setuptools v1.1 page.

In your Python command prompt, type:

python ez_setup.py

Let it work it's magic as it installs a copy of easy_install.exe into your Python/Scripts folder.

Just make sure that folder is in the PATH variable of your shell or system environment variable.

Once you're done with that, test out the script by installing something.

Normally you can install things via:

cd pypackagename-1.0.0
python setup.py install

G0ociY7
All good? Yeaaaaaaaah, all good.

Source

 
Copyright © Twig's Tech Tips
Theme by BloggerThemes & TopWPThemes Sponsored by iBlogtoBlog