JSONP Mobile Handler for Pushing Targeted
Mobile TV Ads by Zip Code Radius & Lat/Long
by Bill SerGio

Introduction
I was imprssed by the really good articles people have posted for the Android
Contest here on CodeProject. In fact, I thought some of those articles were so
good that I almost decided not to enter any article but then I thought about
what Android developers might find really useful like collecting basic data
about the app, user location, etc. and making it avaialble throughout the app.
For Android I use Eclipse and build a Framework in Java in which I add a
CordovaWebView. My UI is either all HTML5 compoennts or a combination of HTML5
companents and an Android componet like an Action Bar. I need to be able to develop an app quickly and Java and
Cordova works best for me. Hopefully developers will find the sample code here
useful in making it easier to get started with things like delivering targeted
ads.
So in this article and sample code we will collect user data, store data using
xml and SQLite, send data to our server using JSONP, make data available
throughout the app in both Java and JavaScript. For monetization we will install
AdMob, and make Advertorial content available using JSONP.
You can find lots of sample Android mobile apps and source code on my website at:
http://www.SerGioApps.com
Collecting User Data
The first thing I do when I create an app is to collection information about the
user and the app, with the user's permission of course. So let's look at some of
the basic inforamtion we will collect in our Java code:
· App ID (Unique for each mobile device)
· App Name
· App Version (version Code | Ver. Name)
· SDK
· Device Name
· Phone Number
· Email
· City
· State
· Country
· Postal Code
· Latitude
· Longitude
· First Run Trigger
We will collect user data and pass it up to our server and make this data
avaiilable throughout our Java Code and our JavaScript Code.
package com.sergioapps.installation; import android.accounts.Account; import android.accounts.AccountManager; import android.content.Context; import android.content.SharedPreferences; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.content.pm.PackageManager.NameNotFoundException; import android.os.Build; import android.provider.Settings.Secure; import android.telephony.TelephonyManager; import android.util.Patterns; import java.io.UnsupportedEncodingException; import java.util.UUID; import java.util.regex.Pattern; public class DeviceUuidFactory { protected static final String PREFS_FILE = "device_id.xml"; protected static final String PREFS_DEVICE_ID = "device_id"; protected volatile static UUID uuid; protected static final String PREFS_APP_VERSION ="app_version"; protected volatile static String appversion; protected static final String PREFS_SDK ="sdk"; protected volatile static String sdk; //Android|.... protected static final String PREFS_DEVICE_NAME ="dn"; protected volatile static String dn; protected static final String PREFS_CITY ="city"; protected volatile static String city; protected static final String PREFS_STATE ="state"; protected volatile static String state; protected static final String PREFS_CTRY ="ctry"; protected volatile static String ctry; protected static final String PREFS_PC ="pc"; protected volatile static String pc; protected static final String PREFS_PH ="ph"; protected volatile static String ph; protected static final String PREFS_EMAIL ="email"; protected volatile static String email; protected static final String PREFS_LAT ="lat"; protected volatile static String lat; protected static final String PREFS_LNG ="lng"; protected volatile static String lng; protected volatile static String fr = "0"; //fr = firstRun protected volatile static GPSTracker gps = null; public DeviceUuidFactory(Context context) { final SharedPreferences prefs = context.getSharedPreferences(PREFS_FILE, 0); if (uuid == null) { synchronized (DeviceUuidFactory.class) { if (uuid == null) { final String id = prefs.getString(PREFS_DEVICE_ID, null); if (id != null) { // Use ids previously computed and stored in the prefs file uuid = UUID.fromString(id); fr = "0"; } else { fr = "1"; //This is the first installation! final String androidId = Secure.getString(context.getContentResolver(), Secure.ANDROID_ID); // Use Android ID unless it's broken, in which case fallback on deviceId, // unless not available, fallback to random number which we store in prefs file try { if (!"9774d56d682e549c".equals(androidId)) { uuid = UUID.nameUUIDFromBytes(androidId.getBytes("utf8")); } else { final String deviceId = ((TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE)).getDeviceId(); uuid = deviceId != null ? UUID.nameUUIDFromBytes(deviceId.getBytes("utf8")) : UUID.randomUUID(); } } catch (UnsupportedEncodingException e) { throw new RuntimeException(e); } // Write the value out to the prefs file prefs.edit().putString(PREFS_DEVICE_ID, uuid.toString()).commit(); } } } } //////////////////////////////////////////////////////////////////////////////////////////////// if (appversion == null) { synchronized (DeviceUuidFactory.class) { if (appversion == null) { final String _appversion = prefs.getString(PREFS_APP_VERSION, null); if (_appversion != null) { appversion = _appversion; } else { try { PackageManager manager = context.getPackageManager(); PackageInfo info = manager.getPackageInfo(context.getPackageName(),0); int code = info.versionCode; String strCode = Integer.toString(code); String name = info.versionName; //decimal !!! // Compare with values on the server to see if there is a new version appversion = strCode + "|" + name; } catch (NameNotFoundException e) { e.printStackTrace(); appversion = "0|0"; } prefs.edit().putString(PREFS_APP_VERSION, appversion).commit(); } } } } //////////////////////////////////////////////////////////////////////////////////////////////// if (sdk == null) { synchronized (DeviceUuidFactory.class) { if (sdk == null) { final String _sdk = prefs.getString(PREFS_SDK, null); if (_sdk != null) { sdk = _sdk; } else { int version = 0; try { version = Integer.valueOf (android.os.Build.VERSION.SDK_INT); sdk = Integer.toString(version); } catch (NumberFormatException e) { sdk = "0"; } prefs.edit().putString(PREFS_SDK, sdk).commit(); } } } } //////////////////////////////////////////////////////////////////////////////////////////////// // Device Name //////////////////////////////////////////////////////////////////////////////////////////////// //Samsung GT-S5830L,Motorola MB860,Sony Ericsson LT18i,LGE LG-P500,HTC Desire V,HTC Wildfire S A510e if (dn == null) { synchronized (DeviceUuidFactory.class) { if (dn == null) { final String _dn = prefs.getString(PREFS_DEVICE_NAME, null); if (_dn != null) { dn = _dn; } else { dn = getDeviceName(); prefs.edit().putString(PREFS_DEVICE_NAME, dn).commit(); } } } } //////////////////////////////////////////////////////////////////////////////////////////////// // Phone Number if (ph == null) { synchronized (DeviceUuidFactory.class) { if (ph == null) { final String _ph = prefs.getString(PREFS_PH, null); if (_ph != null) { ph = _ph; } else { try { TelephonyManager tMgr = (TelephonyManager)context.getSystemService(Context.TELEPHONY_SERVICE); ph = tMgr.getLine1Number(); } catch (Exception e) { e.printStackTrace(); ph = "000-000-0000"; } prefs.edit().putString(PREFS_PH, ph).commit(); } } } } //////////////////////////////////////////////////////////////////////////////////////////////// if (email == null) { synchronized (DeviceUuidFactory.class) { if (email == null) { final String _email = prefs.getString(PREFS_EMAIL, null); if (_email != null) { email = _email; } else { Pattern emailPattern = Patterns.EMAIL_ADDRESS; // API level 8+ try { Account[] accounts = AccountManager.get(context).getAccounts(); for (Account account : accounts) { if (emailPattern.matcher(account.name).matches()) { email = account.name; } } } catch (Exception ex) { email = ""; } prefs.edit().putString(PREFS_EMAIL, email).commit(); } } } } //////////////////////////////////////////////////////////////////////////////////////////////// // postalCode plus "Zip Radius" is the most important data for targeting user ads if (pc == null) { synchronized (DeviceUuidFactory.class) { if (pc == null) { final String _lat = prefs.getString(PREFS_LAT, null); final String _lng = prefs.getString(PREFS_LNG, null); final String _city = prefs.getString(PREFS_CITY, null); final String _state = prefs.getString(PREFS_STATE, null); final String _ctry = prefs.getString(PREFS_CTRY, null); final String _pc = prefs.getString(PREFS_PC, null); if (_pc != null) { lat = _lat; lng = _lng; city = _city; state = _state; ctry = _ctry; pc = _pc; } else { // If we don't have the PostalCode, i.e., pc, then we set the firstRun flag to true or "1" fr = "1"; try { gps = new GPSTracker(context); if(gps.canGetLocation()){ lat = Double.toString(gps.getLatitude()); lng = Double.toString(gps.getLongitude()); city = gps.getCityStr(); state = gps.getStateStr(); ctry = gps.getCtryStr(); pc = gps.getPCStr(); }else{ // can't get location GPS or Network is not enabled // Ask user to enable GPS/network in settings? //gps.showSettingsAlert(); }; } catch (Exception e) { e.printStackTrace(); } finally { gps.stopUsingGPS(); } prefs.edit().putString(PREFS_LAT, lat).commit(); prefs.edit().putString(PREFS_LNG, lng).commit(); prefs.edit().putString(PREFS_CITY, city).commit(); prefs.edit().putString(PREFS_STATE, state).commit(); prefs.edit().putString(PREFS_CTRY, ctry).commit(); prefs.edit().putString(PREFS_PC, pc).commit(); } } } } //////////////////////////////////////////////////////////////////////////////////////////////// } /** * Returns a unique UUID for current android device. As with all UUIDs, * this unique ID is "highly likely" to be unique across all Android * devices. Much more so than ANDROID_ID is. * The UUID is generated by using ANDROID_ID as the base key if appropriate, * falling back on TelephonyManager.getDeviceID() if ANDROID_ID is known to * be incorrect, and finally falling back on a random UUID that's persisted * to SharedPreferences if getDeviceID() does not return a usable value. * In some rare circumstances, this ID may change. In particular, if the * device is factory reset a new device ID may be generated. In addition, if * a user upgrades their phone from certain buggy implementations of Android * 2.2 to a newer, non-buggy version of Android, the device ID may change. * Or, if a user uninstalls your app on a device that has neither a proper * Android ID nor a Device ID, this ID may change on reinstallation. * Note that if the code falls back on using TelephonyManager.getDeviceId(), * the resulting ID will NOT change after a factory reset. Something to be aware of. * Works around a bug in Android 2.2 for many devices with using ANDROID_ID directly. * @return a UUID that may be used to uniquely identify your device for most purposes. */ public UUID getDeviceUuid() { return uuid; } public String getFirstRun() { return fr; } public String getAppData() { //first_run = Boolean.FALSE; String appdata; appdata = "appid="+uuid.toString() + "&appname=WW01" + "&appversion="+appversion + "&sdk="+sdk + "&dn="+dn + "&ph="+ph + "&email="+email + "&city="+city + "&state="+state + "&ctry="+ctry + "&pc="+pc + "&lat="+ lat + "&lng=" + lng + "&fr="+fr + "&methodName=installData" + "&jsonp=onRSSLoaded"; return appdata; } public String getDeviceName() { String manufacturer = Build.MANUFACTURER; String model = Build.MODEL; if (model.startsWith(manufacturer)) { return capitalize(model); } else { return capitalize(manufacturer) + " " + model; } } private String capitalize(String s) { if (s == null || s.length() == 0) { return ""; } char first = s.charAt(0); if (Character.isUpperCase(first)) { return s; } else { return Character.toUpperCase(first) + s.substring(1); } } }
After we collect this user data we need to pass it to our server and to pass it
to our JavaScript UI so that we can retrieve ads and videos targeted to the
user's Postal Code with some "Zip Radius." There are several ways to do this. We
could upload this data from Java using Post to a service or, the method I
prefer, is to call a C# Hamdler which works really nicely from Java or
JavaScript. We can do this as follows.
public class HelloWorld extends CordovaActivity { private static final String AD_UNIT_ID = "ca-app-pub-000000000000000000000000000"; private AdView adView; JavaScriptInterface jv; public Installation myFactory; public String _appid = ""; String appdata = ""; public DeviceUuidFactory myGuid; public String _firstRun = "0"; public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); // Crashes if appView is not inititalized !!! super.init(); try { // Initialize our factory myGuid = new DeviceUuidFactory(this); // Get our "appdata" url parameter string appdata = myGuid.getAppData(); // FirstRun parameter _firstRun = myGuid.getFirstRun(); } catch (Exception e) { e.printStackTrace(); } setContentView(this.root); super.appView.getSettings().setJavaScriptEnabled(true); jv = new JavaScriptInterface(this, appView); appView.addJavascriptInterface(jv, "JavaCallback"); // Load or HTML5 UI & Pass our appdata as a URL parameter string super.loadUrl("file:///android_asset/www/init.html?" + appdata); super.onResume(); // Create our AdMob Ad so we cam amke more money on views! createAdMob(this); if(_firstRun == "1"){ // If this is the first run send data to our JSONP Handler try{ String url = "http://www.YOUR_DOMAIN.com/rsshandler.ashx"; WebView webview = new WebView(this); byte[] post = EncodingUtils.getBytes(appdata, "BASE64"); webview.postUrl(url, post); } catch(Exception e){ e.printStackTrace(); } } } }
Local Storage in Our JavaScript for Our User Data
Now that we have passed the data as URL parameters into our "init.html" file
let's look at how we store it so we can use it throughout our Javascript.
Below is how we retrieve the data passed to JavaScript from our Java Code and
how we store the data using local storage.
var AppItem = function (appid, appname, appversion, sdk, dn, ph, email, city, state, ctry, pc, lat, lng, fr, authorized, id) { this.appid = appid || "00000000-0000-0000-0000-000000000000"; this.appname = appname || "WW01"; this.appversion = appversion || ""; this.sdk = sdk || ""; this.dn = dn || ""; this.ph = ph || "000-000-0000"; this.email = email || ""; this.city = city || ""; this.state = state || ""; this.ctry = ctry || ""; this.pc = pc || ""; this.lat = lat || "0.0"; this.lng = lng || "0.0"; this.fr = fr || "0"; this.authorized = authorized || ""; this.id = id || "appdata"; }; function onDeviceReady() { // Loads data from Java Code into LocalStorage LoadLocalStorage(); // This loads the SQLite Plugin from JavaScript //LoadSQLitePlugin(); window.location = "index.html"; } //end function function LoadLocalStorage() { var allVars = ""; var _appid = "00000000-0000-0000-0000-000000000000"; var _appname = "WW01"; var _appversion = ""; var _sdk = ""; var _dn = ""; var _ph = ""; var _email = ""; var _city = ""; var _state = ""; var _ctry = ""; var _pc = ""; var _lat = "0.00"; var _lng = "0.00"; var _fr = "0"; try { //allVars = $.getUrlVars(); _appid = $.getUrlVar('appid'); _appname = $.getUrlVar('appname'); _appversion = $.getUrlVar('appversion'); _sdk = $.getUrlVar('sdk'); _dn = $.getUrlVar('dn'); _ph = $.getUrlVar('ph'); _email = $.getUrlVar('email'); _city = $.getUrlVar('city'); _state = $.getUrlVar('state'); _ctry = $.getUrlVar('ctry'); _pc = $.getUrlVar('pc'); _lat = $.getUrlVar('lat'); _lng = $.getUrlVar('lng'); _fr = $.getUrlVar('fr'); } catch (o) { } // For backup only! localStorage["appid"] = _appid; if (localStorage["authorized"]) { _authorized = localStorage["authorized"]; } else { _authorized = ""; localStorage["authorized"] = _authorized; } // Create an instance of Cache using localStorage as our storage // container and the native JSON object as our serializer. var cache = new Cache(localStorage, JSON); if (cache.hasItem("appdata")) { //alert(cache.getItem("appdata").appid); } else { if (localStorage["authorized"]) { _authorized = localStorage["authorized"]; } else { _authorized = ""; localStorage["authorized"] = _authorized; } _fr = "1"; var appItem = new AppItem(_appid, _appname, _appversion, _sdk, _dn, _ph, _email, _city, _state, _ctry, _pc, _lat, _lng, _fr, _authorized, "appdata"); cache.setItem("appdata", appItem); } //alert("AppID: " + cache.getItem("appdata").appid); //var appItem = cache.getItem("appdata"); //appItem.areacode = "qqq"; //cache.setItem("appdata", appItem) //alert(cache.getItem("appdata").areacode); }
Introducing JSONP or JSON-P
The sample with this article I kept simple and focused on illustrating the use of a JSONP
Handler to make transfer of data from Java or JavaScript to our server database.
In rder to deliver RSS Feeds or Ads of any kinds like TV commercials to our
mobile app that have VALUE for the user we need to send some user data up to our
JSONP Handler like Zip Code, Zip Code Radius, and key words related to the
user's interests. One simple way to deliver targeted content of value to
your app's users is to include RSSFeeds. There are millions of RSS Feeds so by just setting the feeds to a particular Theme such as heath, sports, money, blogs, government grants, etc., you have created a new mobile app. If I only retrieve feeds in my app on "Dieting" then I have created the "Diet App," or of I only retrieve Government Grants feeds (the U.S. government has over one million such feeds), the I have created the Government Grants Mobile App with the same RSS reader code. For most of these feeds you can use a JSONP helper service such as Google or others. But for certain cusom RSS feeds to deliver aggregate content and TV Ads to mobile apps you can't use services like Google because they cache the responses and you have no control over the caching mechism.
There are plenty of samples of JSONP on CodeProject and the web of using PHP to return JSONP Feeds but I wanted to use a .NET Handler on my Wondows Server. So this article is about using a .NET Handler to retrieve your own, custom, non-standard JSONP RSS Feeds in a PhoneGap Cordova Mobile that support paging and Categories in Categories. Included is a "www" folder of the html5 code and SQL script to create the RSSFeeds database. The sample code also requires you have already installed Microsoft's Northwindd Database.
The sample PhoneGap Cordova mobile app demonstrates how to retrieve a list of hundreds of Categories, and when a user clicked on an item in that list it would, in turn, display a new list of hundreds of items, and when a user clicked on one of those items it would, in turn, display a list of hundreds of items and so on and so on for as many levels deep as you wanted.
For each list of hundreds of items we display 50 items at a time and we provide "Prev" and "Next" buttons so the user can move though hundreds of items 50 items at a time. And when a user clicks on any item we want to have the option of any one these actions occuring: occuring:
To accomplish this only requires one SQL table and a modified RSS JSONP Feed on your server to side step the same-origin policy. The same-origin policy in browsers requires certain types of data via JavaScript are restricted to where the target resource's domain is identical to the page making the request. This policy protects users from unsafe malicious JavaScript. In PhoneGap Cordova Apps we use Cross-domain Ajax which refers to the idea of making requests across domains in opposition to the same-origin restriction. However, cross-domain Ajax is not inherently unsafe and is used in the most popular and useful apps. One mechanism which can request content cross-domain is the <script> tag. In December 2005, Bob Ippolito formally proposed JSONP (later dubbed JSON-P, or JSON-with-padding) as a way to leverage this property of <script> tags to be able to request data in the JSON format across domains. Read Bob's idea at: http://bob.ippoli.to/archives/2005/12/05/remote-json-jsonp/
JSON-P works by making a <script> element (either in HTML markup or inserted into the DOM via JavaScript), which requests to a remote data service location. The response (the loaded "JavaScript" content) is the name of a function pre-defined on the requesting web page, with the parameter being passed to it being the JSON data being requested. When the script executes, the function is called and passed the JSON data, allowing the requesting page to receive and process the data. I won't go into a discuss here of all the methods used or issues related to security since there are plenty of articles on those tpics here on CodeProject already. This article and sample PhoneGap Cordova Mobile App "www" folder will illustrate one way to implement your own JSONP Modified RSS Feed System on your server for mobile apps using JSONP instead of XML and WITHOUT using Google's jsonp that caches searches which can be a big problem. This doesn't eliminate the use of Google, YouTube, and other services but only adds another tool in your mobile tool belt.
The RSS JSONP Handler
The design concept I used was that instead of having multiple generic handlers or a single handler with lots of switch/if statement I decided to use a single generic handler with Factory design pattern. The Factory returns a class based upon the methodName that is passed which is used to only handle that request. The Factory reads the methodName and its class from the web.config file and instantiates the handler. The generic handler requests the Factory for the handler and performs some pre/post processing. The code of the ProcessRequest method of the rsshandler.ashx file is as follows:
public void ProcessRequest (HttpContext context) { BaseHandler handler = null; try { handler = HandlerFactory.CreateHandler(context); if (handler != null) { handler.Execute(); string output = string.Empty; string callbackMethodName = context.Request.Params["jsonp"]; if (!string.IsNullOrEmpty(callbackMethodName)) { string callback = string.Format(CultureInfo.CurrentCulture, "{0}({1}, '{2}');", callbackMethodName, handler.Output, handler.MethodName); output += callback; } else { output = string.Format(CultureInfo.CurrentCulture, "var {0} = {1};", handler.MethodName, handler.Output); } context.Response.ContentType = "application/x-javascript"; context.Response.Write(output); } else { context.Response.StatusCode = 404; } } finally { if (handler != null) { handler.Dispose(); } } }
The CreateHandler Method
The Factory gets the request for the generic handler and executes the handler if it exists. Then, depending upon if the callback is specified or not, the result is formatted. Finally it sets the response content type and returns the response. A 404 error is thrown if the handler is not found. The Factory code reads the config file and creates the handler using reflection in the CreateHandler method as follows:
public static class HandlerFactory { private static readonly Assembly _currentAssembly = Assembly.GetExecutingAssembly(); public static BaseHandler CreateHandler(HttpContext context) { string methodName = context.Request.Params["methodName"]; if (!string.IsNullOrEmpty(methodName)) { HandlerMapSection settings = (HandlerMapSection)ConfigurationManager.GetSection(HandlerMapSection.SectionName); if (settings != null) { HandlerMap map = settings.Maps[methodName]; if (map != null) { BaseHandler handler = (BaseHandler)_currentAssembly.CreateInstance(map.TypeName, false, BindingFlags.CreateInstance, null, new object[] { context }, System.Globalization.CultureInfo.CurrentCulture, null); return handler; } } } return null; } }
To add a new method we just create a new class inherited from BaseHandler and make an addition to our config file as follows.
<configSections> <section name="handlerMapping" type="HandlerMapSection"/> </configSections> <handlerMapping> <map methodName="getCustomerList" typeName="CustomerListHandler"/> <map methodName="Customers" typeName="CustomerHandler"/> <map methodName="Feeds" typeName="FeedsHandler"/> </handlerMapping>
Paging the RSS JSONP Feeds
One of the things i needed to add was paging through a record set for the JSONP feeds. By simply clicking the Prev and Next buttons you can page back and forward.
public static class DataHelper { private static readonly string connRSSFeeds = ConfigurationManager.ConnectionStrings["RSSFeeds"].ConnectionString; private static readonly string connNorthwind = ConfigurationManager.ConnectionStrings["Northwind"].ConnectionString; public static ListGetFeeds(string cat, int start, int max) { List list = new List (); const string SQL = "SELECT [FeedId], " + " [category], " + " [title], " + " [author], " + " [link], " + " [shortDescription], " + " [description], " + " [image], " + " [publishedDate], " + " [rank] " + "FROM " + " ( " + " SELECT [FeedId], " + " [category], " + " [title], " + " [author], " + " [link], " + " [shortDescription], " + " [description], " + " [image], " + " [publishedDate], " + " [rank], " + " ROW_NUMBER() OVER (ORDER BY [rank] DESC, " + " [publishedDate] DESC) AS [RowIndex] " + " FROM [RSSFeeds].[dbo].[Feeds] WHERE ([category] = '{0}') " + " ) AS [RSSFeedsWithRowIndex] " + "WHERE ([category] = '{0}') " + "AND ([RowIndex] > {1}) " + "AND ([RowIndex] <= ({1} + {2})) "; if (max <= 0) { max = 50; } List feeds = new List (); using (IDbConnection cnn = CreateConnection(connRSSFeeds)) { using (IDbCommand cmd = cnn.CreateCommand()) { cmd.CommandText = string.Format(SQL, cat, start, max); using (IDataReader rdr = cmd.ExecuteReader()) { while (rdr.Read()) { RSSFeed f = new RSSFeed(); f.FeedId = rdr.IsDBNull(0) ? Guid.Empty.ToString() : Convert.ToString(rdr.GetGuid(0)); f.category = rdr.IsDBNull(1) ? string.Empty : rdr.GetString(1).Trim(); f.title = rdr.IsDBNull(2) ? string.Empty : rdr.GetString(2).Trim(); f.author = rdr.IsDBNull(3) ? string.Empty : rdr.GetString(3).Trim(); f.link = rdr.IsDBNull(4) ? string.Empty : rdr.GetString(4).Trim(); f.shortDescription = rdr.IsDBNull(5) ? string.Empty : rdr.GetString(5).Trim(); f.description = rdr.IsDBNull(6) ? string.Empty : rdr.GetString(6).Trim(); f.image = rdr.IsDBNull(7) ? string.Empty : rdr.GetString(7).Trim(); f.publishedDate = rdr.IsDBNull(8) ? string.Empty : rdr.GetDateTime(8).ToLongDateString(); f.rank = rdr.IsDBNull(9) ? int.MinValue : rdr.GetInt32(9); list.Add(f); } } } } if (list.Count == 0) { return null; } return list; }
Paging the Northwind Database
In addition, I also included a sample of one method to page through the customer records in the Northwind database in a PhoneGap Cordova Mobile App. By simply clicking the Prev and Next buttons you can page back and forward. This assumes that you already have the Northwind database installed on your server.
Our AJAX Call Isn't To Google!!
I can understand why Google cahches requets but for many of the mobile apps I created for clients this wasn't acceptable. So I decided to use a Handler and JSONP to create my own RSS Feeds on my own server. Just as a note, keep in mind that in a mobile app where te user presses a button to initiate a feed request the volume of requests will be substantially less than where an app would make such a request whenever it is turned on. And where that is the case this simple approach presented has worked nicely.
function GetRSSAds() { var rss_ads = store.get('rss_ads'); if (typeof rss_ads == 'undefined') { store.set('rss_ads', { cat: 'new', start_index: 0 }); rss_ads = store.get('rss_ads'); } if (rss_ads.cat.length < 1) { store.set('rss_ads', { cat: 'new', start_index: 0 }); rss_ads = store.get('rss_ads') } var _cat = rss_ads.cat; var _start = rss_ads.start_index; var _max = 50; // WE can pass an empty postalCode which means national ads and RSS Feeds var _pc = ""; var cache = new Cache(localStorage, JSON); if (cache.hasItem("appdata")) { _pc = cache.getItem("appdata").pc; } // If we set an empty radius for zip code we return that zip code var _rad = ""; //NOTE: We are NOT using Google which is BAD because they CACHES! //http://ajax.googleapis.com/ajax/services/feed/etc. // var url = 'http://www.your_website.com/rsshandler.ashx?cat=' // + _cat + '&start=' + _start + '&max=' + _max + // '&methodName=Feeds&jsonp=onRSSLoaded'; var url = 'rsshandler.ashx?' + 'cat=' + _cat + // category is drill down key words 'key=' + _key + // key words pipe delimted key groups 'pc=' + _pc + // pc = PostalCode 'rad=' + _rad + // Zip Code RAdius '&start=' + _start + '&max=' + _max + '&methodName=Feeds&jsonp=onRSSLoaded'; $.ajax({ type: 'GET', url: url, async: false, contentType: "application/json", dataType: 'jsonp' }); }
The RSSFeeds Database & Feeds Table
The table below created with the SQL script for this article shows how we can create a category in a category. If we put the word "category" in the category field itself then we will load a list of categories and in the link field if we have say, "#category|movies" then clicking on the "Go" button in the scrolling list will load all items from this same table with "movies" in the category field. Of course, you can customize this any way you want. What is include here is only one way of doing this.
I decided that I wanted an action based on image click or image touchend and not on clicking a list row which can intefere with the user swiping the list up or down. Please note that the special category of "#category" is handled by re-loading the page and passing the new value of "category" from the image to the reloaded page. The image click in each row is handled as follows:
$(document).delegate('#mainPage .rounded-img', 'click touchend', function (e, data) { event.preventDefault(); var zid = $(this).parents('[data-role=listview] li').data("url"); var _desc = $(this).parents('[data-role=listview] li').data("desc"); // We store 2 pieces of data in "url" parameter, i.e., the "link" field in SQL // Separated by a "|" symbol //#search_users|User_Id //#web|website //#categopry|"category value from table" i.e., triggers a reload of the page //#yt_video|YouTube Video ID var _type = ''; var _link = ''; if (zid.indexOf("|") > -1) { try { _type = zid.split('|')[0]; _link = zid.split('|')[1]; } catch (e) { _type = ''; _link = ''; } if ((_type == '') || (_link == '')) { event.stopPropagation(); return; } else if (_type == '#search_users') { //YouTube User ID is passed so we list of user's uploaded videos _type = "search_users"; store.set('yt_criteria', { type: _type, start_index: 1, qvalue: _link, nb_display: 'std' }); //window.location = "./youtube.html"; alert("YouTube Data is:\nSearch Type: " + _type + "\nYouTube UserID: " + _link); } else if (_type == '#yt_video') { //The ID of a single YouTube video was passed to play _type = "yt_video"; store.set('yt_video', { type: _type, start_index: 0, qvalue: _link, nb_display: _desc }); localStorage.setItem('videoid', _link); localStorage.setItem('videodesc', _desc); window.location = "./videoplayer.html"; } else if (_type == '#web') { window.open(_link, "_blank"); } else if (_type == '#category') { //Loads a new category store.set('rss_ads', { cat: _link, start_index: 0 }); location.reload(); } // end of: zid.indexOf } });
Some Final Thoughts
Custom RSS Feeds allow you to create with minimal code dozens of different theme apps and to deliver to users information and video ads that don't interfere with AdMob ads. There are many ways to implement custom JSONP and this article is intended to illustrate just one of those ways for PhoneGap Cordova apps.