Twitter: raymondcamden


Address: Lafayette, LA, USA

Tracking application usage with Flex Mobile

09-24-2011 6,861 views Mobile, Flex 2 Comments

On a web site, with good analytic software, it's possible to get estimates for how long the average user spends on your site. This week I was thinking about how one could do the same with a mobile application. In theory, it should be possible to get a precise figure. You know when your application starts and you know when it ends. Therefore, I just need to write the code to handle those events and persist the data somehow. I worked up a few examples here and I welcome any comments on them. I'd especially like to know if anyone is actually doing something like this with their apps now.

For my first iteration, I wrote an application that simply noticed when it began and ended, and on those events would write to a log file. The entire application consists of a few files. First, let's look at the top level ViewNavigatorApplication.

view plain print about
1<?xml version="1.0" encoding="utf-8"?>
2<s:ViewNavigatorApplication xmlns:fx="http://ns.adobe.com/mxml/2009"
3                            xmlns:s="library://ns.adobe.com/flex/spark" firstView="views.LogTestHomeView" initialize="initApp(event)">

4    <fx:Script>
5        <![CDATA[
6        import model.dbController;        
7        import mx.events.FlexEvent;
8        
9        protected function initApp(event:FlexEvent):void {
10            dbController.instance.addLog("opened");
11            NativeApplication.nativeApplication.addEventListener(Event.EXITING, myExiting);
12        }
13
14        protected function myExiting(event:Event):void {
15            dbController.instance.addLog("closed");
16            // Handle exiting event.
17        }
18        ]]>

19    </fx:Script>
20
21</s:ViewNavigatorApplication>

Not much here. You can see one method tied to the initialize event and one to the application exiting event. My dbController is a simple wrapper for database operations. I used a basic singleton approach (cribbed from an Adobe Cookbook recipe). Here's the file.

view plain print about
1package model {
2
3    import flash.data.SQLConnection;
4    import flash.data.SQLResult;
5    import flash.data.SQLStatement;
6    import flash.filesystem.File;
7
8    public class dbController {
9
10        private static var _instance:dbController = new dbController();
11        
12        private var dbFile:File;
13        private var dbCon:SQLConnection;
14        
15        public function dbController() {
16            dbFile = File.applicationStorageDirectory.resolvePath("log.db");
17            trace(dbFile.nativePath);
18            dbCon = new SQLConnection();
19            dbCon.open(dbFile);
20
21            //create default table
22            var sqlStat:SQLStatement = new SQLStatement();
23            sqlStat.text = "CREATE TABLE IF NOT EXISTS log(msg TEXT, timestamp TEXT)";
24            sqlStat.sqlConnection = dbCon;
25            sqlStat.execute();
26
27            if (_instance != null) {
28                throw new Error("dbController can only be accessed through dbController.instance");
29            }
30        }
31
32        public static function get instance():dbController {
33            return _instance;
34        }
35        
36        public function addLog(s:String):void {
37            var sqlStat:SQLStatement = new SQLStatement();
38            sqlStat.sqlConnection = dbCon;
39            sqlStat.text = "insert into log(msg, timestamp) values(:msg,:tst)";
40            sqlStat.parameters[":msg"] = s;
41            sqlStat.parameters[":tst"] = new Date();
42            sqlStat.execute();
43            trace("inserted: "+s);        
44        }
45
46        public function getLog():SQLResult {
47            trace("getting log");
48            var sqlStat:SQLStatement = new SQLStatement();
49            sqlStat.text = "select * from log";
50            sqlStat.sqlConnection = dbCon;
51            sqlStat.execute();
52            return sqlStat.getResult();
53            
54        }
55    }
56}

That's kind of a big file, but you can see it basically handles the database setup, the addition of a log message, and getting the log. (I could make this a bit nicer by making getLog simply return the data attribute. That way calling code could not worry about working with SQLResult objects.) So based on this code, you can see now that when my application starts and ends, it's going to log to the database. Now let's look at the view in my mobile app:

view plain print about
1<?xml version="1.0" encoding="utf-8"?>
2<s:View xmlns:fx="http://ns.adobe.com/mxml/2009"
3        xmlns:s="library://ns.adobe.com/flex/spark" title="HomeView" viewActivate="init(event)">

4    <fx:Script>
5        <![CDATA[
6            import model.dbController;
7            
8            import spark.events.ViewNavigatorEvent;
9            
10            protected function init(event:ViewNavigatorEvent):void {
11                var res:SQLResult = dbController.instance.getLog();
12                for(var i:int=0; i<res.data.length; i++) {
13                    log.text += res.data[i].msg + ' at ' + res.data[i].timestamp + '\n';
14                }
15            }
16            
17        ]]>

18    </fx:Script>
19    
20    <s:Label id="log" />
21        
22</s:View>

All we do here is simply get the log and display it. After running it a few times, here is what the app displays. (Note - the case of the messages changes about half way through. This happened when I changed the messages.)

So that was my first draft. I could - in theory - have used this to generate a "Total Time Used" value. I'd have to loop through the records and figure out the differences. That seemed like a lot of hard work so I decided on an easier version. Since I know when the app starts and ends - why not simply create a timestamp in the application itself? When the application ends, I can simply save the difference. Here's my new top level application:

view plain print about
1<?xml version="1.0" encoding="utf-8"?>
2<s:ViewNavigatorApplication xmlns:fx="http://ns.adobe.com/mxml/2009"
3                            xmlns:s="library://ns.adobe.com/flex/spark" firstView="views.LogTest2HomeView" initialize="initApp(event)">

4
5    <fx:Script>
6        <![CDATA[
7            import model.dbController;
8            
9            import mx.events.FlexEvent;
10            
11            private var beginTS:Date;
12            
13            protected function initApp(event:FlexEvent):void {
14                beginTS = new Date();
15                NativeApplication.nativeApplication.addEventListener(Event.EXITING, myExiting);
16            }
17            
18            protected function myExiting(event:Event):void {
19                var now:Date = new Date();
20                var duration:Number = (Math.floor(now.valueOf()/1000)) - (Math.floor(beginTS.valueOf()/1000));
21                trace("using duration of "+duration);
22                dbController.instance.addLog(duration);
23                // Handle exiting event.
24            }
25        ]]>

26    </fx:Script>
27    
28</s:ViewNavigatorApplication>

You can see now I've got a variable, beginTS, that acts as my startup timestamp. When the application ends, I get the difference, do some math, and store it as the number of seconds. Here's the new dbController. Notice I've tweaked the table structure a bit.

view plain print about
1package model {
2
3    import flash.data.SQLConnection;
4    import flash.data.SQLResult;
5    import flash.data.SQLStatement;
6    import flash.filesystem.File;
7
8    public class dbController {
9
10        private static var _instance:dbController = new dbController();
11        
12        private var dbFile:File;
13        private var dbCon:SQLConnection;
14        
15        public function dbController() {
16            dbFile = File.applicationStorageDirectory.resolvePath("log.db");
17            trace(dbFile.nativePath);
18            dbCon = new SQLConnection();
19            dbCon.open(dbFile);
20
21            //create default table
22            var sqlStat:SQLStatement = new SQLStatement();
23            sqlStat.text = "CREATE TABLE IF NOT EXISTS log(duration INTEGER,timestamp TEXT)";
24            sqlStat.sqlConnection = dbCon;
25            sqlStat.execute();
26
27            if (_instance != null) {
28                throw new Error("dbController can only be accessed through dbController.instance");
29            }
30        }
31
32        public static function get instance():dbController {
33            return _instance;
34        }
35        
36        public function addLog(dur:Number):void {
37            var sqlStat:SQLStatement = new SQLStatement();
38            sqlStat.sqlConnection = dbCon;
39            sqlStat.text = "insert into log(duration, timestamp) values(:dur,:tst)";
40            sqlStat.parameters[":dur"] = dur;
41            sqlStat.parameters[":tst"] = new Date();
42            sqlStat.execute();
43            trace("inserted: "+dur);        
44        }
45
46        public function getLog():SQLResult {
47            trace("getting log");
48            var sqlStat:SQLStatement = new SQLStatement();
49            sqlStat.text = "select * from log";
50            sqlStat.sqlConnection = dbCon;
51            sqlStat.execute();
52            return sqlStat.getResult();
53            
54        }
55
56        public function getTotalUsage():Number {
57            trace("getting duration");
58            var sqlStat:SQLStatement = new SQLStatement();
59            sqlStat.text = "select sum(duration) as total from log";
60            sqlStat.sqlConnection = dbCon;
61            sqlStat.execute();
62            var res:SQLResult = sqlStat.getResult();
63            return res.data[0].total;
64        }
65
66    }
67}

Along with changing the table structure, I've added a method, getTotalUsage, that returns the total number of seconds the application has been used. Now my front end view can display it:

view plain print about
1<?xml version="1.0" encoding="utf-8"?>
2<s:View xmlns:fx="http://ns.adobe.com/mxml/2009"
3        xmlns:s="library://ns.adobe.com/flex/spark" title="HomeView" viewActivate="init(event)">

4
5    <fx:Script>
6        <![CDATA[
7            import model.dbController;
8            
9            import spark.events.ViewNavigatorEvent;
10            
11            protected function init(event:ViewNavigatorEvent):void {
12                var total:Number = dbController.instance.getTotalUsage();
13                trace(total);
14                log.text = "You've used this application for "+total+" seconds.";
15            }
16            
17        ]]>
18    </fx:Script>
19    
20    <s:Label id="
log" />
21</s:View>

Which gives me this:

Technically the message should be something like, "In the past, you've used this application for..." but you get the point. So - any thoughts on this? If the application crashes it won't store anything, but that should be a rare event. If you really were concerned about that you could used a timed event to store a duration ever few minutes or so. If you want to play with my code, I've made FXPs from both projects and added them to a zip attached to this blog entry.

Download attached file

2 Comments

  • Commented on 09-24-2011 at 9:00 PM
    It wasn't for a mobile app, but I've used this in an AIR app before and it interacted with Google Analytics very nicely. http://code.google.com/p/gaforflash/
    Of course, it needed internet access for that, but perhaps you could store locally, then push to GA if/once you have an internet connection?
  • Commented on 09-26-2011 at 11:31 AM
    In theory you could run the GA events later, but I don't believe you can change the time stamps for them so they would not be accurate timewise.

Post Reply

Please refrain from posting large blocks of code as a comment. Use Pastebin or Gists instead. Text wrapped in asterisks (*) will be bold and text wrapped in underscores (_) will be italicized.

Leave this field empty