Add vogar expectation file support for CTS.

Bug 3181338

Change-Id: I97e1f8781d7b2781241aec13f1452c51ed6b91cd
diff --git a/libs/json/Android.mk b/libs/json/Android.mk
new file mode 100644
index 0000000..7ec9e79
--- /dev/null
+++ b/libs/json/Android.mk
@@ -0,0 +1,25 @@
+#
+# Copyright (C) 2010 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+LOCAL_PATH:= $(call my-dir)
+include $(CLEAR_VARS)
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+
+LOCAL_MODULE := jsonlib
+LOCAL_MODULE_TAGS := optional
+
+include $(BUILD_HOST_JAVA_LIBRARY)
+
diff --git a/libs/json/src/com/android/json/stream/JsonReader.java b/libs/json/src/com/android/json/stream/JsonReader.java
new file mode 100644
index 0000000..d912d62
--- /dev/null
+++ b/libs/json/src/com/android/json/stream/JsonReader.java
@@ -0,0 +1,1106 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.json.stream;
+
+import java.io.IOException;
+import java.io.Reader;
+import java.io.Closeable;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Reads a JSON (<a href="http://www.ietf.org/rfc/rfc4627.txt">RFC 4627</a>)
+ * encoded value as a stream of tokens. This stream includes both literal
+ * values (strings, numbers, booleans, and nulls) as well as the begin and
+ * end delimiters of objects and arrays. The tokens are traversed in
+ * depth-first order, the same order that they appear in the JSON document.
+ * Within JSON objects, name/value pairs are represented by a single token.
+ *
+ * <h3>Parsing JSON</h3>
+ * To create a recursive descent parser for your own JSON streams, first create
+ * an entry point method that creates a {@code JsonReader}.
+ *
+ * <p>Next, create handler methods for each structure in your JSON text. You'll
+ * need a method for each object type and for each array type.
+ * <ul>
+ *   <li>Within <strong>array handling</strong> methods, first call {@link
+ *       #beginArray} to consume the array's opening bracket. Then create a
+ *       while loop that accumulates values, terminating when {@link #hasNext}
+ *       is false. Finally, read the array's closing bracket by calling {@link
+ *       #endArray}.
+ *   <li>Within <strong>object handling</strong> methods, first call {@link
+ *       #beginObject} to consume the object's opening brace. Then create a
+ *       while loop that assigns values to local variables based on their name.
+ *       This loop should terminate when {@link #hasNext} is false. Finally,
+ *       read the object's closing brace by calling {@link #endObject}.
+ * </ul>
+ * <p>When a nested object or array is encountered, delegate to the
+ * corresponding handler method.
+ *
+ * <p>When an unknown name is encountered, strict parsers should fail with an
+ * exception. Lenient parsers should call {@link #skipValue()} to recursively
+ * skip the value's nested tokens, which may otherwise conflict.
+ *
+ * <p>If a value may be null, you should first check using {@link #peek()}.
+ * Null literals can be consumed using either {@link #nextNull()} or {@link
+ * #skipValue()}.
+ *
+ * <h3>Example</h3>
+ * Suppose we'd like to parse a stream of messages such as the following: <pre> {@code
+ * [
+ *   {
+ *     "id": 912345678901,
+ *     "text": "How do I read JSON on Android?",
+ *     "geo": null,
+ *     "user": {
+ *       "name": "android_newb",
+ *       "followers_count": 41
+ *      }
+ *   },
+ *   {
+ *     "id": 912345678902,
+ *     "text": "@android_newb just use android.util.JsonReader!",
+ *     "geo": [50.454722, -104.606667],
+ *     "user": {
+ *       "name": "jesse",
+ *       "followers_count": 2
+ *     }
+ *   }
+ * ]}</pre>
+ * This code implements the parser for the above structure: <pre>   {@code
+ *
+ *   public List<Message> readJsonStream(InputStream in) throws IOException {
+ *     JsonReader reader = new JsonReader(new InputStreamReader(in, "UTF-8"));
+ *     return readMessagesArray(reader);
+ *   }
+ *
+ *   public List<Message> readMessagesArray(JsonReader reader) throws IOException {
+ *     List<Message> messages = new ArrayList<Message>();
+ *
+ *     reader.beginArray();
+ *     while (reader.hasNext()) {
+ *       messages.add(readMessage(reader));
+ *     }
+ *     reader.endArray();
+ *     return messages;
+ *   }
+ *
+ *   public Message readMessage(JsonReader reader) throws IOException {
+ *     long id = -1;
+ *     String text = null;
+ *     User user = null;
+ *     List<Double> geo = null;
+ *
+ *     reader.beginObject();
+ *     while (reader.hasNext()) {
+ *       String name = reader.nextName();
+ *       if (name.equals("id")) {
+ *         id = reader.nextLong();
+ *       } else if (name.equals("text")) {
+ *         text = reader.nextString();
+ *       } else if (name.equals("geo") && reader.peek() != JsonToken.NULL) {
+ *         geo = readDoublesArray(reader);
+ *       } else if (name.equals("user")) {
+ *         user = readUser(reader);
+ *       } else {
+ *         reader.skipValue();
+ *       }
+ *     }
+ *     reader.endObject();
+ *     return new Message(id, text, user, geo);
+ *   }
+ *
+ *   public List<Double> readDoublesArray(JsonReader reader) throws IOException {
+ *     List<Double> doubles = new ArrayList<Double>();
+ *
+ *     reader.beginArray();
+ *     while (reader.hasNext()) {
+ *       doubles.add(reader.nextDouble());
+ *     }
+ *     reader.endArray();
+ *     return doubles;
+ *   }
+ *
+ *   public User readUser(JsonReader reader) throws IOException {
+ *     String username = null;
+ *     int followersCount = -1;
+ *
+ *     reader.beginObject();
+ *     while (reader.hasNext()) {
+ *       String name = reader.nextName();
+ *       if (name.equals("name")) {
+ *         username = reader.nextString();
+ *       } else if (name.equals("followers_count")) {
+ *         followersCount = reader.nextInt();
+ *       } else {
+ *         reader.skipValue();
+ *       }
+ *     }
+ *     reader.endObject();
+ *     return new User(username, followersCount);
+ *   }}</pre>
+ *
+ * <h3>Number Handling</h3>
+ * This reader permits numeric values to be read as strings and string values to
+ * be read as numbers. For example, both elements of the JSON array {@code
+ * [1, "1"]} may be read using either {@link #nextInt} or {@link #nextString}.
+ * This behavior is intended to prevent lossy numeric conversions: double is
+ * JavaScript's only numeric type and very large values like {@code
+ * 9007199254740993} cannot be represented exactly on that platform. To minimize
+ * precision loss, extremely large values should be written and read as strings
+ * in JSON.
+ *
+ * <p>Each {@code JsonReader} may be used to read a single JSON stream. Instances
+ * of this class are not thread safe.
+ */
+public final class JsonReader implements Closeable {
+
+    private static final String TRUE = "true";
+    private static final String FALSE = "false";
+
+    /** The input JSON. */
+    private final Reader in;
+
+    /** True to accept non-spec compliant JSON */
+    private boolean lenient = false;
+
+    /**
+     * Use a manual buffer to easily read and unread upcoming characters, and
+     * also so we can create strings without an intermediate StringBuilder.
+     * We decode literals directly out of this buffer, so it must be at least as
+     * long as the longest token that can be reported as a number.
+     */
+    private final char[] buffer = new char[1024];
+    private int pos = 0;
+    private int limit = 0;
+
+    private final List<JsonScope> stack = new ArrayList<JsonScope>();
+    {
+        push(JsonScope.EMPTY_DOCUMENT);
+    }
+
+    /**
+     * The type of the next token to be returned by {@link #peek} and {@link
+     * #advance}. If null, peek() will assign a value.
+     */
+    private JsonToken token;
+
+    /** The text of the next name. */
+    private String name;
+
+    /*
+     * For the next literal value, we may have the text value, or the position
+     * and length in the buffer.
+     */
+    private String value;
+    private int valuePos;
+    private int valueLength;
+
+    /** True if we're currently handling a skipValue() call. */
+    private boolean skipping = false;
+
+    /**
+     * Creates a new instance that reads a JSON-encoded stream from {@code in}.
+     */
+    public JsonReader(Reader in) {
+        if (in == null) {
+            throw new NullPointerException("in == null");
+        }
+        this.in = in;
+    }
+
+    /**
+     * Configure this parser to be  be liberal in what it accepts. By default,
+     * this parser is strict and only accepts JSON as specified by <a
+     * href="http://www.ietf.org/rfc/rfc4627.txt">RFC 4627</a>. Setting the
+     * parser to lenient causes it to ignore the following syntax errors:
+     *
+     * <ul>
+     *   <li>End of line comments starting with {@code //} or {@code #} and
+     *       ending with a newline character.
+     *   <li>C-style comments starting with {@code /*} and ending with
+     *       {@code *}{@code /}. Such comments may not be nested.
+     *   <li>Names that are unquoted or {@code 'single quoted'}.
+     *   <li>Strings that are unquoted or {@code 'single quoted'}.
+     *   <li>Array elements separated by {@code ;} instead of {@code ,}.
+     *   <li>Unnecessary array separators. These are interpreted as if null
+     *       was the omitted value.
+     *   <li>Names and values separated by {@code =} or {@code =>} instead of
+     *       {@code :}.
+     *   <li>Name/value pairs separated by {@code ;} instead of {@code ,}.
+     * </ul>
+     */
+    public void setLenient(boolean lenient) {
+        this.lenient = lenient;
+    }
+
+    /**
+     * Consumes the next token from the JSON stream and asserts that it is the
+     * beginning of a new array.
+     */
+    public void beginArray() throws IOException {
+        expect(JsonToken.BEGIN_ARRAY);
+    }
+
+    /**
+     * Consumes the next token from the JSON stream and asserts that it is the
+     * end of the current array.
+     */
+    public void endArray() throws IOException {
+        expect(JsonToken.END_ARRAY);
+    }
+
+    /**
+     * Consumes the next token from the JSON stream and asserts that it is the
+     * beginning of a new object.
+     */
+    public void beginObject() throws IOException {
+        expect(JsonToken.BEGIN_OBJECT);
+    }
+
+    /**
+     * Consumes the next token from the JSON stream and asserts that it is the
+     * end of the current array.
+     */
+    public void endObject() throws IOException {
+        expect(JsonToken.END_OBJECT);
+    }
+
+    /**
+     * Consumes {@code expected}.
+     */
+    private void expect(JsonToken expected) throws IOException {
+        peek();
+        if (token != expected) {
+            throw new IllegalStateException("Expected " + expected + " but was " + peek());
+        }
+        advance();
+    }
+
+    /**
+     * Returns true if the current array or object has another element.
+     */
+    public boolean hasNext() throws IOException {
+        peek();
+        return token != JsonToken.END_OBJECT && token != JsonToken.END_ARRAY;
+    }
+
+    /**
+     * Returns the type of the next token without consuming it.
+     */
+    public JsonToken peek() throws IOException {
+        if (token != null) {
+          return token;
+        }
+
+        switch (peekStack()) {
+            case EMPTY_DOCUMENT:
+                replaceTop(JsonScope.NONEMPTY_DOCUMENT);
+                JsonToken firstToken = nextValue();
+                if (token != JsonToken.BEGIN_ARRAY && token != JsonToken.BEGIN_OBJECT) {
+                    throw new IOException(
+                            "Expected JSON document to start with '[' or '{' but was " + token);
+                }
+                return firstToken;
+            case EMPTY_ARRAY:
+                return nextInArray(true);
+            case NONEMPTY_ARRAY:
+                return nextInArray(false);
+            case EMPTY_OBJECT:
+                return nextInObject(true);
+            case DANGLING_NAME:
+                return objectValue();
+            case NONEMPTY_OBJECT:
+                return nextInObject(false);
+            case NONEMPTY_DOCUMENT:
+                return token = JsonToken.END_DOCUMENT;
+            case CLOSED:
+                throw new IllegalStateException("JsonReader is closed");
+            default:
+                throw new AssertionError();
+        }
+    }
+
+    /**
+     * Advances the cursor in the JSON stream to the next token.
+     */
+    private JsonToken advance() throws IOException {
+        peek();
+
+        JsonToken result = token;
+        token = null;
+        value = null;
+        name = null;
+        return result;
+    }
+
+    /**
+     * Returns the next token, a {@link JsonToken#NAME property name}, and
+     * consumes it.
+     *
+     * @throws IOException if the next token in the stream is not a property
+     *     name.
+     */
+    public String nextName() throws IOException {
+        peek();
+        if (token != JsonToken.NAME) {
+            throw new IllegalStateException("Expected a name but was " + peek());
+        }
+        String result = name;
+        advance();
+        return result;
+    }
+
+    /**
+     * Returns the {@link JsonToken#STRING string} value of the next token,
+     * consuming it. If the next token is a number, this method will return its
+     * string form.
+     *
+     * @throws IllegalStateException if the next token is not a string or if
+     *     this reader is closed.
+     */
+    public String nextString() throws IOException {
+        peek();
+        if (token != JsonToken.STRING && token != JsonToken.NUMBER) {
+            throw new IllegalStateException("Expected a string but was " + peek());
+        }
+
+        String result = value;
+        advance();
+        return result;
+    }
+
+    /**
+     * Returns the {@link JsonToken#BOOLEAN boolean} value of the next token,
+     * consuming it.
+     *
+     * @throws IllegalStateException if the next token is not a boolean or if
+     *     this reader is closed.
+     */
+    public boolean nextBoolean() throws IOException {
+        peek();
+        if (token != JsonToken.BOOLEAN) {
+            throw new IllegalStateException("Expected a boolean but was " + token);
+        }
+
+        boolean result = (value == TRUE);
+        advance();
+        return result;
+    }
+
+    /**
+     * Consumes the next token from the JSON stream and asserts that it is a
+     * literal null.
+     *
+     * @throws IllegalStateException if the next token is not null or if this
+     *     reader is closed.
+     */
+    public void nextNull() throws IOException {
+        peek();
+        if (token != JsonToken.NULL) {
+            throw new IllegalStateException("Expected null but was " + token);
+        }
+
+        advance();
+    }
+
+    /**
+     * Returns the {@link JsonToken#NUMBER double} value of the next token,
+     * consuming it. If the next token is a string, this method will attempt to
+     * parse it as a double using {@link Double#parseDouble(String)}.
+     *
+     * @throws IllegalStateException if the next token is not a literal value.
+     */
+    public double nextDouble() throws IOException {
+        peek();
+        if (token != JsonToken.STRING && token != JsonToken.NUMBER) {
+            throw new IllegalStateException("Expected a double but was " + token);
+        }
+
+        double result = Double.parseDouble(value);
+        advance();
+        return result;
+    }
+
+    /**
+     * Returns the {@link JsonToken#NUMBER long} value of the next token,
+     * consuming it. If the next token is a string, this method will attempt to
+     * parse it as a long. If the next token's numeric value cannot be exactly
+     * represented by a Java {@code long}, this method throws.
+     *
+     * @throws IllegalStateException if the next token is not a literal value.
+     * @throws NumberFormatException if the next literal value cannot be parsed
+     *     as a number, or exactly represented as a long.
+     */
+    public long nextLong() throws IOException {
+        peek();
+        if (token != JsonToken.STRING && token != JsonToken.NUMBER) {
+            throw new IllegalStateException("Expected a long but was " + token);
+        }
+
+        long result;
+        try {
+            result = Long.parseLong(value);
+        } catch (NumberFormatException ignored) {
+            double asDouble = Double.parseDouble(value); // don't catch this NumberFormatException
+            result = (long) asDouble;
+            if ((double) result != asDouble) {
+                throw new NumberFormatException(value);
+            }
+        }
+
+        advance();
+        return result;
+    }
+
+    /**
+     * Returns the {@link JsonToken#NUMBER int} value of the next token,
+     * consuming it. If the next token is a string, this method will attempt to
+     * parse it as an int. If the next token's numeric value cannot be exactly
+     * represented by a Java {@code int}, this method throws.
+     *
+     * @throws IllegalStateException if the next token is not a literal value.
+     * @throws NumberFormatException if the next literal value cannot be parsed
+     *     as a number, or exactly represented as an int.
+     */
+    public int nextInt() throws IOException {
+        peek();
+        if (token != JsonToken.STRING && token != JsonToken.NUMBER) {
+            throw new IllegalStateException("Expected an int but was " + token);
+        }
+
+        int result;
+        try {
+            result = Integer.parseInt(value);
+        } catch (NumberFormatException ignored) {
+            double asDouble = Double.parseDouble(value); // don't catch this NumberFormatException
+            result = (int) asDouble;
+            if ((double) result != asDouble) {
+                throw new NumberFormatException(value);
+            }
+        }
+
+        advance();
+        return result;
+    }
+
+    /**
+     * Closes this JSON reader and the underlying {@link Reader}.
+     */
+    public void close() throws IOException {
+        value = null;
+        token = null;
+        stack.clear();
+        stack.add(JsonScope.CLOSED);
+        in.close();
+    }
+
+    /**
+     * Skips the next value recursively. If it is an object or array, all nested
+     * elements are skipped. This method is intended for use when the JSON token
+     * stream contains unrecognized or unhandled values.
+     */
+    public void skipValue() throws IOException {
+        skipping = true;
+        try {
+            int count = 0;
+            do {
+                JsonToken token = advance();
+                if (token == JsonToken.BEGIN_ARRAY || token == JsonToken.BEGIN_OBJECT) {
+                    count++;
+                } else if (token == JsonToken.END_ARRAY || token == JsonToken.END_OBJECT) {
+                    count--;
+                }
+            } while (count != 0);
+        } finally {
+            skipping = false;
+        }
+    }
+
+    private JsonScope peekStack() {
+        return stack.get(stack.size() - 1);
+    }
+
+    private JsonScope pop() {
+        return stack.remove(stack.size() - 1);
+    }
+
+    private void push(JsonScope newTop) {
+        stack.add(newTop);
+    }
+
+    /**
+     * Replace the value on the top of the stack with the given value.
+     */
+    private void replaceTop(JsonScope newTop) {
+        stack.set(stack.size() - 1, newTop);
+    }
+
+    private JsonToken nextInArray(boolean firstElement) throws IOException {
+        if (firstElement) {
+            replaceTop(JsonScope.NONEMPTY_ARRAY);
+        } else {
+            /* Look for a comma before each element after the first element. */
+            switch (nextNonWhitespace()) {
+                case ']':
+                    pop();
+                    return token = JsonToken.END_ARRAY;
+                case ';':
+                    checkLenient(); // fall-through
+                case ',':
+                    break;
+                default:
+                    throw syntaxError("Unterminated array");
+            }
+        }
+
+        switch (nextNonWhitespace()) {
+            case ']':
+                if (firstElement) {
+                    pop();
+                    return token = JsonToken.END_ARRAY;
+                }
+                // fall-through to handle ",]"
+            case ';':
+            case ',':
+                /* In lenient mode, a 0-length literal means 'null' */
+                checkLenient();
+                pos--;
+                value = "null";
+                return token = JsonToken.NULL;
+            default:
+                pos--;
+                return nextValue();
+        }
+    }
+
+    private JsonToken nextInObject(boolean firstElement) throws IOException {
+        /*
+         * Read delimiters. Either a comma/semicolon separating this and the
+         * previous name-value pair, or a close brace to denote the end of the
+         * object.
+         */
+        if (firstElement) {
+            /* Peek to see if this is the empty object. */
+            switch (nextNonWhitespace()) {
+                case '}':
+                    pop();
+                    return token = JsonToken.END_OBJECT;
+                default:
+                    pos--;
+            }
+        } else {
+            switch (nextNonWhitespace()) {
+                case '}':
+                    pop();
+                    return token = JsonToken.END_OBJECT;
+                case ';':
+                case ',':
+                    break;
+                default:
+                    throw syntaxError("Unterminated object");
+            }
+        }
+
+        /* Read the name. */
+        int quote = nextNonWhitespace();
+        switch (quote) {
+            case '\'':
+                checkLenient(); // fall-through
+            case '"':
+                name = nextString((char) quote);
+                break;
+            default:
+                checkLenient();
+                pos--;
+                name = nextLiteral(false);
+                if (name.isEmpty()) {
+                    throw syntaxError("Expected name");
+                }
+        }
+
+        replaceTop(JsonScope.DANGLING_NAME);
+        return token = JsonToken.NAME;
+    }
+
+    private JsonToken objectValue() throws IOException {
+        /*
+         * Read the name/value separator. Usually a colon ':'. In lenient mode
+         * we also accept an equals sign '=', or an arrow "=>".
+         */
+        switch (nextNonWhitespace()) {
+            case ':':
+                break;
+            case '=':
+                checkLenient();
+                if ((pos < limit || fillBuffer(1)) && buffer[pos] == '>') {
+                    pos++;
+                }
+                break;
+            default:
+                throw syntaxError("Expected ':'");
+        }
+
+        replaceTop(JsonScope.NONEMPTY_OBJECT);
+        return nextValue();
+    }
+
+    private JsonToken nextValue() throws IOException {
+        int c = nextNonWhitespace();
+        switch (c) {
+            case '{':
+                push(JsonScope.EMPTY_OBJECT);
+                return token = JsonToken.BEGIN_OBJECT;
+
+            case '[':
+                push(JsonScope.EMPTY_ARRAY);
+                return token = JsonToken.BEGIN_ARRAY;
+
+            case '\'':
+                checkLenient(); // fall-through
+            case '"':
+                value = nextString((char) c);
+                return token = JsonToken.STRING;
+
+            default:
+                pos--;
+                return readLiteral();
+        }
+    }
+
+    /**
+     * Returns true once {@code limit - pos >= minimum}. If the data is
+     * exhausted before that many characters are available, this returns
+     * false.
+     */
+    private boolean fillBuffer(int minimum) throws IOException {
+        if (limit != pos) {
+            limit -= pos;
+            System.arraycopy(buffer, pos, buffer, 0, limit);
+        } else {
+            limit = 0;
+        }
+
+        pos = 0;
+        int total;
+        while ((total = in.read(buffer, limit, buffer.length - limit)) != -1) {
+            limit += total;
+            if (limit >= minimum) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    private int nextNonWhitespace() throws IOException {
+        while (pos < limit || fillBuffer(1)) {
+            int c = buffer[pos++];
+            switch (c) {
+                case '\t':
+                case ' ':
+                case '\n':
+                case '\r':
+                    continue;
+
+                case '/':
+                    if (pos == limit && !fillBuffer(1)) {
+                        return c;
+                    }
+
+                    checkLenient();
+                    char peek = buffer[pos];
+                    switch (peek) {
+                        case '*':
+                            // skip a /* c-style comment */
+                            pos++;
+                            if (!skipTo("*/")) {
+                                throw syntaxError("Unterminated comment");
+                            }
+                            pos += 2;
+                            continue;
+
+                        case '/':
+                            // skip a // end-of-line comment
+                            pos++;
+                            skipToEndOfLine();
+                            continue;
+
+                        default:
+                            return c;
+                    }
+
+                case '#':
+                    /*
+                     * Skip a # hash end-of-line comment. The JSON RFC doesn't
+                     * specify this behaviour, but it's required to parse
+                     * existing documents. See http://b/2571423.
+                     */
+                    checkLenient();
+                    skipToEndOfLine();
+                    continue;
+
+                default:
+                    return c;
+            }
+        }
+
+        throw syntaxError("End of input");
+    }
+
+    private void checkLenient() throws IOException {
+        if (!lenient) {
+            throw syntaxError("Use JsonReader.setLenient(true) to accept malformed JSON");
+        }
+    }
+
+    /**
+     * Advances the position until after the next newline character. If the line
+     * is terminated by "\r\n", the '\n' must be consumed as whitespace by the
+     * caller.
+     */
+    private void skipToEndOfLine() throws IOException {
+        while (pos < limit || fillBuffer(1)) {
+            char c = buffer[pos++];
+            if (c == '\r' || c == '\n') {
+                break;
+            }
+        }
+    }
+
+    private boolean skipTo(String toFind) throws IOException {
+        outer:
+        for (; pos + toFind.length() < limit || fillBuffer(toFind.length()); pos++) {
+            for (int c = 0; c < toFind.length(); c++) {
+                if (buffer[pos + c] != toFind.charAt(c)) {
+                    continue outer;
+                }
+            }
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * Returns the string up to but not including {@code quote}, unescaping any
+     * character escape sequences encountered along the way. The opening quote
+     * should have already been read. This consumes the closing quote, but does
+     * not include it in the returned string.
+     *
+     * @param quote either ' or ".
+     * @throws NumberFormatException if any unicode escape sequences are
+     *     malformed.
+     */
+    private String nextString(char quote) throws IOException {
+        StringBuilder builder = null;
+        do {
+            /* the index of the first character not yet appended to the builder. */
+            int start = pos;
+            while (pos < limit) {
+                int c = buffer[pos++];
+
+                if (c == quote) {
+                    if (skipping) {
+                        return "skipped!";
+                    } else if (builder == null) {
+                        return new String(buffer, start, pos - start - 1);
+                    } else {
+                        builder.append(buffer, start, pos - start - 1);
+                        return builder.toString();
+                    }
+
+                } else if (c == '\\') {
+                    if (builder == null) {
+                        builder = new StringBuilder();
+                    }
+                    builder.append(buffer, start, pos - start - 1);
+                    builder.append(readEscapeCharacter());
+                    start = pos;
+                }
+            }
+
+            if (builder == null) {
+                builder = new StringBuilder();
+            }
+            builder.append(buffer, start, pos - start);
+        } while (fillBuffer(1));
+
+        throw syntaxError("Unterminated string");
+    }
+
+    /**
+     * Reads the value up to but not including any delimiter characters. This
+     * does not consume the delimiter character.
+     *
+     * @param assignOffsetsOnly true for this method to only set the valuePos
+     *     and valueLength fields and return a null result. This only works if
+     *     the literal is short; a string is returned otherwise.
+     */
+    private String nextLiteral(boolean assignOffsetsOnly) throws IOException {
+        StringBuilder builder = null;
+        valuePos = -1;
+        valueLength = 0;
+        int i = 0;
+
+        findNonLiteralCharacter:
+        while (true) {
+            for (; pos + i < limit; i++) {
+                switch (buffer[pos + i]) {
+                case '/':
+                case '\\':
+                case ';':
+                case '#':
+                case '=':
+                    checkLenient(); // fall-through
+                case '{':
+                case '}':
+                case '[':
+                case ']':
+                case ':':
+                case ',':
+                case ' ':
+                case '\t':
+                case '\f':
+                case '\r':
+                case '\n':
+                    break findNonLiteralCharacter;
+                }
+            }
+
+            /*
+             * Attempt to load the entire literal into the buffer at once. If
+             * we run out of input, add a non-literal character at the end so
+             * that decoding doesn't need to do bounds checks.
+             */
+            if (i < buffer.length) {
+                if (fillBuffer(i + 1)) {
+                    continue;
+                } else {
+                    buffer[limit] = '\0';
+                    break;
+                }
+            }
+
+            // use a StringBuilder when the value is too long. It must be an unquoted string.
+            if (builder == null) {
+                builder = new StringBuilder();
+            }
+            builder.append(buffer, pos, i);
+            valueLength += i;
+            pos += i;
+            i = 0;
+            if (!fillBuffer(1)) {
+                break;
+            }
+        }
+
+        String result;
+        if (assignOffsetsOnly && builder == null) {
+            valuePos = pos;
+            result = null;
+        } else if (skipping) {
+            result = "skipped!";
+        } else if (builder == null) {
+            result = new String(buffer, pos, i);
+        } else {
+            builder.append(buffer, pos, i);
+            result = builder.toString();
+        }
+        valueLength += i;
+        pos += i;
+        return result;
+    }
+
+    @Override public String toString() {
+        return getClass().getSimpleName() + " near " + getSnippet();
+    }
+
+    /**
+     * Unescapes the character identified by the character or characters that
+     * immediately follow a backslash. The backslash '\' should have already
+     * been read. This supports both unicode escapes "u000A" and two-character
+     * escapes "\n".
+     *
+     * @throws NumberFormatException if any unicode escape sequences are
+     *     malformed.
+     */
+    private char readEscapeCharacter() throws IOException {
+        if (pos == limit && !fillBuffer(1)) {
+            throw syntaxError("Unterminated escape sequence");
+        }
+
+        char escaped = buffer[pos++];
+        switch (escaped) {
+            case 'u':
+                if (pos + 4 > limit && !fillBuffer(4)) {
+                    throw syntaxError("Unterminated escape sequence");
+                }
+                String hex = new String(buffer, pos, 4);
+                pos += 4;
+                return (char) Integer.parseInt(hex, 16);
+
+            case 't':
+                return '\t';
+
+            case 'b':
+                return '\b';
+
+            case 'n':
+                return '\n';
+
+            case 'r':
+                return '\r';
+
+            case 'f':
+                return '\f';
+
+            case '\'':
+            case '"':
+            case '\\':
+            default:
+                return escaped;
+        }
+    }
+
+    /**
+     * Reads a null, boolean, numeric or unquoted string literal value.
+     */
+    private JsonToken readLiteral() throws IOException {
+        value = nextLiteral(true);
+        if (valueLength == 0) {
+            throw syntaxError("Expected literal value");
+        }
+        token = decodeLiteral();
+        if (token == JsonToken.STRING) {
+          checkLenient();
+        }
+        return token;
+    }
+
+    /**
+     * Assigns {@code nextToken} based on the value of {@code nextValue}.
+     */
+    private JsonToken decodeLiteral() throws IOException {
+        if (valuePos == -1) {
+            // it was too long to fit in the buffer so it can only be a string
+            return JsonToken.STRING;
+        } else if (valueLength == 4
+                && ('n' == buffer[valuePos    ] || 'N' == buffer[valuePos    ])
+                && ('u' == buffer[valuePos + 1] || 'U' == buffer[valuePos + 1])
+                && ('l' == buffer[valuePos + 2] || 'L' == buffer[valuePos + 2])
+                && ('l' == buffer[valuePos + 3] || 'L' == buffer[valuePos + 3])) {
+            value = "null";
+            return JsonToken.NULL;
+        } else if (valueLength == 4
+                && ('t' == buffer[valuePos    ] || 'T' == buffer[valuePos    ])
+                && ('r' == buffer[valuePos + 1] || 'R' == buffer[valuePos + 1])
+                && ('u' == buffer[valuePos + 2] || 'U' == buffer[valuePos + 2])
+                && ('e' == buffer[valuePos + 3] || 'E' == buffer[valuePos + 3])) {
+            value = TRUE;
+            return JsonToken.BOOLEAN;
+        } else if (valueLength == 5
+                && ('f' == buffer[valuePos    ] || 'F' == buffer[valuePos    ])
+                && ('a' == buffer[valuePos + 1] || 'A' == buffer[valuePos + 1])
+                && ('l' == buffer[valuePos + 2] || 'L' == buffer[valuePos + 2])
+                && ('s' == buffer[valuePos + 3] || 'S' == buffer[valuePos + 3])
+                && ('e' == buffer[valuePos + 4] || 'E' == buffer[valuePos + 4])) {
+            value = FALSE;
+            return JsonToken.BOOLEAN;
+        } else {
+            value = new String(buffer, valuePos, valueLength);
+            return decodeNumber(buffer, valuePos, valueLength);
+        }
+    }
+
+    /**
+     * Determine whether the characters is a JSON number. Numbers are of the
+     * form -12.34e+56. Fractional and exponential parts are optional. Leading
+     * zeroes are not allowed in the value or exponential part, but are allowed
+     * in the fraction.
+     *
+     * <p>This has a side effect of setting isInteger.
+     */
+    private JsonToken decodeNumber(char[] chars, int offset, int length) {
+        int i = offset;
+        int c = chars[i];
+
+        if (c == '-') {
+            c = chars[++i];
+        }
+
+        if (c == '0') {
+            c = chars[++i];
+        } else if (c >= '1' && c <= '9') {
+            c = chars[++i];
+            while (c >= '0' && c <= '9') {
+                c = chars[++i];
+            }
+        } else {
+            return JsonToken.STRING;
+        }
+
+        if (c == '.') {
+            c = chars[++i];
+            while (c >= '0' && c <= '9') {
+                c = chars[++i];
+            }
+        }
+
+        if (c == 'e' || c == 'E') {
+            c = chars[++i];
+            if (c == '+' || c == '-') {
+                c = chars[++i];
+            }
+            if (c >= '0' && c <= '9') {
+                c = chars[++i];
+                while (c >= '0' && c <= '9') {
+                    c = chars[++i];
+                }
+            } else {
+                return JsonToken.STRING;
+            }
+        }
+
+        if (i == offset + length) {
+            return JsonToken.NUMBER;
+        } else {
+            return JsonToken.STRING;
+        }
+    }
+
+    /**
+     * Throws a new IO exception with the given message and a context snippet
+     * with this reader's content.
+     */
+    public IOException syntaxError(String message) throws IOException {
+        throw new JsonSyntaxException(message + " near " + getSnippet());
+    }
+
+    private CharSequence getSnippet() {
+        StringBuilder snippet = new StringBuilder();
+        int beforePos = Math.min(pos, 20);
+        snippet.append(buffer, pos - beforePos, beforePos);
+        int afterPos = Math.min(limit - pos, 20);
+        snippet.append(buffer, pos, afterPos);
+        return snippet;
+    }
+
+    private static class JsonSyntaxException extends IOException {
+        private JsonSyntaxException(String s) {
+            super(s);
+        }
+    }
+}
diff --git a/libs/json/src/com/android/json/stream/JsonScope.java b/libs/json/src/com/android/json/stream/JsonScope.java
new file mode 100644
index 0000000..12d10e5
--- /dev/null
+++ b/libs/json/src/com/android/json/stream/JsonScope.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.json.stream;
+
+/**
+ * Lexical scoping elements within a JSON reader or writer.
+ */
+enum JsonScope {
+
+    /**
+     * An array with no elements requires no separators or newlines before
+     * it is closed.
+     */
+    EMPTY_ARRAY,
+
+    /**
+     * A array with at least one value requires a comma and newline before
+     * the next element.
+     */
+    NONEMPTY_ARRAY,
+
+    /**
+     * An object with no name/value pairs requires no separators or newlines
+     * before it is closed.
+     */
+    EMPTY_OBJECT,
+
+    /**
+     * An object whose most recent element is a key. The next element must
+     * be a value.
+     */
+    DANGLING_NAME,
+
+    /**
+     * An object with at least one name/value pair requires a comma and
+     * newline before the next element.
+     */
+    NONEMPTY_OBJECT,
+
+    /**
+     * No object or array has been started.
+     */
+    EMPTY_DOCUMENT,
+
+    /**
+     * A document with at an array or object.
+     */
+    NONEMPTY_DOCUMENT,
+
+    /**
+     * A document that's been closed and cannot be accessed.
+     */
+    CLOSED,
+}
diff --git a/libs/json/src/com/android/json/stream/JsonToken.java b/libs/json/src/com/android/json/stream/JsonToken.java
new file mode 100644
index 0000000..a5233a4
--- /dev/null
+++ b/libs/json/src/com/android/json/stream/JsonToken.java
@@ -0,0 +1,82 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.json.stream;
+
+/**
+ * A structure, name or value type in a JSON-encoded string.
+ */
+public enum JsonToken {
+
+    /**
+     * The opening of a JSON array. Written using {@link JsonWriter#beginObject}
+     * and read using {@link JsonReader#beginObject}.
+     */
+    BEGIN_ARRAY,
+
+    /**
+     * The closing of a JSON array. Written using {@link JsonWriter#endArray}
+     * and read using {@link JsonReader#endArray}.
+     */
+    END_ARRAY,
+
+    /**
+     * The opening of a JSON object. Written using {@link JsonWriter#beginObject}
+     * and read using {@link JsonReader#beginObject}.
+     */
+    BEGIN_OBJECT,
+
+    /**
+     * The closing of a JSON object. Written using {@link JsonWriter#endObject}
+     * and read using {@link JsonReader#endObject}.
+     */
+    END_OBJECT,
+
+    /**
+     * A JSON property name. Within objects, tokens alternate between names and
+     * their values. Written using {@link JsonWriter#name} and read using {@link
+     * JsonReader#nextName}
+     */
+    NAME,
+
+    /**
+     * A JSON string.
+     */
+    STRING,
+
+    /**
+     * A JSON number represented in this API by a Java {@code double}, {@code
+     * long}, or {@code int}.
+     */
+    NUMBER,
+
+    /**
+     * A JSON {@code true} or {@code false}.
+     */
+    BOOLEAN,
+
+    /**
+     * A JSON {@code null}.
+     */
+    NULL,
+
+    /**
+     * The end of the JSON stream. This sentinel value is returned by {@link
+     * JsonReader#peek()} to signal that the JSON-encoded value has no more
+     * tokens.
+     */
+    END_DOCUMENT
+}
diff --git a/libs/json/src/com/android/json/stream/JsonWriter.java b/libs/json/src/com/android/json/stream/JsonWriter.java
new file mode 100644
index 0000000..66b21f0
--- /dev/null
+++ b/libs/json/src/com/android/json/stream/JsonWriter.java
@@ -0,0 +1,472 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.json.stream;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.io.Writer;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Writes a JSON (<a href="http://www.ietf.org/rfc/rfc4627.txt">RFC 4627</a>)
+ * encoded value to a stream, one token at a time. The stream includes both
+ * literal values (strings, numbers, booleans and nulls) as well as the begin
+ * and end delimiters of objects and arrays.
+ *
+ * <h3>Encoding JSON</h3>
+ * To encode your data as JSON, create a new {@code JsonWriter}. Each JSON
+ * document must contain one top-level array or object. Call methods on the
+ * writer as you walk the structure's contents, nesting arrays and objects as
+ * necessary:
+ * <ul>
+ *   <li>To write <strong>arrays</strong>, first call {@link #beginArray()}.
+ *       Write each of the array's elements with the appropriate {@link #value}
+ *       methods or by nesting other arrays and objects. Finally close the array
+ *       using {@link #endArray()}.
+ *   <li>To write <strong>objects</strong>, first call {@link #beginObject()}.
+ *       Write each of the object's properties by alternating calls to
+ *       {@link #name} with the property's value. Write property values with the
+ *       appropriate {@link #value} method or by nesting other objects or arrays.
+ *       Finally close the object using {@link #endObject()}.
+ * </ul>
+ *
+ * <h3>Example</h3>
+ * Suppose we'd like to encode a stream of messages such as the following: <pre> {@code
+ * [
+ *   {
+ *     "id": 912345678901,
+ *     "text": "How do I write JSON on Android?",
+ *     "geo": null,
+ *     "user": {
+ *       "name": "android_newb",
+ *       "followers_count": 41
+ *      }
+ *   },
+ *   {
+ *     "id": 912345678902,
+ *     "text": "@android_newb just use android.util.JsonWriter!",
+ *     "geo": [50.454722, -104.606667],
+ *     "user": {
+ *       "name": "jesse",
+ *       "followers_count": 2
+ *     }
+ *   }
+ * ]}</pre>
+ * This code encodes the above structure: <pre>   {@code
+ *   public void writeJsonStream(OutputStream out, List<Message> messages) throws IOException {
+ *     JsonWriter writer = new JsonWriter(new OutputStreamWriter(out, "UTF-8"));
+ *     writer.setIndent("  ");
+ *     writeMessagesArray(writer, messages);
+ *     writer.close();
+ *   }
+ *
+ *   public void writeMessagesArray(JsonWriter writer, List<Message> messages) throws IOException {
+ *     writer.beginArray();
+ *     for (Message message : messages) {
+ *       writeMessage(writer, message);
+ *     }
+ *     writer.endArray();
+ *   }
+ *
+ *   public void writeMessage(JsonWriter writer, Message message) throws IOException {
+ *     writer.beginObject();
+ *     writer.name("id").value(message.getId());
+ *     writer.name("text").value(message.getText());
+ *     if (message.getGeo() != null) {
+ *       writer.name("geo");
+ *       writeDoublesArray(writer, message.getGeo());
+ *     } else {
+ *       writer.name("geo").nullValue();
+ *     }
+ *     writer.name("user");
+ *     writeUser(writer, message.getUser());
+ *     writer.endObject();
+ *   }
+ *
+ *   public void writeUser(JsonWriter writer, User user) throws IOException {
+ *     writer.beginObject();
+ *     writer.name("name").value(user.getName());
+ *     writer.name("followers_count").value(user.getFollowersCount());
+ *     writer.endObject();
+ *   }
+ *
+ *   public void writeDoublesArray(JsonWriter writer, List<Double> doubles) throws IOException {
+ *     writer.beginArray();
+ *     for (Double value : doubles) {
+ *       writer.value(value);
+ *     }
+ *     writer.endArray();
+ *   }}</pre>
+ *
+ * <p>Each {@code JsonWriter} may be used to write a single JSON stream.
+ * Instances of this class are not thread safe. Calls that would result in a
+ * malformed JSON string will fail with an {@link IllegalStateException}.
+ */
+public final class JsonWriter implements Closeable {
+
+    /** The output data, containing at most one top-level array or object. */
+    private final Writer out;
+
+    private final List<JsonScope> stack = new ArrayList<JsonScope>();
+    {
+        stack.add(JsonScope.EMPTY_DOCUMENT);
+    }
+
+    /**
+     * A string containing a full set of spaces for a single level of
+     * indentation, or null for no pretty printing.
+     */
+    private String indent;
+
+    /**
+     * The name/value separator; either ":" or ": ".
+     */
+    private String separator = ":";
+
+    /**
+     * Creates a new instance that writes a JSON-encoded stream to {@code out}.
+     * For best performance, ensure {@link Writer} is buffered; wrapping in
+     * {@link java.io.BufferedWriter BufferedWriter} if necessary.
+     */
+    public JsonWriter(Writer out) {
+        if (out == null) {
+            throw new NullPointerException("out == null");
+        }
+        this.out = out;
+    }
+
+    /**
+     * Sets the indentation string to be repeated for each level of indentation
+     * in the encoded document. If {@code indent.isEmpty()} the encoded document
+     * will be compact. Otherwise the encoded document will be more
+     * human-readable.
+     *
+     * @param indent a string containing only whitespace.
+     */
+    public void setIndent(String indent) {
+        if (indent.isEmpty()) {
+            this.indent = null;
+            this.separator = ":";
+        } else {
+            this.indent = indent;
+            this.separator = ": ";
+        }
+    }
+
+    /**
+     * Begins encoding a new array. Each call to this method must be paired with
+     * a call to {@link #endArray}.
+     *
+     * @return this writer.
+     */
+    public JsonWriter beginArray() throws IOException {
+        return open(JsonScope.EMPTY_ARRAY, "[");
+    }
+
+    /**
+     * Ends encoding the current array.
+     *
+     * @return this writer.
+     */
+    public JsonWriter endArray() throws IOException {
+        return close(JsonScope.EMPTY_ARRAY, JsonScope.NONEMPTY_ARRAY, "]");
+    }
+
+    /**
+     * Begins encoding a new object. Each call to this method must be paired
+     * with a call to {@link #endObject}.
+     *
+     * @return this writer.
+     */
+    public JsonWriter beginObject() throws IOException {
+        return open(JsonScope.EMPTY_OBJECT, "{");
+    }
+
+    /**
+     * Ends encoding the current object.
+     *
+     * @return this writer.
+     */
+    public JsonWriter endObject() throws IOException {
+        return close(JsonScope.EMPTY_OBJECT, JsonScope.NONEMPTY_OBJECT, "}");
+    }
+
+    /**
+     * Enters a new scope by appending any necessary whitespace and the given
+     * bracket.
+     */
+    private JsonWriter open(JsonScope empty, String openBracket) throws IOException {
+        beforeValue(true);
+        stack.add(empty);
+        out.write(openBracket);
+        return this;
+    }
+
+    /**
+     * Closes the current scope by appending any necessary whitespace and the
+     * given bracket.
+     */
+    private JsonWriter close(JsonScope empty, JsonScope nonempty, String closeBracket)
+            throws IOException {
+        JsonScope context = peek();
+        if (context != nonempty && context != empty) {
+            throw new IllegalStateException("Nesting problem: " + stack);
+        }
+
+        stack.remove(stack.size() - 1);
+        if (context == nonempty) {
+            newline();
+        }
+        out.write(closeBracket);
+        return this;
+    }
+
+    /**
+     * Returns the value on the top of the stack.
+     */
+    private JsonScope peek() {
+        return stack.get(stack.size() - 1);
+    }
+
+    /**
+     * Replace the value on the top of the stack with the given value.
+     */
+    private void replaceTop(JsonScope topOfStack) {
+        stack.set(stack.size() - 1, topOfStack);
+    }
+
+    /**
+     * Encodes the property name.
+     *
+     * @param name the name of the forthcoming value. May not be null.
+     * @return this writer.
+     */
+    public JsonWriter name(String name) throws IOException {
+        if (name == null) {
+            throw new NullPointerException("name == null");
+        }
+        beforeName();
+        string(name);
+        return this;
+    }
+
+    /**
+     * Encodes {@code value}.
+     *
+     * @param value the literal string value, or null to encode a null literal.
+     * @return this writer.
+     */
+    public JsonWriter value(String value) throws IOException {
+        if (value == null) {
+            return nullValue();
+        }
+        beforeValue(false);
+        string(value);
+        return this;
+    }
+
+    /**
+     * Encodes {@code null}.
+     *
+     * @return this writer.
+     */
+    public JsonWriter nullValue() throws IOException {
+        beforeValue(false);
+        out.write("null");
+        return this;
+    }
+
+    /**
+     * Encodes {@code value}.
+     *
+     * @return this writer.
+     */
+    public JsonWriter value(boolean value) throws IOException {
+        beforeValue(false);
+        out.write(value ? "true" : "false");
+        return this;
+    }
+
+    /**
+     * Encodes {@code value}.
+     *
+     * @param value a finite value. May not be {@link Double#isNaN() NaNs} or
+     *     {@link Double#isInfinite() infinities}.
+     * @return this writer.
+     */
+    public JsonWriter value(double value) throws IOException {
+        if (Double.isNaN(value) || Double.isInfinite(value)) {
+            throw new IllegalArgumentException("Numeric values must be finite, but was " + value);
+        }
+        beforeValue(false);
+        out.append(Double.toString(value));
+        return this;
+    }
+
+    /**
+     * Encodes {@code value}.
+     *
+     * @return this writer.
+     */
+    public JsonWriter value(long value) throws IOException {
+        beforeValue(false);
+        out.write(Long.toString(value));
+        return this;
+    }
+
+    /**
+     * Ensures all buffered data is written to the underlying {@link Writer}
+     * and flushes that writer.
+     */
+    public void flush() throws IOException {
+        out.flush();
+    }
+
+    /**
+     * Flushes and closes this writer and the underlying {@link Writer}.
+     *
+     * @throws IOException if the JSON document is incomplete.
+     */
+    public void close() throws IOException {
+        out.close();
+
+        if (peek() != JsonScope.NONEMPTY_DOCUMENT) {
+            throw new IOException("Incomplete document");
+        }
+    }
+
+    private void string(String value) throws IOException {
+        out.write("\"");
+        for (int i = 0, length = value.length(); i < length; i++) {
+            char c = value.charAt(i);
+
+            /*
+             * From RFC 4627, "All Unicode characters may be placed within the
+             * quotation marks except for the characters that must be escaped:
+             * quotation mark, reverse solidus, and the control characters
+             * (U+0000 through U+001F)."
+             */
+            switch (c) {
+                case '"':
+                case '\\':
+                case '/':
+                    out.write('\\');
+                    out.write(c);
+                    break;
+
+                case '\t':
+                    out.write("\\t");
+                    break;
+
+                case '\b':
+                    out.write("\\b");
+                    break;
+
+                case '\n':
+                    out.write("\\n");
+                    break;
+
+                case '\r':
+                    out.write("\\r");
+                    break;
+
+                case '\f':
+                    out.write("\\f");
+                    break;
+
+                default:
+                    if (c <= 0x1F) {
+                        out.write(String.format("\\u%04x", (int) c));
+                    } else {
+                        out.write(c);
+                    }
+                    break;
+            }
+
+        }
+        out.write("\"");
+    }
+
+    private void newline() throws IOException {
+        if (indent == null) {
+            return;
+        }
+
+        out.write("\n");
+        for (int i = 1; i < stack.size(); i++) {
+            out.write(indent);
+        }
+    }
+
+    /**
+     * Inserts any necessary separators and whitespace before a name. Also
+     * adjusts the stack to expect the name's value.
+     */
+    private void beforeName() throws IOException {
+        JsonScope context = peek();
+        if (context == JsonScope.NONEMPTY_OBJECT) { // first in object
+            out.write(',');
+        } else if (context != JsonScope.EMPTY_OBJECT) { // not in an object!
+            throw new IllegalStateException("Nesting problem: " + stack);
+        }
+        newline();
+        replaceTop(JsonScope.DANGLING_NAME);
+    }
+
+    /**
+     * Inserts any necessary separators and whitespace before a literal value,
+     * inline array, or inline object. Also adjusts the stack to expect either a
+     * closing bracket or another element.
+     *
+     * @param root true if the value is a new array or object, the two values
+     *     permitted as top-level elements.
+     */
+    private void beforeValue(boolean root) throws IOException {
+        switch (peek()) {
+            case EMPTY_DOCUMENT: // first in document
+                if (!root) {
+                    throw new IllegalStateException(
+                            "JSON must start with an array or an object.");
+                }
+                replaceTop(JsonScope.NONEMPTY_DOCUMENT);
+                break;
+
+            case EMPTY_ARRAY: // first in array
+                replaceTop(JsonScope.NONEMPTY_ARRAY);
+                newline();
+                break;
+
+            case NONEMPTY_ARRAY: // another in array
+                out.append(',');
+                newline();
+                break;
+
+            case DANGLING_NAME: // value for name
+                out.append(separator);
+                replaceTop(JsonScope.NONEMPTY_OBJECT);
+                break;
+
+            case NONEMPTY_DOCUMENT:
+                throw new IllegalStateException(
+                        "JSON must have only one top-level value.");
+
+            default:
+                throw new IllegalStateException("Nesting problem: " + stack);
+        }
+    }
+}
diff --git a/libs/vogar-expect/Android.mk b/libs/vogar-expect/Android.mk
new file mode 100644
index 0000000..075bb43
--- /dev/null
+++ b/libs/vogar-expect/Android.mk
@@ -0,0 +1,25 @@
+#
+# Copyright (C) 2010 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+LOCAL_PATH:= $(call my-dir)
+include $(CLEAR_VARS)
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+LOCAL_MODULE := vogarexpectlib
+LOCAL_MODULE_TAGS := optional
+
+LOCAL_STATIC_JAVA_LIBRARIES := guavalib jsonlib
+include $(BUILD_HOST_JAVA_LIBRARY)
+
diff --git a/libs/vogar-expect/README b/libs/vogar-expect/README
new file mode 100644
index 0000000..eee6f83
--- /dev/null
+++ b/libs/vogar-expect/README
@@ -0,0 +1 @@
+Selected classes taken from http://code.google.com/p/vogar/
diff --git a/libs/vogar-expect/src/vogar/AnnotatedOutcome.java b/libs/vogar-expect/src/vogar/AnnotatedOutcome.java
new file mode 100644
index 0000000..a27ab9e
--- /dev/null
+++ b/libs/vogar-expect/src/vogar/AnnotatedOutcome.java
@@ -0,0 +1,147 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package vogar;
+
+import com.google.common.collect.Lists;
+import com.google.common.collect.Ordering;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.SortedMap;
+
+/**
+ * Contains an outcome for a test, along with some metadata pertaining to the history of this test,
+ * including a list of previous outcomes, an outcome corresponding to the tag Vogar is being run
+ * with, if applicable, and the expectation for this test, so that result value information is
+ * available.
+ */
+public final class AnnotatedOutcome {
+    public static Ordering<AnnotatedOutcome> ORDER_BY_NAME = new Ordering<AnnotatedOutcome>() {
+        @Override public int compare(AnnotatedOutcome a, AnnotatedOutcome b) {
+            return a.getName().compareTo(b.getName());
+       }
+    };
+
+    private final Expectation expectation;
+    private final Outcome outcome;
+    /** a list of previous outcomes for the same action, sorted in chronological order */
+    private final SortedMap<Long, Outcome> previousOutcomes;
+    /** will be null if not comparing to a tag */
+    private final String tagName;
+    private final Outcome tagOutcome;
+    private final boolean hasMetadata;
+
+    AnnotatedOutcome(Outcome outcome, Expectation expectation,
+            SortedMap<Long, Outcome> previousOutcomes, String tagName, Outcome tagOutcome,
+            boolean hasMetadata) {
+        if (previousOutcomes == null) {
+            throw new NullPointerException();
+        }
+        this.expectation = expectation;
+        this.outcome = outcome;
+        this.previousOutcomes = previousOutcomes;
+        this.tagName = tagName;
+        this.tagOutcome = tagOutcome;
+        this.hasMetadata = hasMetadata;
+    }
+
+    public Outcome getOutcome() {
+        return outcome;
+    }
+
+    public String getName() {
+        return outcome.getName();
+    }
+
+    public ResultValue getResultValue() {
+        return outcome.getResultValue(expectation);
+    }
+
+    public List<ResultValue> getPreviousResultValues() {
+        List<ResultValue> previousResultValues = new ArrayList<ResultValue>();
+        for (Outcome previousOutcome : previousOutcomes.values()) {
+            previousResultValues.add(previousOutcome.getResultValue(expectation));
+        }
+        return previousResultValues;
+    }
+
+    /**
+     * Returns the most recent result value of a run of this test (before the current run).
+     */
+    public ResultValue getMostRecentResultValue(ResultValue defaultValue) {
+        List<ResultValue> previousResultValues = getPreviousResultValues();
+        return previousResultValues.isEmpty() ?
+                defaultValue :
+                previousResultValues.get(previousResultValues.size() - 1);
+    }
+
+    public boolean hasTag() {
+        return tagOutcome != null;
+    }
+
+    public String getTagName() {
+        return tagName;
+    }
+
+    public ResultValue getTagResultValue() {
+        return tagOutcome == null ? null : tagOutcome.getResultValue(expectation);
+    }
+
+    /**
+     * Returns true if the outcome is noteworthy given the result value and previous history.
+     */
+    public boolean isNoteworthy() {
+        return getResultValue() != ResultValue.OK || recentlyChanged() || changedSinceTag();
+    }
+
+    public boolean outcomeChanged() {
+        List<Outcome> previousOutcomesList = getOutcomeList();
+        return previousOutcomesList.isEmpty()
+                || !outcome.equals(previousOutcomesList.get(previousOutcomesList.size() - 1));
+    }
+
+    private ArrayList<Outcome> getOutcomeList() {
+        return new ArrayList<Outcome>(previousOutcomes.values());
+    }
+
+    /**
+     * Returns true if the outcome recently changed in result value.
+     */
+    private boolean recentlyChanged() {
+        List<ResultValue> previousResultValues = getPreviousResultValues();
+        if (previousResultValues.isEmpty()) {
+            return false;
+        }
+        return previousResultValues.get(previousResultValues.size() - 1) != getResultValue();
+    }
+
+    private boolean changedSinceTag() {
+        ResultValue tagResultValue = getTagResultValue();
+        return tagResultValue != null && tagResultValue != getResultValue();
+    }
+
+    /**
+     * Returns a Long representing the time the outcome was last run. Returns {@code defaultValue}
+     * if the outcome is not known to have run before.
+     */
+    public Long lastRun(Long defaultValue) {
+        if (!hasMetadata) {
+            return defaultValue;
+        }
+        List<Long> runTimes = Lists.newArrayList(previousOutcomes.keySet());
+        return runTimes.isEmpty() ? defaultValue : runTimes.get(runTimes.size() - 1);
+    }
+}
diff --git a/libs/vogar-expect/src/vogar/Expectation.java b/libs/vogar-expect/src/vogar/Expectation.java
new file mode 100644
index 0000000..f065f42
--- /dev/null
+++ b/libs/vogar-expect/src/vogar/Expectation.java
@@ -0,0 +1,117 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package vogar;
+
+import java.util.Collections;
+import java.util.LinkedHashSet;
+import java.util.Set;
+import java.util.regex.Pattern;
+
+/**
+ * The expected result of an action execution. This is typically encoded in the
+ * expectations text file, which has the following format:
+ * <pre>
+ * test java.io.StreamTokenizer.Reset
+ * result UNSUPPORTED
+ * pattern .*should get token \[, but get -1.*
+ *
+ * # should we fix this?
+ * test java.util.Arrays.CopyMethods
+ * result COMPILE_FAILED
+ * pattern .*cannot find symbol.*
+ * </pre>
+ */
+public final class Expectation {
+
+    /** The pattern to use when no expected output is specified */
+    public static final Pattern MATCH_ALL_PATTERN
+            = Pattern.compile(".*", Pattern.MULTILINE | Pattern.DOTALL);
+
+    /** The expectation of a general successful run. */
+    public static final Expectation SUCCESS = new Expectation(Result.SUCCESS, MATCH_ALL_PATTERN,
+            Collections.<String>emptySet(), "", -1);
+
+    /** Justification for this expectation */
+    private final String description;
+
+    /** The action's expected result, such as {@code EXEC_FAILED}. */
+    private final Result result;
+
+    /** The pattern the expected output will match. */
+    private final Pattern pattern;
+
+    /** Attributes of this test. */
+    private final Set<String> tags;
+
+    /** The tracking bug ID */
+    private final long bug;
+
+    /** True if the identified bug still active. */
+    private boolean bugIsOpen = false;
+
+    public Expectation(Result result, Pattern pattern, Set<String> tags, String description, long bug) {
+        if (result == null || description == null || pattern == null) {
+            throw new IllegalArgumentException(
+                    "result=" + result + " description=" + description + " pattern=" + pattern);
+        }
+
+        this.description = description;
+        this.result = result;
+        this.pattern = pattern;
+        this.tags = new LinkedHashSet<String>(tags);
+        this.bug = bug;
+    }
+
+    public String getDescription() {
+        return description;
+    }
+
+    public long getBug() {
+        return bug;
+    }
+
+    public Result getResult() {
+        return result;
+    }
+
+    public Set<String> getTags() {
+        return tags;
+    }
+
+    /**
+     * Set the current status of this expectation's bug. When a bug is open,
+     * any result (success or failure) is permitted.
+     */
+    public void setBugIsOpen(boolean bugIsOpen) {
+        this.bugIsOpen = bugIsOpen;
+    }
+
+    /**
+     * Returns true if {@code outcome} matches this expectation.
+     */
+    public boolean matches(Outcome outcome) {
+        return patternMatches(outcome) && (bugIsOpen || result == outcome.getResult());
+    }
+
+    private boolean patternMatches(Outcome outcome) {
+        return pattern.matcher(outcome.getOutput()).matches();
+    }
+
+    @Override public String toString() {
+        return "Expectation[description=" + description + " pattern=" + pattern.pattern() + "]";
+    }
+}
diff --git a/libs/vogar-expect/src/vogar/ExpectationStore.java b/libs/vogar-expect/src/vogar/ExpectationStore.java
new file mode 100644
index 0000000..cfa20e9
--- /dev/null
+++ b/libs/vogar-expect/src/vogar/ExpectationStore.java
@@ -0,0 +1,255 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package vogar;
+
+//import com.google.caliper.internal.gson.stream.JsonReader;
+
+import com.android.json.stream.JsonReader;
+import com.google.common.base.Joiner;
+import com.google.common.base.Splitter;
+import com.google.common.collect.Iterables;
+
+import java.io.File;
+import java.io.FileReader;
+import java.io.IOException;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.regex.Pattern;
+import vogar.commands.Command;
+import vogar.util.Log;
+
+/**
+ * A database of expected outcomes. Entries in this database come in two forms.
+ * <ul>
+ *   <li>Outcome expectations name an outcome (or its prefix, such as
+ *       "java.util"), its expected result, and an optional pattern to match
+ *       the expected output.
+ *   <li>Failure expectations include a pattern that may match the output of any
+ *       outcome. These expectations are useful for hiding failures caused by
+ *       cross-cutting features that aren't supported.
+ * </ul>
+ *
+ * <p>If an outcome matches both an outcome expectation and a failure
+ * expectation, the outcome expectation will be returned.
+ */
+public final class ExpectationStore {
+    private static final int PATTERN_FLAGS = Pattern.MULTILINE | Pattern.DOTALL;
+    private final Map<String, Expectation> outcomes = new LinkedHashMap<String, Expectation>();
+    private final Map<String, Expectation> failures = new LinkedHashMap<String, Expectation>();
+
+    private ExpectationStore() {}
+
+    /**
+     * Finds the expected result for the specified action or outcome name. This
+     * returns a value for all names, even if no explicit expectation was set.
+     */
+    public Expectation get(String name) {
+        Expectation byName = getByNameOrPackage(name);
+        return byName != null ? byName : Expectation.SUCCESS;
+    }
+
+    /**
+     * Finds the expected result for the specified outcome after it has
+     * completed. Unlike {@code get()}, this also takes into account the
+     * outcome's output.
+     *
+     * <p>For outcomes that have both a name match and an output match,
+     * exact name matches are preferred, then output matches, then inexact
+     * name matches.
+     */
+    public Expectation get(Outcome outcome) {
+        Expectation exactNameMatch = outcomes.get(outcome.getName());
+        if (exactNameMatch != null) {
+            return exactNameMatch;
+        }
+
+        for (Map.Entry<String, Expectation> entry : failures.entrySet()) {
+            if (entry.getValue().matches(outcome)) {
+                return entry.getValue();
+            }
+        }
+
+        Expectation byName = getByNameOrPackage(outcome.getName());
+        return byName != null ? byName : Expectation.SUCCESS;
+    }
+
+    private Expectation getByNameOrPackage(String name) {
+        while (true) {
+            Expectation expectation = outcomes.get(name);
+            if (expectation != null) {
+                return expectation;
+            }
+
+            int dotOrHash = Math.max(name.lastIndexOf('.'), name.lastIndexOf('#'));
+            if (dotOrHash == -1) {
+                return null;
+            }
+
+            name = name.substring(0, dotOrHash);
+        }
+    }
+
+    public static ExpectationStore parse(Set<File> expectationFiles, ModeId mode) throws IOException {
+        ExpectationStore result = new ExpectationStore();
+        for (File f : expectationFiles) {
+            if (f.exists()) {
+                result.parse(f, mode);
+            }
+        }
+        return result;
+    }
+
+    public void parse(File expectationsFile, ModeId mode) throws IOException {
+        Log.verbose("loading expectations file " + expectationsFile);
+
+        int count = 0;
+        JsonReader reader = null;
+        try {
+            reader = new JsonReader(new FileReader(expectationsFile));
+            reader.setLenient(true);
+            reader.beginArray();
+            while (reader.hasNext()) {
+                readExpectation(reader, mode);
+                count++;
+            }
+            reader.endArray();
+
+            Log.verbose("loaded " + count + " expectations from " + expectationsFile);
+        } finally {
+            if (reader != null) {
+                reader.close();
+            }
+        }
+    }
+
+    private void readExpectation(JsonReader reader, ModeId mode) throws IOException {
+        boolean isFailure = false;
+        Result result = Result.SUCCESS;
+        Pattern pattern = Expectation.MATCH_ALL_PATTERN;
+        Set<String> names = new LinkedHashSet<String>();
+        Set<String> tags = new LinkedHashSet<String>();
+        Set<ModeId> modes = null;
+        String description = "";
+        long buganizerBug = -1;
+
+        reader.beginObject();
+        while (reader.hasNext()) {
+            String name = reader.nextName();
+            if (name.equals("result")) {
+                result = Result.valueOf(reader.nextString());
+            } else if (name.equals("name")) {
+                names.add(reader.nextString());
+            } else if (name.equals("names")) {
+                readStrings(reader, names);
+            } else if (name.equals("failure")) {
+                isFailure = true;
+                names.add(reader.nextString());
+            } else if (name.equals("pattern")) {
+                pattern = Pattern.compile(reader.nextString(), PATTERN_FLAGS);
+            } else if (name.equals("substring")) {
+                pattern = Pattern.compile(".*" + Pattern.quote(reader.nextString()) + ".*", PATTERN_FLAGS);
+            } else if (name.equals("tags")) {
+                readStrings(reader, tags);
+            } else if (name.equals("description")) {
+                Iterable<String> split = Splitter.on("\n").omitEmptyStrings().trimResults().split(reader.nextString());
+                description = Joiner.on("\n").join(split);
+            } else if (name.equals("bug")) {
+                buganizerBug = reader.nextLong();
+            } else if (name.equals("modes")) {
+                modes = readModes(reader);
+            } else {
+                Log.warn("Unhandled name in expectations file: " + name);
+                reader.skipValue();
+            }
+        }
+        reader.endObject();
+
+        if (names.isEmpty()) {
+            throw new IllegalArgumentException("Missing 'name' or 'failure' key in " + reader);
+        }
+        if (modes != null && !modes.contains(mode)) {
+            return;
+        }
+
+        Expectation expectation = new Expectation(result, pattern, tags, description, buganizerBug);
+        Map<String, Expectation> map = isFailure ? failures : outcomes;
+        for (String name : names) {
+            if (map.put(name, expectation) != null) {
+                throw new IllegalArgumentException("Duplicate expectations for " + name);
+            }
+        }
+    }
+
+    private void readStrings(JsonReader reader, Set<String> output) throws IOException {
+        reader.beginArray();
+        while (reader.hasNext()) {
+            output.add(reader.nextString());
+        }
+        reader.endArray();
+    }
+
+    private Set<ModeId> readModes(JsonReader reader) throws IOException {
+        Set<ModeId> result = new LinkedHashSet<ModeId>();
+        reader.beginArray();
+        while (reader.hasNext()) {
+            result.add(ModeId.valueOf(reader.nextString().toUpperCase()));
+        }
+        reader.endArray();
+        return result;
+    }
+
+    /**
+     * Sets the bugIsOpen status on all expectations by querying an external bug
+     * tracker.
+     */
+    public void loadBugStatuses(String openBugsCommand) {
+        Iterable<Expectation> allExpectations = Iterables.concat(outcomes.values(), failures.values());
+
+        // figure out what bug IDs we're interested in
+        Set<String> bugs = new LinkedHashSet<String>();
+        for (Expectation expectation : allExpectations) {
+            if (expectation.getBug() != -1) {
+                bugs.add(Long.toString(expectation.getBug()));
+            }
+        }
+        if (bugs.isEmpty()) {
+            return;
+        }
+
+        // query the external app for open bugs
+        List<String> openBugs = new Command.Builder()
+                .args(openBugsCommand)
+                .args(bugs)
+                .execute();
+        Set<Long> openBugsSet = new LinkedHashSet<Long>();
+        for (String bug : openBugs) {
+            openBugsSet.add(Long.parseLong(bug));
+        }
+
+        Log.verbose("tracking " + openBugsSet.size() + " open bugs: " + openBugs);
+
+        // update our expectations with that set
+        for (Expectation expectation : allExpectations) {
+            if (openBugsSet.contains(expectation.getBug())) {
+                expectation.setBugIsOpen(true);
+            }
+        }
+    }
+}
diff --git a/libs/vogar-expect/src/vogar/ModeId.java b/libs/vogar-expect/src/vogar/ModeId.java
new file mode 100644
index 0000000..3b24cc1
--- /dev/null
+++ b/libs/vogar-expect/src/vogar/ModeId.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package vogar;
+
+public enum ModeId {
+    DEVICE, JVM, ACTIVITY, SIM, HOST;
+
+    public boolean acceptsVmArgs() {
+        return this != ACTIVITY;
+    }
+
+    public boolean isHost() {
+        return this == JVM || this == SIM || this == HOST;
+    }
+
+    public boolean requiresAndroidSdk() {
+        return this == DEVICE || this == ACTIVITY || this == SIM || this == HOST;
+    }
+}
diff --git a/libs/vogar-expect/src/vogar/Outcome.java b/libs/vogar-expect/src/vogar/Outcome.java
new file mode 100644
index 0000000..3d7c68f
--- /dev/null
+++ b/libs/vogar-expect/src/vogar/Outcome.java
@@ -0,0 +1,173 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package vogar;
+
+import com.google.common.collect.Lists;
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.util.Arrays;
+import java.util.Date;
+import java.util.List;
+import vogar.util.Strings;
+
+/**
+ * An outcome of an action. Some actions may have multiple outcomes. For
+ * example, JUnit tests have one outcome for each test method.
+ */
+public final class Outcome {
+
+    private final String outcomeName;
+    private final Result result;
+    private final String output;
+    private final Date date;
+
+    public Outcome(String outcomeName, Result result, List<String> outputLines) {
+        this.outcomeName = outcomeName;
+        this.result = result;
+        this.output = sanitizeOutputLines(outputLines);
+        this.date = new Date();
+    }
+
+    public Outcome(String outcomeName, Result result, String outputLine, Date date) {
+        this.outcomeName = outcomeName;
+        this.result = result;
+        this.output = sanitizeOutputLine(outputLine);
+        this.date = date;
+    }
+
+    public Outcome(String outcomeName, Result result, String outputLine) {
+        this.outcomeName = outcomeName;
+        this.result = result;
+        this.output = sanitizeOutputLine(outputLine);
+        this.date = new Date();
+    }
+
+    public Outcome(String outcomeName, Result result, Throwable throwable) {
+        this.outcomeName = outcomeName;
+        this.result = result;
+        this.output = sanitizeOutputLines(throwableToLines(throwable));
+        this.date = new Date();
+    }
+
+    private String sanitizeOutputLines(List<String> outputLines) {
+        List<String> sanitizedStrings = Lists.newArrayList();
+        for (String line : outputLines) {
+            sanitizedStrings.add(sanitizeOutputLine(line));
+        }
+        return Strings.join(sanitizedStrings, "\n");
+    }
+
+    private String sanitizeOutputLine(String outputLine) {
+        return Strings.xmlSanitize(outputLine.replaceAll("\r\n?", "\n"));
+    }
+
+    public Date getDate() {
+        return date;
+    }
+
+    public String getName() {
+        return outcomeName;
+    }
+
+    public Result getResult() {
+        return result;
+    }
+
+    public String getOutput() {
+        return output;
+    }
+
+    public List<String> getOutputLines() {
+        return Arrays.asList(output.split("\n"));
+    }
+
+    private static List<String> throwableToLines(Throwable t) {
+        StringWriter writer = new StringWriter();
+        PrintWriter out = new PrintWriter(writer);
+        t.printStackTrace(out);
+        return Arrays.asList(writer.toString().split("\\n"));
+    }
+
+    /**
+     * Returns the action's suite name, such as java.lang.Integer or
+     * java.lang.IntegerTest.
+     */
+    public String getSuiteName() {
+        int split = split(outcomeName);
+        return split == -1 ? "defaultpackage" : outcomeName.substring(0, split);
+    }
+
+    /**
+     * Returns the specific action name, such as BitTwiddle or testBitTwiddle.
+     */
+    public String getTestName() {
+        int split = split(outcomeName);
+        return split == -1 ? outcomeName : outcomeName.substring(split + 1);
+    }
+
+    private static int split(String name) {
+        int lastHash = name.indexOf('#');
+        return lastHash == -1 ? name.lastIndexOf('.') : lastHash;
+    }
+
+    /**
+     * Returns whether the result indicates that the contents of the Outcome are important.
+     *
+     * For example, for a test skipped because it is unsupported, we don't care about the result.
+     */
+    private boolean matters() {
+        return result != Result.UNSUPPORTED;
+    }
+
+    public ResultValue getResultValue(Expectation expectation) {
+        if (matters()) {
+            return expectation.matches(this) ? ResultValue.OK : ResultValue.FAIL;
+        }
+        return ResultValue.IGNORE;
+    }
+
+    /**
+     * Returns a filesystem db path for this outcome. For example, a path for an outcome with name
+     * "foo.bar.baz#testName" would be "foo/bar/baz/testName".
+     */
+    public String getPath() {
+        return outcomeName.replaceAll("[\\.#]", "/");
+    }
+
+    @Override public boolean equals(Object o) {
+        if (o instanceof Outcome) {
+            Outcome outcome = (Outcome) o;
+            return outcomeName.equals(outcome.outcomeName)
+                    && result == outcome.result
+                    && output.equals(outcome.output);
+        }
+        return false;
+    }
+
+    @Override public int hashCode() {
+        int hashCode = 17;
+        hashCode = 37 * hashCode + outcomeName.hashCode();
+        hashCode  = 37 * hashCode + result.hashCode();
+        hashCode = 37 * hashCode + output.hashCode();
+        return hashCode;
+    }
+
+    @Override public String toString() {
+        return "Outcome[name=" + outcomeName + " output=" + output + "]";
+    }
+
+}
diff --git a/libs/vogar-expect/src/vogar/Result.java b/libs/vogar-expect/src/vogar/Result.java
new file mode 100644
index 0000000..45c88ce
--- /dev/null
+++ b/libs/vogar-expect/src/vogar/Result.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package vogar;
+
+/**
+ * The result of a test or benchmark execution.
+ */
+public enum Result {
+
+    /**
+     * An action that cannot be run by this harness, such as a shell script.
+     */
+    UNSUPPORTED,
+
+    COMPILE_FAILED,
+    EXEC_FAILED,
+    EXEC_TIMEOUT,
+    ERROR,
+    SUCCESS
+}
diff --git a/libs/vogar-expect/src/vogar/ResultValue.java b/libs/vogar-expect/src/vogar/ResultValue.java
new file mode 100644
index 0000000..2e450f4
--- /dev/null
+++ b/libs/vogar-expect/src/vogar/ResultValue.java
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package vogar;
+
+/**
+ * Represents an evaluation of the goodness of a result.
+ */
+public enum ResultValue {
+    OK,
+    IGNORE,
+    FAIL
+}
diff --git a/libs/vogar-expect/src/vogar/commands/Command.java b/libs/vogar-expect/src/vogar/commands/Command.java
new file mode 100644
index 0000000..d60d77e
--- /dev/null
+++ b/libs/vogar-expect/src/vogar/commands/Command.java
@@ -0,0 +1,289 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package vogar.commands;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.PrintStream;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.Callable;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+import vogar.util.Log;
+import vogar.util.Strings;
+import vogar.util.Threads;
+
+/**
+ * An out of process executable.
+ */
+public final class Command {
+    private final List<String> args;
+    private final Map<String, String> env;
+    private final File workingDirectory;
+    private final boolean permitNonZeroExitStatus;
+    private final PrintStream tee;
+    private final boolean nativeOutput;
+    private volatile Process process;
+
+    public Command(String... args) {
+        this(Arrays.asList(args));
+    }
+
+    public Command(List<String> args) {
+        this.args = new ArrayList<String>(args);
+        this.env = Collections.emptyMap();
+        this.workingDirectory = null;
+        this.permitNonZeroExitStatus = false;
+        this.tee = null;
+        this.nativeOutput = false;
+    }
+
+    private Command(Builder builder) {
+        this.args = new ArrayList<String>(builder.args);
+        this.env = builder.env;
+        this.workingDirectory = builder.workingDirectory;
+        this.permitNonZeroExitStatus = builder.permitNonZeroExitStatus;
+        this.tee = builder.tee;
+        if (builder.maxLength != -1) {
+            String string = toString();
+            if (string.length() > builder.maxLength) {
+                throw new IllegalStateException("Maximum command length " + builder.maxLength
+                                                + " exceeded by: " + string);
+            }
+        }
+        this.nativeOutput = builder.nativeOutput;
+    }
+
+    public void start() throws IOException {
+        if (isStarted()) {
+            throw new IllegalStateException("Already started!");
+        }
+
+        Log.verbose("executing " + this);
+
+        ProcessBuilder processBuilder = new ProcessBuilder()
+                .command(args)
+                .redirectErrorStream(true);
+        if (workingDirectory != null) {
+            processBuilder.directory(workingDirectory);
+        }
+
+        processBuilder.environment().putAll(env);
+
+        process = processBuilder.start();
+    }
+
+    public boolean isStarted() {
+        return process != null;
+    }
+
+    public InputStream getInputStream() {
+        if (!isStarted()) {
+            throw new IllegalStateException("Not started!");
+        }
+
+        return process.getInputStream();
+    }
+
+    public List<String> gatherOutput()
+            throws IOException, InterruptedException {
+        if (!isStarted()) {
+            throw new IllegalStateException("Not started!");
+        }
+
+        BufferedReader in = new BufferedReader(
+                new InputStreamReader(getInputStream(), "UTF-8"));
+        List<String> outputLines = new ArrayList<String>();
+        String outputLine;
+        while ((outputLine = in.readLine()) != null) {
+            if (tee != null) {
+                tee.println(outputLine);
+            }
+            if (nativeOutput) {
+                Log.nativeOutput(outputLine);
+            }
+            outputLines.add(outputLine);
+        }
+
+        if (process.waitFor() != 0 && !permitNonZeroExitStatus) {
+            StringBuilder message = new StringBuilder();
+            for (String line : outputLines) {
+                message.append("\n").append(line);
+            }
+            throw new CommandFailedException(args, outputLines);
+        }
+
+        return outputLines;
+    }
+
+    public List<String> execute() {
+        try {
+            start();
+            return gatherOutput();
+        } catch (IOException e) {
+            throw new RuntimeException("Failed to execute process: " + args, e);
+        } catch (InterruptedException e) {
+            throw new RuntimeException("Interrupted while executing process: " + args, e);
+        }
+    }
+
+    /**
+     * Executes a command with a specified timeout. If the process does not
+     * complete normally before the timeout has elapsed, it will be destroyed.
+     *
+     * @param timeoutSeconds how long to wait, or 0 to wait indefinitely
+     * @return the command's output, or null if the command timed out
+     */
+    public List<String> executeWithTimeout(int timeoutSeconds)
+            throws TimeoutException {
+        if (timeoutSeconds == 0) {
+            return execute();
+        }
+
+        try {
+            return executeLater().get(timeoutSeconds, TimeUnit.SECONDS);
+        } catch (InterruptedException e) {
+            throw new RuntimeException("Interrupted while executing process: " + args, e);
+        } catch (ExecutionException e) {
+            throw new RuntimeException(e);
+        } finally {
+            destroy();
+        }
+    }
+
+    /**
+     * Executes the command on a new background thread. This method returns
+     * immediately.
+     *
+     * @return a future to retrieve the command's output.
+     */
+    public Future<List<String>> executeLater() {
+        ExecutorService executor = Threads.fixedThreadsExecutor("command", 1);
+        Future<List<String>> result = executor.submit(new Callable<List<String>>() {
+            public List<String> call() throws Exception {
+                start();
+                return gatherOutput();
+            }
+        });
+        executor.shutdown();
+        return result;
+    }
+
+    /**
+     * Destroys the underlying process and closes its associated streams.
+     */
+    public void destroy() {
+        if (process == null) {
+            return;
+        }
+
+        process.destroy();
+        try {
+            process.waitFor();
+            int exitValue = process.exitValue();
+            Log.verbose("received exit value " + exitValue
+                    + " from destroyed command " + this);
+        } catch (IllegalThreadStateException destroyUnsuccessful) {
+            Log.warn("couldn't destroy " + this);
+        } catch (InterruptedException e) {
+            Log.warn("couldn't destroy " + this);
+        }
+    }
+
+    @Override public String toString() {
+        String envString = !env.isEmpty() ? (Strings.join(env.entrySet(), " ") + " ") : "";
+        return envString + Strings.join(args, " ");
+    }
+
+    public static class Builder {
+        private final List<String> args = new ArrayList<String>();
+        private final Map<String, String> env = new LinkedHashMap<String, String>();
+        private File workingDirectory;
+        private boolean permitNonZeroExitStatus = false;
+        private PrintStream tee = null;
+        private boolean nativeOutput;
+        private int maxLength = -1;
+
+        public Builder args(Object... objects) {
+            for (Object object : objects) {
+                args(object.toString());
+            }
+            return this;
+        }
+
+        public Builder setNativeOutput(boolean nativeOutput) {
+            this.nativeOutput = nativeOutput;
+            return this;
+        }
+
+        public Builder args(String... args) {
+            return args(Arrays.asList(args));
+        }
+
+        public Builder args(Collection<String> args) {
+            this.args.addAll(args);
+            return this;
+        }
+
+        public Builder env(String key, String value) {
+            env.put(key, value);
+            return this;
+        }
+
+        /**
+         * Sets the working directory from which the command will be executed.
+         * This must be a <strong>local</strong> directory; Commands run on
+         * remote devices (ie. via {@code adb shell}) require a local working
+         * directory.
+         */
+        public Builder workingDirectory(File workingDirectory) {
+            this.workingDirectory = workingDirectory;
+            return this;
+        }
+
+        public Builder tee(PrintStream printStream) {
+            tee = printStream;
+            return this;
+        }
+
+        public Builder maxLength(int maxLength) {
+            this.maxLength = maxLength;
+            return this;
+        }
+
+        public Command build() {
+            return new Command(this);
+        }
+
+        public List<String> execute() {
+            return build().execute();
+        }
+    }
+}
diff --git a/libs/vogar-expect/src/vogar/commands/CommandFailedException.java b/libs/vogar-expect/src/vogar/commands/CommandFailedException.java
new file mode 100644
index 0000000..3e08c11
--- /dev/null
+++ b/libs/vogar-expect/src/vogar/commands/CommandFailedException.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package vogar.commands;
+
+import java.util.List;
+
+/**
+ * Thrown when an out of process executable does not return normally.
+ */
+public class CommandFailedException extends RuntimeException {
+
+    private final List<String> args;
+    private final List<String> outputLines;
+
+    public CommandFailedException(List<String> args, List<String> outputLines) {
+        super(formatMessage(args, outputLines));
+        this.args = args;
+        this.outputLines = outputLines;
+    }
+
+    public List<String> getArgs() {
+        return args;
+    }
+
+    public List<String> getOutputLines() {
+        return outputLines;
+    }
+
+    public static String formatMessage(List<String> args, List<String> outputLines) {
+        StringBuilder result = new StringBuilder();
+        result.append("Command failed:");
+        for (String arg : args) {
+            result.append(" ").append(arg);
+        }
+        for (String outputLine : outputLines) {
+            result.append("\n  ").append(outputLine);
+        }
+        return result.toString();
+    }
+
+    private static final long serialVersionUID = 0;
+}
diff --git a/libs/vogar-expect/src/vogar/commands/Mkdir.java b/libs/vogar-expect/src/vogar/commands/Mkdir.java
new file mode 100644
index 0000000..fc08f1b
--- /dev/null
+++ b/libs/vogar-expect/src/vogar/commands/Mkdir.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package vogar.commands;
+
+import java.io.File;
+
+/**
+ * A mkdir command.
+ */
+public final class Mkdir {
+
+    public void mkdirs(File directory) {
+        new Command("mkdir", "-p", directory.getPath()).execute();
+    }
+}
diff --git a/libs/vogar-expect/src/vogar/commands/Rm.java b/libs/vogar-expect/src/vogar/commands/Rm.java
new file mode 100644
index 0000000..5b39144
--- /dev/null
+++ b/libs/vogar-expect/src/vogar/commands/Rm.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package vogar.commands;
+
+import java.io.File;
+
+/**
+ * A rm command.
+ */
+public final class Rm {
+
+    public void file(File file) {
+        new Command("rm", "-f", file.getPath()).execute();
+    }
+
+    public void directoryTree(File directory) {
+        new Command("rm", "-rf", directory.getPath()).execute();
+    }
+}
diff --git a/libs/vogar-expect/src/vogar/util/IoUtils.java b/libs/vogar-expect/src/vogar/util/IoUtils.java
new file mode 100644
index 0000000..4f1fba1
--- /dev/null
+++ b/libs/vogar-expect/src/vogar/util/IoUtils.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package vogar.util;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.net.Socket;
+
+public final class IoUtils {
+
+    public static void closeQuietly(Closeable c) {
+        if (c != null) {
+            try {
+                c.close();
+            } catch (IOException ignored) {
+            }
+        }
+    }
+
+    public static void closeQuietly(Socket c) {
+        if (c != null) {
+            try {
+                c.close();
+            } catch (IOException ignored) {
+            }
+        }
+    }
+}
diff --git a/libs/vogar-expect/src/vogar/util/Log.java b/libs/vogar-expect/src/vogar/util/Log.java
new file mode 100644
index 0000000..99c0807
--- /dev/null
+++ b/libs/vogar-expect/src/vogar/util/Log.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package vogar.util;
+
+import java.util.List;
+
+public class Log {
+
+    private static LogOutput sLogoutput = null;
+
+    public static void setOutput(LogOutput logOutput) {
+        sLogoutput = logOutput;
+    }
+
+    public static void verbose(String s) {
+        if (sLogoutput != null) {
+            sLogoutput.verbose(s);
+        }
+    }
+
+    public static void warn(String message) {
+        if (sLogoutput != null) {
+            sLogoutput.warn(message);
+        }
+    }
+
+    /**
+     * Warns, and also puts a list of strings afterwards.
+     */
+    public static void warn(String message, List<String> list) {
+        if (sLogoutput != null) {
+            sLogoutput.warn(message, list);
+        }
+    }
+
+    public static void info(String s) {
+        if (sLogoutput != null) {
+            sLogoutput.info(s);
+        }
+    }
+
+    public static void info(String message, Throwable throwable) {
+        if (sLogoutput != null) {
+            sLogoutput.info(message, throwable);
+        }
+    }
+
+    public static void nativeOutput(String outputLine) {
+        if (sLogoutput != null) {
+            sLogoutput.nativeOutput(outputLine);
+        }
+
+    }
+}
diff --git a/libs/vogar-expect/src/vogar/util/LogOutput.java b/libs/vogar-expect/src/vogar/util/LogOutput.java
new file mode 100644
index 0000000..8123a81
--- /dev/null
+++ b/libs/vogar-expect/src/vogar/util/LogOutput.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package vogar.util;
+
+import java.util.List;
+
+public interface LogOutput {
+
+    void verbose(String s);
+
+    void warn(String message);
+
+    /**
+     * Warns, and also puts a list of strings afterwards.
+     */
+    void warn(String message, List<String> list);
+
+    void info(String s);
+
+    void info(String message, Throwable throwable);
+
+    void nativeOutput(String outputLine);
+
+}
diff --git a/libs/vogar-expect/src/vogar/util/MarkResetConsole.java b/libs/vogar-expect/src/vogar/util/MarkResetConsole.java
new file mode 100644
index 0000000..d88ce31
--- /dev/null
+++ b/libs/vogar-expect/src/vogar/util/MarkResetConsole.java
@@ -0,0 +1,87 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package vogar.util;
+
+import java.io.PrintStream;
+
+/**
+ * A console that can erase output back to a previously marked position.
+ */
+public final class MarkResetConsole {
+
+    private final PrintStream out;
+    private int row;
+    private final StringBuilder rowContent = new StringBuilder();
+
+    public MarkResetConsole(PrintStream out) {
+        this.out = out;
+    }
+
+    public void println(String text) {
+        print(text + "\n");
+    }
+
+    public void print(String text) {
+        for (int i = 0; i < text.length(); i++) {
+            if (text.charAt(i) == '\n') {
+                row++;
+                rowContent.delete(0, rowContent.length());
+            } else {
+                rowContent.append(text.charAt(i));
+            }
+        }
+
+        out.print(text);
+        out.flush();
+    }
+
+    public Mark mark() {
+        return new Mark();
+    }
+
+    public class Mark {
+        private final int markRow = row;
+        private final String markRowContent = rowContent.toString();
+
+        private Mark() {}
+
+        public void reset() {
+            /*
+             * ANSI escapes
+             * http://en.wikipedia.org/wiki/ANSI_escape_code
+             *
+             *  \u001b[K   clear the rest of the current line
+             *  \u001b[nA  move the cursor up n lines
+             *  \u001b[nB  move the cursor down n lines
+             *  \u001b[nC  move the cursor right n lines
+             *  \u001b[nD  move the cursor left n columns
+             */
+
+            for (int r = row; r > markRow; r--) {
+                // clear the line, up a line
+                System.out.print("\u001b[0G\u001b[K\u001b[1A");
+            }
+
+            // clear the line, reprint the line
+            out.print("\u001b[0G\u001b[K");
+            out.print(markRowContent);
+            rowContent.delete(0, rowContent.length());
+            rowContent.append(markRowContent);
+            row = markRow;
+        }
+    }
+}
diff --git a/libs/vogar-expect/src/vogar/util/Strings.java b/libs/vogar-expect/src/vogar/util/Strings.java
new file mode 100644
index 0000000..f92edd8
--- /dev/null
+++ b/libs/vogar-expect/src/vogar/util/Strings.java
@@ -0,0 +1,123 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+package vogar.util;
+
+//import com.google.common.collect.Lists;
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.Reader;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Utility methods for strings.
+ */
+public class Strings {
+
+    private static final Pattern XML_INVALID_CHARS
+            = Pattern.compile("[^\\u0009\\u000A\\u000D\\u0020-\\uD7FF\\uE000-\\uFFFD]+");
+
+    public static String readStream(Reader reader) throws IOException {
+        StringBuilder result = new StringBuilder();
+        BufferedReader in = new BufferedReader(reader);
+        String line;
+        while ((line = in.readLine()) != null) {
+            result.append(line);
+            result.append('\n');
+        }
+        in.close();
+        return result.toString();
+    }
+
+    public static String readFile(File f) throws IOException {
+        return readStream(new InputStreamReader(new FileInputStream(f), "UTF-8"));
+    }
+
+    public static List<String> readFileLines(File f) throws IOException {
+        BufferedReader in =
+                new BufferedReader(new InputStreamReader(new FileInputStream(f), "UTF-8"));
+        List<String> list = new ArrayList<String>();
+        String line;
+        while ((line = in.readLine()) != null) {
+            list.add(line);
+        }
+        in.close();
+        return list;
+    }
+
+    public static String join(String delimiter, Object... objects) {
+        return join(Arrays.asList(objects), delimiter);
+    }
+
+    public static String join(Iterable<?> objects, String delimiter) {
+        Iterator<?> i = objects.iterator();
+        if (!i.hasNext()) {
+            return "";
+        }
+
+        StringBuilder result = new StringBuilder();
+        result.append(i.next());
+        while(i.hasNext()) {
+            result.append(delimiter).append(i.next());
+        }
+        return result.toString();
+    }
+
+    public static String[] objectsToStrings(Object[] objects) {
+        String[] result = new String[objects.length];
+        int i = 0;
+        for (Object o : objects) {
+            result[i++] = o.toString();
+        }
+        return result;
+    }
+
+    public static String[] objectsToStrings(Collection<?> objects) {
+        return objectsToStrings(objects.toArray());
+    }
+
+    /**
+     * Replaces XML-invalid characters with the corresponding U+XXXX code point escapes.
+     */
+    public static String xmlSanitize(String text) {
+        StringBuffer result = new StringBuffer();
+        Matcher matcher = XML_INVALID_CHARS.matcher(text);
+        while (matcher.find()) {
+            matcher.appendReplacement(result, "");
+            result.append(escapeCodePoint(matcher.group()));
+        }
+        matcher.appendTail(result);
+        return result.toString();
+    }
+
+    private static String escapeCodePoint(CharSequence cs) {
+        StringBuilder result = new StringBuilder();
+        for (int i = 0; i < cs.length(); ++i) {
+            result.append(String.format("U+%04X", (int) cs.charAt(i)));
+        }
+        return result.toString();
+    }
+}
diff --git a/libs/vogar-expect/src/vogar/util/Threads.java b/libs/vogar-expect/src/vogar/util/Threads.java
new file mode 100644
index 0000000..83410d5
--- /dev/null
+++ b/libs/vogar-expect/src/vogar/util/Threads.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package vogar.util;
+
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.ThreadFactory;
+import java.util.concurrent.ThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Utility methods for working with threads.
+ */
+public final class Threads {
+    private Threads() {}
+
+    public static ThreadFactory daemonThreadFactory(final String name) {
+        return new ThreadFactory() {
+            private int nextId = 0;
+            public synchronized Thread newThread(Runnable r) {
+                Thread thread = new Thread(r, name + "-" + (nextId++));
+                thread.setDaemon(true);
+                return thread;
+            }
+        };
+    }
+
+    public static ExecutorService threadPerCpuExecutor(String name) {
+        return fixedThreadsExecutor(name, Runtime.getRuntime().availableProcessors());
+    }
+
+    public static ExecutorService fixedThreadsExecutor(String name, int count) {
+        ThreadFactory threadFactory = daemonThreadFactory(name);
+
+        return new ThreadPoolExecutor(count, count, 10, TimeUnit.SECONDS,
+                new LinkedBlockingQueue<Runnable>(Integer.MAX_VALUE), threadFactory) {
+            @Override protected void afterExecute(Runnable runnable, Throwable throwable) {                if (throwable != null) {
+                    Log.info("Unexpected failure from " + runnable, throwable);
+                }
+            }
+        };
+    }
+}
diff --git a/libs/vogar-expect/src/vogar/util/TimeUtilities.java b/libs/vogar-expect/src/vogar/util/TimeUtilities.java
new file mode 100644
index 0000000..c5a7e3b
--- /dev/null
+++ b/libs/vogar-expect/src/vogar/util/TimeUtilities.java
@@ -0,0 +1,117 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package vogar.util;
+
+/**
+ * Utilities to make it easier to work with ISO 8601 dates and times.
+ * This is a subset of the original class from http://software.jessies.org/salma-hayek/ --- please submit fixes upstream.
+ */
+public class TimeUtilities {
+    /**
+     * Returns the ISO 8601-format String corresponding to the given duration (measured in milliseconds).
+     */
+    public static String msToIsoString(long duration) {
+        long milliseconds = duration % 1000;
+        duration /= 1000;
+        long seconds = duration % 60;
+        duration /= 60;
+        long minutes = duration % 60;
+        duration /= 60;
+        long hours = duration;
+
+        StringBuilder result = new StringBuilder("P");
+        if (hours != 0) {
+            result.append(hours);
+            result.append('H');
+        }
+        if (result.length() > 1 || minutes != 0) {
+            result.append(minutes);
+            result.append('M');
+        }
+        result.append(seconds);
+        if (milliseconds != 0) {
+            result.append('.');
+            result.append(milliseconds);
+        }
+        result.append('S');
+        return result.toString();
+    }
+    
+    /**
+     * Returns a string representation of the given number of milliseconds.
+     */
+    public static String msToString(long ms) {
+        return nsToString(ms * 1000000);
+    }
+    
+    /**
+     * Returns a string representation of the given number of nanoseconds.
+     */
+    public static String nsToString(long ns) {
+        if (ns < 1000L) {
+            return Long.toString(ns) + "ns";
+        } else if (ns < 1000000L) {
+            return Long.toString(ns/1000L) + "us";
+        } else if (ns < 1000000000L) {
+            return Long.toString(ns/1000000L) + "ms";
+        } else if (ns < 60000000000L) {
+            return String.format("%.2fs", nsToS(ns));
+        } else {
+            long duration = ns;
+            long nanoseconds = duration % 1000;
+            duration /= 1000;
+            long microseconds = duration % 1000;
+            duration /= 1000;
+            long milliseconds = duration % 1000;
+            duration /= 1000;
+            long seconds = duration % 60;
+            duration /= 60;
+            long minutes = duration % 60;
+            duration /= 60;
+            long hours = duration % 24;
+            duration /= 24;
+            long days = duration;
+            
+            StringBuilder result = new StringBuilder();
+            if (days != 0) {
+                result.append(days);
+                result.append('d');
+            }
+            if (result.length() > 1 || hours != 0) {
+                result.append(hours);
+                result.append('h');
+            }
+            if (result.length() > 1 || minutes != 0) {
+                result.append(minutes);
+                result.append('m');
+            }
+            result.append(seconds);
+            result.append('s');
+            return result.toString();
+        }
+    }
+    
+    /**
+     * Converts nanoseconds into (fractional) seconds.
+     */
+    public static double nsToS(long ns) {
+        return ((double) ns)/1000000000.0;
+    }
+
+    private TimeUtilities() {
+    }
+}
diff --git a/tools/utils/Android.mk b/tools/utils/Android.mk
index 5a4597f..0782116 100644
--- a/tools/utils/Android.mk
+++ b/tools/utils/Android.mk
@@ -24,4 +24,6 @@
 
 LOCAL_CLASSPATH := $(HOST_JDK_TOOLS_JAR) $(LOCAL_PATH)/lib/junit.jar
 
+LOCAL_STATIC_JAVA_LIBRARIES := vogarexpectlib
+
 include $(BUILD_HOST_JAVA_LIBRARY)
diff --git a/tools/utils/CollectAllTests.java b/tools/utils/CollectAllTests.java
index 7628ba2..40c950f 100644
--- a/tools/utils/CollectAllTests.java
+++ b/tools/utils/CollectAllTests.java
@@ -18,11 +18,13 @@
 import java.io.File;
 import java.io.FileInputStream;
 import java.io.FileReader;
+import java.io.FilenameFilter;
 import java.io.IOException;
 import java.lang.annotation.Annotation;
 import java.lang.reflect.InvocationTargetException;
 import java.lang.reflect.Method;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.HashSet;
 import java.util.Iterator;
 import java.util.LinkedHashMap;
@@ -43,6 +45,10 @@
 import org.w3c.dom.Node;
 import org.w3c.dom.NodeList;
 
+import vogar.ExpectationStore;
+import vogar.Expectation;
+import vogar.ModeId;
+
 public class CollectAllTests extends DescriptionGenerator {
 
     static final String ATTRIBUTE_RUNNER = "runner";
@@ -97,10 +103,12 @@
     private static String MANIFESTFILE = "";
     private static String TESTSUITECLASS = "";
     private static String ANDROID_MAKE_FILE = "";
+    private static String EXPECTATION_DIR = null;
 
     private static Test TESTSUITE;
 
     static XMLGenerator xmlGenerator;
+    private static ExpectationStore vogarExpectationStore;
 
     public static void main(String[] args) {
         if (args.length > 2) {
@@ -108,11 +116,14 @@
             MANIFESTFILE = args [1];
             TESTSUITECLASS = args[2];
             if (args.length > 3) {
-                ANDROID_MAKE_FILE = args[3];
+                EXPECTATION_DIR = args[3];
+            }
+            if (args.length > 4) {
+                ANDROID_MAKE_FILE = args[4];
             }
         } else {
             System.out.println("usage: \n" +
-                "\t... CollectAllTests <output-file> <manifest-file> <testsuite-class-name> <makefile-file>");
+                "\t... CollectAllTests <output-file> <manifest-file> <testsuite-class-name> <makefile-file> <expectation-dir>");
             System.exit(1);
         }
 
@@ -183,6 +194,14 @@
             System.exit(1);
         }
 
+        try {
+            vogarExpectationStore = provideExpectationStore(EXPECTATION_DIR);
+        } catch (IOException e) {
+            System.err.println("Can't initialize vogar expectation store");
+            e.printStackTrace(System.err);
+            System.exit(1);
+        }
+
         testCases = new LinkedHashMap<String, TestClass>();
         CollectAllTests cat = new CollectAllTests();
         cat.compose();
@@ -302,6 +321,15 @@
         return getAnnotation(testClass, testName, SUPPRESSED_TEST) != null;
     }
 
+    private boolean isVogarKnownFailure(final Class<? extends TestCase> testClass,
+            final String testName) {
+        if (vogarExpectationStore == null) {
+            return false;
+        }
+        String fullTestName = String.format("%s#%s", testClass.getName(), testName);
+        return vogarExpectationStore.get(fullTestName) != Expectation.SUCCESS;
+    }
+
     private String getAnnotation(final Class<? extends TestCase> testClass,
             final String testName, final String annotationName) {
         try {
@@ -349,6 +377,9 @@
         } else if (isSuppressed(test.getClass(), testName)) {
             System.out.println("ignoring suppressed test: " + test);
             return;
+        } else if (isVogarKnownFailure(test.getClass(), testName)) {
+            System.out.println("ignoring vogar known failure: " + test);
+            return;
         }
 
         if (!testName.startsWith("test")) {
@@ -377,4 +408,26 @@
             failed.add(test.getClass().getName());
         }
     }
+
+    public static ExpectationStore provideExpectationStore(String dir) throws IOException {
+        if (dir == null) {
+            return null;
+        }
+        ExpectationStore result = ExpectationStore.parse(getExpectationFiles(dir), ModeId.DEVICE);
+        return result;
+    }
+
+    private static Set<File> getExpectationFiles(String dir) {
+        Set<File> expectSet = new HashSet<File>();
+        File[] files = new File(dir).listFiles(new FilenameFilter() {
+            // ignore obviously temporary files
+            public boolean accept(File dir, String name) {
+                return !name.endsWith("~") && !name.startsWith(".");
+            }
+        });
+        if (files != null) {
+            expectSet.addAll(Arrays.asList(files));
+        }
+        return expectSet;
+    }
 }