"Who are you?" – client device Categorizr for APEX
This article describes a solution to determine the client device category (i.e. desktop, browser or tablet) your APEX application is accessed from.
Until recently, building a web application with Oracle Application Express usually meant to build a desktop browser application. But if you look at the growing number of mobile devices, this will change very soon, if not already has for some of you. Being able to develop applications, that can be accessed by tablets or smart phones offers lots of new possibilities. But it also comes with some challenges. Mobile devices are different. Interaction by touch screen, screen size and (non-) support of certain common web functionality (flash, for example), just to mention a few.
Oracles announcement to integrate the jQuery mobile framework into APEX 4.2 will help us to deal with many of the challenges, developing for multiple devices will bring. Until now, one of the biggest challenges is, to determine the actual client your application is accessed on. Is it a desktop browser, a tablet or a smart phone? In this post I want to offer a solution that will provide an answer to this question, and can be used in APEX.
When building a web application, as a developer, you want to be able anticipate on how your application is being accessed. You might want to redirect the user to a different page or entirely different application. Or you might want to hide (i.e. not render) or show certain portions of a page. To be able to do so, you need to know the type of client used, and, ideally, be able to use this information in (PL/SQL) conditions.
Getting the info you need to decide
Every page request comes with a HTTP header, containing some generic information about the browser client it has been issued from: The HTTP_USER_AGENT string. This string holds information about browser, platform and version. The string itself can easily be accessed in PL/SQL by using the OWA_UTIL package function call:
l_user_agent := owa_util.get_cgi_env ('HTTP_USER_AGENT');
Once you've got the user agent string, the hard work starts. There are all kind of values you can get returned to you in the user agent string. It can be messy - VERY messy. Sometimes browser even lie about who they are, and the format is not standardized.
There are many approaches trying to find a way through this mess, and you can find a few articles on this at the bottom of this post. Some use databases with almost all possible user agent string stored, others try to decipher the string programmatically. Most of the code is available in Java, PHP or .net. I didn't find any appropriate code for PL/SQL. So I had to write my own or port some code I can understand.
Categorizr
What I wanted to achieve, was to be able to categorize the client device rendering the page in my application. I found some PHP code doing just that: Categorizr. The solution has some advantages:
- It is lightweight and has not too many lines of code, which makes it reasonably easy to maintain it. With the growing number of mobile devices, I will have to maintain the algorithm. The approach chosen by the developer assumes the device to be a mobile device and then checks for other categories. By assuming devices are mobile from the beginning, Categorizr aims to be more future friendly. When new phones come out, you don’t need to worry if their new user agent is in your device detection script since devices are assumed mobile from the start.
- The algorithm uses Regular Expressions, which are supported by PL/SQL. This minimizes the changes I have to apply to the code (Year, I know: I’m lazy).
- Due to the minimal codebase and not querying large databases or accessing a web service, it’s fast.
The PLSQL package (specification) I have written offers these functions that you can use in your (APEX) PL/SQL code:
CREATE OR REPLACE PACKAGE categorizr AS /****************************************************************************** NAME: categorizr PURPOSE: detect web user agent device type Based on: Categorizr Version 1.1 http://www.brettjankord.com/2012/01/16/categorizr-a-modern-device-detection-script/ Written by Brett Jankord - Copyright (c) 2011 REVISIONS: Ver Date Author Description --------- ---------- --------------- ------------------------------------ 0.1 30-3-2012 crokitta Created this package. ******************************************************************************/ g_tablets_as_desktops BOOLEAN := FALSE; --If TRUE, tablets will be categorized as desktops g_smarttv_as_desktops BOOLEAN := FALSE; --If TRUE, smartTVs will be categorized as desktops g_user_agent VARCHAR2 (2000); -- User Agent String used for detection g_device VARCHAR2 (100); FUNCTION get_category RETURN VARCHAR2; FUNCTION isdesktop RETURN BOOLEAN; FUNCTION istablet RETURN BOOLEAN; FUNCTION istv RETURN BOOLEAN; FUNCTION ismobile RETURN BOOLEAN; /* The package is initialized automatically when called, trying to fetch the value of the HTTP_USER_AGENT, which naturally only succeeds when called through a web gateway. Additionally the package just offers a mean to test a user agent strings manually by passing the string with a procedure call */ PROCEDURE set_user_agent (http_user_agent_string VARCHAR2 DEFAULT NULL); END categorizr; /
If you are interested the package body, click the link below and it will show up:
CREATE OR REPLACE PACKAGE BODY categorizr AS /****************************************************************************** NAME: categorizr PURPOSE: detect web user agent device type REVISIONS: Ver Date Author Description --------- ---------- --------------- ------------------------------------ 0.1 30-3-2012 crokitta Created this package. ******************************************************************************/ FUNCTION preg_match (pattern VARCHAR2, subject VARCHAR2, switch VARCHAR2 DEFAULT NULL) RETURN BOOLEAN IS l_pattern VARCHAR2 (32767) := pattern; l_subject VARCHAR2 (32767) := subject; BEGIN IF LOWER (switch) = 'i' THEN l_pattern := LOWER (l_pattern); l_subject := LOWER (l_subject); END IF; IF REGEXP_INSTR (l_subject, l_pattern) = 0 THEN RETURN FALSE; ELSE RETURN TRUE; END IF; END; PROCEDURE set_category IS BEGIN CASE -- Check if user agent is a smart TV - http://goo.gl/FocDk WHEN preg_match ('GoogleTV|SmartTV|Internet.TV|NetCast|NETTV|AppleTV|boxee|Kylo|Roku|DLNADOC|CE\-HTML', g_user_agent, 'i') THEN g_device := 'tv'; -- Check if user agent is a TV Based Gaming Console WHEN preg_match ('Xbox|PLAYSTATION.3|Wii', g_user_agent, 'i') THEN g_device := 'tv'; -- Check if user agent is a Tablet WHEN (preg_match ('iP(a|ro)d', g_user_agent, 'i') OR preg_match ('tablet|tsb_cloud_companion', g_user_agent, 'i')) AND (NOT preg_match ('RX-34', g_user_agent, 'i') OR preg_match ('FOLIO', g_user_agent, 'i')) THEN g_device := 'tablet'; -- Check if user agent is an Android Tablet WHEN preg_match ('Linux', g_user_agent, 'i') AND preg_match ('Android', g_user_agent, 'i') AND (NOT preg_match ('Fennec|mobi|HTC.Magic|HTCX06HT|Nexus.One|SC-02B|fone.945', g_user_agent, 'i') --or preg_match ('GT-P1000', g_user_agent, 'i') ) THEN g_device := 'tablet'; -- Check if user agent is a Kindle or Kindle Fire WHEN preg_match ('Kindle', g_user_agent, 'i') OR preg_match ('Mac.OS', g_user_agent, 'i') AND preg_match ('Silk', g_user_agent, 'i') THEN g_device := 'tablet'; -- Check if user agent is a pre Android 3.0 Tablet WHEN preg_match ( 'GT-P10|SC-01C|SHW-M180S|SGH-T849|SCH-I800|SHW-M180L|SPH-P100|SGH-I987|zt180|HTC(.Flyer|\_Flyer)|Sprint.ATP51|ViewPad7|pandigital(sprnova|nova)|Ideos.S7|Dell.Streak.7|Advent.Vega|A101IT|A70BHT|MID7015|Next2|nook', g_user_agent,'i') OR preg_match ('MB511', g_user_agent, 'i') AND preg_match ('RUTEM', g_user_agent, 'i') THEN g_device := 'tablet'; -- Check if user agent is unique Mobile User Agent WHEN preg_match ('BOLT|Fennec|Iris|Maemo|Minimo|Mobi|mowser|NetFront|Novarra|Prism|RX-34|Skyfire|Tear|XV6875|XV6975|Google.Wireless.Transcoder', g_user_agent, 'i') THEN g_device := 'mobile'; -- Check if user agent is an odd Opera User Agent - http:--goo.gl/nK90K WHEN preg_match ('Opera', g_user_agent, 'i') AND preg_match ('Windows.NT.5', g_user_agent, 'i') AND preg_match ('HTC|Xda|Mini|Vario|SAMSUNG\-GT\-i8000|SAMSUNG\-SGH\-i9', g_user_agent, 'i') THEN g_device := 'mobile'; -- Check if user agent is Windows Desktop WHEN preg_match ('Windows.(NT|XP|ME|9)', g_user_agent, 'i') AND NOT preg_match ('Phone', g_user_agent, 'i') OR preg_match ('Win(9|.9|NT)', g_user_agent, 'i') THEN g_device := 'desktop'; -- Check if agent is Mac Desktop WHEN preg_match ('Macintosh|PowerPC', g_user_agent, 'i') AND NOT preg_match ('Silk', g_user_agent, 'i') THEN g_device := 'desktop'; -- Check if user agent is a Linux Desktop WHEN preg_match ('Linux', g_user_agent, 'i') AND preg_match ('X11', g_user_agent, 'i') THEN g_device := 'desktop'; -- Check if user agent is a Solaris, SunOS, BSD Desktop WHEN preg_match ('Solaris|SunOS|BSD', g_user_agent, 'i') THEN g_device := 'desktop'; -- Check if user agent is a Desktop BOT/Crawler/Spider WHEN preg_match ('Bot|Crawler|Spider|Yahoo|ia_archiver|Covario-IDS|findlinks|DataparkSearch|larbin|Mediapartners-Google|NG-Search|Snappy|Teoma|Jeeves|TinEye', g_user_agent, 'i') AND NOT preg_match ('Mobile', g_user_agent, 'i') THEN g_device := 'desktop'; -- Otherwise assume it is a Mobile Device ELSE g_device := 'mobile'; END CASE; -- Categorize Tablets as desktops IF g_tablets_as_desktops AND g_device = 'tablet' THEN g_device := 'desktop'; END IF; -- Categorize TVs as desktops IF g_smarttv_as_desktops AND g_device = 'tv' THEN g_device := 'desktop'; END IF; END; PROCEDURE set_user_agent (http_user_agent_string VARCHAR2 DEFAULT NULL) IS BEGIN g_user_agent := http_user_agent_string; IF g_user_agent IS NULL THEN BEGIN g_user_agent := OWA_UTIL.get_cgi_env ('HTTP_USER_AGENT'); EXCEPTION WHEN OTHERS THEN g_user_agent := NULL; END; END IF; set_category; EXCEPTION WHEN OTHERS THEN g_user_agent := null; END; FUNCTION get_category RETURN VARCHAR2 IS BEGIN RETURN g_device; END; -- Returns true if desktop user agent is detected FUNCTION isdesktop RETURN BOOLEAN IS BEGIN IF g_device = 'desktop' THEN RETURN TRUE; END IF; RETURN FALSE; END; -- Returns true if tablet user agent is detected FUNCTION istablet RETURN BOOLEAN IS BEGIN IF g_device = 'tablet' THEN RETURN TRUE; END IF; RETURN FALSE; END; -- Returns true if SmartTV user agent is detected FUNCTION istv RETURN BOOLEAN IS BEGIN IF g_device = 'tv' THEN RETURN TRUE; END IF; RETURN FALSE; END; -- Returns true if mobile user agent is detected FUNCTION ismobile RETURN BOOLEAN IS BEGIN IF g_device = 'mobile' THEN RETURN TRUE; END IF; RETURN FALSE; END; BEGIN set_user_agent; END categorizr; /
The functions can easily be used in APEX region conditions or any other PL/SQL code. Install the package in your workspace’s schema or in a separate schema with execute granted to public and a public synonym on it.
You can download the whole package HERE.
I tested the package against a list of. 11,000 common user agent strings. The algorithm is not failsafe, but reasonably accurate. Of course I cannot test every client possible. Try the demo page I prepared and drop me an email if you believe the outcome of the package is not correct.
References:
Categorizr – A modern device detection script
User Agent String.Com
WURFL, the Wireless Universal Resource FiLe, is a Device Description Repository (DDR)
Hi Christian,
ReplyDeletegreat stuff!
What about a simple function which also can be called in a SELECT which gets the User-Agent-String as input and returns the category?
Hi Peter,
DeleteGood Idea. I'll have to rewrite the package a little bit. Coming soon, OK?!
Thanks Christian,
ReplyDeletethat comes in very handy for visitor analysis.
Peter,
DeleteThan I'd rather finish the other package, Browser, (or better: integrate both solutions) that you can find on the demo page. It offers a much more fine-grained information of the client, like browser brand, version and OS.
Very nice solution. Great work.
ReplyDeleteA great post. I liked it too.
ReplyDeleteHi Christian
ReplyDeleteAs mentioned, here is my blog post regarding my extension ideas
http://www.grassroots-oracle.com/2012/11/categorizr-for-apex-extended.html
Hi Scott,
ReplyDeleteHave a look at the demo page: I was working on a similar package. The problem is: you have to maintain the algorithm to keep up with browser brands and versions. The usage of the HTTP_USER_AGENT is a mess.
Cheers,
Christian
very informative post when i read i found the new things. thanks for sharing.PCEF on the Client Device
ReplyDeletevery informative post when i read i found the new things. thanks for sharing.PCEF on the Client Device
ReplyDeletehello all,
ReplyDeletewhere in the apex application is the best place (or way) to insert a command that determines the device? ideally i would like to use the login page for that purpose. would anywhere in front of the
:P101_USERNAME := apex_authentication.get_login_username_cookie;
be all right, in page 101? i wouldn't want to mess up any other functionality... any common practice or experience to be shared?
thanks and best regards,
-v-
Hi Tokke,
ReplyDeleteI would suggest to use page 0 for this.
When using page 101 AND public pages, you cannot be sure your command/process is executed.
regards,
Christian