Tuesday, April 29, 2014

Programming things to remember 2014 Febr

Generic method for enums


For some reason (by the time, I'm writing this post, I've already forgotten) I created a class, that can extract an enum from an android Bundle (basically a Map of stuffs, in this case the stuff is String). I think the reason was to make this kind of operation easy. All the null checks are in one place, and it requires 2 lines of code to extract any enum from a bundle.

public class EnumFromBundleExtractor<T extends Enum<T>> {
    @Nonnull
    public T getValueFrom(Bundle savedInstanceState, @Nonnull String key, @Nonnull T defaultValue) {
        Preconditions.checkNotNull(key);
        Preconditions.checkNotNull(defaultValue);
        
        if (savedInstanceState == null) {
            return defaultValue;
        }
        
        final String bundledValueAsString = savedInstanceState.getString(key);
        if (bundledValueAsString == null) {
            return defaultValue;
        }
        
        try {
            return Enum.valueOf(defaultValue.getDeclaringClass(), bundledValueAsString);
        } catch (IllegalArgumentException iae) {
            return defaultValue;
        }
    }
}


Usage (VisualState is an enum):

final EnumFromBundleExtractor visualStateExtractor = new EnumFromBundleExtractor();
final VisualState visualState = visualStateExtractor.getValueFrom(savedInstanceState, BUNDLE_VISUAL_STATE_AS_ORDINAL, VisualState.INITIAL);

Programming things to remember 2014 April

Using Android's DownloadManager with JSESSIONID


We had to download a file from the internet, which was bound to a Java http session. During login, we got the JSESSIONID in the response. For all subsequent http request we have to set a Http header: "Cookie: JSESSIONID=EEDAC46D" (in more detail see this). This also have to be given to the DownloadManager via the addRequestHeader() call:

return new DownloadManager.Request(Uri.parse(url))
                .addRequestHeader("Cookie", cookie) // here we set the JSESSIONID along with the other cookies
                .setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, downloadDecorator.filePathAndName)
                .setAllowedNetworkTypes(DownloadManager.Request.NETWORK_WIFI | DownloadManager.Request.NETWORK_MOBILE)
                .setMimeType(MIME_TYPE_PDF)
                .setDescription("description of the download")
                .setTitle("title of the download");

        final DownloadManager downloadManager = (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE);

        downloadManager.enqueue(downloadRequest); // start the download


Syncing Java and webkit cookie store


When developing an app that uses both Java's Http connections and Html pages embedded in a Webview, and both would need the same jsession, we need to synchronise them. Because both the webkit (and the Webview is part of it as well) uses different CookieStore than the Java side. Fortunately the webkit has setCookie() and sync() methods:

// java cookie store
        final CookieManager cookieManagerNet = (CookieManager) javaCookieManagerWrapper.getDefault();
        final CookieStore javaCookieStore = cookieManagerNet.getCookieStore();

        // webkit cookie store
        android.webkit.CookieManager cookieManager =  webKitCookieManagerWrapper.getInstance();

        // copy cookies
        for(HttpCookie httpCookie : javaCookieStore.get(uri)) {
            cookieManager.setCookie(uri.toString(), httpCookie.toString());
        }

        //sync
        android.webkit.CookieSyncManager.getInstance().sync();

Unfortunately that sync() only schedules a delayed Message to a Handler which is attached to another Thread. Which after a time will do the actual synchronisation. This is unpredictable and may result in the webview having different jsession than the native one. Eg. even though the user logged in the native java app, opening the webpage in a webview (which needs authentication as well) the web login is displayed.

Digging down in the android sources we find the android.webkit.WebSyncManager.SyncHandler.handleMessage() calls the android.webkit.CookieSyncManager.syncFromRamToFlash() which actually does the sync synchronously. This is handy. The downside is it's protected, but using Reflection this isn't an issue (the try-catch block is omitted for brevity):

//sync
        // not android.webkit.CookieSyncManager.getInstance().sync();
        final android.webkit.CookieSyncManager cookieSyncManager = CookieSyncManager.getInstance();

        final Class clazz = ((Object)cookieSyncManager).getClass();
        final Method syncFromRamToFlashMethod = clazz.getDeclaredMethod("syncFromRamToFlash");
        syncFromRamToFlashMethod.setAccessible(true);
        syncFromRamToFlashMethod.invoke(cookieSyncManager);
        syncFromRamToFlashMethod.setAccessible(false);


Works well from Gingerbread. Unfortunately (again) on Gingerbread there is an other cookie related issue: http://android.joao.jp/2010/11/cookiemanager-and-removeallcookie.html