Providing a config files for your Android app

2015/09/15

Ever needed to embed a configuration file in your Android app? What’s the best practice? As far as I know, there isn’t one. Here’s a nice solution. What’d you’d like is to be able to define your configuration in the same format as an Android resource file, like this,

<string name="url">http://www.foo.com</string>

You can put your resource in strings.xml (or any other file in res/values), but then you are stuck with completions. Do you want R.string.url to be a valid string resource in your app? No, it’s not a localizable string and having it respond to code completion isn’t the right thing. Moreover, if you are providing a library, you will expose your configuration values to developers that import your library.

You can put an XML resource file in res/xml, but Android isn’t going to parse it. Here’s a simple helper class to parse a a res/xml resource as an Android resource file,

public class XmlResourceParser {
  protected final Context context;

  private final Map<String, String> strings = new HashMap<String, String>();
  private final Map<String, Integer> integers = new HashMap<String, Integer>();
  private final Map<String, Boolean> booleans = new HashMap<String, Boolean>();

  public XmlResourceParser(Context context) {
    this.context = context;
  }

  public void parse(int id) throws XmlPullParserException, IOException {
    XmlPullParser xpp = context.getResources().getXml(id);
    int eventType = xpp.getEventType();

    while (eventType != XmlPullParser.END_DOCUMENT) {
      if (eventType == XmlPullParser.START_DOCUMENT) {
      } else if (eventType == XmlPullParser.START_TAG) {
        String tag = xpp.getName();
        if ("string".equals(tag)) {
          strings.put(xpp.getAttributeValue(null, "name"), xpp.nextText());
        } else if ("integer".equals(tag)) {
          integers.put(xpp.getAttributeValue(null, "name"), Integer.valueOf(xpp.nextText()));
        } else if ("boolean".equals(tag)) {
          booleans.put(xpp.getAttributeValue(null, "name"), Boolean.valueOf(xpp.nextText()));
        }
      } else if (eventType == XmlPullParser.END_TAG) {
      } else if (eventType == XmlPullParser.TEXT) {
      }
      eventType = xpp.next();
    }
  }

  public String getString(String key, String def) {
    if (strings.containsKey(key)) {
      return strings.get(key);
    }
    return def;
  }

  public int getInt(String key, int def) {
    if (integers.containsKey(key)) {
      return integers.get(key);
    }
    return def;
  }

  public boolean getBoolean(String key, boolean def) {
    if (booleans.containsKey(key)) {
      return booleans.get(key);
    }
    return def;
  }
}

Obviously this only handles strings, ints, and bools. The rest is left for an exercise for the reader. You use it like this,

XmlResourceParser xrp = new XmlResourceParser().parse(R.xml.myresources);
String url = xrp.getString("url");

Linear Grid Layout

2014/12/18

Pre API-21, GridLayout has no concept of weights. That means it’s impossible to make a GridLayout that stretches it’s rows and columns to fill the available space. Apparently API 21 fixes this, so says the Javadocs,

As of API 21, GridLayout’s distribution of excess space accomodates the principle of weight. In the event that no weights are specified, the previous conventions are respected and columns and rows are taken as flexible if their views specify some form of alignment within their groups.

Pre API 21, here’s a simple solution. LinearGridLayout is a view that simulates a grid layout by nesting LinearLayouts. This is just sample code and while it worked for my particular situation you may need to tweak it to your requirements.

To use, just insert into your XML,

    <LinearGridLayout
        auto:columns="2"
        auto:margins="10dp"
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>

And in your code, add some views,

LinearGridLayout layout = ...;
layout.addViews(myViews);

Here’s the source …

public class LinearGridLayout extends LinearLayout {

  private int columns;
  private int margins;

  public LinearGridLayout(Context context) {
    super(context);
  }

  public LinearGridLayout(Context context, AttributeSet attrs) {
    super(context, attrs);
    applyAttrs(context, attrs);
  }

  public LinearGridLayout(Context context, AttributeSet attrs, int defStyle) {
    super(context, attrs, defStyle);
    applyAttrs(context, attrs);
  }

  private void applyAttrs(Context context, AttributeSet attrs) {
    TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.ContactlessLogosView, 0, 0);

    try {
        columns = a.getInt(R.styleable.ContactlessLogosView_columns, Integer.MAX_VALUE);
        margins = a.getDimensionPixelSize(R.styleable.ContactlessLogosView_margins, 0);
    } finally {
      a.recycle();
    }

    setOrientation(VERTICAL);
  }

  public void addViews(Collection<View> views) {
    Preconditions.checkArgument(columns != 0);

    removeAllViews();

    int c = 0;
    LinearLayout layout = null;

    for (View v: views) {
      if (c == columns) {
        c = 0;
        layout = null;
      }

      if (layout == null) {
        layout = new LinearLayout(getContext());
        layout.setOrientation(HORIZONTAL);
        LayoutParams rowParams = new LayoutParams(LayoutParams.MATCH_PARENT, 0, 1);
        layout.setLayoutParams(rowParams);
        addView(layout);
      }

      LayoutParams lp = new LayoutParams(0, LayoutParams.WRAP_CONTENT, 1);
      lp.setMargins(margins, margins, margins, margins);
      v.setLayoutParams(lp);
      layout.addView(v);

      c++;
    }

    if (columns != Integer.MAX_VALUE) {
      for (int i = c; i < columns; i++) {
        LayoutParams lp = new LayoutParams(0, LayoutParams.WRAP_CONTENT, 1);
        View v = new View(getContext());
        v.setLayoutParams(lp);
        layout.addView(v);
      }
    }
  }
}

BANNED – Nexus 6 Stock Check App

2014/11/25

In my quest for a Nexus 6, I wrote a small app to pound Google’s product pages waiting for it to come in stock. Not surprisingly Google found it a “SPAM violation” (fair enough) and removed it from the Play store. There are other stock check apps on the store today that aren’t flagged, so I’m not quite sure how mine is worse. Don’t get me wrong, I completely understand why they pulled it.

Anyway, I think the app is fun so here it is¬†on Github. The source is there of course if you are interested / worried about what it’s doing. Like I mentioned, there are other such apps on the Play Store, but this one is ad-free, and works quite cleanly with little system overhead (and battery drain).

If Google had pre-order (hello?) then there’d be no need for apps like this. It’s the difference between an orderly line and a mob. I don’t understand their reasoning. Perhaps they want to encourage people to visit and re-visit their site looking for the product.


The problem with ACRA

2013/07/03

ACRA is a popular, open-source crash report framework for Android. It however has a fundamental flaw: it is quite likely to cause an ANR when reporting a crash. Here is why …

In a nutshell, ACRA adds an uncaught exception handler, and in the handler starts a thread to do the work of sending the report and return normally from the custom uncaught exception handler method. The work done by the send thread can be arbitrary (it’s a plugin interface), but it will typically send the report to a server somewhere. When the thread is done, it either calls the default uncaught exception handler or calls System.exit(), depending on its configuration.

The problem is what happens when you return from the uncaught exception handler method without either calling the uncaught exception handler, or calling System.exit(). Try it yourself,

public class MainActivity extends Activity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        Thread.setDefaultUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() {
            @Override
            public void uncaughtException(Thread thread, Throwable ex) {
                ex.printStackTrace();
                // don't call default uncaught exception handler
            }
        });
    }

    @Override
    protected void onResume() {
        super.onResume();
        throw new AssertionError("hello, assertion.");
    }
}

When run, this causes an ANR. If ACRA’s send thread takes longer than the ANR timeout (~5 seconds), then it too will cause an ANR for the same reason. Since the send thread is typically performing network operations, it is quite common for it to block for longer periods of time. If the use clicks “wait” on the ANR dialog, the app process is killed and the report it not immediately send. This is not a problem, since ACRA won’t delete it’s file cached copy of the crash report until the send thread responds that it successfully sent the report.

This is not all terribly bad. The user will see an ANR dialog in stead of a force close dialog. The report will be sent at a later time, probably the next time the app is started by the user. It could however be fixed. ACRA shouldn’t even try to send the crash report when it’s crashing. It should simply write the cache copy to the file system, on the main thread, and then allow the app to exit normally. It can send the report during normal app operation the next time it is started.

As a side note, ACRA’s “silent mode” should be avoided. This causes ACRA to not call the default Android uncaught exception handler and simply kill the process and call System.exit() (which is redundant, but anyway). However, it skips the logic in the default Android uncaught exception handler. This includes calling out to Google’s crash report service. It also hides the force close dialog which is confusing to the user.


2013/01/18

As many people have noted, my Android application alogcat does not work correctly on Jellybean devices. The reason is that applications can no longer read log entries created by other applications. They can still read log entries created by themselves, but obviously that doesn’t help alogcat.

The logic makes sense I suppose. A poorly written application may log sensitive information. Allowing other applications to read this is a bad thing.

For alogcat, there is a workaround. You must explicitly grant alogcat the READ_LOGS permission from the command line. From the Android shell,

shell@android:/ $ pm grant org.jtb.alogcat android.permission.READ_LOGS    

Or if you have ADB installed, from your computer’s terminal with your device connected,

$ adb shell pm grant org.jtb.alogcat android.permission.READ_LOGS    

This of course requires that you install an Android terminal emulator, or have ADB installed on your computer. Android Terminal Emulator is a good choice for an Android terminal. The Android SDK, which includes ADB can be downloaded here.


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.


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);
	}
}