Android Log Message Truncation

2012/09/08

Frustrated by Android’s inability to log messages over 4k? In my case, I had some heft SOAP messages that were getting cut off. The simple solution is to use System.out.println(), but that always logs at the info level. Here’s something neater,

    void v(String msg) {
      println(Log.VERBOSE, msg);
    }

    void d(String msg) { ... }
    void i(String msg) { ... }
    void w(String msg) { ... }
    void e(String msg) { ... }

    private int println(int priority, String msg) {
        int l = msg.length();
        int c = Log.println(priority, TAG, msg);
        if (c < l) {
            return c + println(priority, TAG, msg.substring(c+1));
        } else {
            return c;
        }
    }

In short, take advantage of the fact that the low-level Log.println() call returns the number of bytes written. Use that fact to recursively call ourselves until we log all of the message.


Spiral Order, Java

2012/03/19

This is a problem I’ve heard mentioned but never really had a reason to look into. As they say, the science is gone from computer science. I sat down today and put some thought into it.

Spiral order is what you’d think. Given a 2D array, you move from left to right, top to bottom, right to left, and bottom to top printing out the values in order. For example,

  1  2  3  4
  5  6  7  8
  9 10 11 12
 13 14 15 16

The spiral order of this matrix is,

{  1, 2, 3, 4, 8, 12, 16, 15, 14, 13, 9, 5, 6, 7, 11, 10 }

The algorithm to walk in spiral order works nicely with recursion. You recursively, “peel” off the top, right, bottom, left portions of the matrix until there’s nothing left. For example (forgive the ASCII art), peel off the top,

  1 2 3 4

  5  6  7  8
  9 10 11 12
 13 14 15 16

peel off the right,

  5  6  7     8
  9 10 11    12
 13 14 15    16

peel off the bottom,

  5  6  7
  9 10 11 

 13 14 15

peel off the left,

 5     6  7
 9    10 11

peel off the top (again, starting over here),

 6 7

 10 11

peel the right again,

 10    11

and finally, one more peel of the bottom gets the last element,

 10

and we are done.

The code is below. spiralOrderR() implements the recursive algorithm. It calls peelTop(), which calls peelLeft(), which calls peelBottom(),which calls peelLeft(), and so on, until it’s done.  The peeling happens in the arguments to the recursive call, where the bounds of the matrix are reduced as appropriate at each pass.

public class SpiralOrder {
	public static <T> List<T> spiralOrderR(T[][] matrix) {
		int startx = 0;
		int starty = 0;
		int endx = matrix.length - 1;
		int endy = matrix.length - 1;

		List<T> spiral = new ArrayList<T>();

		peelTopRight(matrix, startx, endx, starty, endy, spiral);
		return spiral;
	}

	private static <T> void peelTop(T[][] matrix, int startx, int endx, int starty, int endy, List<T> spiral) {
		if (startx > matrix.length / 2) {
			return;
		}
		for (int i = startx; i <= endx; i++) {
			spiral.add(matrix[i][starty]);
		}
		peelRight(matrix, startx, endx, starty+1, endy, spiral);
	}

	private static <T> void peelRight(T[][] matrix, int startx, int endx, int starty, int endy, List<T> spiral) {
		for (int i = starty; i <= endy; i++) {
			spiral.add(matrix[endx][i]);
		}
		peelBottom(matrix, startx, endx-1, starty, endy, spiral);
	}

	private static <T> void peelBottom(T[][] matrix, int startx, int endx, int starty, int endy, List<T> spiral) {
		for (int i = endx; i >= startx; i--) {
			spiral.add(matrix[i][endy]);
		}
		peelLeft(matrix, startx, endx, starty, endy-1, spiral);
	}

	private static <T> void peelLeft(T[][] matrix, int startx, int endx, int starty, int endy, List<T> spiral) {
		for (int i = endy; i >= starty; i--) {
			spiral.add(matrix[startx][i]);
		}
		peelTop(matrix, startx+1, endx, starty, endy, spiral);
	}

	public static void main(String[] args) {
		Integer[][] ints = new Integer[5][5];
		int count = 1;
		for (int i = 0; i < ints.length; i++) {
			for (int j = 0; j < ints[i].length; j++) {
				ints[j][i] = count++;
			}
		}

		List<Integer> spiral = spiralOrder(ints);
		System.out.println(spiral);
		spiral = spiralOrderR(ints);
		System.out.println(spiral);
	}
}

Android Advanced Logger

2012/03/08

The Android Log utility class is simple enough and does the job, but there are a few nagging problems I’ve found with it.

No context. There is no supplied as to what triggered the log statement. This leads to log records like,

D/MyApp: Something happened ...

Great, but happened where? What class? Which method? For small apps with a single developer this isn’t a problem, but for larger projects where the person that’s doing the debugging did not write the code … By convention, developers can add contextual information into the log statements but this requires everyone to remember to do this, consistently. Can’t the log utility do it for us?

Tag inconsistencies. Some apps use different tags for each class, others use the same tag across the entire app. The former makes it impossible to tell which log statements came from the same app, unless you keep a mapping from tab to class to app around. That’s why I prefer the latter. However, now we are forced to pass the exact same argument (TAG) into every log statement. Can the logging framework insert it for us?

Level. The Android logger does not allow me to change the level, only filter the log output by level via logcat arguments. What if i simply want to avoid logging things at particular levels?

Argument evaluation. A common logging anti-pattern,

Log.i("here's the object: " + myObj);

This is problematic because regardless of the level, regardless whether the statement will actually get into the log, myObj.toString() is called and a new String object that is the concatenation of “here’s the object: ” and myObj.toString() is created. Over a large app with many log statements, this can significantly hamper performance. Commonly, the antidote is something like,

if (LOG_LEVEL == Level.INFO) {
    Log.i("here's the object: " + myObj);
}

But this really crufts up the code. Can we avoid this sort of check?

Small log window. The Android log buffer is about 50k. On a device with a good number of apps installed, the entire log window can scroll in a matter of minutes. This makes it impossible to go back and examine the log for specific events.

To solve these problems, I created a simple utility class, ALog (advanced log).

To add context, it automatically appends the class and method name, and line number to the beginning of every statement. The result is clear, contextual log statements like this,

W/my-app(23659): InstallManager.<init>@120: failed to make APK dir: /mnt/sdcard/Download

ALog avoids evaluating arguments by accepting a log pattern plus arguments,

ALog.w("oh noes, a problem occured when i %s and then also at %s", msg1, msg2);

The first argument, the format, must conform to the interface dedefined by String.format().

ALog support setting a global tag for the entire app, and setting the level. Do so in your application class,

public class MyApplication extends Application {
    @Override
    public void onCreate() {
        ALog.setTag("MyApp");
        ALog.setLevel(CLog.Level.D);
    }
}
ALog optionally writes every log record to a file on your SD card, to keep a nearly infinite log of exactly what happened in your app. Just enable file logging like,
ALog.setFileLogging(true);
Files are created at: <Environment.getExternalStorageDirectory()>/alog/<tag>.log
Note that this is extremely inefficient. It should only be used temporarily to find bugs then disabled. It should never be used in production. You can look at the code below, but it starts a thread that take()’s from a blocking queue. Logging a message offers()’s into this queue. The file is opened, appended, and closed at each log statement; it does not keep the file open.
Here’s the source,
public class ALog {
	private static class LogContext {
		LogContext(StackTraceElement element) {
			// this.className = element.getClassName();
			this.simpleClassName = getSimpleClassName(element.getClassName());
			this.methodName = element.getMethodName();
			this.lineNumber = element.getLineNumber();
		}

		// String className;
		String simpleClassName;
		String methodName;
		int lineNumber;
	}

	public enum Level {
		V(1), D(2), I(3), W(4), E(5);

		private int value;

		private Level(int value) {
			this.value = value;
		}

		int getValue() {
			return value;
		}
	};

	private static final DateFormat FLOG_FORMAT = new SimpleDateFormat(
			"yyyy-MM-dd HH:mm:ss.SSS");
	private static final File LOG_DIR = new File(
			Environment.getExternalStorageDirectory() + File.separator + "alog");
	private static boolean fileLogging = false;
	private static String tag = "<tag unset>";
	private static Level level = Level.V;
	private static final BlockingQueue<String> logQueue = new LinkedBlockingQueue<String>();
	private static Runnable queueRunner = new Runnable() {
		@Override
		public void run() {
			String line;
			try {
				while ((line = logQueue.take()) != null) {

					if (!Environment.getExternalStorageState().equals(
							Environment.MEDIA_MOUNTED)) {
						continue;
					}
					if (!LOG_DIR.exists() && !LOG_DIR.mkdirs()) {
						continue;
					}

					File logFile = new File(LOG_DIR, tag + ".log");
					Writer w = null;
					try {
						w = new FileWriter(logFile, true);
						w.write(line);
						w.close();
					} catch (IOException e) {
					} finally {
						if (w != null) {
							try {
								w.close();
							} catch (IOException e1) {
							}
						}
					}
				}
			} catch (InterruptedException e) {
			}
		}
	};

	static {
		new Thread(queueRunner).start();
	}

	private static LogContext getContext() {
		StackTraceElement[] trace = Thread.currentThread().getStackTrace();
		StackTraceElement element = trace[5]; // frame below us; the caller
		LogContext context = new LogContext(element);
		return context;
	}

	private static final String getMessage(String s, Object... args) {
		s = String.format(s, args);
		LogContext c = getContext();
		String msg = c.simpleClassName + "." + c.methodName + "@"
				+ c.lineNumber + ": " + s;
		return msg;
	}

	private static String getSimpleClassName(String className) {
		int i = className.lastIndexOf(".");
		if (i == -1) {
			return className;
		}
		return className.substring(i + 1);
	}

	public static void setLevel(Level l) {
		level = l;
	}

	public static void setTag(String t) {
		tag = t;
	}

	public static void setFileLogging(boolean enable) {
		fileLogging = enable;
	}

	public static void v(String format, Object... args) {
		if (level.getValue() > Level.V.getValue()) {
			return;
		}
		String msg = getMessage(format, args);
		Log.v(tag, msg);
		if (fileLogging) {
			flog(Level.V, msg);
		}
	}

	public static void d(String format, Object... args) {
		if (level.getValue() > Level.D.getValue()) {
			return;
		}
		String msg = getMessage(format, args);
		Log.d(tag, msg);
		if (fileLogging) {
			flog(Level.D, msg);
		}
	}

	public static void i(String format, Object... args) {
		if (level.getValue() > Level.I.getValue()) {
			return;
		}
		String msg = getMessage(format, args);
		Log.i(tag, msg);
		if (fileLogging) {
			flog(Level.I, msg);
		}
	}

	public static void w(String format, Object... args) {
		if (level.getValue() > Level.W.getValue()) {
			return;
		}
		String msg = getMessage(format, args);
		Log.w(tag, msg);
		if (fileLogging) {
			flog(Level.W, msg);
		}
	}

	public static void w(String format, Throwable t, Object... args) {
		if (level.getValue() > Level.W.getValue()) {
			return;
		}
		String msg = getMessage(format, args);
		Log.w(tag, msg, t);
		if (fileLogging) {
			flog(Level.W, msg, t);
		}
	}

	public static void e(String format, Object... args) {
		if (level.getValue() > Level.E.getValue()) {
			return;
		}
		String msg = getMessage(format, args);
		Log.e(tag, msg);
		if (fileLogging) {
			flog(Level.E, msg);
		}
	}

	public static void e(String format, Throwable t, Object... args) {
		if (level.getValue() > Level.E.getValue()) {
			return;
		}
		String msg = getMessage(format, args);
		Log.e(tag, msg, t);
		if (fileLogging) {
			flog(Level.E, msg, t);
		}
	}

	public static void trace() {
		try {
			throw new Throwable("dumping stack trace ...");
		} catch (Throwable t) {
			ALog.e("trace:", t);
		}
	}

	public static String getStackTraceString(Throwable tr) {
		if (tr == null) {
			return "";
		}

		Throwable t = tr;
		while (t != null) {
			if (t instanceof UnknownHostException) {
				return "";
			}
			t = t.getCause();
		}

		StringWriter sw = new StringWriter();
		PrintWriter pw = new PrintWriter(sw);
		tr.printStackTrace(pw);
		return sw.toString();
	}

	private static void flog(Level l, String msg) {
		flog(l, msg, null);
	}

	private static void flog(Level l, String msg, Throwable t) {
		String timeString = FLOG_FORMAT.format(new Date());
		String line = timeString + " " + l.toString() + "/" + tag + ": " + msg
				+ "\n";
		if (t != null) {
			line += getStackTraceString(t) + "\n";
		}
		logQueue.offer(line);
	}
}

JSON Flattener

2012/01/21

I recently ran into the problem of needing to store a JSON object in a key, value store. I pasted a little utility class that does the job below. Here’s an example run against some nasty JSON,

ORIGINAL:

{ string: 'aString', integer: -1, nested: { x: 1, y: 2, z: 3}, moreNested: { a: [1,2,3], b: [4,5,6], c: [7,8,9]}, arrayFromHell: [{ innerArray: [{ innerInnerArray: [1,2,3]}]}]}

ENCODED:

{"arrayFromHell.0.innerArray.0.innerInnerArray.0":1,"arrayFromHell.0.innerArray.0.innerInnerArray.1":2,"arrayFromHell.0.innerArray.0.innerInnerArray.2":3,"nested.z":3,"nested.y":2,"nested.x":1,"integer":-1,"string":"aString","moreNested.b.0":4,"moreNested.b.1":5,"moreNested.b.2":6,"moreNested.c.0":7,"moreNested.c.1":8,"moreNested.c.2":9,"moreNested.a.0":1,"moreNested.a.1":2,"moreNested.a.2":3}

DECODED:

{"nested":{"z":3,"y":2,"x":1},"arrayFromHell":[{"innerArray":[{"innerInnerArray":[1,2,3]}]}],"integer":-1,"string":"aString","moreNested":{"b":[4,5,6],"c":[7,8,9],"a":[1,2,3]}}

As you can see, it simply looks at every leaf value (string, int) and derives the key path and stores the value there. For example,

{ x: { y: { z: 1 } } }

becomes,

{ x.y.z: 1 }

Arrays are a special case. Each value in an array becomes the path so far to the array appended with the array index. For example,

{ anArray: [1,2,3] }

becomes,

{ anArray.0: 1, anArray.1: 2, anArray.2: 3 }
Of course this isn’t foolproof. If you have dots in your key names, it will break. If you use numbers as keys, it will break as it makes an assumption that a numbered key path element indicates an array. Here’s the source.
package org.test.flatjson;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

public class JsonFlattener {
	public static String encode(JSONObject jo) throws JSONException {
		String s = "{" + encode(null, jo) + "}";
		return s;

	}

	public static String encode(String json) throws JSONException {
		JSONObject jo = new JSONObject(json);
		return encode(jo);
	}

	private static String encode(String parent, Object val)
			throws JSONException {
		StringBuilder sb = new StringBuilder();
		if (val instanceof JSONObject) {
			JSONObject jo = (JSONObject) val;
			for (Iterator<String> i = jo.keys(); i.hasNext();) {
				String key = i.next();
				String hkey = (parent == null) ? key : parent + "." + key;
				Object jval = jo.get(key);
				String json = encode(hkey, jval);
				sb.append(json);
				if (i.hasNext()) {
					sb.append(",");
				}
			}
		} else if (val instanceof JSONArray) {
			JSONArray ja = (JSONArray) val;
			for (int i = 0; i < ja.length(); i++) {
				String hkey = (parent == null) ? "" + i : parent + "." + i;
				Object aval = ja.get(i);
				String json = encode(hkey, aval);
				sb.append(json);
				if (i < ja.length() - 1) {
					sb.append(",");
				}
			}
		} else if (val instanceof String) {
			sb.append("\"").append(parent).append("\"").append(":");
			String s = (String) val;
			sb.append(JSONObject.quote(s));
		} else if (val instanceof Integer) {
			sb.append("\"").append(parent).append("\"").append(":");
			Integer integer = (Integer) val;
			sb.append(integer);
		}

		return sb.toString();
	}

	public static String decode(String flatJson) throws JSONException {
		JSONObject encoded = new JSONObject(flatJson);
		return decodeToString(encoded);
	}

	public static String decodeToString(JSONObject encoded)
			throws JSONException {
		return decodeToObject(encoded).toString();
	}

	public static JSONObject decodeToObject(JSONObject encoded)
			throws JSONException {
		JSONObject decoded = new JSONObject();

		for (Iterator<String> i = encoded.keys(); i.hasNext();) {
			String hkey = i.next();
			String[] keys = hkey.split("\\.");

			Object json = decoded;

			for (int j = 0; j < keys.length; j++) {
				if (j == keys.length - 1) {
					Object val = encoded.get(hkey);
					if (json instanceof JSONObject) {
						JSONObject jo = (JSONObject)json;
						jo.put(keys[j], val);
					} else if (json instanceof JSONArray) {
						JSONArray ja = (JSONArray)json;
						int index = Integer.parseInt(keys[j]);
						ja.put(index, val);
					}
				} else {
					// we're NOT at a leaf key

					if (!isNumber(keys[j + 1])) {
						// next index is an object

						JSONObject joChild;

						if (json instanceof JSONObject) {
							// last index was an object
							// we're creating an object in an object
							JSONObject jo = (JSONObject)json;
							if (jo.has(keys[j])) {
								joChild = jo.getJSONObject(keys[j]);
							} else {
								joChild = new JSONObject();
								jo.put(keys[j], joChild);
							}
						} else if (json instanceof JSONArray) {
							// last index was an array
							// we're creating an object in an array
							JSONArray ja = (JSONArray)json;
							int index = Integer.parseInt(keys[j]);
							if (!ja.isNull(index)) {
								joChild = ja.getJSONObject(index);
							} else {
								joChild = new JSONObject();
								ja.put(index, joChild);
							}
						} else {
							throw new AssertionError("unhandled object type");
						}
						json = joChild;
					} else {
						// next index is an array element

						JSONArray jaChild;

						if (json instanceof JSONObject) {
							// last index was an object,
							// we're creating an array in an object
							JSONObject jo = (JSONObject)json;
							if (jo.has(keys[j])) {
								jaChild = jo.getJSONArray(keys[j]);
							} else {
								jaChild = new JSONArray();
								jo.put(keys[j], jaChild);
							}
						} else if (json instanceof JSONArray) {
							// last index was an array
							// we're creating an array in an array
							JSONArray ja = (JSONArray)json;
							int index = Integer.parseInt(keys[j + 1]);
							if (!ja.isNull(index)) {
								jaChild = ja.getJSONArray(index);
							} else {
								jaChild = new JSONArray();
								ja.put(index, jaChild);
							}
						} else {
							throw new AssertionError("unhandled object type");
						}
						json = jaChild;
					}
				}
			}
		}
		return decoded;
	}

	private static boolean isNumber(String s) {
		try {
			Integer.parseInt(s);
			return true;
		} catch (NumberFormatException e) {
			return false;
		}

	}
}

Android Bitmap Scaling

2011/01/27

Scaling images is a common task for many applications including those written for Android. The most obvious approach, as described on this page, won’t work in most cases. The problem as you would soon find out if you tried it is that your phone doesn’t have enough memory. Luckily we can get around that, but not easily.

The trick ends up being a two step process. First we decode the bitmap, setting the sampling level as high as we can while avoiding loss of quality, then we scale this smaller bitmap in memory down to the exact specified size. I’ve included this in a nicely packaged class BitmapScaler, included at the end of this post.The class itself looks quite long but this is only because it has the flexibility of scaling bitmaps from resources, assets, and files.

BitmapScaler preserves the aspect ratio. You may only pass a new width. The resulting height will be scaled equal to the amount the width was scaled.

For example,

BitmapScaler scaler = new BitmapScaler(getResources(), R.drawable.moorwen, newWidth);
imageView.setImageBitmap(scaler.getScaled());

BitmapScaler class follows,

class BitmapScaler {
	private static class Size {
		int sample;
		float scale;
	}

	private Bitmap scaled;

	BitmapScaler(Resources resources, int resId, int newWidth)
			throws IOException {
		Size size = getRoughSize(resources, resId, newWidth);
		roughScaleImage(resources, resId, size);
		scaleImage(newWidth);
	}

	BitmapScaler(File file, int newWidth) throws IOException {
		InputStream is = null;
		try {
			is = new FileInputStream(file);
			Size size = getRoughSize(is, newWidth);
			try {
				is = new FileInputStream(file);
				roughScaleImage(is, size);
				scaleImage(newWidth);
			} finally {
				is.close();
			}
		} finally {
			is.close();
		}
	}

	BitmapScaler(AssetManager manager, String assetName, int newWidth)
			throws IOException {
		InputStream is = null;
		try {
			is = manager.open(assetName);
			Size size = getRoughSize(is, newWidth);
			try {
				is = manager.open(assetName);
				roughScaleImage(is, size);
				scaleImage(newWidth);
			} finally {
				is.close();
			}
		} finally {
			is.close();
		}
	}

	Bitmap getScaled() {
		return scaled;
	}

	private void scaleImage(int newWidth) {
		int width = scaled.getWidth();
		int height = scaled.getHeight();

		float scaleWidth = ((float) newWidth) / width;
		float ratio = ((float) scaled.getWidth()) / newWidth;
		int newHeight = (int) (height / ratio);
		float scaleHeight = ((float) newHeight) / height;

		Matrix matrix = new Matrix();
		matrix.postScale(scaleWidth, scaleHeight);

		scaled = Bitmap.createBitmap(scaled, 0, 0, width, height, matrix, true);
	}

	private void roughScaleImage(InputStream is, Size size) {
		Matrix matrix = new Matrix();
		matrix.postScale(size.scale, size.scale);

		BitmapFactory.Options scaledOpts = new BitmapFactory.Options();
		scaledOpts.inSampleSize = size.sample;
		scaled = BitmapFactory.decodeStream(is, null, scaledOpts);
	}

	private void roughScaleImage(Resources resources, int resId, Size size) {
		Matrix matrix = new Matrix();
		matrix.postScale(size.scale, size.scale);

		BitmapFactory.Options scaledOpts = new BitmapFactory.Options();
		scaledOpts.inSampleSize = size.sample;
		scaled = BitmapFactory.decodeResource(resources, resId, scaledOpts);
	}

	private Size getRoughSize(InputStream is, int newWidth) {
		BitmapFactory.Options o = new BitmapFactory.Options();
		o.inJustDecodeBounds = true;
		BitmapFactory.decodeStream(is, null, o);

		Size size = getRoughSize(o.outWidth, o.outHeight, newWidth);
		return size;
	}

	private Size getRoughSize(Resources resources, int resId, int newWidth) {
		BitmapFactory.Options o = new BitmapFactory.Options();
		o.inJustDecodeBounds = true;
		BitmapFactory.decodeResource(resources, resId, o);

		Size size = getRoughSize(o.outWidth, o.outHeight, newWidth);
		return size;
	}

	private Size getRoughSize(int outWidth, int outHeight, int newWidth) {
		Size size = new Size();
		size.scale = outWidth / newWidth;
		size.sample = 1;

		int width = outWidth;
		int height = outHeight;

		int newHeight = (int) (outHeight / size.scale);

		while (true) {
			if (width / 2 < newWidth || height / 2 < newHeight) {
				break;
			}
			width /= 2;
			height /= 2;
			size.sample *= 2;
		}
		return size;
	}
}

Or, try out the Eclipse test project.


Android Shortcuts

2011/01/23

Shortcuts are an often overlooked usability enhancer for your Android app.  They are a nice alternative to providing quick access to particular locations or views of data, where you would otherwise have “favorites” or “most recently used” function. I wanted to take a moment to share my implementation experience.

My use case: I am the author of the Clear Sky Droid (and the donate version) app which is available on the Android market. Being a tad esoteric, it’s probably not the best example, but I’ll try my best to explain the usability problem in abstract terms. Essentially, the user can define a list of favorites by searching for items. From the list of favorites, they can then select a detailed view of the item. The usability problem is that many users want direct access to the detailed view of a particular favorite – something that is normally 3 clicks away.

Shortcuts to the rescue. With this feature, the user can set up a desktop shortcut for direct access to the detailed view of a particular favorite. It’s now one click away.

What exactly is a shortcut? It’s an Android desktop artifact. It is not part of your application. When clicked, it broadcasts an intent that you have defined. So you can see where this is going. You simply need to set up your application to respond appropriately to whatever intent is configured into the shortcut.

First we will register an activity into the Android “create shortcuts” home screen menu. Create a new activity, or add the functionality to an existing activity. Respond to the create shortcut intent,

<intent-filter>
    <action android:name="android.intent.action.CREATE_SHORTCUT" />
    <category android:name="android.intent.category.DEFAULT" />
</intent-filter>

You can look at CSDroid’s AndroidManifest.xml to see this in context.

This is all it takes to make your application show up when the user long presses on the home screen and select “shortcuts”.

You must now make your application handle the intent. In CSDroid, look at the startService() method in the class TabwidgetActivity, find the line where it’s checking if the intent’s action is “android.intent.action.CREATE_SHORTCUT”. This means the user has long pressed the home screen and selected your app.

Gather any information necessary to customize the shortcut to access to a particular aspect of your application.  For CSDroid, we open a dialog that lets the user select a favorite item. Looking at the code in TabwidgetActivity mentioned above, you can see that a dialog  is shown when the CREATE_SHORTCUT intent is received.  The dialog builder class is FaveShortcutDialogBuilder.

FaveShortcutDialogBuilder just gets the favorite item then calls back to TabwidgetActivity‘s saveShortcut() method. This is where we request creation of the desktop shortcut. Here are the steps,

  1. Create an intent that targets your application, setting action, data, extras, etc accordingly to perform the specific action requested when the user clicks the shortcut
  2. Wrap that intent in another intent that will be sent back to the Android “create shortcut” activity
  3. Set this wrapping intent as the result of your activity, and finish

Here’s the code from CSDroid,

void saveShortcut(Site site) {
    Intent shortcutIntent = new Intent(this, TabWidgetActivity.class);
    shortcutIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
    shortcutIntent.putExtra("org.jtb.csdroid.site.id", site.getId());

    Intent intent = new Intent();
    intent.putExtra(Intent.EXTRA_SHORTCUT_INTENT, shortcutIntent);
    intent.putExtra(Intent.EXTRA_SHORTCUT_NAME, site.getName());
    intent.putExtra(Intent.EXTRA_SHORTCUT_ICON_RESOURCE, Intent.ShortcutIconResource.fromContext(this, R.drawable.icon));
    intent.setAction("com.android.launcher.action.INSTALL_SHORTCUT");
    setResult(RESULT_OK, intent);
    finish();
}

In this case, if CSDroid sees a site ID in the intent’s extras, it will perform the shortcut action for that site ID.

I should not that in the context of CSDroid the code could be organized better. A better pattern would separate the main activity from the short cut creation, and shortcut handling activities. So you might have MainActivity, CreateShortcutActivity, and HandleShortcutActivity.

One last gotcha … my first idea was to auto-create the shortcut from the app. In other words, circumvent the steps where the user must long-press the home screen, select shortcuts, select my app, then select a favorite – all from within the application. CSDroid already has an activity that lists the user’s favorites. I wanted to simply add a “add shortcut” option right there. Well, you can’t do that. You cannot programmatically create shortcuts. Only the user can do that.

CSDroid is open source. Visit the CSDroid Google code project page to find the source code, etc.

And finally, all due credit to Attilla Danko, the owner of ClearDarkSky.com, the data source for this application.


Gracefully Handle When Location is Unavailable (Android)

2010/12/24

This seemed like a useful pattern so I thought I’d document it here, as it seems quite common. The scenario is that you have an application that provides some location-based service or information, but needs to gracefully handle the situation when the location cannot be obtained.

Ther are various reason why the location might not be obtainable. The most common is that the user disabled one or all location services requested by your app. Just because you request location permissions and the user accepts doesn’t mean that they actually have those services enabled. And worse, when you request a location in this situation, you don’t get any sort of indicator that it has been disabled. Your location changed callback is just never called.

My application grabs earthquake data from USGS and provides information to the user with respect to their proximity to the quake. If the app can’t get the user’s location, it should functionally normally, minuses providing distances to quake epicenters. For reference, the app is called QuakeAlert! and you can find it on the market, and the source on Google code.

QuakeAlert! has a service that runs periodically and alerts the user of any new earthquakes that match their criteria. When the service runs, it should get the location if possible, process the data from USGS, and potentially alert the user. The problem is in how Android location callbacks operate. The service can request a location update, but there’s no guarantee it will ever be called (and it won’t if the user has disabled the location service).  Here is the solution,

UpdateService- this is an intent service that does the real work, whatever that may be. In the case of QuakeAlert! it fetches the data from USGS and processes it, sends notifications, and updates the user interface (if it’s running).

LocationService- this is a regular (non-intent) service that does the following,

  1. Registers for location updates
  2. Schedules a TimerTask for execution (say a few minutes in the future)

If the service gets an “on location changed” event before the time is up, it cancels the timer. If the timer runs first, it cancels the “on location changed” registration. In both cases, it calls the UpdateService to do the work.

UpdateService gets the location by calling getLastKnownLocation(). If there was a location obtained through the running of LocationService then it will get that location and use it. The last known location may have also been obtained by a different application at some earlier time getting location updates. That’s okay to, we know we’ve tried our best to get the most accurate location we can at the time.

The implementation of LocationService in QuakeAlert! is reusable. Just pass it a timeout (in milliseconds) and a broadcast intent to send when either the location in obtained or when the timer executes. For example,

// create broadcast intent that will ultimately start your
// intent service
Intent broadcastIntent = new Intent(...);

Intent locationIntent = new Intent(context, LocationService.class);
locationIntent.putExtra("timeout", 1000 * 60 * 2); // 2 minutes
locationIntent.putExtra("broadcastIntent", broadcastIntent);
context.startService(locationIntent);

Why pass a broadcast intent, instead of an intent that would start the update service directly? Since UpdateService is an intent service, we must obtain a wake lock before it is run, and the pattern for doing that is to receive an intent (in a receiver), grab the lock there, start the service from the reciever’s handler, then release the lock in the intent service when the work is done.

Note that this same pattern applied to a a foreground activity is much more straighforward: 1) register for location updates 2) open a cancelable progress dialog. If we get an “on location changed” event, dismiss the dialog, and start the UpdateService. If the user cancels, dismiss the dialog, and start the UpdateService. In both cases remember to remove thelocation listener.