Friday, 2 December 2011

Vertically Centering Multi-Line text in CSS

There are many, many blog posts on this subject already, but I've struggled to get many of the methods to work so I'm saving this for posterity.

<div class="label">
    <div class="label-inner">
        foobar<br>baz
    </div>
</div>

.label {
    display: table;
    width: 300px;
    height: 200px;
    border: 1px solid black;
}

.label-inner {
    display: table-cell;
    vertical-align: middle;
    text-align: center;
}

Result

Wednesday, 16 November 2011

Android Phonegap :active CSS pseudo class doesn't work. A workaround..

If you're building a cross-platform Phonegap app, you may well run in to the issue that your CSS :active pseudo-classes simply don't work when running on Android. They may work on your a (link) elements, but not on div or others.

Luckily there's a simple workaround. This workaround will continue using the fast, native, JavaScript-free :active class on iOS, and drops back to using the JavaScript on Android.

  • First, in your CSS, find every instance of the :active class. Add another rule that maps a class called fake-active to the same style.
E.g.

.my-button:active {
 background-color: blue;
}

Becomes:

my-button:active, .my-button.fake-active {
 background-color: blue;
}
  • Now, in your document ready event, when running on Android only, attach event handlers for touchstart and touchend to all the classes that have used :active. Make these add and remove the fake-active class.
if (navigator.userAgent.toLowerCase().indexOf("android") > -1) {
    $(".my-button")
    .bind("touchstart", function () {
        $(this).addClass("fake-active");
    })
   .bind("touchend", function() {
        $(this).removeClass("fake-active");
    });
    .bind("touchcancel", function() {
        // sometimes Android fires a touchcancel event rather than a touchend. Handle this too.
        $(this).removeClass("fake-active");
    });
}

Note that you can use the $(".my-button, .my-other-element, .even-more-elements") jQuery syntax to bind the events to other classes too.

Thursday, 10 November 2011

Edit AndroidManifest.xml in Android Phonegap Build App

Phonegap build is an awesome service that allows you to convert a bunch of HTML files into actual mobile apps for iPhone, Android, Blackberry, and more. It's great, so great in fact that Adobe recently bought the company behind it (Nitobi).

You can read more about the details at their site, I won't repeat it all here.

I've been playing around with the Android version of this for a client project I've been working on, and came up with a bit of a problem.  There's a file called AndroidManifest.xml that every Android app includes that sets up loads of important stuff such as the intent filters and the permissions needed to run the app.

Phonegap build allows you to change some of these settings using it's config.xml system.  But for other settings you are, it seems, stuck.  I needed to change the intent filter for one of my activities, and there was no way that the config.xml could do this. I posted on their forums and the very helpful guy said they may add the feature in a future release.

In the meantime though, I found another way.

It's possible to decompile the apk file that you download from Phonegap Build, change the AndroidManifest.xml file, re-compile, re-sign, and install it onto your device.

Getting the tools we need
I'm assuming that you have the Android SDK installed, if not install it and follow the instructions to put the build tools into your path.
First you need to download apktool.  This great tool allows you to decode and re-encode apk files. Download it and follow the instructions to install for your platform.
We're also going to use:

  • jarsigner - part of the Java JVN (I have a binary at /usr/bin/jarsigner) 
  • adb - part of the Android SDK installation (my binary is at ~/android/android-sdk-mac_x86/platform-tools/adb)
Extracting the APK file

  • Download the apk file for your app from Phonegap build.
  • Open up the terminal and type (substituting your folders for path/to and filenames where appropriate)
apktool d path/to/yourApp.apk path/to/output-folder
  • This will extract the contents of the app into the folder output-folder.
  • The AndroidManifest.xml file will be at the root of that folder.
Now you can make whatever changes to the AndroidManifest you need (setting intent filters etc).

Re-building the APK file

  • Once you've finished making your changes, you can rebuild the APK file from the contents of the output-folder
apktool b path/to/output-folder path/to/yourAppV2.apk 
Re-signing the APK
If you try to install the new apk file you'll find that it fails with an invalid certificate error.
This is because apktool doesn't re-sign the apk.  It's pretty easy to re-sign the APK with the development certificate (which is what Eclipse uses when making APKs for testing).

Here's how:
jarsigner -verbose -keystore ~/.android/debug.keystore path/to/yourAppV2.apk androiddebugkey
By default the development certificate is in the ~/.android folder. When you run this, it'll ask for a passphrase - use android.

Your APK is now ready for installation!
adb install path/to/yourAppV2.apk

Wednesday, 27 July 2011

Knockout.js: Making an observable that fits into an undo system

Knockout is a JavaScript library that helps you to create rich, responsive display and editor user interfaces with a clean underlying data model. Any time you have sections of UI that update dynamically (e.g., changing depending on the user’s actions or when an external data source changes), KO can help you implement it more simply and maintainably.
(from the homepage)

I'm using knockout fairly extensively thoughout a new app I'm building. This app has a full undo / redo system for all user actions, and this caused a bit of a problem with knockout. By default knockout will immediately update the data model with the results of user actions. Furthermore, when it notifies you of changes to values, it only tells you the value that the data has changed to - and it's too late to find out what the value was before.

Of course there are hacky ways around this, but a much better solution suggested by (someone on a mailing list I need to look up again) was to create a 'subclass' of observable that allows access to the previous value of a variable.

Here's the code:
$("document").ready(function() {

/**
* An observable that allows access to the last value. Useful for undo
* purposes.
*
* Usage:
* viewModel.foobar = ko.lastValueObservable();
* viewModel.foobar.subscribe(function(val) { alert("foobar changed from " + viewModel.foobar.lastValue() + " to " + val); } );
*/
ko.lastValueObservable = function(initValue) {
var _value = ko.observable(initValue);
var _lastValue = initValue;

var result = ko.dependentObservable({
// Just return the real value
read: function() {
return _value();
},

write: function(newValue) {
// store the last value before writing...
_lastValue = _value();

_value(newValue);
}
});

// Add a new function to return the last value
result.lastValue= function() {
return _lastValue;
};

return result;
};
});

Tuesday, 26 July 2011

HTML5: Transferring localStorage data between machines & browsers

An app I've been building recently makes extensive use of the HTML5 localStorage API, which is beautifully simple and just works. This system stores a limited (normally maximum 5MB) of data as name / value string pairs on a per-domain basis in the user's browser. Check out my other post on localStorage for more details

I came across an issue today where a colleague had saved some data locally that I needed to use, and it seemed there wasn't an easy way to transfer the data across. Luckily a few minutes of fiddling showed it was in fact super-easy.

Just write out the total contents of the localStorage file as a JSON string, email or otherwise transfer it to the other computer, and then read it back into localStorage.

Here's the code. I ran this by opening the JavaScript console in my browser and just typed the commands
// Write out the whole contents of the localStorage..
JSON.stringify(localStorage);
// Copy the output..

// Now on the other computer, read it in again..

var storage = !! PASTE THE DATA HERE !!;
for (var name in storage) { localStorage.setItem(name, storage[name] ); }

Monday, 25 July 2011

Creating an XML-to-JSONP converter using node.js

A client of ours had a simple request to include an RSS feed in a mobile website and have it dynamically update. The obvious solution to this was to use AJAX, parse the XML and generate the required HTML.
However the RSS feed was on a different domain to the website itself, so we quickly run into 'traditional' cross-domain restrictions. Simply put, you can only AJAX data from the same domain that your website is served from.

Now, the JSONP format is widely considered a great way to get around this issue. The way this works is that a script tag is dynamically inserted into the HTML document. The src attribute of the script tag is set to the data source, e.g.

<script src="http://www.example.com/foo.js?callback=myCallback" type="application/javascript">


The 'callback' query string parameter is used to specify the name of a JavaScript function that will be executed when the data is downloaded. This function will be passed the data itself as the first parameter. E.g. in this example, we'd create a JavaScript function like this:

function myCallback(data) { console.log(data); }


The data parameter would then be a JavaScript object containing the data that was loaded. Pretty neat, I think you'll agree!

If you use the inspector in your browser to see what's actually downloaded, you'll see something like this:

myCallback( {  a:1, b:2, c:3 } );


It's simply the JSON-encoded data wrapped in a call to the function we specified in the query string parameter earlier on.

What we're going to do in the remainder of this article is to create a node.js server that we pass the URL of an RSS feed in & the name of the callback, and we get a JSONP response back again.

Now, going back to the requirements, our client asked for a RSS feed reader. Of course RSS is an XML-based format and not a JSON format so we're going to need to convert XML into JSON.

Happily, there's a module for doing exactly this called xml2js available via npm, the node package manager.

sudo npm install -g xml2js


The -g call tells npm to install the package globally - so it's available to all node apps.

We're also going to use journey, a node module that simplifies mapping request urls to js functions.

sudo npm install -g journey


Here's the code that sets up the server itself and creates the mapping for the url /xml2jsonp. This is the js file that should be run using the node binary.

server.js


require.paths.push('/usr/local/lib/node_modules'); // my installation required this for any modules to load.. your directory may be different..
var http = require('http'),
journey = require('journey'),
xml2Jsonp = require('./xml2jsonp'), // this is the file containing the implementation of the actual conversion.. We'll create this in a bit!

//======================================================================
// Create the routing table request => handling function
//======================================================================
var router = new (journey.Router);
router.map(function () {

// Send a welcome message if you hit the root
//==================================================================
this.root.bind(function (req, res) {
res.send("Welcome")
});

// map /xml2jsonp
//==================================================================
this.get("/xml2jsonp").bind(function (req, res, params) {

// the 'params' parameter is filled in with the query string as an object, provided your client includes a query string in the request url (?a=b&c=d)

// the xml2jsonp.get function is defined in the xml2jsonp.js file defined later on.
// we get out the 'url' and 'callback' parameters from the query string and pass them in.
xml2Jsonp.get(res, params.url, params.callback, function(result) {
// we've got back the text! use res.send to respond to our client!
res.send(200, {'Content-Type': 'application/javascript'}, result);
});
});
});

//======================================================================
// Set up the server
//======================================================================
http.createServer(
function (req, res) {

// This section is boilerplate code from the journey docs to read the request from the client and pass it onto the routing table above.
var body = "";

req.addListener('data', function (chunk) {
body += chunk
});
req.addListener('end', function () {
// Dispatch the request to the router
router.handle(req, body, function (result) {
res.writeHead(result.status, result.headers);
res.end(result.body);
});
});

}).listen(80);

console.log('Server running..');


Now here's the interesting bit, the implementation of the actual XML 2 JSONP conversion.

xml2jsonp.js

var
url = require("url"),
xml2js = require("xml2js"),
http = require("http");

/**
* Downloads the XML file, converts it to JSONP, and calls the callback function with the result.
*
* @param res the http response object
* @param xmlUrl the url of the xml we want to download
* @param callbackFunctionName the name of the JSONP callback function
* @param callback the function to run once this call succeeds. The first parameter will be the result as a string.
*/
exports.get = function(res, xmlUrl, callbackFunctionName, callback) {

if (!xmlUrl || !callbackFunctionName) {
callback(callbackFunctionName + "({error:'invalid parameters'}");
return;
}

// get query passed in..
var urlIn = url.parse(xmlUrl);

if (!callbackFunctionName) {
callbackFunctionName = "";
}

var options = {
host: urlIn.hostname,
port: 80,
path: urlIn.pathname,
"user-agent": "node.js" // some web servers require a user agent string..
};

// download the XML as a string..
downloadTextFile(options,
function success(data) {

// use the xml2js library to parse into a JS object.
var parser = new xml2js.Parser();
parser.addListener('end', function(result) {
// Use JSON.stringify to convert back into the JSON text we'll return. Note that in a production environment you'll
// probably want to omit the null, 4 part on this call. It makes the output more human-readable but increases the file size.
callback(callbackFunctionName + "(" + JSON.stringify(result, null, 4) + ");");
// Note here we're wrapping the JSON in a call to the JSONP callback function - this is the key part of the JSONP format!
});
parser.parseString(data);
},
function error(msg, e) {
// Note: because we're in an async environment it's important that we call the callback even if things fail, else the client will hang around waiting for a response.
callback(callbackFunctionName + '({error:' + msg + '})');
});
};

/**
* Call to download a text file from the specified url
* @param options {host:[host name],port:[port],path:[path] }
* @param success
* @param error
*/
function downloadTextFile(options, success, error) {
http.get(options,
function(res) {

console.log("Got response: " + res.statusCode);

var data = "";

res
.on('data', function (chunk) {
data += chunk;
})
.on('end', function() {
success(data)
});

})
.on('error', function(e) {
var msg = "Failed to download text from " + options.host + options.path + " " + e.message;
console.log(msg);
error(msg, e);
});
}


I'm hosting the above code on a joyent node.js smartmachine a (currently at the time of writing) free service from joyent. I thoroughly recommend trying it out if you're looking for free node.js hosting. It's a little fiddly getting started, but if you follow the docs at http://wiki.joyent.com/display/node/Getting+Started+with+a+Node.js+SmartMachine and ignore the many obsolete documents I kept finding on google then you should be ok.

Update: I've added this project to github, and updated it a little to also provide json to jsonp conversion.
https://github.com/benvium/nodejs-xml2jsonp

Wednesday, 4 May 2011

Insert a Link into a Google Spreadsheet in Google Docs

In the Google document editor, it's super easy to insert a link. Just hit the 'link' button, type the URL and link text and your done. Or even better, type the link text direct into your document, select it and press CMD-K (CTRL-K on a PC).

In Google spreadsheets, there's no such button. Happily it is still possible, though rather more fiddly.

Select a cell and type:

=HYPERLINK("http://www.example.com/link"; "Link Text")

The link will appear, and it will behave exactly the same as links in Google Docs. I'm looking forward to Google adding a button to make this easier.

Monday, 28 February 2011

Android: Fading between two Activities

Here's the code to do a nice smooth fade between two Activities..

Create a file called fadein.xml in res/anim
<?xml version="1.0" encoding="utf-8"?>
<alpha xmlns:android="http://schemas.android.com/apk/res/android"
   android:interpolator="@android:anim/accelerate_interpolator"
   android:fromAlpha="0.0" android:toAlpha="1.0" android:duration="2000" />



Create a file called fadeout.xml in res/anim
<?xml version="1.0" encoding="utf-8"?>
<alpha xmlns:android="http://schemas.android.com/apk/res/android"
   android:interpolator="@android:anim/accelerate_interpolator"
   android:fromAlpha="1.0" android:toAlpha="0.0" android:duration="2000" />


If you want to fade from Activity A to Activity B, put the following in the onCreate method for Activity B before setContentView.
overridePendingTransition(R.anim.fadein, R.anim.fadeout);

If the fades are too slow for you, change android:duration in the xml files above to something smaller

Sunday, 27 February 2011

iPhone-Style Buttons for Android

I recently started work on apps for android and quickly tired of the default buttons that Android provide - especially the nasty default flashes of green and orange when the button is tapped. Eurgh.

Happily, the Android SDK provides a neat way to build custom buttons, and to use them throughout your application. Here I present some vaguely iPhone-style gradient-filled buttons I created using this method.



Background Images
First up you need to create the button background images themselves. You need separate images for at least the normal and selected states, and optionally the 'highlighted' state too. Highlighted is when the user selects the button using the (in my opinion entirely pointless) trackpad. Here I've been a bit lazy and used the selected state for highlighted rather than making a whole new image.

You need to make versions of the image for hi-res devices like the Nexus One or HTC Desire (hdpi), and medium res versions for other devices like the HTC Legend (mdpi). So far I've used the same images for medium res and low res, and it seems fine.

Here are the images I created (right-click to download).



Hi-res png. Normal and Tapped




Medium res png. Normal and tapped



Making the images stretchable using draw9patch
Next you need to tell Android how these images can be stretched to make buttons of different sizes.

Run the draw9patch.jar application (in your android sdk's tools/lib folder).
The official instructions for the draw9patch tool are rather vague, so here's what I did.
  • Drag-and-drop one of the button images onto the tool.
  • The image will appear in the left panel.
  • Choose an edge. I start with the top.
  • Decide on which part of the image should contain the button content. For a button like this one you'd choose the largest possible straight bit of the edge you're currently working on.
  • The image is displayed with an extra one-pixel gap around the edge. You're going to be drawing in the one-pixel gap to mark up the edges.
  • Click and drag the mouse over the one-pixel gap to draw in black pixels to mark the largest possible straight part - make sure you don't miss any pixels.
  • In the image above I've coloured in roughly the middle third of the top edge. If you make a mistake, hold shift and click to erase pixels.
  • Repeat with the other edges.
  • If all has gone well, you'll see a preview on the right of what the button looks like stretched.
  • Repeat for each edge.
  • Save the file. When you give it a name, don't provide the extension. The file will be saved as a 'name.9.png' file.
  • Repeat for each image.
  • My filenames were button_black.9.png and button_black_tap.9.png. Make sure you give the medium and high res images the equivalent names. This is important later!
Copy the .9.png files to your project
Copy the high-res .9.png images into your res/drawable-hdpi folder (make it if it doesn't exist), and the medium-res into res/drawable-mdpi AND res/drawable-ldpi. If you omit the ldpi image Android appears to use the high-res image on low-res devices.

Create the drawable to define normal, tapped, and highlighted states
Now we can create our custom button style. We're going to define a 'drawable' called black_button that will use the black_button and black_button_tap images we made above.

Put this in an xml file called 'black_button.xml' inside the res/drawable folder.
<?xml version="1.0" encoding="utf-8"?>
<selector android="http://schemas.android.com/apk/res/android">
<item state_focused="true" state_pressed="false" drawable="@drawable/button_black_tap">
<item state_focused="true" state_pressed="true" drawable="@drawable/button_black_tap">
<item state_focused="false" state_pressed="true" drawable="@drawable/button_black_tap">
<item drawable="@drawable/button_black">
</item></item></item></item></selector>
This document tells android to use the tap image for the focused and pressed states when you use the drawable called 'black_button'.

Create the style
Next we need to define a style which we can use for the button. Android styles are a powerful way to collect together images, font, and layout settings that are common to widgets in your app. Making a style now will make it super-simple to make 'black_button' styled buttons later on.

Place this in the res/values folder in a file called styles.xml (or add the 'style' part to your existing styles if you have them). Note that we've set the background property to point to the drawable 'button_black' we just defined.
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="button_black">
<item name="android:background">@drawable/black_button</item>
<item name="android:textColor">#FFFFFF</item>
<item name="android:typeface">sans</item>
<item name="android:textStyle">bold</item>
<item name="android:textSize">20sp</item>
<item name="android:gravity">center</item>
<item name="android:layout_margin">3sp</item>
</style>
</resources>
Using the style on your buttons
Now we are ready to apply this style to buttons in our app. All you need to do is to set the 'style' attribute on the Button definition in your layout.xml files to point to the style we just defined.

Here's an example.
<Button android:text="Add from URL" android:id="@+id/addButton"
android:layout_height="wrap_content" android:layout_width="fill_parent"
android:drawableLeft="@drawable/menu_add"
style="@style/button_black"
></Button>

Thursday, 17 February 2011

How to hide .svn folders from file search in Eclipse

By default, whenever you do a file search in Eclipse, it'll search inside .svn folders too. This is obviously very irrititating!

Here's how to turn it off:
  • Click on the root of your project in the Package Explorer window
  • On the main manu, select Project / Properties / resource / Resource Filters
  • Click Add..
  • Set 'Filter Type' to 'Exclude all'
  • Set 'Applies to' to 'Folders'
  • Check 'All Children (recursive)' (this is essential!)
  • Under File and Folder Attributes, type .svn. This should work for CVS folders too (type .cvs)
  • Click OK
  • Repeat for all projects in your workspace


Apparently installing subclipse (subversion plugin for eclipse) will also solve this problem but I haven't tried it myself.

Wednesday, 16 February 2011

Android: Adding a new Activity using Eclipse

To add a new screen to your android app you'll want to add a new activity. This is a slightly manual process however, so I've noted down here for future reference.

This example will add a new activity called 'Foo'.

Step 1 - Add the activity class
  • In the Package Explorer, right click your main package (e.g. com.calvium.myApp) and select New / Class
  • Set the new class Name (e.g. Foo.java), and set it the superclass to android.app.Activity
  • Add an override for the onCreate method.
  • Easy method: Open the new file and place your cursor inside the main { } block. Press CTRL-SPACE and up pops a list of possibilities. Type 'onCreate' and hit enter to generate the method stub.
  • After super.onCreate add:

setContentView(R.layout.foo);

Step 2 - Create the Activity Layout File
  • Back in the package explorer, right-click res / layout.
  • Select New / Other / Android / Android XML File
  • Call the new file foo.xml
  • Add whatever controls you want to the xml file using the designer tool in eclipse, or by typing stuff manually.
Step 3 - Register the new activity in the AndroidManifest.xml file
  • Open AndroidManifest.xml
  • Just before the closing tag insert:

<activity android:name=".Foo">activity>

Thursday, 13 January 2011

Installing an archived application to an iPhone from XCode Organizer


The organizer window in Xcode has a handy feature where all the versions of applications you have submitted to the App Store are listed.
Today I needed to install an old version of an app to my iPhone in order to reproduce an issue a customer was having. It wasn’t as easy as I was expecting so I thought it was worth recording the process. Essentially you cannot simply copy the archived app to your device manually, as it was signed using the App Store certificate. We need to re-sign it using an Ad Hoc certificate instead - then it’ll run.
First up you need to make sure that you have an Ad Hoc distribution certificate handy, and that said certificate includes the device that you’re trying to distribute to.

UPDATE: August 2012: This process is far easier if you use this free open-source app called iResign.  This great app lets you take an existing IPA and re-sign it with your own certificate. Much, much simpler

Making the Ad Hoc Certificate
Ignore this part if you already have an Ad Hoc distribution certificate.
  1. Get your team agent to log into the iOS provisioning portal
  2. Go to provisioning / distribution tab
  3. Hit New Profile
  1. Choose ‘Ad Hoc’ as the distribution method
  2. Choose the relevant app ID (I chose an app id that allows any application by Calvium)
  3. Select the devices you want to be able to run on. I just chose all by selecting the select all button.
  4. Hit ok. Wait a minute or so, and hit refresh to be sure that the certificate is ready.
Re-signing the app using the Ad Hoc Certificate
  1. Go back to Xcode organizer. Open the ‘archived application’ section and choose the app you want to use.
  2. On the right-hand panel, choose the version to use. I’ve chosen 1.2. Apologies for the censorship in the images here..
  1. Click Share
  1. Click the ‘identity’ drop-down and under the heading ‘Ad Hoc Distribution’, select ‘iPhone Distribution’
  2. Now click ‘Save to Disk’
  3. Choose a filename (doesn’t matter what it is)
  4. Xcode will grind away for a bit re-signing the app, and then save a .ipa file containing the app.
Installing the app on your device (the .ipa file)
There are various ways of installing the ipa on the device, including via iTunes. I find this method by far the easiest - no syncing required.
  1. Open Xcode organizer again
  2. Attach the device
  3. Open the ‘Devices’ section, and select the device
  1. Under the Applications box, click the + arrow.
  2. Select the .ipa file you just saved, and lo and behold the app will be installed
  3. If it fails with an ‘no provisioning profile found’ or ‘invalid entitlements’ or similar, check that the ‘Provisioning’ box above contains the Ad Hoc profile you created earlier. If not, download it from the iOS provisioning portal, and manually upload it by clicking the + button under provisioning.

Screen sharing on Mac OS X Server using VNC

Chicken of the VNC is a free open-source Mac application that allows you to view a remote desktop on another networked machine.

Here's how I set it up on my Mac running 10.6, and our Mac Server (10.6 too).
  • Run Server Admin on your local machine
  • Open up the server you want to connect to
  • Select Firewall.
  • Go to Settings / Services tab
  • If 'Allow traffic only to these ports' is selected we need to add the ports that VNC will use.
  • Click the little + button at the bottom-left of the list of services
  • Add a new service called VNC 1, using port 5800, TCP + UDP
  • Add a second service, VNC 2, using port 5900, TCP + UDP
  • Stop and restart the firewall
  • Download Chicken of the VNC and install it.
  • Run the app
  • Go to Connection / New Connection
  • Enter the IP address or name of your server
  • Leave 'display' as 0 (if you choose 1 you'd have needed to open ports 5801/5901 above for example)
  • Leave 'profile' as 'default profile'
  • Check the 'save server' box.
  • Hit connect.
If all goes well you should have a window pop up displaying the screen of the server.