Splashdust.net

Random bits of geeky stuff

How to open a PDF in an external viewer from an AIR app running on Android.

July 22nd, 2011

Opening files in external apps from AIR on Android is not as trivial as it would seem.

Here’s the problem: AIRs mobile profile does not support File.openWithDefaultApplication(). I know, this is retarded.

I tried countless approaches to this seemingly trivial problem, including using StageWebView, server-side conversion of the PDFs to SWF together with SWFLoader, and various combinations of these. Nothing would work to my satisfaction.

Eventually I came across a method of extending an AIR app with native Android OS functionality. That way it is possible to open files with Android’s native Intent class. Here’s the steps I took to accomplish this:

Please be aware that I am not very familiar with Android development (this is part of the reason why I choose AIR in the first place, doh), so there may be better ways to accomplish this. However, the steps below worked for me.

Preparations

  1. Set up Eclipse with Android SDK: http://www.vogella.de/articles/Android/article.html#installation_eclipse
  2. Set up a project in Eclipse: http://www.jamesward.com/2011/05/11/extending-air-for-android/ (Steps 1–10)
  3. In AndroidManifest.xml, Add at least the following permissions:
    android.permission.INTERNET
    android.permission.WRITE_EXTERNAL_STORAGE

Code

If you followed James Ward’s terrific guide (many thanks!) up until to step 10 you should now have a MainApp.java file in your project. Put this inside it: (remember to change package to whatever you use)

package com.mypackage;

import air.app.AppEntry;
import android.app.AlertDialog;
import android.content.ActivityNotFoundException;
import android.content.DialogInterface;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;

import java.io.BufferedInputStream;
import java.io.DataInputStream;
import java.io.File;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import android.os.Looper;
import android.util.Log;

public class MainApp extends AppEntry {

	private boolean stopped=false;
	private Thread serverThread;
	private ServerSocket ss;

	@Override
	public void onCreate(Bundle arg0) {
		super.onCreate(arg0);

		serverThread = new Thread(new Runnable() {

			public void run()
			{
				try
				{
					Looper.prepare();
					ss = new ServerSocket(12345);
					ss.setReuseAddress(true);
					ss.setPerformancePreferences(100, 100, 1);

					while (!stopped)
					{
						System.out.println("test test");

						Socket accept = ss.accept();
						accept.setPerformancePreferences(10, 100, 1);
						accept.setKeepAlive(true);

						DataInputStream _in = null;
						try
						{
							_in = new DataInputStream(new BufferedInputStream(accept.getInputStream(),1024));
						}
						catch (IOException e2)
						{
							e2.printStackTrace();
						}

						int method =_in.readInt();

						switch (method)
						{
						// Open PDF
						case 1:
							String path = _in.readLine();

							// Remove junk in the beginning of the string. Not sure if this is necessary in every case.
							path = path.substring(2);

							// Contruct the Intent
							File file = new File(path);
							Intent i = new Intent(Intent.ACTION_VIEW);
							i.setDataAndType(Uri.fromFile(file), "application/pdf");
							i.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);

							// Try to start the intent activity. If no activity is found, tell the user via an alert.
							try {
								startActivity(i);
							}
							catch (ActivityNotFoundException e) {
								messageHandler.sendMessage(Message.obtain(messageHandler, 0));
							}

							break;
						}
					}
				}

				catch (Throwable e)
				{
					e.printStackTrace();
					Log.e(getClass().getSimpleName(), "Error in Listener",e);
				}

				try
				{
					ss.close();
				}
				catch (IOException e)
				{
					Log.e(getClass().getSimpleName(), "keep it simple");
				}
			}

		},"Server thread");
		serverThread.start();

	}

	@Override
	public void onDestroy() {
		stopped=true;
		try {
			ss.close();
		} catch (IOException e) {}
		serverThread.interrupt();
		try {
			serverThread.join();
		} catch (InterruptedException e) {}
	}

	// This is a handler for displaying alerts
	private Handler messageHandler = new Handler() {

		@Override
		public void handleMessage(Message msg) {
			switch(msg.what) {
			case 0:
				AlertDialog alertDialog = new AlertDialog.Builder(MainApp.this).create();
				alertDialog.setTitle("No PDF viewer found");
				alertDialog.setMessage("Please install one from the Market.");
				alertDialog.setButton("OK", new DialogInterface.OnClickListener() {
					public void onClick(DialogInterface dialog, int which) {
						return;
					} });
				alertDialog.show();
				break;
			}
		}

	};

}

This code will recieve a path from AIR via the socket, and open the file with the preferred application using the Intent class. If a capable app is not installed on the device, an alert will inform the user.

Ok, so that’s the Android part. Now for the AIR part.

Add this code to your AIR app, where appropriate:

var s:Socket = new Socket();
s.connect("localhost", 12345);
s.addEventListener(Event.CONNECT, function(event:Event):void {
	trace('connected!');
	(event.currentTarget as Socket).writeInt(1);
	(event.currentTarget as Socket).writeUTF("/sdcard/MyApp/pdfdocument.pdf");
	(event.currentTarget as Socket).flush();
	(event.currentTarget as Socket).close();
});
s.addEventListener(IOErrorEvent.IO_ERROR, function(event:IOErrorEvent):void {
	trace('error! ' + event.errorID);
});
s.addEventListener(ProgressEvent.SOCKET_DATA, function(event:ProgressEvent):void {
	trace('progress ');
});

 

And that’s it! Took me two freaking days to figure this stuff out.

I hope this post will save someone else from wasting a bunch of time.

SVGQuartzRenderer: A basic iPhone SDK compatible SVG renderer

September 28th, 2010

I’m working on a game for the iPhone platform. The levels will be quite big, and on top of that I need them in 2 different resolutions (because of the iPhone 4 retina display). Having them in the bundle in a bitmap based format would make the final bundle several hundred megabytes big, which is unacceptable. So I figured that it would be better to store them in a vector based format instead, since I create the levels in InkScape anyway.

At first I was thinking about the PDF format. But then I realized that I could use the same vector data to construct the physics world in Box2D. And for this task, the SVG format is brilliant. It’s an easy-to-parse XML format and it can have arbitrary text-based data associated with each object. That means I can put implementation specific data in the file directly from InkScape (like if an object should be static or dynamic in Box2D for example).

As it turns out, though, CoreGraphics does not support the SVG format. Bummer. Sure, UIWebView can render SVG’s, but I could not find a good way to use that for off-screen rendering. So I went and started looking for an open source library that could render an SVG into a pixel buffer. I found only one; librsvg. And then a few others, but they all seemed to depend on librsvg.

There are two problems with librsvg:
1. It’s LGPL, which if I’ve understood correctly will require me to release my game under a compatible licence unless I link librsvg dynamically. Unfortunatly, this cannot be done in the iPhone SDK.
2. After hours of trying, I could not get librsvg to compile for the iPhone.

So I gave up on 3rd party libs and began writing my own SVG renderer in Obj-c. Turns out it wasn’t very hard to get the most basic stuff up and running.

And this is it. SVGQuartzRender. It’s by no means a complete implementation of the SVG specification, but it can render the most basic stuff found in an SVG, and it can conveniently render it to a CGImageRef.

This is how to use it, in a nutshell:
Somewhere in your class, the init method for example:

SVGQuartzRenderer *svgRenderer = [[SVGQuartzRenderer alloc] init];
// A delegate must be specified
[svgRenderer setDelegate:self];

You also need to implement the <SVGQuartzRendererDelegate> protocol:

// This is called before rendering begins. Here you need to supply the CGContext where you want the
//rendering to be done. If you just want an empty, off-screen bitmap context, you can use the
//convenience method  -(CGContextRef)createBitmapContext
- (CGContextRef)svgRenderer:(id)renderer
	requestedCGContextWithSize:(CGSize)size
{
	return [renderer createBitmapContext];;
}

// Will be called when rendering is complete
- (void)svgRenderer:(id)renderer
	didFinnishRenderingFile:(NSString *)file
	inCGContext:(CGContextRef)context
{
	// Here's your CGImageRef!
	CGImageRef svgDrawing = CGBitmapContextCreateImage(context);
}

In the XCode project, there is a simple Cocoa app that demostrates more thoroughly how it is used.

I’ve put up a GitHub repo for SVGQuartzRenderer. If you want it, you should get it from there.

SVGQuartzRenderer on GitHub

Feel free to use this code how ever you like, commercially or non-commercially. Should you make any improvements I’d love to get a patch! :)

IE Hack: CSS drop shadow

May 6th, 2010

Easy-to-apply, dynamic, auto-expanding drop shadows have always been a bit of a pain to apply across all the major browsers. We fairly recently got the box-shadow attribute, which can be applied with -moz-box-shadow and -webkit-box-shadow on gecko and webkit browsers. But it leaves the IE family out in the cold.

The easiest solution to the is the IE-proprietary filter attribute. It does not producte quite as nice shadows as the box-shadow attribute, but for simple just-a-few-pixels drop shadows it does the trick. One noticeable drawback is that IE for some reason disables anti-aliasing on elements that has this attribute. But I can live with that in most cases.

This box has a shadow
.shadow {
	zoom:1; /* This enables hasLayout, which is required for older IE browsers */
	filter: progid:DXImageTransform.Microsoft.Shadow(color='#b0b0b0', Direction=135, Strength=3);
	-moz-box-shadow:2px 2px 2px #b0b0b0;
	-webkit-box-shadow:2px 2px 2px #b0b0b0;
	box-shadow:2px 2px 2px #b0b0b0;
}

This should look fairly similar in all major browsers, including IE 6+

IE Hack: inline-block

May 4th, 2010

I just decided that it would be a good idea to collect solutions to common IE css issues here on my blog. Sure, these are all over the web now a days, but hey, another place can’t hurt right? :)
I’m going to start posting them under the IE hacks category whenever I come across one.

This time it’s about the inline-block display property (introduced in CSS 2.1), which can be very useful in certain scenarios. The problem is that it is not supported IE <= 7 for elements that are block level by default.

Without further ado, here's the most simple solution I could find. Just append these two lines at the end of the CSS block where the inline-block style is set:

zoom: 1;
*display: inline;

Basically it triggers hasLayout for the target element, which is "kinda like magical fairy dust you can sprinkle on rendering issues and make them disappear".

Source: http://blog.mozilla.com/webdev/2009/02/20/cross-browser-inline-block/

Mouse tracking in NSView

February 13th, 2010

Tracking the position of the mouse does seem like a trivial task. At a first glance it seemed like it would be a matter to override the -mouseMoved: selector in my NSView subclass and make sure the window accepts mouseMoved events by calling [[self window] setAcceptsMouseMovedEvents:YES]; when the view had loaded. Well, not exactly. I tried every conceivable combination of placements of these peices of code, including making the view the first responder, but I could never manage to intercept the mouseMoved event.

Then I found out about NSTrackingArea. Perhaps I was being overly ignorant, but from reading the event handling docs, it was not clear to me that I would need to use a tracking area to be able to recieve mouseMoved events.

Using NSTrackingArea is pretty straight forward, although at first I got a cryptic error message in the console:

trackingArea options 0x2 do not specify when the tracking area is active

As it turns out, NSTrackingArea requires a combination of several options to work properly. In this case I needed to supply “NSTrackingMouseMoved+NSTrackingActiveInKeyWindow” in the options: attribute.

Putting the following code in the -viewDidMoveToWindow method in my NSView subclass finally managed to get the view to recieve mouseMoved events:

NSTrackingArea *trackingArea = [[NSTrackingArea alloc] initWithRect:[self frame]
	options:NSTrackingMouseMoved+NSTrackingActiveInKeyWindow
	owner:self
	userInfo:nil];
[self addTrackingArea:trackingArea];
[self becomeFirstResponder];