×
Namespaces

Variants
Actions
Revision as of 07:42, 19 July 2013 by hamishwillee (Talk | contribs)

(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)

Hybrid - Applications for Mobile

From Nokia Developer Wiki
Jump to: navigation, search

This article explains how to develop cross platform/hybrid applications using HTML5, jQuery and CSS.

WP Metro Icon Porting.png
SignpostIcon XAML 40.png
WP Metro Icon WP8.png
SignpostIcon WP7 70px.png
Article Metadata
Code ExampleTested with
SDK: Windows Phone 7.1
Devices(s): Nokia Lumia 710
Compatibility
Device(s): must have camera
Article
Keywords: Hybrid Application Windows Phone 7, HTML 5, jQuery and CSS
Created: pavan.pareta (29 Jun 2012)
Last edited: hamishwillee (19 Jul 2013)

Contents

Introduction

In this article we will learn how to develop a hybrid application for multiple mobile platforms such as Symbian, Windows Phone7, iOS and BlackBarry etc. In this article I have targeted Nokia's Lumia Windows Phone7 platform for developing a hybrid application. Hybid application basically is a power of HTML5, JavaScript and CSS3 as well as Native API for cross platform.

General

Before moving ahead to Hybrid application development lets introduce ourselves to basic fundamental features of Hybrid architecture for Hybrid Mobile Application Development.

002 hybrid design architecture.png

Hybrid architecture:- Hybrid architecture basically allows developer to use composite technology to use; in a single application such as HTML5, CSS3, JavaScript and Native mobile platform functionality (combining native components and web components). That is called Hybrid Mobile application. The core advantage of Hybrid architecture to use in mobile application is RAD (Rapid Application Development), reusability of code; make consistent UX (User Experience) on multiple mobile platforms using a single codebase; all most code can be reused and shared across multiple mobile platforms. Good integration with device native services. That can be support online/offline operations.

003 - hybrid design architecture.png

HTML5:- HTML5 standard for Hypertext markup language, HTML 5 is a core technology presenting contents over the web. HTML5 is the latest version of HTML.

CSS3:- CSS standard for Cascading Style Sheets provide the detailed formatting instructions for web pages. CSS3 is the latest version of it.

JavaScript:-JavaScript is an interpreted language used to program intelligence into client browser. To display dynamic controls, popping up /alert dialog etc.

SPA: - SPA is standard for (Single Page Application); SPA is a method for developing web application for mobile browser that allows better UX (User Experience), fast UI navigation, it downloads all require data in client-side in first request and then store it in a browser’s cache.


Baseline

Here we are covering and integration of HTML5 in mobile native applications embedding HTML5 in Windows Phone application. Here we are covering following features:-

  • Camera
  • Single Page App
  • Canvas
  • Geo-location
  • Accelerometer

Important points to remember while developing/designing web pages for mobile devices by using mobile jQuery framework (HTML5, jQuery and CSS).

  1. The <!DOCTYPE> declaration, must be declare before the <html> tag. <!DOCTYPE html>
  2. Use Meta tag for small device screen control.
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    Here: view port is a virtual Windows which control the web page size and scaling/zoom.
  3. Must have the same version of jQuery and CSS.
    In this application, I have embedded a few html pages, which will be used locally in the Windows Phone 7 application.

Step 1: Creating Windows Phone application using traditional way.

Create a Windows Phone 7 project, using traditional way File -> New Project -> Select template Silverlight for Windows Phone7 and choose “Windows Phone Application” 004 HMA vs project.png

Step 2: Creating a directory

  • apphtml – directory will hold all the HTML5 pages.
  • apphtml/jsAndcss directory will hold JavaScript and CSS in the project.

005 HMA vs solution.png

Step 3: Adding a Phone WebBrowser control to Mainpage.xaml

Add a WebBrowser control in the grid layout root. To render all HTML pages into phone web browser control.

See the following Xaml code to add the phone web browser control and set the required properties and event. Such as IsGeolocationEnabled = true, IsScriptEnabled = true and ScriptNotify event. IsGeolocationEnabled property basically allows to get current geo location (latitude and longitude) and IsScriptEnabled property allows to execute JavaScript in the phone web browser control, by default it is disabled and ScriptNotify event occurs when Javascript calls.

XAML code snippet:

<Grid x:Name="LayoutRoot" Background="Transparent">
<phone:WebBrowser IsGeolocationEnabled="True"
Source="apphtml/myPage.html"
IsScriptEnabled="True"
ScriptNotify=" webBrowser1_ScriptNotify"
Name="webBrowser1"
Height="auto" Width="auto" />
</Grid>

Design view user interface

006 HMA design view.png

Step 4: Creating HTML5 pages using jQuery Mobile and css. In this project I have developed following html pages for each feature. I have used the following directory structure to keep all HTML pages into /apphtml directory and Javascript and css with in /apphtml/jsAndcss directory.

/apphtml

  • Default.html
  • MobileCamera.html
  • Two.html
  • Canvas.html
  • GeoLocation.html
  • Accelerometer.html


/apphtml/jsAndcss

  • jquery-1.6.4.min.js
  • jquery.mobile-1.0.min.css
  • jquery.mobile-1.0.min.js
  • MobileAppStyles.css


Here is mobile jQuery files available here

- Default.html

<!DOCTYPE html>
<html class="ui-mobile">
<head>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title>Hybrid App</title>
<!-- Js -->
<link rel="stylesheet" href="jsAndcss/jquery.mobile-1.0.min.css" />
<script type="text/javascript" src="jsAndcss/jquery-1.6.4.min.js"></script>
<script type="text/javascript" src="jsAndcss/jquery.mobile-1.0.min.js"></script>
</head>
<body class="ui-mobile-viewport">
<!-- first page: one -->
<div data-role="page" id="one" data-url="one" tabindex="0" class="ui-page ui-body-c ui-page-active"
style="min-height: 200px;">
<!-- start page header section -->
<div data-role="header" class="ui-header ui-bar-a" role="banner">
<h1 class="ui-title" tabindex="0" role="heading" aria-level="1">
Main Menu</h1>
</div>
<!-- end page header section -->
<!-- start page content section -->
<div data-role="content" class="ui-content" role="main">
<h2>
HTML 5</h2>
<p>
<a href="MobileCamera.html" rel="external" data-role="button" data-theme="c" class="ui-btn ui-btn-corner-all ui-shadow ui-btn-up-c">
<span class="ui-btn-inner ui-btn-corner-all"><span class="ui-btn-text">Camera</span></span></a></p>
<p>
<a href="two.html" rel="external" data-role="button" data-theme="c" class="ui-btn ui-btn-corner-all ui-shadow ui-btn-up-c">
<span class="ui-btn-inner ui-btn-corner-all" aria-hidden="true"><span class="ui-btn-text">
Single page</span></span></a>
</p>
<p>
<a href="GeoLocation.html" rel="external" data-role="button" data-theme="c" class="ui-btn ui-btn-corner-all ui-shadow ui-btn-up-c">
<span class="ui-btn-inner ui-btn-corner-all" aria-hidden="true"><span class="ui-btn-text">
Geo-Location</span></span></a>
</p>
<p>
<a href="Canvas.html" rel="external" data-role="button" data-theme="c" class="ui-btn ui-btn-corner-all ui-shadow ui-btn-up-c">
<span class="ui-btn-inner ui-btn-corner-all" aria-hidden="true"><span class="ui-btn-text">
Canvas</span></span></a></p>
<p>
<a href="Accelerometer.html" rel="external" data-role="button" data-theme="c" class="ui-btn ui-btn-corner-all ui-shadow ui-btn-up-c">
<span class="ui-btn-inner ui-btn-corner-all" aria-hidden="true"><span class="ui-btn-text">
Accelerometer</span></span></a></p>
</div>
<!-- end page content section -->
<!-- start page footer section -->
<div data-role="footer" data-theme="a" class="ui-footer ui-bar-d" role="contentinfo">
<h4 class="ui-title" tabindex="0" role="heading" aria-level="1">
@WmDev</h4>
</div>
<!-- end page footer section -->
</div>
</body>
</html>

- MobileCamera.html

<!DOCTYPE html>
<html class="ui-mobile">
<head>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta charset="utf-8">
<title>Hybrid App</title>
<!-- -->
<link rel="stylesheet" href="jsAndcss/jquery.mobile-1.0.min.css" />
<script type="text/javascript" src="jsAndcss/jquery-1.6.4.min.js"></script>
<script type="text/javascript" src="jsAndcss/jquery.mobile-1.0.min.js"></script>
<script type="text/javascript">
function capturePicture() {
window.external.notify("capturePicture");
}
 
function captureImageCallback(src) {
//window.external.notify("Log: capturePicture(" + src + ")");
document.getElementById("imgPic").src = src;
}
</script>
</head>
<body onload="onLoad()">
<div data-role="page" id="one" data-url="one" tabindex="0" class="ui-page ui-body-c ui-page-active"
style="min-height: 200px;">
<!-- start header -->
<div data-role="header" class="ui-header ui-bar-a" role="banner">
<h1 class="ui-title" tabindex="0" role="heading" aria-level="1">
Mobile Camera</h1>
</div>
<!-- end header -->
<!-- start content -->
<div data-role="content" class="ui-content" role="main">
<h2>
HTML 5</h2>
<p>
<a href="#" data-role="button" onclick="capturePicture()" data-theme="c" class="ui-btn ui-btn-corner-all ui-shadow ui-btn-up-c">
<span class="ui-btn-inner ui-btn-corner-all"><span class="ui-btn-text">Camera</span></span></a></p>
<p>
<img alt="capture picture" id="imgPic" width="300px" height="240px" />
</p>
</div>
<!-- end content -->
<!-- start footer -->
<div data-role="footer" data-theme="d" class="ui-footer ui-bar-d" role="contentinfo">
<h4 class="ui-title" tabindex="0" role="heading" aria-level="1">
@WmDev</h4>
</div>
<!-- end footer -->
</div>
</body>
</html>

-Two.html

<!DOCTYPE html>
<html class="ui-mobile">
<head>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta charset="utf-8">
<title>Single Page App</title>
<!-- -->
<link rel="stylesheet" href="jsAndcss/jquery.mobile-1.0.min.css">
<script src="jsAndcss/jquery-1.6.4.min.js"></script>
<script src="jsAndcss/jquery.mobile-1.0.min.js"></script>
</head>
<body class="ui-mobile-viewport">
<!-- Start of first page: #one -->
<div data-role="page" id="one" data-url="one" tabindex="0" class="ui-page ui-body-c ui-page-active"
style="min-height: 624px;">
<div data-role="header" class="ui-header ui-bar-a" role="banner">
<h1 class="ui-title" tabindex="0" role="heading" aria-level="1">
Multi-page</h1>
</div>
<!-- /header -->
<div data-role="content" class="ui-content" role="main">
<p>
<a href="Default.html" rel="external" data-role="button" data-theme="c" class="ui-btn ui-btn-corner-all ui-shadow ui-btn-up-c">
<span class="ui-btn-inner ui-btn-corner-all" aria-hidden="true"><span class="ui-btn-text">
Page "two"</span></span></a>
</p>
<p>
<a href="Default.html" rel="external" data-role="button" data-theme="c" class="ui-btn ui-btn-corner-all ui-shadow ui-btn-up-c">
<span class="ui-btn-inner ui-btn-corner-all" aria-hidden="true"><span class="ui-btn-text">
Back</span></span></a></p>
</div>
<!-- /content -->
<div data-role="footer" data-theme="d" class="ui-footer ui-bar-d" role="contentinfo">
<h4 class="ui-title" tabindex="0" role="heading" aria-level="1">
Page Footer</h4>
</div>
<!-- /footer -->
</div>
<!-- /page one -->
</body>
</html>

-Canvas.html

<!DOCTYPE html>
<html class="ui-mobile">
<head>
<meta name="MobileOptimized" content="width" />
<meta name="HandheldFriendly" content="true" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Hybrid App</title>
<link rel="stylesheet" href="jsAndcss/jquery.mobile-1.0.min.css" />
<script src="jsAndcss/jquery-1.6.4.min.js"></script>
<script src="jsAndcss/jquery.mobile-1.0.min.js"></script>
</head>
<body>
<div id="container">
<div data-role="header" class="ui-header ui-bar-a" role="banner">
<h1 class="ui-title" tabindex="0" role="heading" aria-level="1">
Canvas</h1>
</div>
<div>
<canvas id="appCanvas" width="400" height="350" style="border: 2px solid #c3c3c3;">
Your browser does not support the canvas element.
</canvas>
<p>
<a href="Default.html" rel="external" data-role="button" data-theme="c" class="ui-btn ui-btn-corner-all ui-shadow ui-btn-up-c">
<span class="ui-btn-inner ui-btn-corner-all" aria-hidden="true"><span class="ui-btn-text">
Back</span></span></a>
</p>
</div>
<div data-role="footer" data-theme="d" class="ui-footer ui-bar-d" role="contentinfo">
<h4 class="ui-title" tabindex="0" role="heading" aria-level="1">
Page Footer</h4>
</div>
<!-- /footer -->
<script type="text/javascript">
// --------------- Rectangle -----------------------
var objCanvas = document.getElementById("appCanvas");
var context = objCanvas.getContext("2d");
context.fillStyle = "#FF00EE";
context.fillRect(75, 37, 150, 75);
 
// --------------- Circle -----------------------
context.fillStyle = "#FF0000";
context.beginPath();
context.arc(275, 80, 15, 0, Math.PI * 2, true);
context.closePath();
context.fill();
 
// --------------- Triangle -----------------------
var width = 125; // Triangle Width
var height = 105; // Triangle Height
var padding = 20;
// Draw a path
context.beginPath();
context.moveTo(padding + width / 2, padding); // Top Corner
context.lineTo(padding + width, height + padding); // Bottom Right
context.lineTo(padding, height + padding); // Bottom Left
context.closePath();
// Fill the path
context.fillStyle = "#BBAACC";
context.fill();
 
 
// --------------- Text -----------------------
var context = objCanvas.getContext("2d");
 
var lineHeight = 25;
var x = (objCanvas.width - 300) / 2;
var y = 200;
var text = "Hello this Hybrid Application";
context.font = "16pt Calibri";
context.fillStyle = "#333";
context.fillText(text, x, y);
 
// --------------- Image -----------------------
 
var imageObj = new Image();
 
imageObj.onload = function () {
context.drawImage(imageObj, 69, 220);
};
imageObj.src = "http://www.developer.nokia.com/dynamic/profile/photo.html?size=78&username=pavan.pareta&status=private";
</script>
</body>
</html>

-GeoLocation.html

<!DOCTYPE html>
<html>
<head>
<title>Geo Location</title>
<meta name="viewport" content="width=device-width, height=device-height initial-scale=1, user-scalable=yes" />
<!-- <meta name="viewport" content="width=480, height=800, user-scalable=yes" />-->
<meta name="MobileOptimized" content="width" />
<meta name="HandheldFriendly" content="true" />
<link href="jsAndcss/MobileAppStyles.css" rel="Stylesheet" />
<script type="text/javascript">
function getLocation() {
if (navigator.geolocation) {
navigator.geolocation.getCurrentPosition(showPosition);
navigator.geolocation.watchPosition(showPosition);
}
}
 
function showPosition(position) {
var latlon = position.coords.latitude + "," + position.coords.longitude;
var img = new Image();
img.width = 300;
img.height = 500;
img.src = "http://maps.googleapis.com/maps/api/staticmap?center=" + latlon + "&zoom=16&size=300x500&scale=2&markers=color:green%7CLabel:A%7C" + latlon + "&sensor=false";
img.onload = function () {
var ctx = document.getElementById("geocanvas").getContext("2d");
ctx.drawImage(img, 0, 0, 300, 500);
};
}
</script>
</head>
<body id="background" onload="getLocation()">
<div id="container">
<div id="mapholder">
<canvas id="geocanvas" height="800" width="480">
</canvas>
</div>
</div>
</body>
</html>

-Accelerometer.html

<!DOCTYPE html>
<html class="ui-mobile">
<head>
<meta name="MobileOptimized" content="width" />
<meta name="HandheldFriendly" content="true" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta charset="utf-8">
<title>Hybrid App</title>
<link rel="stylesheet" href="jsAndcss/jquery.mobile-1.0.min.css" />
<script src="jsAndcss/jquery-1.6.4.min.js"></script>
<script src="jsAndcss/jquery.mobile-1.0.min.js"></script>
</head>
<body id="background" onload="onLoad()">
<div id="container">
<h3>
Accelerometer</h3>
<input id="sensorX" type="text" value="0" />
<input id="sensorY" type="text" value="0" />
<input id="sensorZ" type="text" value="0" />
</div>
<script type="text/javascript">
function onLoad() {
window.external.notify("StartAccelerometer");
}
var sensorX = document.getElementById("sensorX");
var sensorY = document.getElementById("sensorY");
var sensorZ = document.getElementById("sensorZ");
 
function CallbackAccelerometer(x, y, z) {
sensorX.value = x;
sensorY.value = y;
sensorZ.value = z;
}
</script>
</body>
</html>

HTML Code explanation for MobileCamera.html In this html we have used JavaScript to invoke the mobile camera. There is a work flow, how to invoke native functionality through JavaScript.

Step 5: Talking between application and JavaScript

Here are the three core methods and events that play major role to communicate between HTML with in JavaScript and Silverlight application. 1. window.external.notify(“<data>”) In HTML file 2. WebBrowser.ScriptNotify In Silverlight application 3. WebBrowser.InvokeScript() In Silverlight application

Web browser call the application asynchronously by using window.external.notify(“<data>”) and whenever WebBrowser.ScriptNotify event trigger in the Silverlight application get control and perform the native operation and pass required result to back using WebBrowser.InvokeScript() method, this method basically take an arguments as name of JavaScript method, that allows you to communicate between application and callback to the JavaScript using WebBrowser.InvokeScript(). It is an opposite communication when OnScriptNotify event occurs, in our case we have used WebBrowser.InvokeScript("javascript method name", "list of argument as string");.

Plugin architecture

007 HMA javascriptfllowdiagram.png

MainPage.cs

private void browser_ScriptNotify(object sender, NotifyEventArgs e)
{
try
{
var request = JavascriptRequestHandler.Create(e.Value);
var command = BrowserInteropCommandFactory.Create(request);
command.Invoke(webBrowser1);
}
catch (Exception ex)
{
Debug.WriteLine(ex);
}
}

Implementation of the ScriptNotify to callback in C# code, here received a request from JavaScript window.external.notify(), and NotifyEventArgs holding a request, in the below code snippet we have a java request handler class to parse the request and pass to BrowserInteropCommandFactory class then command factory invoke respective method with browser.InvokeScript().

MainPage.cs

private void browser_ScriptNotify(object sender, NotifyEventArgs e)
{
try
{
var request = JavascriptRequestHandler.Create(e.Value);
var command = BrowserInteropCommandFactory.Create(request);
command.Invoke(webBrowser1);
}
catch (Exception ex)
{
Debug.WriteLine(ex);
}
}

JavascriptRequestHandler.cs

public class JavascriptRequestHandler
{
public string MethodName { get; set; }
public IList<object> Arguments { get; set; }
 
public static JavascriptRequestHandler Create(string javascriptArgumentString)
{
var parameters = javascriptArgumentString.Split('|');
var javascriptArgument = new JavascriptRequestHandler { MethodName = parameters[0] };
 
if (parameters.Length == 1)
return javascriptArgument;
 
javascriptArgument.Arguments = new List<object>(parameters.Length - 2);
for (var i = 1; i < parameters.Length; i++)
javascriptArgument.Arguments.Add(parameters[i]);
 
return javascriptArgument;
}
}


BrowserInteropCommandFactory.cs

public static class BrowserInteropCommandFactory
{
public static BrowserInteropCommand Create(JavascriptRequestHandler request)
{
switch (request.MethodName.ToUpperInvariant())
{
case "CAPTUREPICTURE":
return new CameraCommand(request);
case "STARTACCELEROMETER":
return new AccelerometerCommand(request);
 
default:
throw new NotImplementedException("The method request by the browser component is not implemented");
}
}
}


CameraCommand.cs

public class CameraCommand : BrowserInteropCommand
{
public CameraCommand(JavascriptRequestHandler jsRequest) : base(jsRequest){ }
 
 
public override void Invoke(WebBrowser browser)
{
var photoCameraCapture = new CameraCaptureTask();
photoCameraCapture.Completed += (sender, result) =>
{
if (result.TaskResult != TaskResult.OK)
return;
SaveToIsolatedStorage(result.ChosenPhoto, "apphtml\\capture.jpg");
browser.InvokeScript("captureImageCallback", "capture.jpg");
};
photoCameraCapture.Show();
}
 
private void SaveToIsolatedStorage(Stream imageStream, string fileName)
{
using (var store = IsolatedStorageFile.GetUserStoreForApplication())
{
if (store.FileExists(fileName))
store.DeleteFile(fileName);
 
using (var fileStream = store.CreateFile(fileName))
{
var bitmap = new BitmapImage();
bitmap.SetSource(imageStream);
 
var wb = new WriteableBitmap(bitmap);
wb.SaveJpeg(fileStream, wb.PixelWidth, wb.PixelHeight, 0, 100);
}
}
}
}

Step 6: Load HTML, JS and CSS file into application Isolated Storage in local application domain.

It is very important to copy all HTML, JS, and CSS files to respective directory to Isolated Storage file in local application domain. In this example application use these html pages as embedded.

Important to do this select all HTML Pages from solution explorer and press F4 Key and set the Build Action - Embedded Resource, and Copy to output directory – Copy always and now set the Build Action – Content for all javaScript and css file

008 HMA page-property.png

Now in the constructor of MainPage, add loaded event and implement code to copy all files Html, JavaScript and css using C# code.

// Constructor
public MainPage()
{
InitializeComponent();
Loaded += new RoutedEventHandler(MainPage_Loaded);
}
 
 
// Loaded event
void MainPage_Loaded(object sender, RoutedEventArgs e)
{
using (var store = IsolatedStorageFile.GetUserStoreForApplication())
{
//-----------------------------------------------------------
// Must copy all html, css and js files in the ISO
//-----------------------------------------------------------
if (!store.DirectoryExists("apphtml")) store.CreateDirectory("apphtml");
CopyToIsolatedStorage("apphtml\\myPage.html", store);
CopyToIsolatedStorage("apphtml\\two.html", store);
CopyToIsolatedStorage("apphtml\\GeoLocation.html", store);
CopyToIsolatedStorage("apphtml\\MobileCamera.html", store);
CopyToIsolatedStorage("apphtml\\Accelerometer.html", store);
CopyToIsolatedStorage("apphtml\\Canvas.html", store);
CopyToIsolatedStorage("apphtml\\Pages-Multi.html", store);
 
if (!store.DirectoryExists("apphtml\\jsAndcss")) store.CreateDirectory("apphtml\\jsAndcss");
CopyToIsolatedStorage("apphtml\\jsAndcss\\jquery-1.6.4.min.js", store);
CopyToIsolatedStorage("apphtml\\jsAndcss\\jquery.mobile-1.0.min.js", store);
CopyToIsolatedStorage("apphtml\\jsAndcss\\jquery.mobile-1.0.min.css", store);
CopyToIsolatedStorage("apphtml\\jsAndcss\\MobileAppStyles.css", store);
 
}
}
 
private static void CopyToIsolatedStorage(string file, IsolatedStorageFile store, bool overwrite = true)
{
if (store.FileExists(file) && !overwrite)
return;
 
using (var resourceStream = Application.GetResourceStream(new Uri(file, UriKind.Relative)).Stream)
using (var fileStream = store.OpenFile(file, FileMode.Create, FileAccess.ReadWrite, FileShare.ReadWrite))
{
int bytesRead;
var buffer = new byte[resourceStream.Length];
while ((bytesRead = resourceStream.Read(buffer, 0, buffer.Length)) > 0)
fileStream.Write(buffer, 0, bytesRead);
}
}

Step 6: Run it by pressing F5 Key or Menu option Debug -> Start Debugging

Output screens

Hma-wmdev.gif

This page was last modified on 19 July 2013, at 07:42.
181 page views in the last 30 days.
×