diff --git a/.classpath b/.classpath
new file mode 100644
index 0000000000000000000000000000000000000000..adc2064a617b125af403a5c4294d3c3aea502d3e
--- /dev/null
+++ b/.classpath
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<classpath>
+	<classpathentry kind="src" path="src"/>
+	<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER"/>
+	<classpathentry combineaccessrules="false" kind="src" path="/tradefederation"/>
+	<classpathentry kind="con" path="org.eclipse.jdt.junit.JUNIT_CONTAINER/3"/>
+	<classpathentry exported="true" kind="var" path="TRADEFED_ROOT/prebuilts/misc/common/json/json-prebuilt.jar"/>
+	<classpathentry kind="output" path="bin"/>
+</classpath>
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000000000000000000000000000000000000..ddb0a2d48c183c5b8a2f200765c660a849e6ea62
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,2 @@
+bin
+.settings
diff --git a/.project b/.project
new file mode 100644
index 0000000000000000000000000000000000000000..427863301aac5955b306a61e926e566bafb3eb4d
--- /dev/null
+++ b/.project
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<projectDescription>
+	<name>loganalysis</name>
+	<comment></comment>
+	<projects>
+	</projects>
+	<buildSpec>
+		<buildCommand>
+			<name>org.eclipse.jdt.core.javabuilder</name>
+			<arguments>
+			</arguments>
+		</buildCommand>
+	</buildSpec>
+	<natures>
+		<nature>org.eclipse.jdt.core.javanature</nature>
+	</natures>
+</projectDescription>
diff --git a/Android.mk b/Android.mk
new file mode 100644
index 0000000000000000000000000000000000000000..ead7a6da4963d2cece249b196b3b14f21aaee9bc
--- /dev/null
+++ b/Android.mk
@@ -0,0 +1,41 @@
+# Copyright (C) 2013 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)
+
+# Only compile source java files in this lib.
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+
+LOCAL_JAVACFLAGS += -g -Xlint
+
+LOCAL_MODULE := loganalysis
+LOCAL_MODULE_TAGS := optional
+LOCAL_STATIC_JAVA_LIBRARIES := json-prebuilt junit
+
+include $(BUILD_HOST_JAVA_LIBRARY)
+
+# makefile rules to copy jars to HOST_OUT/tradefed
+# so tradefed.sh can automatically add to classpath
+
+DEST_JAR := $(HOST_OUT)/tradefed/$(LOCAL_MODULE).jar
+$(DEST_JAR): $(LOCAL_BUILT_MODULE)
+	$(copy-file-to-new-target)
+
+# this dependency ensure the above rule will be executed if module is built
+$(LOCAL_INSTALLED_MODULE) : $(DEST_JAR)
+
+# Build all sub-directories
+include $(call all-makefiles-under,$(LOCAL_PATH))
diff --git a/src/com/android/loganalysis/item/AnrItem.java b/src/com/android/loganalysis/item/AnrItem.java
new file mode 100644
index 0000000000000000000000000000000000000000..d6bbae3de64f1c8643cef8dde9b7a96dcaa4d2e9
--- /dev/null
+++ b/src/com/android/loganalysis/item/AnrItem.java
@@ -0,0 +1,152 @@
+/*
+ * Copyright (C) 2012 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.loganalysis.item;
+
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * An {@link IItem} used to store ANR info.
+ */
+public class AnrItem extends GenericLogcatItem {
+    public static final String TYPE = "ANR";
+
+    /**
+     * An enum used to select the CPU usage category.
+     */
+    public enum CpuUsageCategory {
+        TOTAL,
+        USER,
+        KERNEL,
+        IOWAIT,
+    }
+
+    /**
+     * An enum used to select the load category.
+     */
+    public enum LoadCategory {
+        LOAD_1,
+        LOAD_5,
+        LOAD_15;
+    }
+
+    private static final String ACTIVITY = "ACTIVITY";
+    private static final String REASON = "REASON";
+    private static final String STACK = "STACK";
+    private static final String TRACE = "TRACE";
+
+    private static final Set<String> ATTRIBUTES = new HashSet<String>(Arrays.asList(
+        CpuUsageCategory.TOTAL.toString(),
+        CpuUsageCategory.USER.toString(),
+        CpuUsageCategory.KERNEL.toString(),
+        CpuUsageCategory.IOWAIT.toString(),
+        LoadCategory.LOAD_1.toString(),
+        LoadCategory.LOAD_5.toString(),
+        LoadCategory.LOAD_15.toString(),
+        ACTIVITY, REASON, STACK, TRACE));
+
+    /**
+     * The constructor for {@link AnrItem}.
+     */
+    public AnrItem() {
+        super(TYPE, ATTRIBUTES);
+    }
+
+    /**
+     * Get the CPU usage for a given category.
+     */
+    public Double getCpuUsage(CpuUsageCategory category) {
+        return (Double) getAttribute(category.toString());
+    }
+
+    /**
+     * Set the CPU usage for a given category.
+     */
+    public void setCpuUsage(CpuUsageCategory category, Double usage) {
+        setAttribute(category.toString(), usage);
+    }
+
+    /**
+     * Get the load for a given category.
+     */
+    public Double getLoad(LoadCategory category) {
+        return (Double) getAttribute(category.toString());
+    }
+
+    /**
+     * Set the load for a given category.
+     */
+    public void setLoad(LoadCategory category, Double usage) {
+        setAttribute(category.toString(), usage);
+    }
+
+    /**
+     * Get the activity for the ANR.
+     */
+    public String getActivity() {
+        return (String) getAttribute(ACTIVITY);
+    }
+
+    /**
+     * Set the activity for the ANR.
+     */
+    public void setActivity(String activity) {
+        setAttribute(ACTIVITY, activity);
+    }
+
+    /**
+     * Get the reason for the ANR.
+     */
+    public String getReason() {
+        return (String) getAttribute(REASON);
+    }
+
+    /**
+     * Set the reason for the ANR.
+     */
+    public void setReason(String reason) {
+        setAttribute(REASON, reason);
+    }
+
+    /**
+     * Get the stack for the ANR.
+     */
+    public String getStack() {
+        return (String) getAttribute(STACK);
+    }
+
+    /**
+     * Set the stack for the ANR.
+     */
+    public void setStack(String stack) {
+        setAttribute(STACK, stack);
+    }
+
+    /**
+     * Get the main trace for the ANR.
+     */
+    public String getTrace() {
+        return (String) getAttribute(TRACE);
+    }
+
+    /**
+     * Set the main trace for the ANR.
+     */
+    public void setTrace(String trace) {
+        setAttribute(TRACE, trace);
+    }
+}
diff --git a/src/com/android/loganalysis/item/BugreportItem.java b/src/com/android/loganalysis/item/BugreportItem.java
new file mode 100644
index 0000000000000000000000000000000000000000..11a41fe6d67d5ff50271caae8fc2251670df2c65
--- /dev/null
+++ b/src/com/android/loganalysis/item/BugreportItem.java
@@ -0,0 +1,114 @@
+/*
+ * Copyright (C) 2012 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.loganalysis.item;
+
+import java.util.Arrays;
+import java.util.Date;
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * An {@link IItem} used to store Bugreport info.
+ */
+public class BugreportItem extends GenericItem {
+    public static final String TYPE = "BUGREPORT";
+
+    private static final String TIME = "TIME";
+    private static final String MEM_INFO = "MEM_INFO";
+    private static final String PROCRANK = "PROCRANK";
+    private static final String SYSTEM_LOG = "SYSTEM_LOG";
+    private static final String SYSTEM_PROPS = "SYSTEM_PROPS";
+
+    private static final Set<String> ATTRIBUTES = new HashSet<String>(Arrays.asList(
+            TIME, MEM_INFO, PROCRANK, SYSTEM_LOG, SYSTEM_PROPS));
+
+    /**
+     * The constructor for {@link BugreportItem}.
+     */
+    public BugreportItem() {
+        super(TYPE, ATTRIBUTES);
+    }
+
+    /**
+     * Get the time of the bugreport.
+     */
+    public Date getTime() {
+        return (Date) getAttribute(TIME);
+    }
+
+    /**
+     * Set the time of the bugreport.
+     */
+    public void setTime(Date time) {
+        setAttribute(TIME, time);
+    }
+
+    /**
+     * Get the {@link MemInfoItem} of the bugreport.
+     */
+    public MemInfoItem getMemInfo() {
+        return (MemInfoItem) getAttribute(MEM_INFO);
+    }
+
+    /**
+     * Set the {@link MemInfoItem} of the bugreport.
+     */
+    public void setMemInfo(MemInfoItem memInfo) {
+        setAttribute(MEM_INFO, memInfo);
+    }
+
+    /**
+     * Get the {@link ProcrankItem} of the bugreport.
+     */
+    public ProcrankItem getProcrank() {
+        return (ProcrankItem) getAttribute(PROCRANK);
+    }
+
+    /**
+     * Set the {@link ProcrankItem} of the bugreport.
+     */
+    public void setProcrank(ProcrankItem procrank) {
+        setAttribute(PROCRANK, procrank);
+    }
+
+    /**
+     * Get the {@link LogcatItem} of the bugreport.
+     */
+    public LogcatItem getSystemLog() {
+        return (LogcatItem) getAttribute(SYSTEM_LOG);
+    }
+
+    /**
+     * Set the {@link LogcatItem} of the bugreport.
+     */
+    public void setSystemLog(LogcatItem systemLog) {
+        setAttribute(SYSTEM_LOG, systemLog);
+    }
+
+    /**
+     * Get the {@link SystemPropsItem} of the bugreport.
+     */
+    public SystemPropsItem getSystemProps() {
+        return (SystemPropsItem) getAttribute(SYSTEM_PROPS);
+    }
+
+    /**
+     * Set the {@link SystemPropsItem} of the bugreport.
+     */
+    public void setSystemProps(SystemPropsItem systemProps) {
+        setAttribute(SYSTEM_PROPS, systemProps);
+    }
+}
diff --git a/src/com/android/loganalysis/item/ConflictingItemException.java b/src/com/android/loganalysis/item/ConflictingItemException.java
new file mode 100644
index 0000000000000000000000000000000000000000..75d1e33213743e5795e6e9f66ffe186f638b2dbb
--- /dev/null
+++ b/src/com/android/loganalysis/item/ConflictingItemException.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2011 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.loganalysis.item;
+
+/**
+ * Thrown if there is conflicting information when trying to combine two items.
+ */
+public class ConflictingItemException extends Exception {
+
+    private static final long serialVersionUID = 3303627598068792143L;
+
+    /**
+     * Creates a {@link ConflictingItemException}.
+     *
+     * @param message The reason for the conflict.
+     */
+    ConflictingItemException(String message) {
+        super(message);
+    }
+}
diff --git a/src/com/android/loganalysis/item/GenericItem.java b/src/com/android/loganalysis/item/GenericItem.java
new file mode 100644
index 0000000000000000000000000000000000000000..7da3662bc363090c193449ac272559021b9e5fa8
--- /dev/null
+++ b/src/com/android/loganalysis/item/GenericItem.java
@@ -0,0 +1,206 @@
+/*
+ * Copyright (C) 2011 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.loganalysis.item;
+
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * An implementation of the {@link IItem} interface which implements helper methods.
+ */
+public class GenericItem implements IItem {
+    private Map<String, Object> mAttributes = new HashMap<String, Object>();
+    private Set<String> mAllowedAttributes;
+    private String mType = null;
+
+    protected GenericItem(String type, Set<String> allowedAttributes) {
+        mAllowedAttributes = new HashSet<String>();
+        mAllowedAttributes.addAll(allowedAttributes);
+        mType = type;
+    }
+
+    protected GenericItem(String type, Set<String> allowedAttributes,
+            Map<String, Object> attributes) {
+        this(type, allowedAttributes);
+
+        for (Map.Entry<String, Object> entry : attributes.entrySet()) {
+            setAttribute(entry.getKey(), entry.getValue());
+        }
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public String getType() {
+        return mType;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public IItem merge(IItem other) throws ConflictingItemException {
+        if (this == other) {
+            return this;
+        }
+        if (other == null || getClass() != other.getClass()) {
+            throw new ConflictingItemException("Conflicting class types");
+        }
+
+        return new GenericItem(getType(), mAllowedAttributes, mergeAttributes(other));
+    }
+
+    /**
+     * Merges the attributes from the item and another and returns a Map of the merged attributes.
+     * <p>
+     * Goes through each field in the item preferring non-null attributes over null attributes.
+     * </p>
+     *
+     * @param other The other item
+     * @return A Map from Strings to Objects containing merged attributes.
+     * @throws ConflictingItemException If the two items are not consistent.
+     */
+    protected Map<String, Object> mergeAttributes(IItem other) throws ConflictingItemException {
+        if (this == other) {
+            return mAttributes;
+        }
+        if (other == null || getClass() != other.getClass()) {
+            throw new ConflictingItemException("Conflicting class types");
+        }
+
+        GenericItem item = (GenericItem) other;
+        Map<String, Object> mergedAttributes = new HashMap<String, Object>();
+        for (String attribute : mAllowedAttributes) {
+            mergedAttributes.put(attribute,
+                    mergeObjects(getAttribute(attribute), item.getAttribute(attribute)));
+        }
+        return mergedAttributes;
+    }
+
+    /**
+     * {@inhertiDoc}
+     */
+    @Override
+    public boolean isConsistent(IItem other) {
+        if (this == other) {
+            return true;
+        }
+        if (other == null || getClass() != other.getClass()) {
+            return false;
+        }
+
+        GenericItem item = (GenericItem) other;
+        for (String attribute : mAllowedAttributes) {
+            if (!areConsistent(getAttribute(attribute), item.getAttribute(attribute))) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public boolean equals(Object other) {
+        if (this == other) {
+            return true;
+        }
+        if (other == null || getClass() != other.getClass()) {
+            return false;
+        }
+
+        GenericItem item = (GenericItem) other;
+        for (String attribute : mAllowedAttributes) {
+            if (!areEqual(getAttribute(attribute), item.getAttribute(attribute))) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    /**
+     * Set an attribute to a value.
+     *
+     * @param attribute The name of the attribute.
+     * @param value The value.
+     * @throws IllegalArgumentException If the attribute is not in allowedAttributes.
+     */
+    protected void setAttribute(String attribute, Object value) throws IllegalArgumentException {
+        if (!mAllowedAttributes.contains(attribute)) {
+            throw new IllegalArgumentException();
+        }
+        mAttributes.put(attribute, value);
+    }
+
+    /**
+     * Get the value of an attribute.
+     *
+     * @param attribute The name of the attribute.
+     * @return The value or null if the attribute has not been set.
+     * @throws IllegalArgumentException If the attribute is not in allowedAttributes.
+     */
+    protected Object getAttribute(String attribute) throws IllegalArgumentException {
+        if (!mAllowedAttributes.contains(attribute)) {
+            throw new IllegalArgumentException();
+        }
+        return mAttributes.get(attribute);
+    }
+
+    /**
+     * Helper method to return if two objects are equal.
+     *
+     * @param object1 The first object
+     * @param object2 The second object
+     * @return True if object1 and object2 are both null or if object1 is equal to object2, false
+     * otherwise.
+     */
+    static protected boolean areEqual(Object object1, Object object2) {
+        return object1 == null ? object2 == null : object1.equals(object2);
+    }
+
+    /**
+     * Helper method to return if two objects are consistent.
+     *
+     * @param object1 The first object
+     * @param object2 The second object
+     * @return True if either object1 or object2 is null or if object1 is equal to object2, false if
+     * both objects are not null and not equal.
+     */
+    static protected boolean areConsistent(Object object1, Object object2) {
+        return object1 == null || object2 == null ? true : object1.equals(object2);
+    }
+
+    /**
+     * Helper method used for merging two objects.
+     *
+     * @param object1 The first object
+     * @param object2 The second object
+     * @return If both objects are null, then null, else the non-null item if both items are equal.
+     * @throws ConflictingItemException If both objects are not null and they are not equal.
+     */
+    static protected Object mergeObjects(Object object1, Object object2)
+            throws ConflictingItemException {
+        if (!areConsistent(object1, object2)) {
+            throw new ConflictingItemException(String.format("%s conflicts with %s", object1,
+                    object2));
+        }
+        return object1 == null ? object2 : object1;
+    }
+}
diff --git a/src/com/android/loganalysis/item/GenericLogcatItem.java b/src/com/android/loganalysis/item/GenericLogcatItem.java
new file mode 100644
index 0000000000000000000000000000000000000000..4a3642d867966f51168e91357257ff6acaa01df7
--- /dev/null
+++ b/src/com/android/loganalysis/item/GenericLogcatItem.java
@@ -0,0 +1,140 @@
+/*
+ * Copyright (C) 2012 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.loganalysis.item;
+
+import java.util.Arrays;
+import java.util.Date;
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * A generic item containing attributes for time, process, and thread and can be extended for
+ * items such as {@link AnrItem} and {@link JavaCrashItem}.
+ */
+public abstract class GenericLogcatItem extends GenericItem {
+    private static final String EVENT_TIME = "EVENT_TIME";
+    private static final String PID = "PID";
+    private static final String TID = "TID";
+    private static final String APP = "APP";
+    private static final String LAST_PREAMBLE = "LAST_PREAMBLE";
+    private static final String PROC_PREAMBLE = "PROC_PREAMBLE";
+
+    private static final Set<String> ATTRIBUTES = new HashSet<String>(Arrays.asList(
+            EVENT_TIME, PID, TID, APP, LAST_PREAMBLE, PROC_PREAMBLE));
+
+    /**
+     * Constructor for {@link GenericLogcatItem}.
+     *
+     * @param type The type of the item.
+     * @param attributes A list of allowed attributes.
+     */
+    protected GenericLogcatItem(String type, Set<String> attributes) {
+        super(type, getAllAttributes(attributes));
+    }
+
+    /**
+     * Get the {@link Date} object when the event happened.
+     */
+    public Date getEventTime() {
+        return (Date) getAttribute(EVENT_TIME);
+    }
+
+    /**
+     * Set the {@link Date} object when the event happened.
+     */
+    public void setEventTime(Date time) {
+        setAttribute(EVENT_TIME, time);
+    }
+
+    /**
+     * Get the PID of the event.
+     */
+    public Integer getPid() {
+        return (Integer) getAttribute(PID);
+    }
+
+    /**
+     * Set the PID of the event.
+     */
+    public void setPid(Integer pid) {
+        setAttribute(PID, pid);
+    }
+
+    /**
+     * Get the TID of the event.
+     */
+    public Integer getTid() {
+        return (Integer) getAttribute(TID);
+    }
+
+    /**
+     * Set the TID of the event.
+     */
+    public void setTid(Integer tid) {
+        setAttribute(TID, tid);
+    }
+
+    /**
+     * Get the app or package name of the event.
+     */
+    public String getApp() {
+        return (String) getAttribute(APP);
+    }
+
+    /**
+     * Set the app or package name of the event.
+     */
+    public void setApp(String app) {
+        setAttribute(APP, app);
+    }
+
+    /**
+     * Get the last preamble for of the event.
+     */
+    public String getLastPreamble() {
+        return (String) getAttribute(LAST_PREAMBLE);
+    }
+
+    /**
+     * Set the last preamble for of the event.
+     */
+    public void setLastPreamble(String preamble) {
+        setAttribute(LAST_PREAMBLE, preamble);
+    }
+
+    /**
+     * Get the process preamble for of the event.
+     */
+    public String getProcessPreamble() {
+        return (String) getAttribute(PROC_PREAMBLE);
+    }
+
+    /**
+     * Set the process preamble for of the event.
+     */
+    public void setProcessPreamble(String preamble) {
+        setAttribute(PROC_PREAMBLE, preamble);
+    }
+
+    /**
+     * Combine an array of attributes with the internal list of attributes.
+     */
+    private static Set<String> getAllAttributes(Set<String> attributes) {
+        Set<String> allAttributes = new HashSet<String>(ATTRIBUTES);
+        allAttributes.addAll(attributes);
+        return allAttributes;
+    }
+}
diff --git a/src/com/android/loganalysis/item/GenericMapItem.java b/src/com/android/loganalysis/item/GenericMapItem.java
new file mode 100644
index 0000000000000000000000000000000000000000..e197db242decf1a153b9465abab11a13604bce3f
--- /dev/null
+++ b/src/com/android/loganalysis/item/GenericMapItem.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2011 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.loganalysis.item;
+
+import java.util.HashMap;
+
+/**
+ * An IItem that just represents a simple key/value map
+ */
+@SuppressWarnings("serial")
+public class GenericMapItem<K, V> extends HashMap<K,V> implements IItem {
+    private String mType = null;
+
+    /**
+     * No-op zero-arg constructor
+     */
+    public GenericMapItem() {}
+
+    /**
+     * Convenience constructor that sets the type
+     */
+    public GenericMapItem(String type) {
+        setType(type);
+    }
+
+    /**
+     * Set the self-reported type that this {@link GenericMapItem} represents.
+     */
+    public void setType(String type) {
+        mType = type;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public String getType() {
+        return mType;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public IItem merge(IItem other) {
+        // FIXME
+        return this;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public boolean isConsistent(IItem other) {
+        // FIXME
+        return true;
+    }
+}
diff --git a/src/com/android/loganalysis/item/IItem.java b/src/com/android/loganalysis/item/IItem.java
new file mode 100644
index 0000000000000000000000000000000000000000..d52ec5db6a2e5b3815452a20905a3d844499757a
--- /dev/null
+++ b/src/com/android/loganalysis/item/IItem.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2011 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.loganalysis.item;
+
+/**
+ * Interface for all items that are created by any parser.
+ */
+public interface IItem {
+
+    /**
+     * Determine what type this IItem represents.  May return {@code null}
+     */
+    public String getType();
+
+    /**
+     * Merges the item and another into an item with the most complete information.
+     *
+     * <p>
+     * Goes through each field in the item preferring non-null fields over null fields.
+     * </p>
+     *
+     * @param other The other item
+     * @return The product of both items combined.
+     * @throws ConflictingItemException If the two items are not consistent.
+     */
+    public IItem merge(IItem other) throws ConflictingItemException;
+
+    /**
+     * Checks that the item and another are consistent.
+     *
+     * <p>
+     * Consistency means that no individual fields in either item conflict with the other.
+     * However, one item might contain more complete information.  Two items of different types
+     * are never consistent.
+     * </p>
+     *
+     * @param other The other item.
+     * @return True if the objects are the same type and all the fields are either equal or one of
+     * the fields is null.
+     */
+    public boolean isConsistent(IItem other);
+}
diff --git a/src/com/android/loganalysis/item/JavaCrashItem.java b/src/com/android/loganalysis/item/JavaCrashItem.java
new file mode 100644
index 0000000000000000000000000000000000000000..042cf61d84b51d22a4b6d234c9248fa7b4f8578a
--- /dev/null
+++ b/src/com/android/loganalysis/item/JavaCrashItem.java
@@ -0,0 +1,83 @@
+/*
+ * Copyright (C) 2012 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.loganalysis.item;
+
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * An {@link IItem} used to store Java crash info.
+ */
+public class JavaCrashItem extends GenericLogcatItem {
+    public static final String TYPE = "JAVA CRASH";
+
+    private static final String EXCEPTION = "EXCEPTION";
+    private static final String MESSAGE = "MESSAGE";
+    private static final String STACK = "STACK";
+
+    private static final Set<String> ATTRIBUTES = new HashSet<String>(Arrays.asList(
+            EXCEPTION, MESSAGE, STACK));
+
+    /**
+     * The constructor for {@link JavaCrashItem}.
+     */
+    public JavaCrashItem() {
+        super(TYPE, ATTRIBUTES);
+    }
+
+    /**
+     * Get the exception for the Java crash.
+     */
+    public String getException() {
+        return (String) getAttribute(EXCEPTION);
+    }
+
+    /**
+     * Get the exception for the Java crash.
+     */
+    public void setException(String exception) {
+        setAttribute(EXCEPTION, exception);
+    }
+
+    /**
+     * Get the message for the Java crash.
+     */
+    public String getMessage() {
+        return (String) getAttribute(MESSAGE);
+    }
+
+    /**
+     * Set the message for the Java crash.
+     */
+    public void setMessage(String message) {
+        setAttribute(MESSAGE, message);
+    }
+
+    /**
+     * Get the stack for the ANR.
+     */
+    public String getStack() {
+        return (String) getAttribute(STACK);
+    }
+
+    /**
+     * Set the stack for the ANR.
+     */
+    public void setStack(String stack) {
+        setAttribute(STACK, stack);
+    }
+}
diff --git a/src/com/android/loganalysis/item/LogcatItem.java b/src/com/android/loganalysis/item/LogcatItem.java
new file mode 100644
index 0000000000000000000000000000000000000000..7bf42a2e57582f49ebc6fcc56cbc4f83944b10a2
--- /dev/null
+++ b/src/com/android/loganalysis/item/LogcatItem.java
@@ -0,0 +1,131 @@
+/*
+ * Copyright (C) 2012 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.loganalysis.item;
+
+import java.util.Arrays;
+import java.util.Date;
+import java.util.HashSet;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * An {@link IItem} used to store logcat info.
+ */
+public class LogcatItem extends GenericItem {
+    public static final String TYPE = "LOGCAT";
+
+    private static final String START_TIME = "START_TIME";
+    private static final String STOP_TIME = "STOP_TIME";
+    private static final String EVENTS = "EVENTS";
+
+    private static final Set<String> ATTRIBUTES = new HashSet<String>(Arrays.asList(
+            START_TIME, STOP_TIME, EVENTS));
+
+    private class ItemList extends LinkedList<IItem> {
+        private static final long serialVersionUID = 1088529764741812025L;
+    }
+
+    /**
+     * The constructor for {@link LogcatItem}.
+     */
+    public LogcatItem() {
+        super(TYPE, ATTRIBUTES);
+
+        setAttribute(EVENTS, new ItemList());
+    }
+
+    /**
+     * Get the start time of the logcat.
+     */
+    public Date getStartTime() {
+        return (Date) getAttribute(START_TIME);
+    }
+
+    /**
+     * Set the start time of the logcat.
+     */
+    public void setStartTime(Date time) {
+        setAttribute(START_TIME, time);
+    }
+
+    /**
+     * Get the stop time of the logcat.
+     */
+    public Date getStopTime() {
+        return (Date) getAttribute(STOP_TIME);
+    }
+
+    /**
+     * Set the stop time of the logcat.
+     */
+    public void setStopTime(Date time) {
+        setAttribute(STOP_TIME, time);
+    }
+
+    /**
+     * Get the list of all {@link IItem} events.
+     */
+    public List<IItem> getEvents() {
+        return (ItemList) getAttribute(EVENTS);
+    }
+
+    /**
+     * Add an {@link IItem} event to the end of the list of events.
+     */
+    public void addEvent(IItem event) {
+        ((ItemList) getAttribute(EVENTS)).add(event);
+    }
+
+    /**
+     * Get the list of all {@link AnrItem} events.
+     */
+    public List<AnrItem> getAnrs() {
+        List<AnrItem> anrs = new LinkedList<AnrItem>();
+        for (IItem item : getEvents()) {
+            if (item instanceof AnrItem) {
+                anrs.add((AnrItem) item);
+            }
+        }
+        return anrs;
+    }
+
+    /**
+     * Get the list of all {@link JavaCrashItem} events.
+     */
+    public List<JavaCrashItem> getJavaCrashes() {
+        List<JavaCrashItem> jcs = new LinkedList<JavaCrashItem>();
+        for (IItem item : getEvents()) {
+            if (item instanceof JavaCrashItem) {
+                jcs.add((JavaCrashItem) item);
+            }
+        }
+        return jcs;
+    }
+
+    /**
+     * Get the list of all {@link NativeCrashItem} events.
+     */
+    public List<NativeCrashItem> getNativeCrashes() {
+        List<NativeCrashItem> ncs = new LinkedList<NativeCrashItem>();
+        for (IItem item : getEvents()) {
+            if (item instanceof NativeCrashItem) {
+                ncs.add((NativeCrashItem) item);
+            }
+        }
+        return ncs;
+    }
+}
diff --git a/src/com/android/loganalysis/item/MemInfoItem.java b/src/com/android/loganalysis/item/MemInfoItem.java
new file mode 100644
index 0000000000000000000000000000000000000000..3246cba6e3dcf889935afeaf40defeaec6913600
--- /dev/null
+++ b/src/com/android/loganalysis/item/MemInfoItem.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2011 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.loganalysis.item;
+
+/**
+ * An {@link IItem} used to store the memory info output.
+ */
+public class MemInfoItem extends GenericMapItem<String, Integer> {
+    private static final long serialVersionUID = 2648395553885243585L;
+
+    public static final String TYPE = "MEMORY INFO";
+
+    public MemInfoItem() {
+        super(TYPE);
+    }
+}
diff --git a/src/com/android/loganalysis/item/MonkeyLogItem.java b/src/com/android/loganalysis/item/MonkeyLogItem.java
new file mode 100644
index 0000000000000000000000000000000000000000..4f89917d852962f07cd36472e97657985c4fa8b3
--- /dev/null
+++ b/src/com/android/loganalysis/item/MonkeyLogItem.java
@@ -0,0 +1,331 @@
+/*
+ * Copyright (C) 2012 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.loganalysis.item;
+
+import java.util.Arrays;
+import java.util.Date;
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * An {@link IItem} used to store monkey log info.
+ */
+public class MonkeyLogItem extends GenericItem {
+
+    private class StringSet extends HashSet<String> {
+        private static final long serialVersionUID = -2206822563602989856L;
+    }
+
+    public enum DroppedCategory {
+        KEYS,
+        POINTERS,
+        TRACKBALLS,
+        FLIPS,
+        ROTATIONS
+    }
+
+    private static final String TYPE = "MONKEY_LOG";
+
+    private static final String START_TIME = "START_TIME";
+    private static final String STOP_TIME = "STOP_TIME";
+    private static final String PACKAGES = "PACKAGES";
+    private static final String CATEGORIES = "CATEGORIES";
+    private static final String THROTTLE = "THROTTLE";
+    private static final String SEED = "SEED";
+    private static final String TARGET_COUNT = "TARGET_COUNT";
+    private static final String IGNORE_SECURITY_EXCEPTIONS = "IGNORE_SECURITY_EXCEPTIONS";
+    private static final String TOTAL_DURATION = "TOTAL_TIME";
+    private static final String START_UPTIME_DURATION = "START_UPTIME";
+    private static final String STOP_UPTIME_DURATION = "STOP_UPTIME";
+    private static final String IS_FINISHED = "IS_FINISHED";
+    private static final String NO_ACTIVITIES = "NO_ACTIVITIES";
+    private static final String INTERMEDIATE_COUNT = "INTERMEDIATE_COUNT";
+    private static final String FINAL_COUNT = "FINAL_COUNT";
+    private static final String CRASH = "CRASH";
+
+    private static final Set<String> ATTRIBUTES = new HashSet<String>(Arrays.asList(
+            START_TIME, STOP_TIME, PACKAGES, CATEGORIES, THROTTLE, SEED, TARGET_COUNT,
+            IGNORE_SECURITY_EXCEPTIONS, TOTAL_DURATION, START_UPTIME_DURATION, STOP_UPTIME_DURATION,
+            IS_FINISHED, NO_ACTIVITIES, INTERMEDIATE_COUNT, FINAL_COUNT, CRASH,
+            DroppedCategory.KEYS.toString(),
+            DroppedCategory.POINTERS.toString(),
+            DroppedCategory.TRACKBALLS.toString(),
+            DroppedCategory.FLIPS.toString(),
+            DroppedCategory.ROTATIONS.toString()));
+
+    /**
+     * The constructor for {@link MonkeyLogItem}.
+     */
+    public MonkeyLogItem() {
+        super(TYPE, ATTRIBUTES);
+
+        setAttribute(PACKAGES, new StringSet());
+        setAttribute(CATEGORIES, new StringSet());
+        setAttribute(THROTTLE, 0);
+        setAttribute(IGNORE_SECURITY_EXCEPTIONS, false);
+        setAttribute(IS_FINISHED, false);
+        setAttribute(NO_ACTIVITIES, false);
+        setAttribute(INTERMEDIATE_COUNT, 0);
+    }
+
+    /**
+     * Get the start time of the monkey log.
+     */
+    public Date getStartTime() {
+        return (Date) getAttribute(START_TIME);
+    }
+
+    /**
+     * Set the start time of the monkey log.
+     */
+    public void setStartTime(Date time) {
+        setAttribute(START_TIME, time);
+    }
+
+    /**
+     * Get the stop time of the monkey log.
+     */
+    public Date getStopTime() {
+        return (Date) getAttribute(STOP_TIME);
+    }
+
+    /**
+     * Set the stop time of the monkey log.
+     */
+    public void setStopTime(Date time) {
+        setAttribute(STOP_TIME, time);
+    }
+
+    /**
+     * Get the set of packages that the monkey is run on.
+     */
+    public Set<String> getPackages() {
+        return (StringSet) getAttribute(PACKAGES);
+    }
+
+    /**
+     * Add a package to the set that the monkey is run on.
+     */
+    public void addPackage(String thePackage) {
+        ((StringSet) getAttribute(PACKAGES)).add(thePackage);
+    }
+
+    /**
+     * Get the set of categories that the monkey is run on.
+     */
+    public Set<String> getCategories() {
+        return (StringSet) getAttribute(CATEGORIES);
+    }
+
+    /**
+     * Add a category to the set that the monkey is run on.
+     */
+    public void addCategory(String category) {
+        ((StringSet) getAttribute(CATEGORIES)).add(category);
+    }
+
+    /**
+     * Get the throttle for the monkey run.
+     */
+    public int getThrottle() {
+        return (Integer) getAttribute(THROTTLE);
+    }
+
+    /**
+     * Set the throttle for the monkey run.
+     */
+    public void setThrottle(int throttle) {
+        setAttribute(THROTTLE, throttle);
+    }
+
+    /**
+     * Get the seed for the monkey run.
+     */
+    public Integer getSeed() {
+        return (Integer) getAttribute(SEED);
+    }
+
+    /**
+     * Set the seed for the monkey run.
+     */
+    public void setSeed(int seed) {
+        setAttribute(SEED, seed);
+    }
+
+    /**
+     * Get the target count for the monkey run.
+     */
+    public Integer getTargetCount() {
+        return (Integer) getAttribute(TARGET_COUNT);
+    }
+
+    /**
+     * Set the target count for the monkey run.
+     */
+    public void setTargetCount(int count) {
+        setAttribute(TARGET_COUNT, count);
+    }
+
+    /**
+     * Get if the ignore security exceptions flag is set for the monkey run.
+     */
+    public boolean getIgnoreSecurityExceptions() {
+        return (Boolean) getAttribute(IGNORE_SECURITY_EXCEPTIONS);
+    }
+
+    /**
+     * Set if the ignore security exceptions flag is set for the monkey run.
+     */
+    public void setIgnoreSecurityExceptions(boolean ignore) {
+        setAttribute(IGNORE_SECURITY_EXCEPTIONS, ignore);
+    }
+
+    /**
+     * Get the total duration of the monkey run in milliseconds.
+     */
+    public Long getTotalDuration() {
+        return (Long) getAttribute(TOTAL_DURATION);
+    }
+
+    /**
+     * Set the total duration of the monkey run in milliseconds.
+     */
+    public void setTotalDuration(long time) {
+        setAttribute(TOTAL_DURATION, time);
+    }
+
+    /**
+     * Get the start uptime duration of the monkey run in milliseconds.
+     */
+    public Long getStartUptimeDuration() {
+        return (Long) getAttribute(START_UPTIME_DURATION);
+    }
+
+    /**
+     * Set the start uptime duration of the monkey run in milliseconds.
+     */
+    public void setStartUptimeDuration(long uptime) {
+        setAttribute(START_UPTIME_DURATION, uptime);
+    }
+
+    /**
+     * Get the stop uptime duration of the monkey run in milliseconds.
+     */
+    public Long getStopUptimeDuration() {
+        return (Long) getAttribute(STOP_UPTIME_DURATION);
+    }
+
+    /**
+     * Set the stop uptime duration of the monkey run in milliseconds.
+     */
+    public void setStopUptimeDuration(long uptime) {
+        setAttribute(STOP_UPTIME_DURATION, uptime);
+    }
+
+    /**
+     * Get if the monkey run finished without crashing.
+     */
+    public boolean getIsFinished() {
+        return (Boolean) getAttribute(IS_FINISHED);
+    }
+
+    /**
+     * Set if the monkey run finished without crashing.
+     */
+    public void setIsFinished(boolean finished) {
+        setAttribute(IS_FINISHED, finished);
+    }
+
+    /**
+     * Get if the monkey run aborted due to no activies to run.
+     */
+    public boolean getNoActivities() {
+        return (Boolean) getAttribute(NO_ACTIVITIES);
+    }
+
+    /**
+     * Set if the monkey run aborted due to no activies to run.
+     */
+    public void setNoActivities(boolean noActivities) {
+        setAttribute(NO_ACTIVITIES, noActivities);
+    }
+
+
+    /**
+     * Get the intermediate count for the monkey run.
+     * <p>
+     * This count starts at 0 and increments every 100 events. This number should be within 100 of
+     * the final count.
+     * </p>
+     */
+    public int getIntermediateCount() {
+        return (Integer) getAttribute(INTERMEDIATE_COUNT);
+    }
+
+    /**
+     * Set the intermediate count for the monkey run.
+     * <p>
+     * This count starts at 0 and increments every 100 events. This number should be within 100 of
+     * the final count.
+     * </p>
+     */
+    public void setIntermediateCount(int count) {
+        setAttribute(INTERMEDIATE_COUNT, count);
+    }
+
+    /**
+     * Get the final count for the monkey run.
+     */
+    public Integer getFinalCount() {
+        return (Integer) getAttribute(FINAL_COUNT);
+    }
+
+    /**
+     * Set the final count for the monkey run.
+     */
+    public void setFinalCount(int count) {
+        setAttribute(FINAL_COUNT, count);
+    }
+
+    /**
+     * Get the dropped events count for a {@link DroppedCategory} for the monkey run.
+     */
+    public Integer getDroppedCount(DroppedCategory category) {
+        return (Integer) getAttribute(category.toString());
+    }
+
+    /**
+     * Set the dropped events count for a {@link DroppedCategory} for the monkey run.
+     */
+    public void setDroppedCount(DroppedCategory category, int count) {
+        setAttribute(category.toString(), count);
+    }
+
+    /**
+     * Get the {@link AnrItem} or {@link JavaCrashItem} for the monkey run or null if there was no
+     * crash.
+     */
+    public GenericLogcatItem getCrash() {
+        return (GenericLogcatItem) getAttribute(CRASH);
+    }
+
+    /**
+     * Set the {@link AnrItem} or {@link JavaCrashItem} for the monkey run.
+     */
+    public void setCrash(GenericLogcatItem crash) {
+        setAttribute(CRASH, crash);
+    }
+}
diff --git a/src/com/android/loganalysis/item/NativeCrashItem.java b/src/com/android/loganalysis/item/NativeCrashItem.java
new file mode 100644
index 0000000000000000000000000000000000000000..e06e0a42c035630df24718a7058151e4326a4b76
--- /dev/null
+++ b/src/com/android/loganalysis/item/NativeCrashItem.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright (C) 2012 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.loganalysis.item;
+
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * An {@link IItem} used to store native crash info.
+ */
+public class NativeCrashItem extends GenericLogcatItem {
+    public static final String TYPE = "NATIVE CRASH";
+
+    private static final String FINGERPRINT = "FINGERPRINT";
+    private static final String STACK = "STACK";
+
+    private static final Set<String> ATTRIBUTES = new HashSet<String>(Arrays.asList(
+            FINGERPRINT, STACK));
+
+    /**
+     * The constructor for {@link NativeCrashItem}.
+     */
+    public NativeCrashItem() {
+        super(TYPE, ATTRIBUTES);
+    }
+
+    /**
+     * Get the fingerprint for the crash.
+     */
+    public String getFingerprint() {
+        return (String) getAttribute(FINGERPRINT);
+    }
+
+    /**
+     * Set the fingerprint for the crash.
+     */
+    public void setFingerprint(String fingerprint) {
+        setAttribute(FINGERPRINT, fingerprint);
+    }
+
+    /**
+     * Get the stack for the crash.
+     */
+    public String getStack() {
+        return (String) getAttribute(STACK);
+    }
+
+    /**
+     * Set the stack for the crash.
+     */
+    public void setStack(String stack) {
+        setAttribute(STACK, stack);
+    }
+}
diff --git a/src/com/android/loganalysis/item/ProcrankItem.java b/src/com/android/loganalysis/item/ProcrankItem.java
new file mode 100644
index 0000000000000000000000000000000000000000..a2d7e74552f6f5d5cec3a285e24d1912bc5dbfa2
--- /dev/null
+++ b/src/com/android/loganalysis/item/ProcrankItem.java
@@ -0,0 +1,146 @@
+/*
+ * Copyright (C) 2011 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.loganalysis.item;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+
+
+/**
+ * An {@link IItem} used to procrank info.
+ */
+public class ProcrankItem implements IItem {
+    public static final String TYPE = "PROCRANK";
+
+    private class ProcrankValue {
+        public String mProcessName = null;
+        public int mVss;
+        public int mRss;
+        public int mPss;
+        public int mUss;
+
+        public ProcrankValue(String processName, int vss, int rss, int pss, int uss) {
+            mProcessName = processName;
+            mVss = vss;
+            mRss = rss;
+            mPss = pss;
+            mUss = uss;
+        }
+    }
+
+    private Map<Integer, ProcrankValue> mProcrankLines = new HashMap<Integer, ProcrankValue>();
+
+    /**
+     * Add a line from the procrank output to the {@link ProcrankItem}.
+     *
+     * @param pid The PID from the output
+     * @param processName The process name from the cmdline column
+     * @param vss The VSS in KB
+     * @param rss The RSS in KB
+     * @param pss The PSS in KB
+     * @param uss The USS in KB
+     */
+    public void addProcrankLine(int pid, String processName, int vss, int rss, int pss, int uss) {
+        mProcrankLines.put(pid, new ProcrankValue(processName, vss, rss, pss, uss));
+    }
+
+    /**
+     * Get a set of PIDs seen in the procrank output.
+     */
+    public Set<Integer> getPids() {
+        return mProcrankLines.keySet();
+    }
+
+    /**
+     * Get the process name for a given PID.
+     */
+    public String getProcessName(int pid) {
+        if (!mProcrankLines.containsKey(pid)) {
+            return null;
+        }
+
+        return mProcrankLines.get(pid).mProcessName;
+    }
+
+    /**
+     * Get the VSS for a given PID.
+     */
+    public Integer getVss(int pid) {
+        if (!mProcrankLines.containsKey(pid)) {
+            return null;
+        }
+
+        return mProcrankLines.get(pid).mVss;
+    }
+
+    /**
+     * Get the RSS for a given PID.
+     */
+    public Integer getRss(int pid) {
+        if (!mProcrankLines.containsKey(pid)) {
+            return null;
+        }
+
+        return mProcrankLines.get(pid).mRss;
+    }
+
+    /**
+     * Get the PSS for a given PID.
+     */
+    public Integer getPss(int pid) {
+        if (!mProcrankLines.containsKey(pid)) {
+            return null;
+        }
+
+        return mProcrankLines.get(pid).mPss;
+    }
+
+    /**
+     * Get the USS for a given PID.
+     */
+    public Integer getUss(int pid) {
+        if (!mProcrankLines.containsKey(pid)) {
+            return null;
+        }
+
+        return mProcrankLines.get(pid).mUss;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public String getType() {
+        return TYPE;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public IItem merge(IItem other) throws ConflictingItemException {
+        throw new ConflictingItemException("Procrank items cannot be merged");
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public boolean isConsistent(IItem other) {
+        return false;
+    }
+}
diff --git a/src/com/android/loganalysis/item/SystemPropsItem.java b/src/com/android/loganalysis/item/SystemPropsItem.java
new file mode 100644
index 0000000000000000000000000000000000000000..67e8de8b53e24011a9877337317d2acb9e186acc
--- /dev/null
+++ b/src/com/android/loganalysis/item/SystemPropsItem.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2011 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.loganalysis.item;
+
+/**
+ * An {@link IItem} used to store the system props info.
+ */
+public class SystemPropsItem extends GenericMapItem<String, String> {
+
+    private static final long serialVersionUID = 7280770512647682477L;
+
+    public static final String TYPE = "SYSTEM PROPERTIES";
+
+    public SystemPropsItem() {
+        super(TYPE);
+    }
+}
diff --git a/src/com/android/loganalysis/item/TracesItem.java b/src/com/android/loganalysis/item/TracesItem.java
new file mode 100644
index 0000000000000000000000000000000000000000..1dfff7639e79f5bbf3c0cd8d9310627bfa1bf891
--- /dev/null
+++ b/src/com/android/loganalysis/item/TracesItem.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright (C) 2011 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.loganalysis.item;
+
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Set;
+
+
+/**
+ * An {@link IItem} used to store traces info.
+ * <p>
+ * For now, this only stores info about the main stack trace from the first process. It is used to
+ * get a stack from {@code /data/anr/traces.txt} which can be used to give some context about the
+ * ANR. If there is a need, this item can be expanded to store all stacks from all processes.
+ * </p>
+ */
+public class TracesItem extends GenericItem {
+    public static final String TYPE = "TRACES_ITEM";
+
+    private static final String PID = "PID";
+    private static final String APP = "APP";
+    private static final String STACK = "STACK";
+
+    private static final Set<String> ATTRIBUTES = new HashSet<String>(Arrays.asList(
+            PID, APP, STACK));
+
+    /**
+     * The constructor for {@link TracesItem}.
+     */
+    public TracesItem() {
+        super(TYPE, ATTRIBUTES);
+    }
+
+    /**
+     * Get the PID of the event.
+     */
+    public Integer getPid() {
+        return (Integer) getAttribute(PID);
+    }
+
+    /**
+     * Set the PID of the event.
+     */
+    public void setPid(Integer pid) {
+        setAttribute(PID, pid);
+    }
+
+    /**
+     * Get the app or package name of the event.
+     */
+    public String getApp() {
+        return (String) getAttribute(APP);
+    }
+
+    /**
+     * Set the app or package name of the event.
+     */
+    public void setApp(String app) {
+        setAttribute(APP, app);
+    }
+
+    /**
+     * Get the stack for the crash.
+     */
+    public String getStack() {
+        return (String) getAttribute(STACK);
+    }
+
+    /**
+     * Set the stack for the crash.
+     */
+    public void setStack(String stack) {
+        setAttribute(STACK, stack);
+    }
+}
diff --git a/src/com/android/loganalysis/parser/AbstractSectionParser.java b/src/com/android/loganalysis/parser/AbstractSectionParser.java
new file mode 100644
index 0000000000000000000000000000000000000000..c57546d4506edfbfd876ee525c7b2d38de05a4a0
--- /dev/null
+++ b/src/com/android/loganalysis/parser/AbstractSectionParser.java
@@ -0,0 +1,124 @@
+/*
+ * Copyright (C) 2011 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.loganalysis.parser;
+
+import com.android.loganalysis.item.IItem;
+import com.android.loganalysis.util.RegexTrie;
+
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * A {@link IParser} that splits an input file into discrete sections and passes each section to an
+ * {@link IParser} to parse.
+ * <p>
+ * Before parsing input, {@link IParser}s can be added with
+ * {@link #addSectionParser(IParser, String)}. The default parser is {@link NoopParser} but this can
+ * be overwritten by calling {@link #setParser(IParser)} before parsing the input.
+ * </p>
+ */
+public abstract class AbstractSectionParser implements IParser {
+    private RegexTrie<IParser> mSectionTrie = new RegexTrie<IParser>();
+    private IParser mCurrentParser = new NoopParser();
+    private List<String> mParseBlock = new LinkedList<String>();
+    private Map<String, IItem> mSections = new HashMap<String, IItem>();
+
+    /**
+     * A method to add a given section parser to the set of potential parsers to use.
+     *
+     * @param parser The {@link IParser} to add
+     * @param pattern The regular expression to trigger this parser
+    */
+    protected void addSectionParser(IParser parser, String pattern) {
+        if (parser == null) {
+            throw new NullPointerException("Parser is null");
+        }
+        if (pattern == null) {
+            throw new NullPointerException("Pattern is null");
+        }
+        mSectionTrie.put(parser, pattern);
+    }
+
+    /**
+     * Parse a line of input, either adding the input to the current block or switching parsers and
+     * running the current parser.
+     *
+     * @param line The line to parse
+     */
+    protected void parseLine(String line) {
+        IParser nextParser = mSectionTrie.retrieve(line);
+
+        if (nextParser == null) {
+            // no match, so buffer this for the current parser, if there is one
+            if (mCurrentParser != null) {
+                mParseBlock.add(line);
+            } else {
+                // CLog.w("Line outside of parsed section: %s", line);
+            }
+        } else {
+            runCurrentParser();
+            mParseBlock.clear();
+            mCurrentParser = nextParser;
+
+            onSwitchParser();
+        }
+    }
+
+    /**
+     * Signal that the input has finished and run the last parser.
+     */
+    protected void commit() {
+        runCurrentParser();
+    }
+
+    /**
+     * Gets the {@link IItem} for a given section.
+     *
+     * @param section The {@link IItem} type for the section.
+     * @return The {@link IItem}.
+     */
+    protected IItem getSection(String section) {
+        return mSections.get(section);
+    }
+
+    /**
+     * Set the {@link IParser}. Used to set the initial parser.
+     *
+     * @param parser The {@link IParser} to set.
+     */
+    protected void setParser(IParser parser) {
+        mCurrentParser = parser;
+    }
+
+    protected void onSwitchParser() {
+    }
+
+    /**
+     * Run the current parser and add the {@link IItem} to the sections map.
+     */
+    private void runCurrentParser() {
+        if (mCurrentParser != null) {
+            IItem item = mCurrentParser.parse(mParseBlock);
+            if (item != null && !(mCurrentParser instanceof NoopParser)) {
+                mSections.put(item.getType(), item);
+                // CLog.v("Just ran the %s parser", mCurrentParser.getClass().getSimpleName());
+            }
+        }
+    }
+}
+
diff --git a/src/com/android/loganalysis/parser/AnrParser.java b/src/com/android/loganalysis/parser/AnrParser.java
new file mode 100644
index 0000000000000000000000000000000000000000..5102436d75a6aa4e4d544f1fb9e774c7c87f2d16
--- /dev/null
+++ b/src/com/android/loganalysis/parser/AnrParser.java
@@ -0,0 +1,114 @@
+/*
+ * Copyright (C) 2012 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.loganalysis.parser;
+
+import com.android.loganalysis.item.AnrItem;
+
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * An {@link IParser} to handle ANRs.
+ */
+public class AnrParser implements IParser {
+    /**
+     * Matches: ANR (application not responding) in process: app
+     * Matches: ANR in app
+     * Matches: ANR in app (class/package)
+     */
+    public static final Pattern START = Pattern.compile(
+            "^ANR (?:\\(application not responding\\) )?in (?:process: )?(\\S+).*$");
+    /**
+     * Matches: Reason: reason
+     */
+    private static final Pattern REASON = Pattern.compile("^Reason: (.*)$");
+    /**
+     * Matches: Load: 0.71 / 0.83 / 0.51
+     */
+    private static final Pattern LOAD = Pattern.compile(
+            "^Load: (\\d+\\.\\d+) / (\\d+\\.\\d+) / (\\d+\\.\\d+)$");
+
+    /**
+     * Matches: 33% TOTAL: 21% user + 11% kernel + 0.3% iowait
+     */
+    private static final Pattern TOTAL = Pattern.compile("^(\\d+(\\.\\d+)?)% TOTAL: .*$");
+    private static final Pattern USER = Pattern.compile("^.* (\\d+(\\.\\d+)?)% user.*$");
+    private static final Pattern KERNEL = Pattern.compile("^.* (\\d+(\\.\\d+)?)% kernel.*$");
+    private static final Pattern IOWAIT = Pattern.compile("^.* (\\d+(\\.\\d+)?)% iowait.*$");
+
+    /**
+     * {@inheritDoc}
+     *
+     * @return The {@link AnrItem}.
+     */
+    @Override
+    public AnrItem parse(List<String> lines) {
+        AnrItem anr = null;
+        StringBuilder stack = new StringBuilder();
+        boolean matchedTotal = false;
+
+        for (String line : lines) {
+            Matcher m = START.matcher(line);
+            // Ignore all input until the start pattern is matched.
+            if (m.matches()) {
+                anr = new AnrItem();
+                anr.setApp(m.group(1));
+            }
+
+            if (anr != null) {
+                m = REASON.matcher(line);
+                if (m.matches()) {
+                    anr.setReason(m.group(1));
+                }
+
+                m = LOAD.matcher(line);
+                if (m.matches()) {
+                    anr.setLoad(AnrItem.LoadCategory.LOAD_1, Double.parseDouble(m.group(1)));
+                    anr.setLoad(AnrItem.LoadCategory.LOAD_5, Double.parseDouble(m.group(2)));
+                    anr.setLoad(AnrItem.LoadCategory.LOAD_15, Double.parseDouble(m.group(3)));
+                }
+
+                m = TOTAL.matcher(line);
+                if (!matchedTotal && m.matches()) {
+                    matchedTotal = true;
+                    anr.setCpuUsage(AnrItem.CpuUsageCategory.TOTAL, Double.parseDouble(m.group(1)));
+
+                    m = USER.matcher(line);
+                    Double usage = m.matches() ? Double.parseDouble(m.group(1)) : 0.0;
+                    anr.setCpuUsage(AnrItem.CpuUsageCategory.USER, usage);
+
+                    m = KERNEL.matcher(line);
+                    usage = m.matches() ? Double.parseDouble(m.group(1)) : 0.0;
+                    anr.setCpuUsage(AnrItem.CpuUsageCategory.KERNEL, usage);
+
+                    m = IOWAIT.matcher(line);
+                    usage = m.matches() ? Double.parseDouble(m.group(1)) : 0.0;
+                    anr.setCpuUsage(AnrItem.CpuUsageCategory.IOWAIT, usage);
+                }
+
+                stack.append(line);
+                stack.append("\n");
+            }
+        }
+
+        if (anr != null) {
+            anr.setStack(stack.toString().trim());
+        }
+        return anr;
+    }
+}
+
diff --git a/src/com/android/loganalysis/parser/BugreportParser.java b/src/com/android/loganalysis/parser/BugreportParser.java
new file mode 100644
index 0000000000000000000000000000000000000000..748f751838edd237e25df299faf6370fb5c3a009
--- /dev/null
+++ b/src/com/android/loganalysis/parser/BugreportParser.java
@@ -0,0 +1,199 @@
+/*
+ * Copyright (C) 2011 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.loganalysis.parser;
+
+import com.android.loganalysis.item.AnrItem;
+import com.android.loganalysis.item.BugreportItem;
+import com.android.loganalysis.item.GenericLogcatItem;
+import com.android.loganalysis.item.IItem;
+import com.android.loganalysis.item.LogcatItem;
+import com.android.loganalysis.item.MemInfoItem;
+import com.android.loganalysis.item.ProcrankItem;
+import com.android.loganalysis.item.SystemPropsItem;
+import com.android.loganalysis.item.TracesItem;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.text.DateFormat;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.List;
+import java.util.ListIterator;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * A {@link IParser} to parse Android bugreports.
+ */
+public class BugreportParser extends AbstractSectionParser {
+    private static final String MEM_INFO_SECTION_REGEX = "------ MEMORY INFO .*";
+    private static final String PROCRANK_SECTION_REGEX = "------ PROCRANK .*";
+    private static final String SYSTEM_PROP_SECTION_REGEX = "------ SYSTEM PROPERTIES .*";
+    private static final String SYSTEM_LOG_SECTION_REGEX =
+            "------ (SYSTEM|MAIN|MAIN AND SYSTEM) LOG .*";
+    private static final String ANR_TRACES_SECTION_REGEX = "------ VM TRACES AT LAST ANR .*";
+    private static final String NOOP_SECTION_REGEX = "------ .*";
+
+    /**
+     * Matches: == dumpstate: 2012-04-26 12:13:14
+     */
+    private static final Pattern DATE = Pattern.compile(
+            "^== dumpstate: (\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2})$");
+
+    private LogcatParser mLogcatParser = new LogcatParser();
+    private BugreportItem mBugreport = null;
+
+    /**
+     * Parse a bugreport from a {@link BufferedReader} into an {@link BugreportItem} object.
+     *
+     * @param input a {@link BufferedReader}.
+     * @return The {@link BugreportItem}.
+     * @see #parse(List)
+     */
+    public BugreportItem parse(BufferedReader input) throws IOException {
+        String line;
+
+        setup();
+        while ((line = input.readLine()) != null) {
+            parseLine(line);
+        }
+        commit();
+
+        return mBugreport;
+    }
+
+    /**
+     * {@inheritDoc}
+     *
+     * @return The {@link BugreportItem}.
+     */
+    @Override
+    public BugreportItem parse(List<String> lines) {
+        setup();
+        for (String line : lines) {
+            parseLine(line);
+        }
+        commit();
+
+        return mBugreport;
+    }
+
+    /**
+     * Sets up the parser by adding the section parsers and adding an initial {@link IParser} to
+     * parse the bugreport header.
+     */
+    protected void setup() {
+        // Set the initial parser explicitly since the header isn't part of a section.
+        setParser(new IParser() {
+            @Override
+            public BugreportItem parse(List<String> lines) {
+                BugreportItem bugreport = new BugreportItem();
+                for (String line : lines) {
+                    Matcher m = DATE.matcher(line);
+                    if (m.matches()) {
+                        bugreport.setTime(parseTime(m.group(1)));
+                    }
+                }
+                return bugreport;
+            }
+        });
+        addSectionParser(new MemInfoParser(), MEM_INFO_SECTION_REGEX);
+        addSectionParser(new ProcrankParser(), PROCRANK_SECTION_REGEX);
+        addSectionParser(new SystemPropsParser(), SYSTEM_PROP_SECTION_REGEX);
+        addSectionParser(new TracesParser(), ANR_TRACES_SECTION_REGEX);
+        addSectionParser(mLogcatParser, SYSTEM_LOG_SECTION_REGEX);
+        addSectionParser(new NoopParser(), NOOP_SECTION_REGEX);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    protected void commit() {
+        // signal EOF
+        super.commit();
+
+        if (mBugreport != null) {
+            mBugreport.setMemInfo((MemInfoItem) getSection(MemInfoItem.TYPE));
+            mBugreport.setProcrank((ProcrankItem) getSection(ProcrankItem.TYPE));
+            mBugreport.setSystemLog((LogcatItem) getSection(LogcatItem.TYPE));
+            mBugreport.setSystemProps((SystemPropsItem) getSection(SystemPropsItem.TYPE));
+
+            if (mBugreport.getSystemLog() != null && mBugreport.getProcrank() != null) {
+                for (IItem item : mBugreport.getSystemLog().getEvents()) {
+                    if (item instanceof GenericLogcatItem &&
+                            ((GenericLogcatItem) item).getApp() == null) {
+                        GenericLogcatItem logcatItem = (GenericLogcatItem) item;
+                        logcatItem.setApp(mBugreport.getProcrank().getProcessName(
+                                logcatItem.getPid()));
+                    }
+                }
+            }
+
+            TracesItem traces = (TracesItem) getSection(TracesItem.TYPE);
+            if (traces != null && traces.getApp() != null && traces.getStack() != null &&
+                    mBugreport.getSystemLog() != null) {
+                addAnrTrace(mBugreport.getSystemLog().getAnrs(), traces.getApp(),
+                        traces.getStack());
+
+            }
+        }
+    }
+
+    /**
+     * Add the trace from {@link TracesItem} to the last seen {@link AnrItem} matching a given app.
+     */
+    private void addAnrTrace(List<AnrItem> anrs, String app, String trace) {
+        ListIterator<AnrItem> li = anrs.listIterator(anrs.size());
+
+        while (li.hasPrevious()) {
+            AnrItem anr = li.previous();
+            if (app.equals(anr.getApp())) {
+                anr.setTrace(trace);
+                return;
+            }
+        }
+    }
+
+    /**
+     * Set the {@link BugreportItem} and the year of the {@link LogcatParser} from the bugreport
+     * header.
+     */
+    @Override
+    protected void onSwitchParser() {
+        if (mBugreport == null) {
+            mBugreport = (BugreportItem) getSection(BugreportItem.TYPE);
+            if (mBugreport.getTime() != null) {
+                mLogcatParser.setYear(new SimpleDateFormat("yyyy").format(mBugreport.getTime()));
+            }
+        }
+    }
+
+    /**
+     * Converts a {@link String} into a {@link Date}.
+     */
+    private static Date parseTime(String timeStr) {
+        DateFormat formatter = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
+        try {
+            return formatter.parse(timeStr);
+        } catch (ParseException e) {
+            // CLog.e("Could not parse time string %s", timeStr);
+            return null;
+        }
+    }
+}
+
diff --git a/src/com/android/loganalysis/parser/IParser.java b/src/com/android/loganalysis/parser/IParser.java
new file mode 100644
index 0000000000000000000000000000000000000000..43750d7ba1e20d95007480bae91bef2bacae54c0
--- /dev/null
+++ b/src/com/android/loganalysis/parser/IParser.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2011 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.loganalysis.parser;
+
+import com.android.loganalysis.item.IItem;
+
+import java.util.List;
+
+/**
+ * An interface which defines the behavior for a parser.  The parser will receive a block of data
+ * that it can consider complete.  It parses the input and returns a single {@link IItem} instance.
+ * Furthermore, the parser should be robust against invalid input -- the input format may drift over
+ * time.
+ */
+public interface IParser {
+
+    /**
+     * Parses a list of {@link String} objects and returns a {@link IItem}.
+     *
+     * @param lines A list of {@link String} objects.
+     * @return The parsed {@link IItem} object.
+     */
+    public IItem parse(List<String> lines);
+}
+
diff --git a/src/com/android/loganalysis/parser/JavaCrashParser.java b/src/com/android/loganalysis/parser/JavaCrashParser.java
new file mode 100644
index 0000000000000000000000000000000000000000..f1c87a17a55349e5acb957b37e904bae907fd6fe
--- /dev/null
+++ b/src/com/android/loganalysis/parser/JavaCrashParser.java
@@ -0,0 +1,120 @@
+/*
+ * Copyright (C) 2012 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.loganalysis.parser;
+
+import com.android.loganalysis.item.JavaCrashItem;
+
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * An {@link IParser} to handle Java crashes.
+ */
+public class JavaCrashParser implements IParser {
+
+    /**
+     * Matches: java.lang.Exception
+     * Matches: java.lang.Exception: reason
+     */
+    private static final Pattern EXCEPTION = Pattern.compile("^([^\\s:]+)(: (.*))?$");
+    /**
+     * Matches: Caused by: java.lang.Exception
+     */
+    private static final Pattern CAUSEDBY = Pattern.compile("^Caused by: .+$");
+    /**
+     * Matches: \tat class.method(Class.java:1)
+     */
+    private static final Pattern AT = Pattern.compile("^\tat .+$");
+
+    /**
+     * {@inheritDoc}
+     *
+     * @return The {@link JavaCrashItem}.
+     */
+    @Override
+    public JavaCrashItem parse(List<String> lines) {
+        JavaCrashItem jc = null;
+        StringBuilder stack = new StringBuilder();
+        StringBuilder message = new StringBuilder();
+        boolean inMessage = false;
+        boolean inCausedBy = false;
+        boolean inStack = false;
+
+        for (String line : lines) {
+            if (!inStack) {
+                Matcher exceptionMatch = EXCEPTION.matcher(line);
+                if (exceptionMatch.matches()) {
+                    inMessage = true;
+                    inStack = true;
+
+                    jc = new JavaCrashItem();
+                    jc.setException(exceptionMatch.group(1));
+                    if (exceptionMatch.group(3) != null) {
+                        message.append(exceptionMatch.group(3));
+                    }
+                }
+            } else {
+                // Match: Caused by: java.lang.Exception
+                Matcher causedByMatch = CAUSEDBY.matcher(line);
+                if (causedByMatch.matches()) {
+                    inMessage = false;
+                    inCausedBy = true;
+                }
+
+                // Match: \tat class.method(Class.java:1)
+                Matcher atMatch = AT.matcher(line);
+                if (atMatch.matches()) {
+                    inMessage = false;
+                    inCausedBy = false;
+                }
+
+                if (!causedByMatch.matches() && !atMatch.matches()) {
+                    if (inMessage) {
+                        message.append("\n");
+                        message.append(line);
+                    }
+                    if (!inMessage && !inCausedBy) {
+                        addMessageStack(jc, message.toString(), stack.toString());
+                        return jc;
+                    }
+                }
+            }
+
+            if (inStack) {
+                stack.append(line);
+                stack.append("\n");
+            }
+        }
+
+        addMessageStack(jc, message.toString(), stack.toString());
+        return jc;
+    }
+
+    /**
+     * Adds the message and stack to the {@link JavaCrashItem}.
+     */
+    private void addMessageStack(JavaCrashItem jc, String message, String stack) {
+        if (jc != null) {
+            if (message.length() > 0) {
+                jc.setMessage(message);
+            }
+            jc.setStack(stack.trim());
+        }
+    }
+}
+
diff --git a/src/com/android/loganalysis/parser/LogcatParser.java b/src/com/android/loganalysis/parser/LogcatParser.java
new file mode 100644
index 0000000000000000000000000000000000000000..9edbede9f96d6166f032d5718dcf1e8aa84cacf7
--- /dev/null
+++ b/src/com/android/loganalysis/parser/LogcatParser.java
@@ -0,0 +1,352 @@
+/*
+ * Copyright (C) 2011 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.loganalysis.parser;
+
+import com.android.loganalysis.item.GenericLogcatItem;
+import com.android.loganalysis.item.LogcatItem;
+import com.android.loganalysis.util.ArrayUtil;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.text.DateFormat;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.ListIterator;
+import java.util.Map;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * An {@link IParser} to handle logcat.  The parser can handle the time and threadtime logcat
+ * formats.
+ * <p>
+ * Since the timestamps in the logcat do not have a year, the year can be set manually when the
+ * parser is created or through {@link #setYear(String)}.  If a year is not set, the current year
+ * will be used.
+ * </p>
+ */
+public class LogcatParser implements IParser {
+
+    /**
+     * Match a single line of `logcat -v threadtime`, such as:
+     * 05-26 11:02:36.886  5689  5689 D AndroidRuntime: CheckJNI is OFF
+     */
+    private static final Pattern THREADTIME_LINE = Pattern.compile(
+            "^(\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}.\\d{3})\\s+" +  /* timestamp [1] */
+                "(\\d+)\\s+(\\d+)\\s+([A-Z])\\s+" +  /* pid/tid and log level [2-4] */
+                "(.+?)\\s*: (.*)$" /* tag and message [5-6]*/);
+
+    /**
+     * Match a single line of `logcat -v time`, such as:
+     * 06-04 02:32:14.002 D/dalvikvm(  236): GC_CONCURRENT freed 580K, 51% free [...]
+     */
+    private static final Pattern TIME_LINE = Pattern.compile(
+            "^(\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}.\\d{3})\\s+" +  /* timestamp [1] */
+                "(\\w)/(.+?)\\(\\s*(\\d+)\\): (.*)$");  /* level, tag, pid, msg [2-5] */
+
+    /**
+     * Class for storing logcat meta data for a particular grouped list of lines.
+     */
+    private class LogcatData {
+        public Integer mPid = null;
+        public Integer mTid = null;
+        public Date mTime = null;
+        public String mLevel = null;
+        public String mTag = null;
+        public String mLastPreamble = null;
+        public String mProcPreamble = null;
+        public List<String> mLines = new LinkedList<String>();
+
+        public LogcatData(Integer pid, Integer tid, Date time, String level, String tag,
+                String lastPreamble, String procPreamble) {
+            mPid = pid;
+            mTid = tid;
+            mTime = time;
+            mLevel = level;
+            mTag = tag;
+            mLastPreamble = lastPreamble;
+            mProcPreamble = procPreamble;
+        }
+    }
+
+    private static final int MAX_BUFF_SIZE = 500;
+    private static final int MAX_LAST_PREAMBLE_SIZE = 15;
+    private static final int MAX_PROC_PREAMBLE_SIZE = 15;
+
+    private LinkedList<String> mRingBuffer = new LinkedList<String>();
+    private String mYear = null;
+
+    LogcatItem mLogcat = new LogcatItem();
+
+    Map<String, LogcatData> mDataMap = new HashMap<String, LogcatData>();
+    List<LogcatData> mDataList = new LinkedList<LogcatData>();
+
+    private Date mStartTime = null;
+    private Date mStopTime = null;
+
+    /**
+     * Constructor for {@link LogcatParser}.
+     */
+    public LogcatParser() {
+    }
+
+    /**
+     * Constructor for {@link LogcatParser}.
+     *
+     * @param year The year as a string.
+     */
+    public LogcatParser(String year) {
+        setYear(year);
+    }
+
+    /**
+     * Sets the year for {@link LogcatParser}.
+     *
+     * @param year The year as a string.
+     */
+    public void setYear(String year) {
+        mYear = year;
+    }
+
+    /**
+     * Parse a logcat from a {@link BufferedReader} into an {@link LogcatItem} object.
+     *
+     * @param input a {@link BufferedReader}.
+     * @return The {@link LogcatItem}.
+     * @see #parse(List)
+     */
+    public LogcatItem parse(BufferedReader input) throws IOException {
+        String line;
+        while ((line = input.readLine()) != null) {
+            parseLine(line);
+        }
+        commit();
+
+        return mLogcat;
+    }
+
+    /**
+     * {@inheritDoc}
+     *
+     * @return The {@link LogcatItem}.
+     */
+    @Override
+    public LogcatItem parse(List<String> lines) {
+        for (String line : lines) {
+            parseLine(line);
+        }
+        commit();
+
+        return mLogcat;
+    }
+
+    /**
+     * Parse a line of input.
+     *
+     * @param line The line to parse
+     */
+    private void parseLine(String line) {
+        Integer pid = null;
+        Integer tid = null;
+        Date time = null;
+        String level = null;
+        String tag = null;
+        String msg = null;
+
+        Matcher m = THREADTIME_LINE.matcher(line);
+        Matcher tm = TIME_LINE.matcher(line);
+        if (m.matches()) {
+            time = parseTime(m.group(1));
+            pid = Integer.parseInt(m.group(2));
+            tid = Integer.parseInt(m.group(3));
+            level = m.group(4);
+            tag = m.group(5);
+            msg = m.group(6);
+        } else if (tm.matches()) {
+            time = parseTime(tm.group(1));
+            level = tm.group(2);
+            tag = tm.group(3);
+            pid = Integer.parseInt(tm.group(4));
+            msg = tm.group(5);
+        } else {
+            // CLog.w("Failed to parse line '%s'", line);
+            return;
+        }
+
+        if (mStartTime == null) {
+            mStartTime = time;
+        }
+        mStopTime = time;
+
+        // ANRs are split when START matches a line.  The newest entry is kept in the dataMap
+        // for quick lookup while all entries are added to the list.
+        if ("E".equals(level) && "ActivityManager".equals(tag)) {
+            String key = encodeLine(pid, tid, level, tag);
+            LogcatData data;
+            if (!mDataMap.containsKey(key) || AnrParser.START.matcher(msg).matches()) {
+                data = new LogcatData(pid, tid, time, level, tag, getLastPreamble(),
+                        getProcPreamble(pid));
+                mDataMap.put(key, data);
+                mDataList.add(data);
+            } else {
+                data = mDataMap.get(key);
+            }
+            data.mLines.add(msg);
+        }
+
+        // PID and TID are enough to separate Java and native crashes.
+        if (("E".equals(level) && "AndroidRuntime".equals(tag)) ||
+                ("I".equals(level) && "DEBUG".equals(tag))) {
+            String key = encodeLine(pid, tid, level, tag);
+            LogcatData data;
+            if (!mDataMap.containsKey(key)) {
+                data = new LogcatData(pid, tid, time, level, tag, getLastPreamble(),
+                        getProcPreamble(pid));
+                mDataMap.put(key, data);
+                mDataList.add(data);
+            } else {
+                data = mDataMap.get(key);
+            }
+            data.mLines.add(msg);
+        }
+
+        // After parsing the line, add it the the buffer for the preambles.
+        mRingBuffer.add(line);
+        if (mRingBuffer.size() > MAX_BUFF_SIZE) {
+            mRingBuffer.removeFirst();
+        }
+    }
+
+    /**
+     * Signal that the input has finished.
+     */
+    private void commit() {
+        for (LogcatData data : mDataList) {
+            GenericLogcatItem item = null;
+            if ("E".equals(data.mLevel) && "ActivityManager".equals(data.mTag)) {
+                // CLog.v("Parsing ANR: %s", data.mLines);
+                item = new AnrParser().parse(data.mLines);
+            } else if ("E".equals(data.mLevel) && "AndroidRuntime".equals(data.mTag)) {
+                // CLog.v("Parsing Java crash: %s", data.mLines);
+                item = new JavaCrashParser().parse(data.mLines);
+            } else if ("I".equals(data.mLevel) && "DEBUG".equals(data.mTag)) {
+                // CLog.v("Parsing native crash: %s", data.mLines);
+                item = new NativeCrashParser().parse(data.mLines);
+            }
+            if (item != null) {
+                item.setEventTime(data.mTime);
+                item.setPid(data.mPid);
+                item.setTid(data.mTid);
+                item.setLastPreamble(data.mLastPreamble);
+                item.setProcessPreamble(data.mProcPreamble);
+                mLogcat.addEvent(item);
+            }
+        }
+
+        mLogcat.setStartTime(mStartTime);
+        mLogcat.setStopTime(mStopTime);
+    }
+
+    /**
+     * Create an identifier that "should" be unique for a given logcat. In practice, we do use it as
+     * a unique identifier.
+     */
+    private static String encodeLine(Integer pid, Integer tid, String level, String tag) {
+        if (tid == null) {
+            return String.format("%d|%s|%s", pid, level, tag);
+        }
+        return String.format("%d|%d|%s|%s", pid, tid, level, tag);
+    }
+
+    /**
+     * Parse the timestamp and return a {@link Date}.  If year is not set, the current year will be
+     * used.
+     *
+     * @param timeStr The timestamp in the format {@code MM-dd HH:mm:ss.SSS}.
+     * @return The {@link Date}.
+     */
+    private Date parseTime(String timeStr) {
+        // If year is null, just use the current year.
+        if (mYear == null) {
+            DateFormat yearFormatter = new SimpleDateFormat("yyyy");
+            mYear = yearFormatter.format(new Date());
+        }
+
+        DateFormat formatter = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
+        try {
+            return formatter.parse(String.format("%s-%s", mYear, timeStr));
+        } catch (ParseException e) {
+            // CLog.e("Could not parse time string %s", timeStr);
+            return null;
+        }
+    }
+
+    /**
+     * Get the last {@value #MAX_LAST_PREAMBLE_SIZE} lines of logcat.
+     */
+    private String getLastPreamble() {
+        final int size = mRingBuffer.size();
+        List<String> preamble;
+        if (size > getLastPreambleSize()) {
+            preamble = mRingBuffer.subList(size - getLastPreambleSize(), size);
+        } else {
+            preamble = mRingBuffer;
+        }
+        return ArrayUtil.join("\n", preamble).trim();
+    }
+
+    /**
+     * Get the last {@value #MAX_PROC_PREAMBLE_SIZE} lines of logcat which match the given pid.
+     */
+    private String getProcPreamble(int pid) {
+        LinkedList<String> preamble = new LinkedList<String>();
+
+        ListIterator<String> li = mRingBuffer.listIterator(mRingBuffer.size());
+        while (li.hasPrevious()) {
+            String line = li.previous();
+
+            Matcher m = THREADTIME_LINE.matcher(line);
+            Matcher tm = TIME_LINE.matcher(line);
+            if ((m.matches() && pid == Integer.parseInt(m.group(2))) ||
+                    (tm.matches() && pid == Integer.parseInt(tm.group(4)))) {
+                preamble.addFirst(line);
+            }
+
+            if (preamble.size() == getProcPreambleSize()) {
+                return ArrayUtil.join("\n", preamble).trim();
+            }
+        }
+        return ArrayUtil.join("\n", preamble).trim();
+    }
+
+    /**
+     * Get the number of lines in the last preamble. Exposed for unit testing.
+     */
+    int getLastPreambleSize() {
+        return MAX_LAST_PREAMBLE_SIZE;
+    }
+
+    /**
+     * Get the number of lines in the process preamble. Exposed for unit testing.
+     */
+    int getProcPreambleSize() {
+        return MAX_PROC_PREAMBLE_SIZE;
+    }
+}
diff --git a/src/com/android/loganalysis/parser/MemInfoParser.java b/src/com/android/loganalysis/parser/MemInfoParser.java
new file mode 100644
index 0000000000000000000000000000000000000000..3aeaf05c633610948c6c979cb9e5ba2ff7dce5ab
--- /dev/null
+++ b/src/com/android/loganalysis/parser/MemInfoParser.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2011 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.loganalysis.parser;
+
+import com.android.loganalysis.item.MemInfoItem;
+
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * A {@link IParser} to handle the output from {@code /proc/meminfo}.
+ */
+public class MemInfoParser implements IParser {
+
+    /** Match a single MemoryInfo line, such as "MemFree:           65420 kB" */
+    private static final Pattern INFO_LINE = Pattern.compile("^([^:]+):\\s+(\\d+) kB");
+
+    /**
+     * {@inheritDoc}
+     *
+     * @return The {@link MemInfoItem}.
+     */
+    @Override
+    public MemInfoItem parse(List<String> block) {
+        MemInfoItem item = new MemInfoItem();
+
+        for (String line : block) {
+            Matcher m = INFO_LINE.matcher(line);
+            if (m.matches()) {
+                String key = m.group(1);
+                Integer value = Integer.parseInt(m.group(2));
+                item.put(key, value);
+            } else {
+                // CLog.w("Failed to parse line '%s'", line);
+            }
+        }
+
+        return item;
+    }
+}
diff --git a/src/com/android/loganalysis/parser/MonkeyLogParser.java b/src/com/android/loganalysis/parser/MonkeyLogParser.java
new file mode 100644
index 0000000000000000000000000000000000000000..60cc42844d7215a48c2b4e7f7fc16c7f4157968f
--- /dev/null
+++ b/src/com/android/loganalysis/parser/MonkeyLogParser.java
@@ -0,0 +1,284 @@
+/*
+ * Copyright (C) 2012 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.loganalysis.parser;
+
+import com.android.loganalysis.item.AnrItem;
+import com.android.loganalysis.item.GenericLogcatItem;
+import com.android.loganalysis.item.MonkeyLogItem;
+import com.android.loganalysis.item.MonkeyLogItem.DroppedCategory;
+import com.android.loganalysis.item.TracesItem;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * A {@link IParser} to parse monkey logs.
+ */
+public class MonkeyLogParser implements IParser {
+    private static final Pattern THROTTLE = Pattern.compile(
+            "adb shell monkey.* --throttle (\\d+).*");
+    private static final Pattern SEED_AND_TARGET_COUNT = Pattern.compile(
+            ":Monkey: seed=(\\d+) count=(\\d+)");
+    private static final Pattern SECURITY_EXCEPTIONS = Pattern.compile(
+            "adb shell monkey.* --ignore-security-exceptions.*");
+
+    private static final Pattern PACKAGES = Pattern.compile(":AllowPackage: (\\S+)");
+    private static final Pattern CATEGORIES = Pattern.compile(":IncludeCategory: (\\S+)");
+
+    private static final Pattern START_UPTIME = Pattern.compile(
+            "# (.*) - device uptime = (\\d+\\.\\d+): Monkey command used for this test:");
+    private static final Pattern STOP_UPTIME = Pattern.compile(
+            "# (.*) - device uptime = (\\d+\\.\\d+): Monkey command ran for: " +
+            "(\\d+):(\\d+) \\(mm:ss\\)");
+
+    private static final Pattern INTERMEDIATE_COUNT = Pattern.compile(
+            "\\s+// Sending event #(\\d+)");
+    private static final Pattern FINISHED = Pattern.compile("// Monkey finished");
+    private static final Pattern FINAL_COUNT = Pattern.compile("Events injected: (\\d+)");
+    private static final Pattern NO_ACTIVITIES = Pattern.compile(
+            "\\*\\* No activities found to run, monkey aborted.");
+
+    private static final Pattern DROPPED_KEYS = Pattern.compile(":Dropped: .*keys=(\\d+).*");
+    private static final Pattern DROPPED_POINTERS = Pattern.compile(
+            ":Dropped: .*pointers=(\\d+).*");
+    private static final Pattern DROPPED_TRACKBALLS = Pattern.compile(
+            ":Dropped: .*trackballs=(\\d+).*");
+    private static final Pattern DROPPED_FLIPS = Pattern.compile(":Dropped: .*flips=(\\d+).*");
+    private static final Pattern DROPPED_ROTATIONS = Pattern.compile(
+            ":Dropped: .*rotations=(\\d+).*");
+
+    private static final Pattern ANR = Pattern.compile(
+            "// NOT RESPONDING: (\\S+) \\(pid (\\d+)\\)");
+    private static final Pattern JAVA_CRASH = Pattern.compile(
+            "// CRASH: (\\S+) \\(pid (\\d+)\\)");
+
+    private static final Pattern TRACES_START = Pattern.compile("anr traces:");
+    private static final Pattern TRACES_STOP = Pattern.compile("// anr traces status was \\d+");
+
+    private boolean mMatchingAnr = false;
+    private boolean mMatchingJavaCrash = false;
+    private boolean mMatchingTraces = false;
+    private List<String> mBlock = null;
+    private String mApp = null;
+    private int mPid = 0;
+
+    private MonkeyLogItem mMonkeyLog = new MonkeyLogItem();
+
+    /**
+     * Parse a monkey log from a {@link BufferedReader} into an {@link MonkeyLogItem} object.
+     *
+     * @param input a {@link BufferedReader}.
+     * @return The {@link MonkeyLogItem}.
+     * @see #parse(List)
+     */
+    public MonkeyLogItem parse(BufferedReader input) throws IOException {
+        String line;
+        while ((line = input.readLine()) != null) {
+            parseLine(line);
+        }
+
+        return mMonkeyLog;
+    }
+
+    /**
+     * {@inheritDoc}
+     *
+     * @return The {@link MonkeyLogItem}.
+     */
+    @Override
+    public MonkeyLogItem parse(List<String> lines) {
+        for (String line : lines) {
+            parseLine(line);
+        }
+
+        return mMonkeyLog;
+    }
+
+    /**
+     * Parse a line of input.
+     */
+    private void parseLine(String line) {
+        Matcher m;
+
+        if (mMatchingAnr || mMatchingJavaCrash) {
+            if (mMatchingJavaCrash) {
+                line = line.replace("// ", "");
+            }
+            if ("".equals(line)) {
+                GenericLogcatItem crash;
+                if (mMatchingAnr) {
+                    crash = new AnrParser().parse(mBlock);
+                } else {
+                    crash = new JavaCrashParser().parse(mBlock);
+                }
+                if (crash != null) {
+                    crash.setPid(mPid);
+                    crash.setApp(mApp);
+                    mMonkeyLog.setCrash(crash);
+                }
+
+                mMatchingAnr = false;
+                mMatchingJavaCrash = false;
+                mBlock = null;
+                mApp = null;
+                mPid = 0;
+            } else {
+                mBlock.add(line);
+            }
+            return;
+        }
+
+        if (mMatchingTraces) {
+            m = TRACES_STOP.matcher(line);
+            if (m.matches()) {
+                TracesItem traces = new TracesParser().parse(mBlock);
+
+                // Set the trace if the crash is an ANR and if the app for the crash and trace match
+                if (traces != null && traces.getApp() != null && traces.getStack() != null &&
+                        mMonkeyLog.getCrash() instanceof AnrItem &&
+                        traces.getApp().equals(mMonkeyLog.getCrash().getApp())) {
+                    ((AnrItem) mMonkeyLog.getCrash()).setTrace(traces.getStack());
+                }
+
+                mMatchingTraces = false;
+                mBlock = null;
+            } else {
+                mBlock.add(line);
+            }
+        }
+
+        m = THROTTLE.matcher(line);
+        if (m.matches()) {
+            mMonkeyLog.setThrottle(Integer.parseInt(m.group(1)));
+        }
+        m = SEED_AND_TARGET_COUNT.matcher(line);
+        if (m.matches()) {
+            mMonkeyLog.setSeed(Integer.parseInt(m.group(1)));
+            mMonkeyLog.setTargetCount(Integer.parseInt(m.group(2)));
+        }
+        m = SECURITY_EXCEPTIONS.matcher(line);
+        if (m.matches()) {
+            mMonkeyLog.setIgnoreSecurityExceptions(true);
+        }
+        m = PACKAGES.matcher(line);
+        if (m.matches()) {
+            mMonkeyLog.addPackage(m.group(1));
+        }
+        m = CATEGORIES.matcher(line);
+        if (m.matches()) {
+            mMonkeyLog.addCategory(m.group(1));
+        }
+        m = START_UPTIME.matcher(line);
+        if (m.matches()) {
+            mMonkeyLog.setStartTime(parseTime(m.group(1)));
+            mMonkeyLog.setStartUptimeDuration((long) (Double.parseDouble(m.group(2)) * 1000));
+        }
+        m = STOP_UPTIME.matcher(line);
+        if (m.matches()) {
+            mMonkeyLog.setStopTime(parseTime(m.group(1)));
+            mMonkeyLog.setStopUptimeDuration((long) (Double.parseDouble(m.group(2)) * 1000));
+            mMonkeyLog.setTotalDuration(60 * 1000 * Integer.parseInt(m.group(3)) +
+                    1000 *Integer.parseInt(m.group(4)));
+        }
+        m = INTERMEDIATE_COUNT.matcher(line);
+        if (m.matches()) {
+            mMonkeyLog.setIntermediateCount(Integer.parseInt(m.group(1)));
+        }
+        m = FINAL_COUNT.matcher(line);
+        if (m.matches()) {
+            mMonkeyLog.setFinalCount(Integer.parseInt(m.group(1)));
+        }
+        m = FINISHED.matcher(line);
+        if (m.matches()) {
+            mMonkeyLog.setIsFinished(true);
+        }
+        m = NO_ACTIVITIES.matcher(line);
+        if (m.matches()) {
+            mMonkeyLog.setNoActivities(true);
+        }
+        m = DROPPED_KEYS.matcher(line);
+        if (m.matches()) {
+            mMonkeyLog.setDroppedCount(DroppedCategory.KEYS, Integer.parseInt(m.group(1)));
+        }
+        m = DROPPED_POINTERS.matcher(line);
+        if (m.matches()) {
+            mMonkeyLog.setDroppedCount(DroppedCategory.POINTERS, Integer.parseInt(m.group(1)));
+        }
+        m = DROPPED_TRACKBALLS.matcher(line);
+        if (m.matches()) {
+            mMonkeyLog.setDroppedCount(DroppedCategory.TRACKBALLS, Integer.parseInt(m.group(1)));
+        }
+        m = DROPPED_FLIPS.matcher(line);
+        if (m.matches()) {
+            mMonkeyLog.setDroppedCount(DroppedCategory.FLIPS, Integer.parseInt(m.group(1)));
+        }
+        m = DROPPED_ROTATIONS.matcher(line);
+        if (m.matches()) {
+            mMonkeyLog.setDroppedCount(DroppedCategory.ROTATIONS, Integer.parseInt(m.group(1)));
+        }
+        m = ANR.matcher(line);
+        if (m.matches()) {
+            mApp = m.group(1);
+            mPid = Integer.parseInt(m.group(2));
+            mBlock = new LinkedList<String>();
+            mMatchingAnr = true;
+        }
+        m = JAVA_CRASH.matcher(line);
+        if (m.matches()) {
+            mApp = m.group(1);
+            mPid = Integer.parseInt(m.group(2));
+            mBlock = new LinkedList<String>();
+            mMatchingJavaCrash = true;
+        }
+        m = TRACES_START.matcher(line);
+        if (m.matches()) {
+            mBlock = new LinkedList<String>();
+            mMatchingTraces = true;
+        }
+    }
+
+    /**
+     * Parse the timestamp and return a date.
+     *
+     * @param timeStr The timestamp in the format {@code E, MM/dd/yyyy hh:mm:ss a} or
+     * {@code EEE MMM dd HH:mm:ss zzz yyyy}.
+     * @return The {@link Date}.
+     */
+    private Date parseTime(String timeStr) {
+        try {
+            return new SimpleDateFormat("EEE MMM dd HH:mm:ss zzz yyyy").parse(timeStr);
+        } catch (ParseException e) {
+            // CLog.v("Could not parse date %s with format EEE MMM dd HH:mm:ss zzz yyyy", timeStr);
+        }
+
+        try {
+            return new SimpleDateFormat("E, MM/dd/yyyy hh:mm:ss a").parse(timeStr);
+        } catch (ParseException e) {
+            // CLog.v("Could not parse date %s with format E, MM/dd/yyyy hh:mm:ss a", timeStr);
+        }
+
+        // CLog.e("Could not parse date %s", timeStr);
+        return null;
+    }
+
+}
diff --git a/src/com/android/loganalysis/parser/NativeCrashParser.java b/src/com/android/loganalysis/parser/NativeCrashParser.java
new file mode 100644
index 0000000000000000000000000000000000000000..362ed8e6287efea587a88643675a0eae40136971
--- /dev/null
+++ b/src/com/android/loganalysis/parser/NativeCrashParser.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright (C) 2012 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.loganalysis.parser;
+
+import com.android.loganalysis.item.NativeCrashItem;
+
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * An {@link IParser} to handle native crashes.
+ */
+public class NativeCrashParser implements IParser {
+
+    /** Matches: *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** */
+    private static final Pattern START = Pattern.compile("^(?:\\*\\*\\* ){15}\\*\\*\\*$");
+    /** Matches: Build fingerprint: 'fingerprint' */
+    private static final Pattern FINGERPRINT = Pattern.compile("^Build fingerprint: '(.*)'$");
+    /** Matches: pid: 957, tid: 963  >>> com.android.camera <<< */
+    private static final Pattern APP = Pattern.compile(
+            "^pid: \\d+, tid: \\d+(, name: \\S+)?  >>> (\\S+) <<<$");
+
+
+    /**
+     * {@inheritDoc}
+     *
+     * @return The {@link NativeCrashItem}.
+     */
+    @Override
+    public NativeCrashItem parse(List<String> lines) {
+        NativeCrashItem nc = null;
+        StringBuilder stack = new StringBuilder();
+
+        for (String line : lines) {
+            Matcher m = START.matcher(line);
+            if (m.matches()) {
+                nc = new NativeCrashItem();
+            }
+
+            if (nc != null) {
+                m = FINGERPRINT.matcher(line);
+                if (m.matches()) {
+                    nc.setFingerprint(m.group(1));
+                }
+                m = APP.matcher(line);
+                if (m.matches()) {
+                    nc.setApp(m.group(2));
+                }
+
+                stack.append(line);
+                stack.append("\n");
+            }
+        }
+        if (nc != null) {
+            nc.setStack(stack.toString().trim());
+        }
+        return nc;
+    }
+}
+
diff --git a/src/com/android/loganalysis/parser/NoopParser.java b/src/com/android/loganalysis/parser/NoopParser.java
new file mode 100644
index 0000000000000000000000000000000000000000..2c787420bd6b2716b160e4b32dc23427b6957662
--- /dev/null
+++ b/src/com/android/loganalysis/parser/NoopParser.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2011 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.loganalysis.parser;
+
+import com.android.loganalysis.item.IItem;
+
+import java.util.List;
+
+/**
+ * A {@link IParser} that consumes nothing.
+ */
+public class NoopParser implements IParser {
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public IItem parse(List<String> block) {
+        // ignore
+        return null;
+    }
+}
+
diff --git a/src/com/android/loganalysis/parser/ProcrankParser.java b/src/com/android/loganalysis/parser/ProcrankParser.java
new file mode 100644
index 0000000000000000000000000000000000000000..938a62c98c463cd9fc61adf1a47591c18d3a069a
--- /dev/null
+++ b/src/com/android/loganalysis/parser/ProcrankParser.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2011 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.loganalysis.parser;
+
+import com.android.loganalysis.item.ProcrankItem;
+
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * A {@link IParser} to handle the output of {@code procrank}.  Memory values returned are in units
+ * of kilobytes.
+ */
+public class ProcrankParser implements IParser {
+
+    /** Match a valid line, such as:
+     * " 1313   78128K   77996K   48603K   45812K  com.google.android.apps.maps" */
+    private static final Pattern LINE_PAT = Pattern.compile(
+            "\\s*(\\d+)\\s+" + /* PID [1] */
+            "(\\d+)K\\s+(\\d+)K\\s+(\\d+)K\\s+(\\d+)K\\s+" + /* Vss Rss Pss Uss [2-5] */
+            "(\\S+)" /* process name [6] */);
+
+    /** Match the end of the Procrank table, determined by three sets of "------". */
+    private static final Pattern END_PAT = Pattern.compile("^\\s+-{6}\\s+-{6}\\s+-{6}");
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public ProcrankItem parse(List<String> lines) {
+        ProcrankItem item = new ProcrankItem();
+
+        for (String line : lines) {
+            // If we have reached the end.
+            Matcher endMatcher = END_PAT.matcher(line);
+            if (endMatcher.matches()) {
+                return item;
+            }
+
+            Matcher m = LINE_PAT.matcher(line);
+            if (m.matches()) {
+                item.addProcrankLine(Integer.parseInt(m.group(1)), m.group(6),
+                        Integer.parseInt(m.group(2)), Integer.parseInt(m.group(3)),
+                        Integer.parseInt(m.group(4)), Integer.parseInt(m.group(5)));
+            }
+        }
+
+        return item;
+    }
+}
+
diff --git a/src/com/android/loganalysis/parser/SystemPropsParser.java b/src/com/android/loganalysis/parser/SystemPropsParser.java
new file mode 100644
index 0000000000000000000000000000000000000000..a1cd9da1c75d833ec269394e30863b3cb4160f31
--- /dev/null
+++ b/src/com/android/loganalysis/parser/SystemPropsParser.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2011 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.loganalysis.parser;
+
+import com.android.loganalysis.item.SystemPropsItem;
+
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * A {@link IParser} to handle the output from {@code getprop}.
+ */
+public class SystemPropsParser implements IParser {
+    /** Match a single property line, such as "[gsm.sim.operator.numeric]: []" */
+    private static final Pattern PROP_LINE = Pattern.compile("^\\[(.*)\\]: \\[(.*)\\]$");
+
+    /**
+     * {@inheritDoc}
+     *
+     * @return The {@link SystemPropsItem}.
+     */
+    @Override
+    public SystemPropsItem parse(List<String> lines) {
+        SystemPropsItem item = new SystemPropsItem();
+
+        for (String line : lines) {
+            Matcher m = PROP_LINE.matcher(line);
+            if (m.matches()) {
+                item.put(m.group(1), m.group(2));
+            } else {
+                // CLog.w("Failed to parse line '%s'", line);
+            }
+        }
+        return item;
+    }
+}
+
diff --git a/src/com/android/loganalysis/parser/TracesParser.java b/src/com/android/loganalysis/parser/TracesParser.java
new file mode 100644
index 0000000000000000000000000000000000000000..4d0f5810aff22c7232e13617d2622c948643bc45
--- /dev/null
+++ b/src/com/android/loganalysis/parser/TracesParser.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright (C) 2011 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.loganalysis.parser;
+
+import com.android.loganalysis.item.TracesItem;
+
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * A {@link IParser} to parse Android traces files.
+ * <p>
+ * For now, this only extracts the main stack trace from the first process. It is used to get a
+ * stack from {@code /data/anr/traces.txt} which can be used to give some context about the ANR. If
+ * there is a need, this parser can be expanded to parse all stacks from all processes.
+ */
+public class TracesParser implements IParser {
+
+    /**
+     * Matches: ----- pid PID at YYYY-MM-DD hh:mm:ss -----
+     */
+    private static final Pattern PID = Pattern.compile(
+            "^----- pid (\\d+) at \\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2} -----$");
+
+    /**
+     * Matches: Cmd line: APP
+     */
+    private static final Pattern APP = Pattern.compile("^Cmd line: (\\S+)$");
+
+    /**
+     * Matches: "main" prio=5 tid=1 STATE
+     */
+    private static final Pattern STACK = Pattern.compile("^\"main\" .*$");
+
+    /**
+     * {@inheritDoc}
+     *
+     * @return The {@link TracesItem}.
+     */
+    @Override
+    public TracesItem parse(List<String> lines) {
+        TracesItem traces = new TracesItem();
+        StringBuffer stack = null;
+
+        for (String line : lines) {
+            if (stack == null) {
+                Matcher m = PID.matcher(line);
+                if (m.matches()) {
+                    traces.setPid(Integer.parseInt(m.group(1)));
+                }
+                m = APP.matcher(line);
+                if (m.matches()) {
+                    traces.setApp(m.group(1));
+                }
+                m = STACK.matcher(line);
+                if (m.matches()) {
+                    stack = new StringBuffer();
+                    stack.append(line);
+                    stack.append("\n");
+                }
+            } else if (!"".equals(line)) {
+                stack.append(line);
+                stack.append("\n");
+            } else {
+                traces.setStack(stack.toString().trim());
+                return traces;
+            }
+        }
+        if (stack == null) {
+            return null;
+        }
+        traces.setStack(stack.toString().trim());
+        return traces;
+    }
+
+}
diff --git a/src/com/android/loganalysis/util/ArrayUtil.java b/src/com/android/loganalysis/util/ArrayUtil.java
new file mode 100644
index 0000000000000000000000000000000000000000..95b1634f4111c6c3f1339ba1e52812589c12ba36
--- /dev/null
+++ b/src/com/android/loganalysis/util/ArrayUtil.java
@@ -0,0 +1,114 @@
+/*
+ * Copyright (C) 2011 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.loganalysis.util;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.List;
+
+/**
+ * Utility methods for arrays
+ */
+// TODO: Use libTF once this is copied over.
+public class ArrayUtil {
+
+    private ArrayUtil() {
+    }
+
+    /**
+     * Build an array from the provided contents.
+     *
+     * <p>
+     * The resulting array will be the concatenation of <var>arrays</var> input arrays, in their
+     * original order.
+     * </p>
+     *
+     * @param arrays the arrays to concatenate
+     * @return the newly constructed array
+     */
+    public static String[] buildArray(String[]... arrays) {
+        int length = 0;
+        for (String[] array : arrays) {
+            length += array.length;
+        }
+        String[] newArray = new String[length];
+        int offset = 0;
+        for (String[] array : arrays) {
+            System.arraycopy(array, 0, newArray, offset, array.length);
+            offset += array.length;
+        }
+        return newArray;
+    }
+
+    /**
+     * Convert a varargs list/array to an {@link List}.  This is useful for building instances of
+     * {@link List} by hand.  Note that this differs from {@link java.util.Arrays#asList} in that
+     * the returned array is mutable.
+     *
+     * @param inputAry an array, or a varargs list
+     * @return a {@link List} instance with the identical contents
+     */
+    public static <T> List<T> list(T... inputAry) {
+        List<T> retList = new ArrayList<T>(inputAry.length);
+        for (T item : inputAry) {
+            retList.add(item);
+        }
+        return retList;
+    }
+
+    private static String internalJoin(String sep, Collection<Object> pieces) {
+        StringBuilder sb = new StringBuilder();
+        boolean skipSep = true;
+        Iterator<Object> iter = pieces.iterator();
+        while (iter.hasNext()) {
+            if (skipSep) {
+                skipSep = false;
+            } else {
+                sb.append(sep);
+            }
+
+            Object obj = iter.next();
+            if (obj == null) {
+                sb.append("null");
+            } else {
+                sb.append(obj.toString());
+            }
+        }
+        return sb.toString();
+    }
+
+    /**
+     * Turns a sequence of objects into a string, delimited by {@code sep}.  If a single
+     * {@code Collection} is passed, it is assumed that the elements of that Collection are to be
+     * joined.  Otherwise, wraps the passed {@link Object}(s) in a {@link List} and joins the
+     * generated list.
+     *
+     * @param sep the string separator to delimit the different output segments.
+     * @param pieces A {@link Collection} or a varargs {@code Array} of objects.
+     */
+    @SuppressWarnings("unchecked")
+    public static String join(String sep, Object... pieces) {
+        if ((pieces.length == 1) && (pieces[0] instanceof Collection)) {
+            // Don't re-wrap the Collection
+            return internalJoin(sep, (Collection<Object>) pieces[0]);
+        } else {
+            return internalJoin(sep, Arrays.asList(pieces));
+        }
+    }
+}
+
diff --git a/src/com/android/loganalysis/util/RegexTrie.java b/src/com/android/loganalysis/util/RegexTrie.java
new file mode 100644
index 0000000000000000000000000000000000000000..f3c2d6ab42290c46d46c2313c9cb5245a173042d
--- /dev/null
+++ b/src/com/android/loganalysis/util/RegexTrie.java
@@ -0,0 +1,293 @@
+/*
+ * 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.loganalysis.util;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * The RegexTrie is a trie where each _stored_ segment of the key is a regex {@link Pattern}.  Thus,
+ * the full _stored_ key is a List<Pattern> rather than a String as in a standard trie.  Note that
+ * the {@link #get(Object key)} method requires a List<String>, which will be matched against the
+ * {@link Pattern}s, rather than checked for equality as in a standard trie.  It will likely perform
+ * poorly for large datasets.
+ * <p />
+ * One can also use a {@code null} entry in the {@code Pattern} sequence to serve as a wildcard.  If
+ * a {@code null} is encountered, all subsequent entries in the sequence will be ignored.
+ * When the retrieval code encounters a {@code null} {@code Pattern}, it will first wait to see if a
+ * more-specific entry matches the sequence.  If one does, that more-specific entry will proceed,
+ * even if it subsequently fails to match.
+ * <p />
+ * If no more-specific entry matches, the wildcard match will add all remaining {@code String}s
+ * to the list of captures (if enabled) and return the value associated with the wildcard.
+ * <p />
+ * A short sample of the wildcard functionality:
+ * <pre>
+ * List<List<String>> captures = new LinkedList<List<String>>();
+ * RegexTrie<Integer> trie = new RegexTrie<Integer>();
+ * trie.put(2, "a", null);
+ * trie.put(4, "a", "b");
+ * trie.retrieve(captures, "a", "c", "e");
+ * // returns 2.  captures is now [[], ["c"], ["e"]]
+ * trie.retrieve(captures, "a", "b");
+ * // returns 4.  captures is now [[], []]
+ * trie.retrieve(captures, "a", "b", "c");
+ * // returns null.  captures is now [[], []]
+ * </pre>
+ */
+//TODO: Use libTF once this is copied over.
+public class RegexTrie<V> {
+    private V mValue = null;
+    private Map<CompPattern, RegexTrie<V>> mChildren =
+            new LinkedHashMap<CompPattern, RegexTrie<V>>();
+
+    /**
+     * Patterns aren't comparable by default, which prevents you from retrieving them from a
+     * HashTable.  This is a simple stub class that makes a Pattern with a working
+     * {@link CompPattern#equals()} method.
+     */
+    static class CompPattern {
+        protected final Pattern mPattern;
+
+        CompPattern(Pattern pattern) {
+            if (pattern == null) {
+                throw new NullPointerException();
+            }
+            mPattern = pattern;
+        }
+
+        @Override
+        public boolean equals(Object other) {
+            Pattern otherPat;
+            if (other instanceof Pattern) {
+                otherPat = (Pattern) other;
+            } else if (other instanceof CompPattern) {
+                CompPattern otherCPat = (CompPattern) other;
+                otherPat = otherCPat.mPattern;
+            } else {
+                return false;
+            }
+            return mPattern.toString().equals(otherPat.toString());
+        }
+
+        @Override
+        public int hashCode() {
+            return mPattern.toString().hashCode();
+        }
+
+        @Override
+        public String toString() {
+            return String.format("CP(%s)", mPattern.toString());
+        }
+
+        public Matcher matcher(String string) {
+            return mPattern.matcher(string);
+        }
+    }
+
+    public void clear() {
+        mValue = null;
+        for (RegexTrie child : mChildren.values()) {
+            child.clear();
+        }
+        mChildren.clear();
+    }
+
+    boolean containsKey(String... strings) {
+        return retrieve(strings) != null;
+    }
+
+    V recursivePut(V value, List<CompPattern> patterns) {
+        // Cases:
+        // 1) patterns is empty -- set our value
+        // 2) patterns is non-empty -- recurse downward, creating a child if necessary
+        if (patterns.isEmpty()) {
+            V oldValue = mValue;
+            mValue = value;
+            return oldValue;
+        } else {
+            CompPattern curKey = patterns.get(0);
+            List<CompPattern> nextKeys = patterns.subList(1, patterns.size());
+
+            // Create a new child to handle
+            RegexTrie<V> nextChild = mChildren.get(curKey);
+            if (nextChild == null) {
+                nextChild = new RegexTrie<V>();
+                mChildren.put(curKey, nextChild);
+            }
+            return nextChild.recursivePut(value, nextKeys);
+        }
+    }
+
+    /**
+     * A helper method to consolidate validation before adding an entry to the trie.
+     *
+     * @param value The value to set
+     * @param patterns The sequence of {@link CompPattern}s that must be sequentially matched to
+     *        retrieve the associated {@code value}
+     */
+    private V validateAndPut(V value, List<CompPattern> pList) {
+        if (pList.size() == 0) {
+            throw new IllegalArgumentException("pattern list must be non-empty.");
+        }
+        return recursivePut(value, pList);
+    }
+
+    /**
+     * Add an entry to the trie.
+     *
+     * @param value The value to set
+     * @param patterns The sequence of {@link Pattern}s that must be sequentially matched to
+     *        retrieve the associated {@code value}
+     */
+    public V put(V value, Pattern... patterns) {
+        List<CompPattern> pList = new ArrayList<CompPattern>(patterns.length);
+        for (Pattern pat : patterns) {
+            if (pat == null) {
+                pList.add(null);
+                break;
+            }
+            pList.add(new CompPattern(pat));
+        }
+        return validateAndPut(value, pList);
+    }
+
+    /**
+     * This helper method takes a list of regular expressions as {@link String}s and compiles them
+     * on-the-fly before adding the subsequent {@link Pattern}s to the trie
+     *
+     * @param value The value to set
+     * @param patterns The sequence of regular expressions (as {@link String}s) that must be
+     *        sequentially matched to retrieve the associated {@code value}.  Each String will be
+     *        compiled as a {@link Pattern} before invoking {@link #put(V, Pattern...)}.
+     */
+    public V put(V value, String... regexen) {
+        List<CompPattern> pList = new ArrayList<CompPattern>(regexen.length);
+        for (String regex : regexen) {
+            if (regex == null) {
+                pList.add(null);
+                break;
+            }
+            Pattern pat = Pattern.compile(regex);
+            pList.add(new CompPattern(pat));
+        }
+        return validateAndPut(value, pList);
+    }
+
+    V recursiveRetrieve(List<List<String>> captures, List<String> strings) {
+        // Cases:
+        // 1) strings is empty -- return our value
+        // 2) strings is non-empty -- find the first child that matches, recurse downward
+        if (strings.isEmpty()) {
+            return mValue;
+        } else {
+            boolean wildcardMatch = false;
+            V wildcardValue = null;
+            String curKey = strings.get(0);
+            List<String> nextKeys = strings.subList(1, strings.size());
+
+            for (Map.Entry<CompPattern, RegexTrie<V>> child : mChildren.entrySet()) {
+                CompPattern pattern = child.getKey();
+                if (pattern == null) {
+                    wildcardMatch = true;
+                    wildcardValue = child.getValue().getValue();
+                    continue;
+                }
+
+                Matcher matcher = pattern.matcher(curKey);
+                if (matcher.matches()) {
+                    if (captures != null) {
+                        List<String> curCaptures = new ArrayList<String>(matcher.groupCount());
+                        for (int i = 0; i < matcher.groupCount(); i++) {
+                            // i+1 since group 0 is the entire matched string
+                            curCaptures.add(matcher.group(i+1));
+                        }
+                        captures.add(curCaptures);
+                    }
+
+                    return child.getValue().recursiveRetrieve(captures, nextKeys);
+                }
+            }
+
+            if (wildcardMatch) {
+                // Stick the rest of the query string into the captures list and return
+                if (captures != null) {
+                    for (String str : strings) {
+                        captures.add(Arrays.asList(str));
+                    }
+                }
+                return wildcardValue;
+            }
+
+            // no match
+            return null;
+        }
+    }
+
+    /**
+     * Fetch a value from the trie, by matching the provided sequence of {@link String}s to a
+     * sequence of {@link Pattern}s stored in the trie.
+     *
+     * @param strings A sequence of {@link String}s to match
+     * @return The associated value, or {@code null} if no value was found
+     */
+    public V retrieve(String... strings) {
+        return retrieve(null, strings);
+    }
+
+    /**
+     * Fetch a value from the trie, by matching the provided sequence of {@link String}s to a
+     * sequence of {@link Pattern}s stored in the trie.  This version of the method also returns
+     * a {@link List} of capture groups for each {@link Pattern} that was matched.
+     * <p />
+     * Each entry in the outer List corresponds to one level of {@code Pattern} in the trie.
+     * For each level, the list of capture groups will be stored.  If there were no captures
+     * for a particular level, an empty list will be stored.
+     * <p />
+     * Note that {@code captures} will be {@link List#clear()}ed before the retrieval begins.
+     * Also, if the retrieval fails after a partial sequence of matches, {@code captures} will
+     * still reflect the capture groups from the partial match.
+     *
+     * @param captures A {@code List<List<String>>} through which capture groups will be returned.
+     * @param strings A sequence of {@link String}s to match
+     * @return The associated value, or {@code null} if no value was found
+     */
+    public V retrieve(List<List<String>> captures, String... strings) {
+        if (strings.length == 0) {
+            throw new IllegalArgumentException("string list must be non-empty");
+        }
+        List<String> sList = Arrays.asList(strings);
+        if (captures != null) {
+            captures.clear();
+        }
+        return recursiveRetrieve(captures, sList);
+    }
+
+    private V getValue() {
+        return mValue;
+    }
+
+    @Override
+    public String toString() {
+        return String.format("{V: %s, C: %s}", mValue, mChildren);
+    }
+}
+
diff --git a/tests/.classpath b/tests/.classpath
new file mode 100644
index 0000000000000000000000000000000000000000..32d4a0f0ad30f01b0ecf9507fe436b4174a68182
--- /dev/null
+++ b/tests/.classpath
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<classpath>
+	<classpathentry kind="src" path="src"/>
+	<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER"/>
+        <classpathentry kind="con" path="org.eclipse.jdt.junit.JUNIT_CONTAINER/3"/>
+	<classpathentry combineaccessrules="false" kind="src" path="/loganalysis"/>
+        <classpathentry exported="true" kind="var" path="TRADEFED_ROOT/out/host/common/obj/JAVA_LIBRARIES/easymock_intermediates/javalib.jar" sourcepath="/TRADEFED_ROOT/external/easymock/src"/>
+	<classpathentry kind="output" path="bin"/>
+</classpath>
diff --git a/tests/.project b/tests/.project
new file mode 100644
index 0000000000000000000000000000000000000000..f4a1c0b5a8e195317ab96584689bd4a55a9ceb87
--- /dev/null
+++ b/tests/.project
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<projectDescription>
+	<name>loganalysis-tests</name>
+	<comment></comment>
+	<projects>
+	</projects>
+	<buildSpec>
+		<buildCommand>
+			<name>org.eclipse.jdt.core.javabuilder</name>
+			<arguments>
+			</arguments>
+		</buildCommand>
+	</buildSpec>
+	<natures>
+		<nature>org.eclipse.jdt.core.javanature</nature>
+	</natures>
+</projectDescription>
diff --git a/tests/Android.mk b/tests/Android.mk
new file mode 100644
index 0000000000000000000000000000000000000000..32019a7b3e4d92d55b2527de091d271391b43d37
--- /dev/null
+++ b/tests/Android.mk
@@ -0,0 +1,42 @@
+# Copyright (C) 2013 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)
+
+# Only compile source java files in this lib.
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+
+LOCAL_JAVACFLAGS += -g -Xlint
+
+LOCAL_MODULE := loganalysis-tests
+LOCAL_MODULE_TAGS := optional
+LOCAL_STATIC_JAVA_LIBRARIES := easymock junit
+LOCAL_JAVA_LIBRARIES := loganalysis
+
+include $(BUILD_HOST_JAVA_LIBRARY)
+
+# makefile rules to copy jars to HOST_OUT/tradefed
+# so tradefed.sh can automatically add to classpath
+
+DEST_JAR := $(HOST_OUT)/tradefed/$(LOCAL_MODULE).jar
+$(DEST_JAR): $(LOCAL_BUILT_MODULE)
+	$(copy-file-to-new-target)
+
+# this dependency ensure the above rule will be executed if module is built
+$(LOCAL_INSTALLED_MODULE) : $(DEST_JAR)
+
+# Build all sub-directories
+include $(call all-makefiles-under,$(LOCAL_PATH))
diff --git a/tests/src/com/android/loganalysis/FuncTests.java b/tests/src/com/android/loganalysis/FuncTests.java
new file mode 100644
index 0000000000000000000000000000000000000000..7a06cbd46485b8063a5685c5099141ee6d8470bf
--- /dev/null
+++ b/tests/src/com/android/loganalysis/FuncTests.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2013 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.loganalysis;
+
+import com.android.loganalysis.parser.BugreportParserFuncTest;
+import com.android.loganalysis.parser.LogcatParserFuncTest;
+import com.android.loganalysis.parser.MonkeyLogParserFuncTest;
+
+import junit.framework.Test;
+import junit.framework.TestSuite;
+
+/**
+ * A test suite for all log analysis functional tests.
+ */
+public class FuncTests extends TestSuite {
+
+    public FuncTests() {
+        super();
+
+        addTestSuite(BugreportParserFuncTest.class);
+        addTestSuite(LogcatParserFuncTest.class);
+        addTestSuite(MonkeyLogParserFuncTest.class);
+    }
+
+    public static Test suite() {
+        return new FuncTests();
+    }
+}
diff --git a/tests/src/com/android/loganalysis/UnitTests.java b/tests/src/com/android/loganalysis/UnitTests.java
new file mode 100644
index 0000000000000000000000000000000000000000..083d571454336f5b2353178aea96ccf908d73537
--- /dev/null
+++ b/tests/src/com/android/loganalysis/UnitTests.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2013 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.loganalysis;
+
+import com.android.loganalysis.item.GenericItemTest;
+import com.android.loganalysis.parser.AbstractSectionParserTest;
+import com.android.loganalysis.parser.AnrParserTest;
+import com.android.loganalysis.parser.BugreportParserTest;
+import com.android.loganalysis.parser.JavaCrashParserTest;
+import com.android.loganalysis.parser.LogcatParserTest;
+import com.android.loganalysis.parser.MemInfoParserTest;
+import com.android.loganalysis.parser.MonkeyLogParserTest;
+import com.android.loganalysis.parser.NativeCrashParserTest;
+import com.android.loganalysis.parser.ProcrankParserTest;
+import com.android.loganalysis.parser.SystemPropsParserTest;
+import com.android.loganalysis.parser.TracesParserTest;
+import com.android.loganalysis.util.ArrayUtilTest;
+import com.android.loganalysis.util.RegexTrieTest;
+
+import junit.framework.Test;
+import junit.framework.TestSuite;
+
+/**
+ * A test suite for all Trade Federation unit tests.
+ * <p/>
+ * All tests listed here should be self-contained, and should not require any external dependencies.
+ */
+public class UnitTests extends TestSuite {
+
+    public UnitTests() {
+        super();
+
+        // item
+        addTestSuite(GenericItemTest.class);
+
+        // parser
+        addTestSuite(AbstractSectionParserTest.class);
+        addTestSuite(AnrParserTest.class);
+        addTestSuite(BugreportParserTest.class);
+        addTestSuite(JavaCrashParserTest.class);
+        addTestSuite(LogcatParserTest.class);
+        addTestSuite(MemInfoParserTest.class);
+        addTestSuite(MonkeyLogParserTest.class);
+        addTestSuite(NativeCrashParserTest.class);
+        addTestSuite(ProcrankParserTest.class);
+        addTestSuite(SystemPropsParserTest.class);
+        addTestSuite(TracesParserTest.class);
+
+        // util
+        addTestSuite(ArrayUtilTest.class);
+        addTestSuite(RegexTrieTest.class);
+    }
+
+    public static Test suite() {
+        return new UnitTests();
+    }
+}
diff --git a/tests/src/com/android/loganalysis/item/GenericItemTest.java b/tests/src/com/android/loganalysis/item/GenericItemTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..037f39a98dca21ce190fa0ca029d2873a36dff63
--- /dev/null
+++ b/tests/src/com/android/loganalysis/item/GenericItemTest.java
@@ -0,0 +1,221 @@
+/*
+ * Copyright (C) 2011 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.loganalysis.item;
+
+import junit.framework.TestCase;
+
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Unit test for {@link GenericItem}.
+ */
+public class GenericItemTest extends TestCase {
+    private static final Set<String> ATTRIBUTES = new HashSet<String>(Arrays.asList(
+            "integer", "string"));
+
+    private String mStringAttribute = "String";
+    private Integer mIntegerAttribute = 1;
+
+    /** Empty item with no attributes set */
+    private GenericItem mEmptyItem1;
+    /** Empty item with no attributes set */
+    private GenericItem mEmptyItem2;
+    /** Item with only the string attribute set */
+    private GenericItem mStringItem;
+    /** Item with only the integer attribute set */
+    private GenericItem mIntegerItem;
+    /** Item with both attributes set, product of mStringItem and mIntegerItem */
+    private GenericItem mFullItem1;
+    /** Item with both attributes set, product of mStringItem and mIntegerItem */
+    private GenericItem mFullItem2;
+    /** Item that is inconsistent with the others */
+    private GenericItem mInconsistentItem;
+
+    @Override
+    public void setUp() {
+        mEmptyItem1 = new GenericItem(null, ATTRIBUTES);
+        mEmptyItem2 = new GenericItem(null, ATTRIBUTES);
+        mStringItem = new GenericItem(null, ATTRIBUTES);
+        mStringItem.setAttribute("string", mStringAttribute);
+        mIntegerItem = new GenericItem(null, ATTRIBUTES);
+        mIntegerItem.setAttribute("integer", mIntegerAttribute);
+        mFullItem1 = new GenericItem(null, ATTRIBUTES);
+        mFullItem1.setAttribute("string", mStringAttribute);
+        mFullItem1.setAttribute("integer", mIntegerAttribute);
+        mFullItem2 = new GenericItem(null, ATTRIBUTES);
+        mFullItem2.setAttribute("string", mStringAttribute);
+        mFullItem2.setAttribute("integer", mIntegerAttribute);
+        mInconsistentItem = new GenericItem(null, ATTRIBUTES);
+        mInconsistentItem.setAttribute("string", "gnirts");
+        mInconsistentItem.setAttribute("integer", 2);
+    }
+
+    /**
+     * Test for {@link GenericItem#mergeAttributes(IItem)}.
+     */
+    public void testMergeAttributes() throws ConflictingItemException {
+        Map<String, Object> attributes;
+
+        attributes = mEmptyItem1.mergeAttributes(mEmptyItem1);
+        assertNull(attributes.get("string"));
+        assertNull(attributes.get("integer"));
+
+        attributes = mEmptyItem1.mergeAttributes(mEmptyItem2);
+        assertNull(attributes.get("string"));
+        assertNull(attributes.get("integer"));
+
+        attributes = mEmptyItem2.mergeAttributes(mEmptyItem1);
+        assertNull(attributes.get("string"));
+        assertNull(attributes.get("integer"));
+
+        attributes = mEmptyItem1.mergeAttributes(mStringItem);
+        assertEquals(mStringAttribute, attributes.get("string"));
+        assertNull(attributes.get("integer"));
+
+        attributes = mStringItem.mergeAttributes(mEmptyItem1);
+        assertEquals(mStringAttribute, attributes.get("string"));
+        assertNull(attributes.get("integer"));
+
+        attributes = mIntegerItem.mergeAttributes(mStringItem);
+        assertEquals(mStringAttribute, attributes.get("string"));
+        assertEquals(mIntegerAttribute, attributes.get("integer"));
+
+        attributes = mEmptyItem1.mergeAttributes(mFullItem1);
+        assertEquals(mStringAttribute, attributes.get("string"));
+        assertEquals(mIntegerAttribute, attributes.get("integer"));
+
+        attributes = mFullItem1.mergeAttributes(mEmptyItem1);
+        assertEquals(mStringAttribute, attributes.get("string"));
+        assertEquals(mIntegerAttribute, attributes.get("integer"));
+
+        attributes = mFullItem1.mergeAttributes(mFullItem2);
+        assertEquals(mStringAttribute, attributes.get("string"));
+        assertEquals(mIntegerAttribute, attributes.get("integer"));
+
+        try {
+            mFullItem1.mergeAttributes(mInconsistentItem);
+            fail("Expecting a ConflictingItemException");
+        } catch (ConflictingItemException e) {
+            // Expected
+        }
+    }
+
+    /**
+     * Test for {@link GenericItem#isConsistent(IItem)}.
+     */
+    public void testIsConsistent() {
+        assertTrue(mEmptyItem1.isConsistent(mEmptyItem1));
+        assertFalse(mEmptyItem1.isConsistent(null));
+        assertTrue(mEmptyItem1.isConsistent(mEmptyItem2));
+        assertTrue(mEmptyItem2.isConsistent(mEmptyItem1));
+        assertTrue(mEmptyItem1.isConsistent(mStringItem));
+        assertTrue(mStringItem.isConsistent(mEmptyItem1));
+        assertTrue(mIntegerItem.isConsistent(mStringItem));
+        assertTrue(mEmptyItem1.isConsistent(mFullItem1));
+        assertTrue(mFullItem1.isConsistent(mEmptyItem1));
+        assertTrue(mFullItem1.isConsistent(mFullItem2));
+        assertFalse(mFullItem1.isConsistent(mInconsistentItem));
+    }
+
+    /**
+     * Test {@link GenericItem#equals(Object)}.
+     */
+    public void testEquals() {
+        assertTrue(mEmptyItem1.equals(mEmptyItem1));
+        assertFalse(mEmptyItem1.equals(null));
+        assertTrue(mEmptyItem1.equals(mEmptyItem2));
+        assertTrue(mEmptyItem2.equals(mEmptyItem1));
+        assertFalse(mEmptyItem1.equals(mStringItem));
+        assertFalse(mStringItem.equals(mEmptyItem1));
+        assertFalse(mIntegerItem.equals(mStringItem));
+        assertFalse(mEmptyItem1.equals(mFullItem1));
+        assertFalse(mFullItem1.equals(mEmptyItem1));
+        assertTrue(mFullItem1.equals(mFullItem2));
+        assertFalse(mFullItem1.equals(mInconsistentItem));
+    }
+
+    /**
+     * Test for {@link GenericItem#setAttribute(String, Object)} and
+     * {@link GenericItem#getAttribute(String)}.
+     */
+    public void testAttributes() {
+        GenericItem item = new GenericItem(null, ATTRIBUTES);
+
+        assertNull(item.getAttribute("string"));
+        assertNull(item.getAttribute("integer"));
+
+        item.setAttribute("string", mStringAttribute);
+        item.setAttribute("integer", mIntegerAttribute);
+
+        assertEquals(mStringAttribute, item.getAttribute("string"));
+        assertEquals(mIntegerAttribute, item.getAttribute("integer"));
+
+        item.setAttribute("string", null);
+        item.setAttribute("integer", null);
+
+        assertNull(item.getAttribute("string"));
+        assertNull(item.getAttribute("integer"));
+
+        try {
+            item.setAttribute("object", new Object());
+            fail("Failed to throw IllegalArgumentException");
+        } catch (IllegalArgumentException e) {
+            // Expected because "object" is not "string" or "integer".
+        }
+    }
+
+    /**
+     * Test for {@link GenericItem#areEqual(Object, Object)}
+     */
+    public void testAreEqual() {
+        assertTrue(GenericItem.areEqual(null, null));
+        assertTrue(GenericItem.areEqual("test", "test"));
+        assertFalse(GenericItem.areEqual(null, "test"));
+        assertFalse(GenericItem.areEqual("test", null));
+        assertFalse(GenericItem.areEqual("test", ""));
+    }
+
+    /**
+     * Test for {@link GenericItem#areConsistent(Object, Object)}
+     */
+    public void testAreConsistent() {
+        assertTrue(GenericItem.areConsistent(null, null));
+        assertTrue(GenericItem.areConsistent("test", "test"));
+        assertTrue(GenericItem.areConsistent(null, "test"));
+        assertTrue(GenericItem.areConsistent("test", null));
+        assertFalse(GenericItem.areConsistent("test", ""));
+    }
+
+    /**
+     * Test for {@link GenericItem#mergeObjects(Object, Object)}
+     */
+    public void testMergeObjects() throws ConflictingItemException {
+        assertNull(GenericItem.mergeObjects(null, null));
+        assertEquals("test", GenericItem.mergeObjects("test", "test"));
+        assertEquals("test", GenericItem.mergeObjects(null, "test"));
+        assertEquals("test", GenericItem.mergeObjects("test", null));
+
+        try {
+            assertEquals("test", GenericItem.mergeObjects("test", ""));
+            fail("Expected ConflictingItemException to be thrown");
+        } catch (ConflictingItemException e) {
+            // Expected because "test" conflicts with "".
+        }
+    }
+}
diff --git a/tests/src/com/android/loganalysis/parser/AbstractSectionParserTest.java b/tests/src/com/android/loganalysis/parser/AbstractSectionParserTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..1092d0e247af13f59def234c26f8ad55b008e900
--- /dev/null
+++ b/tests/src/com/android/loganalysis/parser/AbstractSectionParserTest.java
@@ -0,0 +1,98 @@
+/*
+ * Copyright (C) 2011 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.loganalysis.parser;
+
+import com.android.loganalysis.item.IItem;
+
+import junit.framework.TestCase;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Unit tests for {@link AbstractSectionParser}
+ */
+public class AbstractSectionParserTest extends TestCase {
+    AbstractSectionParser mParser = null;
+
+    @Override
+    public void setUp() throws Exception {
+        super.setUp();
+        mParser = new AbstractSectionParser() {
+            @Override
+            public IItem parse(List<String> lines) {
+                for (String line : lines) {
+                    parseLine(line);
+                }
+                commit();
+                return null;
+            }
+        };
+    }
+
+    private static class FakeBlockParser implements IParser {
+        private String mExpected = null;
+        private int mCalls = 0;
+
+        public FakeBlockParser(String expected) {
+            mExpected = expected;
+        }
+
+        public int getCalls() {
+            return mCalls;
+        }
+
+        @Override
+        public IItem parse(List<String> input) {
+            assertEquals(1, input.size());
+            assertEquals("parseBlock() got unexpected input!", mExpected, input.get(0));
+            mCalls += 1;
+            return null;
+        }
+    }
+
+    /**
+     * Verifies that {@link AbstractSectionParser} switches between parsers as expected
+     */
+    public void testSwitchParsers() {
+        final String lineFormat = "howdy, parser %d!";
+        final String linePattern = "I spy %d candles";
+        final int nParsers = 4;
+        FakeBlockParser[] parsers = new FakeBlockParser[nParsers];
+        final List<String> lines = new ArrayList<String>(2*nParsers);
+
+        for (int i = 0; i < nParsers; ++i) {
+            String line = String.format(lineFormat, i);
+            FakeBlockParser parser = new FakeBlockParser(line);
+            mParser.addSectionParser(parser, String.format(linePattern, i));
+            parsers[i] = parser;
+
+            // add the parser trigger
+            lines.add(String.format(linePattern, i));
+            // and then add the line that the parser is expecting
+            lines.add(String.format(lineFormat, i));
+        }
+
+        mParser.parse(lines);
+
+        // Verify that all the parsers were run
+        for (int i = 0; i < nParsers; ++i) {
+            assertEquals(String.format("Parser %d has wrong call count!", i), 1,
+                    parsers[i].getCalls());
+        }
+    }
+}
+
diff --git a/tests/src/com/android/loganalysis/parser/AnrParserTest.java b/tests/src/com/android/loganalysis/parser/AnrParserTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..119f3a8d5cee3a160e63b2908b2abb7941755803
--- /dev/null
+++ b/tests/src/com/android/loganalysis/parser/AnrParserTest.java
@@ -0,0 +1,129 @@
+/*
+ * Copyright (C) 2012 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.loganalysis.parser;
+
+import com.android.loganalysis.item.AnrItem;
+import com.android.loganalysis.util.ArrayUtil;
+
+import junit.framework.TestCase;
+
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * Unit tests for {@link AnrParser}.
+ */
+public class AnrParserTest extends TestCase {
+
+    /**
+     * Test that ANRs are parsed for the header "ANR (application not responding) in process: app"
+     */
+    public void testParse_application_not_responding() {
+        List<String> lines = Arrays.asList(
+                "ANR (application not responding) in process: com.android.package",
+                "Reason: keyDispatchingTimedOut",
+                "Load: 0.71 / 0.83 / 0.51",
+                "CPU usage from 4357ms to -1434ms ago:",
+                "  22% 3378/com.android.package: 19% user + 3.6% kernel / faults: 73 minor 1 major",
+                "  16% 312/system_server: 12% user + 4.1% kernel / faults: 1082 minor 6 major",
+                "33% TOTAL: 21% user + 11% kernel + 0.3% iowait",
+                "CPU usage from 907ms to 1431ms later:",
+                "  14% 121/mediaserver: 11% user + 3.7% kernel / faults: 17 minor",
+                "    3.7% 183/AudioOut_2: 3.7% user + 0% kernel",
+                "  12% 312/system_server: 5.5% user + 7.4% kernel / faults: 6 minor",
+                "    5.5% 366/InputDispatcher: 0% user + 5.5% kernel",
+                "18% TOTAL: 11% user + 7.5% kernel");
+
+        AnrItem anr = new AnrParser().parse(lines);
+        assertNotNull(anr);
+        assertEquals("com.android.package", anr.getApp());
+        assertEquals("keyDispatchingTimedOut", anr.getReason());
+        assertEquals(0.71, anr.getLoad(AnrItem.LoadCategory.LOAD_1));
+        assertEquals(0.83, anr.getLoad(AnrItem.LoadCategory.LOAD_5));
+        assertEquals(0.51, anr.getLoad(AnrItem.LoadCategory.LOAD_15));
+        assertEquals(33.0, anr.getCpuUsage(AnrItem.CpuUsageCategory.TOTAL));
+        assertEquals(21.0, anr.getCpuUsage(AnrItem.CpuUsageCategory.USER));
+        assertEquals(11.0, anr.getCpuUsage(AnrItem.CpuUsageCategory.KERNEL));
+        assertEquals(0.3, anr.getCpuUsage(AnrItem.CpuUsageCategory.IOWAIT));
+        assertEquals(ArrayUtil.join("\n", lines), anr.getStack());
+    }
+
+    /**
+     * Test that ANRs are parsed for the header "ANR in app"
+     */
+    public void testParse_anr_in_app() {
+        List<String> lines = Arrays.asList(
+                "ANR in com.android.package",
+                "Reason: keyDispatchingTimedOut",
+                "Load: 0.71 / 0.83 / 0.51",
+                "CPU usage from 4357ms to -1434ms ago:",
+                "  22% 3378/com.android.package: 19% user + 3.6% kernel / faults: 73 minor 1 major",
+                "  16% 312/system_server: 12% user + 4.1% kernel / faults: 1082 minor 6 major",
+                "33% TOTAL: 21% user + 11% kernel + 0.3% iowait",
+                "CPU usage from 907ms to 1431ms later:",
+                "  14% 121/mediaserver: 11% user + 3.7% kernel / faults: 17 minor",
+                "    3.7% 183/AudioOut_2: 3.7% user + 0% kernel",
+                "  12% 312/system_server: 5.5% user + 7.4% kernel / faults: 6 minor",
+                "    5.5% 366/InputDispatcher: 0% user + 5.5% kernel",
+                "18% TOTAL: 11% user + 7.5% kernel");
+
+        AnrItem anr = new AnrParser().parse(lines);
+        assertNotNull(anr);
+        assertEquals("com.android.package", anr.getApp());
+        assertEquals("keyDispatchingTimedOut", anr.getReason());
+        assertEquals(0.71, anr.getLoad(AnrItem.LoadCategory.LOAD_1));
+        assertEquals(0.83, anr.getLoad(AnrItem.LoadCategory.LOAD_5));
+        assertEquals(0.51, anr.getLoad(AnrItem.LoadCategory.LOAD_15));
+        assertEquals(33.0, anr.getCpuUsage(AnrItem.CpuUsageCategory.TOTAL));
+        assertEquals(21.0, anr.getCpuUsage(AnrItem.CpuUsageCategory.USER));
+        assertEquals(11.0, anr.getCpuUsage(AnrItem.CpuUsageCategory.KERNEL));
+        assertEquals(0.3, anr.getCpuUsage(AnrItem.CpuUsageCategory.IOWAIT));
+        assertEquals(ArrayUtil.join("\n", lines), anr.getStack());
+    }
+
+    /**
+     * Test that ANRs are parsed for the header "ANR in app (class/package)"
+     */
+    public void testParse_anr_in_app_class_package() {
+        List<String> lines = Arrays.asList(
+                "ANR in com.android.package (com.android.package/.Activity)",
+                "Reason: keyDispatchingTimedOut",
+                "Load: 0.71 / 0.83 / 0.51",
+                "CPU usage from 4357ms to -1434ms ago:",
+                "  22% 3378/com.android.package: 19% user + 3.6% kernel / faults: 73 minor 1 major",
+                "  16% 312/system_server: 12% user + 4.1% kernel / faults: 1082 minor 6 major",
+                "33% TOTAL: 21% user + 11% kernel + 0.3% iowait",
+                "CPU usage from 907ms to 1431ms later:",
+                "  14% 121/mediaserver: 11% user + 3.7% kernel / faults: 17 minor",
+                "    3.7% 183/AudioOut_2: 3.7% user + 0% kernel",
+                "  12% 312/system_server: 5.5% user + 7.4% kernel / faults: 6 minor",
+                "    5.5% 366/InputDispatcher: 0% user + 5.5% kernel",
+                "18% TOTAL: 11% user + 7.5% kernel");
+
+        AnrItem anr = new AnrParser().parse(lines);
+        assertNotNull(anr);
+        assertEquals("com.android.package", anr.getApp());
+        assertEquals("keyDispatchingTimedOut", anr.getReason());
+        assertEquals(0.71, anr.getLoad(AnrItem.LoadCategory.LOAD_1));
+        assertEquals(0.83, anr.getLoad(AnrItem.LoadCategory.LOAD_5));
+        assertEquals(0.51, anr.getLoad(AnrItem.LoadCategory.LOAD_15));
+        assertEquals(33.0, anr.getCpuUsage(AnrItem.CpuUsageCategory.TOTAL));
+        assertEquals(21.0, anr.getCpuUsage(AnrItem.CpuUsageCategory.USER));
+        assertEquals(11.0, anr.getCpuUsage(AnrItem.CpuUsageCategory.KERNEL));
+        assertEquals(0.3, anr.getCpuUsage(AnrItem.CpuUsageCategory.IOWAIT));
+        assertEquals(ArrayUtil.join("\n", lines), anr.getStack());
+    }
+}
diff --git a/tests/src/com/android/loganalysis/parser/BugreportParserFuncTest.java b/tests/src/com/android/loganalysis/parser/BugreportParserFuncTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..2a456d283580da97bf083b6bfbd49a8c474cc706
--- /dev/null
+++ b/tests/src/com/android/loganalysis/parser/BugreportParserFuncTest.java
@@ -0,0 +1,100 @@
+/*
+ * Copyright (C) 2011 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.loganalysis.parser;
+
+import com.android.loganalysis.item.BugreportItem;
+
+import junit.framework.TestCase;
+
+import java.io.BufferedReader;
+import java.io.FileNotFoundException;
+import java.io.FileReader;
+import java.io.IOException;
+
+/**
+ * Functional tests for {@link BugreportParser}
+ */
+public class BugreportParserFuncTest extends TestCase {
+    // FIXME: Make bugreport file configurable.
+    private static final String BUGREPORT_PATH = "/tmp/bugreport.txt";
+
+    /**
+     * A test that is intended to force Brillopad to parse a bugreport. The purpose of this is to
+     * assist a developer in checking why a given bugreport file might not be parsed correctly by
+     * Brillopad.
+     */
+    public void testParse() {
+        BufferedReader bugreportReader = null;
+        try {
+            bugreportReader = new BufferedReader(new FileReader(BUGREPORT_PATH));
+        } catch (FileNotFoundException e) {
+            fail(String.format("File not found at %s", BUGREPORT_PATH));
+        }
+        BugreportItem bugreport = null;
+        try {
+            long start = System.currentTimeMillis();
+            bugreport = new BugreportParser().parse(bugreportReader);
+            long stop = System.currentTimeMillis();
+            System.out.println(String.format("Bugreport took %d ms to parse.", stop - start));
+        } catch (IOException e) {
+            fail(String.format("IOException: %s", e.toString()));
+        } finally {
+            if (bugreportReader != null) {
+                try {
+                    bugreportReader.close();
+                } catch (IOException e) {
+                    // Ignore
+                }
+            }
+        }
+
+        assertNotNull(bugreport);
+        assertNotNull(bugreport.getTime());
+
+        assertNotNull(bugreport.getSystemProps());
+        assertTrue(bugreport.getSystemProps().size() > 0);
+
+        assertNotNull(bugreport.getMemInfo());
+        assertTrue(bugreport.getMemInfo().size() > 0);
+
+        assertNotNull(bugreport.getProcrank());
+        assertTrue(bugreport.getProcrank().getPids().size() > 0);
+
+        assertNotNull(bugreport.getSystemLog());
+        assertNotNull(bugreport.getSystemLog().getStartTime());
+        assertNotNull(bugreport.getSystemLog().getStopTime());
+
+        System.out.println(String.format("Stats for bugreport:\n" +
+                "  Time: %s\n" +
+                "  System Properties: %d items\n" +
+                "  Mem info: %d items\n" +
+                "  Procrank: %d items\n" +
+                "  System Log:\n" +
+                "    Start time: %s\n" +
+                "    Stop time: %s\n" +
+                "    %d ANR(s), %d Java Crash(es), %d Native Crash(es)",
+                bugreport.getTime(),
+                bugreport.getSystemProps().size(),
+                bugreport.getMemInfo().size(),
+                bugreport.getProcrank().getPids().size(),
+                bugreport.getSystemLog().getStartTime().toString(),
+                bugreport.getSystemLog().getStopTime().toString(),
+                bugreport.getSystemLog().getAnrs().size(),
+                bugreport.getSystemLog().getJavaCrashes().size(),
+                bugreport.getSystemLog().getNativeCrashes().size()));
+    }
+}
+
diff --git a/tests/src/com/android/loganalysis/parser/BugreportParserTest.java b/tests/src/com/android/loganalysis/parser/BugreportParserTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..b5f973e1306a9b4a023c5df4c1e2977a107be988
--- /dev/null
+++ b/tests/src/com/android/loganalysis/parser/BugreportParserTest.java
@@ -0,0 +1,347 @@
+/*
+ * Copyright (C) 2011 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.loganalysis.parser;
+
+import com.android.loganalysis.item.BugreportItem;
+import com.android.loganalysis.util.ArrayUtil;
+
+import junit.framework.TestCase;
+
+import java.text.DateFormat;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.Arrays;
+import java.util.Date;
+import java.util.List;
+
+/**
+ * Unit tests for {@link BugreportParser}
+ */
+public class BugreportParserTest extends TestCase {
+
+    /**
+     * Test that a bugreport can be parsed.
+     */
+    public void testParse() throws ParseException {
+        List<String> lines = Arrays.asList(
+                "========================================================",
+                "== dumpstate: 2012-04-25 20:45:10",
+                "========================================================",
+                "------ SECTION ------",
+                "",
+                "------ MEMORY INFO (/proc/meminfo) ------",
+                "MemTotal:         353332 kB",
+                "MemFree:           65420 kB",
+                "Buffers:           20800 kB",
+                "Cached:            86204 kB",
+                "SwapCached:            0 kB",
+                "",
+                "------ PROCRANK (procrank) ------",
+                "  PID      Vss      Rss      Pss      Uss  cmdline",
+                "  178   87136K   81684K   52829K   50012K  system_server",
+                " 1313   78128K   77996K   48603K   45812K  com.google.android.apps.maps",
+                " 3247   61652K   61492K   33122K   30972K  com.android.browser",
+                "                          ------   ------  ------",
+                "                          203624K  163604K  TOTAL",
+                "RAM: 731448K total, 415804K free, 9016K buffers, 108548K cached",
+                "[procrank: 1.6s elapsed]",
+                "",
+                "------ SYSTEM LOG (logcat -v threadtime -d *:v) ------",
+                "04-25 09:55:47.799  3064  3082 E AndroidRuntime: java.lang.Exception",
+                "04-25 09:55:47.799  3064  3082 E AndroidRuntime: \tat class.method1(Class.java:1)",
+                "04-25 09:55:47.799  3064  3082 E AndroidRuntime: \tat class.method2(Class.java:2)",
+                "04-25 09:55:47.799  3064  3082 E AndroidRuntime: \tat class.method3(Class.java:3)",
+                "04-25 17:17:08.445   312   366 E ActivityManager: ANR (application not responding) in process: com.android.package",
+                "04-25 17:17:08.445   312   366 E ActivityManager: Reason: keyDispatchingTimedOut",
+                "04-25 17:17:08.445   312   366 E ActivityManager: Load: 0.71 / 0.83 / 0.51",
+                "04-25 17:17:08.445   312   366 E ActivityManager: 33% TOTAL: 21% user + 11% kernel + 0.3% iowait",
+                "04-25 18:33:27.273   115   115 I DEBUG   : *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***",
+                "04-25 18:33:27.273   115   115 I DEBUG   : Build fingerprint: 'product:build:target'",
+                "04-25 18:33:27.273   115   115 I DEBUG   : pid: 3112, tid: 3112  >>> com.google.android.browser <<<",
+                "04-25 18:33:27.273   115   115 I DEBUG   : signal 11 (SIGSEGV), code 1 (SEGV_MAPERR), fault addr 00000000",
+                "",
+                "------ SYSTEM PROPERTIES ------",
+                "[dalvik.vm.dexopt-flags]: [m=y]",
+                "[dalvik.vm.heapgrowthlimit]: [48m]",
+                "[dalvik.vm.heapsize]: [256m]",
+                "[gsm.version.ril-impl]: [android moto-ril-multimode 1.0]",
+                "",
+                "------ SECTION ------",
+                "",
+                "------ VM TRACES AT LAST ANR (/data/anr/traces.txt: 2012-04-25 17:17:08) ------",
+                "",
+                "",
+                "----- pid 2887 at 2012-04-25 17:17:08 -----",
+                "Cmd line: com.android.package",
+                "",
+                "DALVIK THREADS:",
+                "(mutexes: tll=0 tsl=0 tscl=0 ghl=0)",
+                "",
+                "\"main\" prio=5 tid=1 SUSPENDED",
+                "  | group=\"main\" sCount=1 dsCount=0 obj=0x00000001 self=0x00000001",
+                "  | sysTid=2887 nice=0 sched=0/0 cgrp=foreground handle=0000000001",
+                "  | schedstat=( 0 0 0 ) utm=5954 stm=1017 core=0",
+                "  at class.method1(Class.java:1)",
+                "  at class.method2(Class.java:2)",
+                "  at class.method2(Class.java:2)",
+                "",
+                "----- end 2887 -----",
+                "",
+                "------ SECTION ------",
+                "");
+
+        BugreportItem bugreport = new BugreportParser().parse(lines);
+        assertNotNull(bugreport);
+        assertEquals(parseTime("2012-04-25 20:45:10.000"), bugreport.getTime());
+
+        assertNotNull(bugreport.getMemInfo());
+        assertEquals(5, bugreport.getMemInfo().size());
+
+        assertNotNull(bugreport.getProcrank());
+        assertEquals(3, bugreport.getProcrank().getPids().size());
+
+        assertNotNull(bugreport.getSystemLog());
+        assertEquals(parseTime("2012-04-25 09:55:47.799"), bugreport.getSystemLog().getStartTime());
+        assertEquals(parseTime("2012-04-25 18:33:27.273"), bugreport.getSystemLog().getStopTime());
+        assertEquals(3, bugreport.getSystemLog().getEvents().size());
+        assertEquals(1, bugreport.getSystemLog().getAnrs().size());
+        assertNotNull(bugreport.getSystemLog().getAnrs().get(0).getTrace());
+
+        assertNotNull(bugreport.getSystemProps());
+        assertEquals(4, bugreport.getSystemProps().size());
+    }
+
+    /**
+     * Test that the logcat year is set correctly from the bugreport timestamp.
+     */
+    public void testParse_set_logcat_year() throws ParseException {
+        List<String> lines = Arrays.asList(
+                "========================================================",
+                "== dumpstate: 1999-01-01 02:03:04",
+                "========================================================",
+                "------ SYSTEM LOG (logcat -v threadtime -d *:v) ------",
+                "01-01 01:02:03.000     1     1 I TAG     : message",
+                "01-01 01:02:04.000     1     1 I TAG     : message",
+                "");
+
+        BugreportItem bugreport = new BugreportParser().parse(lines);
+        assertNotNull(bugreport);
+        assertEquals(parseTime("1999-01-01 02:03:04.000"), bugreport.getTime());
+        assertNotNull(bugreport.getSystemLog());
+        assertEquals(parseTime("1999-01-01 01:02:03.000"), bugreport.getSystemLog().getStartTime());
+        assertEquals(parseTime("1999-01-01 01:02:04.000"), bugreport.getSystemLog().getStopTime());
+    }
+
+    /**
+     * Test that the trace is set correctly if there is only one ANR.
+     */
+    public void testSetAnrTrace_single() {
+        List<String> lines = Arrays.asList(
+                "========================================================",
+                "== dumpstate: 2012-04-25 20:45:10",
+                "========================================================",
+                "------ SYSTEM LOG (logcat -v threadtime -d *:v) ------",
+                "04-25 17:17:08.445   312   366 E ActivityManager: ANR (application not responding) in process: com.android.package",
+                "04-25 17:17:08.445   312   366 E ActivityManager: Reason: keyDispatchingTimedOut",
+                "04-25 17:17:08.445   312   366 E ActivityManager: Load: 0.71 / 0.83 / 0.51",
+                "04-25 17:17:08.445   312   366 E ActivityManager: 33% TOTAL: 21% user + 11% kernel + 0.3% iowait",
+                "",
+                "------ VM TRACES AT LAST ANR (/data/anr/traces.txt: 2012-04-25 17:17:08) ------",
+                "",
+                "----- pid 2887 at 2012-04-25 17:17:08 -----",
+                "Cmd line: com.android.package",
+                "",
+                "DALVIK THREADS:",
+                "(mutexes: tll=0 tsl=0 tscl=0 ghl=0)",
+                "",
+                "\"main\" prio=5 tid=1 SUSPENDED",
+                "  | group=\"main\" sCount=1 dsCount=0 obj=0x00000001 self=0x00000001",
+                "  | sysTid=2887 nice=0 sched=0/0 cgrp=foreground handle=0000000001",
+                "  | schedstat=( 0 0 0 ) utm=5954 stm=1017 core=0",
+                "  at class.method1(Class.java:1)",
+                "  at class.method2(Class.java:2)",
+                "  at class.method2(Class.java:2)",
+                "",
+                "----- end 2887 -----",
+                "");
+
+        List<String> expectedStack = Arrays.asList(
+                "\"main\" prio=5 tid=1 SUSPENDED",
+                "  | group=\"main\" sCount=1 dsCount=0 obj=0x00000001 self=0x00000001",
+                "  | sysTid=2887 nice=0 sched=0/0 cgrp=foreground handle=0000000001",
+                "  | schedstat=( 0 0 0 ) utm=5954 stm=1017 core=0",
+                "  at class.method1(Class.java:1)",
+                "  at class.method2(Class.java:2)",
+                "  at class.method2(Class.java:2)");
+
+        BugreportItem bugreport = new BugreportParser().parse(lines);
+
+        assertNotNull(bugreport.getSystemLog());
+        assertEquals(1, bugreport.getSystemLog().getAnrs().size());
+        assertEquals(ArrayUtil.join("\n", expectedStack),
+                bugreport.getSystemLog().getAnrs().get(0).getTrace());
+    }
+
+    /**
+     * Test that the trace is set correctly if there are multiple ANRs.
+     */
+    public void testSetAnrTrace_multiple() {
+        List<String> lines = Arrays.asList(
+                "========================================================",
+                "== dumpstate: 2012-04-25 20:45:10",
+                "========================================================",
+                "------ SYSTEM LOG (logcat -v threadtime -d *:v) ------",
+                "04-25 17:17:08.445   312   366 E ActivityManager: ANR (application not responding) in process: com.android.package",
+                "04-25 17:17:08.445   312   366 E ActivityManager: Reason: keyDispatchingTimedOut",
+                "04-25 17:17:08.445   312   366 E ActivityManager: Load: 0.71 / 0.83 / 0.51",
+                "04-25 17:17:08.445   312   366 E ActivityManager: 33% TOTAL: 21% user + 11% kernel + 0.3% iowait",
+                "04-25 17:18:08.445   312   366 E ActivityManager: ANR (application not responding) in process: com.android.package",
+                "04-25 17:18:08.445   312   366 E ActivityManager: Reason: keyDispatchingTimedOut",
+                "04-25 17:18:08.445   312   366 E ActivityManager: Load: 0.71 / 0.83 / 0.51",
+                "04-25 17:18:08.445   312   366 E ActivityManager: 33% TOTAL: 21% user + 11% kernel + 0.3% iowait",
+                "04-25 17:19:08.445   312   366 E ActivityManager: ANR (application not responding) in process: com.android.different.pacakge",
+                "04-25 17:19:08.445   312   366 E ActivityManager: Reason: keyDispatchingTimedOut",
+                "04-25 17:19:08.445   312   366 E ActivityManager: Load: 0.71 / 0.83 / 0.51",
+                "04-25 17:19:08.445   312   366 E ActivityManager: 33% TOTAL: 21% user + 11% kernel + 0.3% iowait",
+                "",
+                "------ VM TRACES AT LAST ANR (/data/anr/traces.txt: 2012-04-25 17:18:08) ------",
+                "",
+                "----- pid 2887 at 2012-04-25 17:17:08 -----",
+                "Cmd line: com.android.package",
+                "",
+                "DALVIK THREADS:",
+                "(mutexes: tll=0 tsl=0 tscl=0 ghl=0)",
+                "",
+                "\"main\" prio=5 tid=1 SUSPENDED",
+                "  | group=\"main\" sCount=1 dsCount=0 obj=0x00000001 self=0x00000001",
+                "  | sysTid=2887 nice=0 sched=0/0 cgrp=foreground handle=0000000001",
+                "  | schedstat=( 0 0 0 ) utm=5954 stm=1017 core=0",
+                "  at class.method1(Class.java:1)",
+                "  at class.method2(Class.java:2)",
+                "  at class.method2(Class.java:2)",
+                "",
+                "----- end 2887 -----",
+                "");
+
+        List<String> expectedStack = Arrays.asList(
+                "\"main\" prio=5 tid=1 SUSPENDED",
+                "  | group=\"main\" sCount=1 dsCount=0 obj=0x00000001 self=0x00000001",
+                "  | sysTid=2887 nice=0 sched=0/0 cgrp=foreground handle=0000000001",
+                "  | schedstat=( 0 0 0 ) utm=5954 stm=1017 core=0",
+                "  at class.method1(Class.java:1)",
+                "  at class.method2(Class.java:2)",
+                "  at class.method2(Class.java:2)");
+
+        BugreportItem bugreport = new BugreportParser().parse(lines);
+
+        assertNotNull(bugreport.getSystemLog());
+        assertEquals(3, bugreport.getSystemLog().getAnrs().size());
+        assertNull(bugreport.getSystemLog().getAnrs().get(0).getTrace());
+        assertEquals(ArrayUtil.join("\n", expectedStack),
+                bugreport.getSystemLog().getAnrs().get(1).getTrace());
+        assertNull(bugreport.getSystemLog().getAnrs().get(2).getTrace());
+    }
+
+    /**
+     * Test that the trace is set correctly if there is not traces file.
+     */
+    public void testSetAnrTrace_no_traces() {
+        List<String> lines = Arrays.asList(
+                "========================================================",
+                "== dumpstate: 2012-04-25 20:45:10",
+                "========================================================",
+                "------ SYSTEM LOG (logcat -v threadtime -d *:v) ------",
+                "04-25 17:17:08.445   312   366 E ActivityManager: ANR (application not responding) in process: com.android.package",
+                "04-25 17:17:08.445   312   366 E ActivityManager: Reason: keyDispatchingTimedOut",
+                "04-25 17:17:08.445   312   366 E ActivityManager: Load: 0.71 / 0.83 / 0.51",
+                "04-25 17:17:08.445   312   366 E ActivityManager: 33% TOTAL: 21% user + 11% kernel + 0.3% iowait",
+                "",
+                "*** NO ANR VM TRACES FILE (/data/anr/traces.txt): No such file or directory",
+                "");
+
+        BugreportItem bugreport = new BugreportParser().parse(lines);
+
+        assertNotNull(bugreport.getSystemLog());
+        assertEquals(1, bugreport.getSystemLog().getAnrs().size());
+        assertNull(bugreport.getSystemLog().getAnrs().get(0).getTrace());
+    }
+
+    /**
+     * Test that app names from logcat events are populated by matching the logcat PIDs with the
+     * PIDs from the logcat.
+     */
+    public void testSetAppsFromProcrank() {
+        List<String> lines = Arrays.asList(
+                "========================================================",
+                "== dumpstate: 2012-04-25 20:45:10",
+                "========================================================",
+                "------ PROCRANK (procrank) ------",
+                "  PID      Vss      Rss      Pss      Uss  cmdline",
+                " 3064   87136K   81684K   52829K   50012K  com.android.package",
+                "                          ------   ------  ------",
+                "                          203624K  163604K  TOTAL",
+                "RAM: 731448K total, 415804K free, 9016K buffers, 108548K cached",
+                "[procrank: 1.6s elapsed]",
+                "------ SYSTEM LOG (logcat -v threadtime -d *:v) ------",
+                "04-25 09:55:47.799  3064  3082 E AndroidRuntime: java.lang.Exception",
+                "04-25 09:55:47.799  3064  3082 E AndroidRuntime: \tat class.method1(Class.java:1)",
+                "04-25 09:55:47.799  3064  3082 E AndroidRuntime: \tat class.method2(Class.java:2)",
+                "04-25 09:55:47.799  3064  3082 E AndroidRuntime: \tat class.method3(Class.java:3)",
+                "04-25 09:55:47.799  3065  3083 E AndroidRuntime: java.lang.Exception",
+                "04-25 09:55:47.799  3065  3083 E AndroidRuntime: \tat class.method1(Class.java:1)",
+                "04-25 09:55:47.799  3065  3083 E AndroidRuntime: \tat class.method2(Class.java:2)",
+                "04-25 09:55:47.799  3065  3083 E AndroidRuntime: \tat class.method3(Class.java:3)");
+
+        BugreportItem bugreport = new BugreportParser().parse(lines);
+        assertNotNull(bugreport.getSystemLog());
+        assertEquals(2, bugreport.getSystemLog().getJavaCrashes().size());
+        assertEquals("com.android.package",
+                bugreport.getSystemLog().getJavaCrashes().get(0).getApp());
+        assertNull(bugreport.getSystemLog().getJavaCrashes().get(1).getApp());
+    }
+
+    private Date parseTime(String timeStr) throws ParseException {
+        DateFormat formatter = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
+        return formatter.parse(timeStr);
+    }
+
+    /**
+     * Some Android devices refer to SYSTEM LOG as MAIN LOG. Check that parser recognizes this
+     * alternate syntax.
+     */
+    public void testSystemLogAsMainLog() {
+        List<String> lines = Arrays.asList(
+                "------ MAIN LOG (logcat -b main -b system -v threadtime -d *:v) ------",
+                "--------- beginning of /dev/log/system",
+                "12-11 19:48:07.945  1484  1508 D BatteryService: update start");
+        BugreportItem bugreport = new BugreportParser().parse(lines);
+        assertNotNull(bugreport.getSystemLog());
+    }
+
+    /**
+     * Some Android devices refer to SYSTEM LOG as MAIN AND SYSTEM LOG. Check that parser
+     * recognizes this alternate syntax.
+     */
+    public void testSystemAndMainLog() {
+        List<String> lines = Arrays.asList(
+                "------ MAIN AND SYSTEM LOG (logcat -b main -b system -v threadtime -d *:v) ------",
+                "--------- beginning of /dev/log/system",
+                "12-17 15:15:12.877  1994  2019 D UiModeManager: updateConfigurationLocked: ");
+        BugreportItem bugreport = new BugreportParser().parse(lines);
+        assertNotNull(bugreport.getSystemLog());
+    }
+}
+
diff --git a/tests/src/com/android/loganalysis/parser/JavaCrashParserTest.java b/tests/src/com/android/loganalysis/parser/JavaCrashParserTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..723633f69699e630b5ca7d231bc520d9601d99f2
--- /dev/null
+++ b/tests/src/com/android/loganalysis/parser/JavaCrashParserTest.java
@@ -0,0 +1,127 @@
+/*
+ * Copyright (C) 2012 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.loganalysis.parser;
+
+import com.android.loganalysis.item.JavaCrashItem;
+import com.android.loganalysis.util.ArrayUtil;
+
+import junit.framework.TestCase;
+
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * Unit tests for {@link JavaCrashParser}.
+ */
+public class JavaCrashParserTest extends TestCase {
+
+    /**
+     * Test that Java crashes are parsed with no message.
+     */
+    public void testParse_no_message() {
+        List<String> lines = Arrays.asList(
+                "java.lang.Exception",
+                "\tat class.method1(Class.java:1)",
+                "\tat class.method2(Class.java:2)",
+                "\tat class.method3(Class.java:3)");
+
+        JavaCrashItem jc = new JavaCrashParser().parse(lines);
+        assertNotNull(jc);
+        assertEquals("java.lang.Exception", jc.getException());
+        assertNull(jc.getMessage());
+        assertEquals(ArrayUtil.join("\n", lines), jc.getStack());
+    }
+
+    /**
+     * Test that Java crashes are parsed with a message.
+     */
+    public void testParse_message() {
+        List<String> lines = Arrays.asList(
+                "java.lang.Exception: This is the message",
+                "\tat class.method1(Class.java:1)",
+                "\tat class.method2(Class.java:2)",
+                "\tat class.method3(Class.java:3)");
+
+        JavaCrashItem jc = new JavaCrashParser().parse(lines);
+        assertNotNull(jc);
+        assertEquals("java.lang.Exception", jc.getException());
+        assertEquals("This is the message", jc.getMessage());
+        assertEquals(ArrayUtil.join("\n", lines), jc.getStack());
+    }
+
+    /**
+     * Test that Java crashes are parsed if the message spans multiple lines.
+     */
+    public void testParse_multiline_message() {
+        List<String> lines = Arrays.asList(
+                "java.lang.Exception: This message",
+                "is many lines",
+                "long.",
+                "\tat class.method1(Class.java:1)",
+                "\tat class.method2(Class.java:2)",
+                "\tat class.method3(Class.java:3)");
+
+        JavaCrashItem jc = new JavaCrashParser().parse(lines);
+        assertNotNull(jc);
+        assertEquals("java.lang.Exception", jc.getException());
+        assertEquals("This message\nis many lines\nlong.", jc.getMessage());
+        assertEquals(ArrayUtil.join("\n", lines), jc.getStack());
+    }
+
+    /**
+     * Test that caused by sections of Java crashes are parsed, with no message or single or
+     * multiline messages.
+     */
+    public void testParse_caused_by() {
+        List<String> lines = Arrays.asList(
+                "java.lang.Exception: This is the message",
+                "\tat class.method1(Class.java:1)",
+                "\tat class.method2(Class.java:2)",
+                "\tat class.method3(Class.java:3)",
+                "Caused by: java.lang.Exception",
+                "\tat class.method4(Class.java:4)",
+                "Caused by: java.lang.Exception: This is the caused by message",
+                "\tat class.method5(Class.java:5)",
+                "Caused by: java.lang.Exception: This is a multiline",
+                "caused by message",
+                "\tat class.method6(Class.java:6)");
+
+        JavaCrashItem jc = new JavaCrashParser().parse(lines);
+        assertNotNull(jc);
+        assertEquals("java.lang.Exception", jc.getException());
+        assertEquals("This is the message", jc.getMessage());
+        assertEquals(ArrayUtil.join("\n", lines), jc.getStack());
+    }
+
+    /**
+     * Test that the Java crash is cutoff if an unexpected line is handled.
+     */
+    public void testParse_cutoff() {
+        List<String> lines = Arrays.asList(
+                "java.lang.Exception: This is the message",
+                "\tat class.method1(Class.java:1)",
+                "\tat class.method2(Class.java:2)",
+                "\tat class.method3(Class.java:3)",
+                "Invalid line",
+                "java.lang.Exception: This is the message");
+
+        JavaCrashItem jc = new JavaCrashParser().parse(lines);
+        assertNotNull(jc);
+        assertEquals("java.lang.Exception", jc.getException());
+        assertEquals("This is the message", jc.getMessage());
+        assertEquals(ArrayUtil.join("\n", lines.subList(0, lines.size()-2)), jc.getStack());
+    }
+}
diff --git a/tests/src/com/android/loganalysis/parser/LogcatParserFuncTest.java b/tests/src/com/android/loganalysis/parser/LogcatParserFuncTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..7d63b19d4ed32dd55ac99943b2e20790556aba72
--- /dev/null
+++ b/tests/src/com/android/loganalysis/parser/LogcatParserFuncTest.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2011 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.loganalysis.parser;
+
+import com.android.loganalysis.item.LogcatItem;
+
+import junit.framework.TestCase;
+
+import java.io.BufferedReader;
+import java.io.FileNotFoundException;
+import java.io.FileReader;
+import java.io.IOException;
+
+/**
+ * Functional tests for {@link LogcatParser}
+ */
+public class LogcatParserFuncTest extends TestCase {
+    // FIXME: Make logcat file configurable.
+    private static final String LOGCAT_PATH = "/tmp/logcat.txt";
+
+    /**
+     * A test that is intended to force Brillopad to parse a logcat. The purpose of this is to
+     * assist a developer in checking why a given logcat file might not be parsed correctly by
+     * Brillopad.
+     */
+    public void testParse() {
+        BufferedReader logcatReader = null;
+        try {
+            logcatReader = new BufferedReader(new FileReader(LOGCAT_PATH));
+        } catch (FileNotFoundException e) {
+            fail(String.format("File not found at %s", LOGCAT_PATH));
+        }
+        LogcatItem logcat = null;
+        try {
+            long start = System.currentTimeMillis();
+            logcat = new LogcatParser().parse(logcatReader);
+            long stop = System.currentTimeMillis();
+            System.out.println(String.format("Logcat took %d ms to parse.", stop - start));
+        } catch (IOException e) {
+            fail(String.format("IOException: %s", e.toString()));
+        } finally {
+            if (logcatReader != null) {
+                try {
+                    logcatReader.close();
+                } catch (IOException e) {
+                    // Ignore
+                }
+            }        }
+
+        assertNotNull(logcat);
+        assertNotNull(logcat.getStartTime());
+        assertNotNull(logcat.getStopTime());
+
+        System.out.println(String.format("Stats for logcat:\n" +
+                "  Start time: %s\n" +
+                "  Stop time: %s\n" +
+                "  %d ANR(s), %d Java Crash(es), %d Native Crash(es)",
+                logcat.getStartTime().toString(),
+                logcat.getStopTime().toString(),
+                logcat.getAnrs().size(),
+                logcat.getJavaCrashes().size(),
+                logcat.getNativeCrashes().size()));
+    }
+}
+
diff --git a/tests/src/com/android/loganalysis/parser/LogcatParserTest.java b/tests/src/com/android/loganalysis/parser/LogcatParserTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..37a39b68512d88f69224700a827b95134f7676db
--- /dev/null
+++ b/tests/src/com/android/loganalysis/parser/LogcatParserTest.java
@@ -0,0 +1,358 @@
+/*
+ * Copyright (C) 2012 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.loganalysis.parser;
+
+import com.android.loganalysis.item.LogcatItem;
+import com.android.loganalysis.util.ArrayUtil;
+
+import junit.framework.TestCase;
+
+import java.text.DateFormat;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.Arrays;
+import java.util.Date;
+import java.util.List;
+
+/**
+ * Unit tests for {@link LogcatParserTest}.
+ */
+public class LogcatParserTest extends TestCase {
+
+    /**
+     * Test that an ANR is parsed in the log.
+     */
+    public void testParse_anr() throws ParseException {
+        List<String> lines = Arrays.asList(
+                "04-25 17:17:08.445   312   366 E ActivityManager: ANR (application not responding) in process: com.android.package",
+                "04-25 17:17:08.445   312   366 E ActivityManager: Reason: keyDispatchingTimedOut",
+                "04-25 17:17:08.445   312   366 E ActivityManager: Load: 0.71 / 0.83 / 0.51",
+                "04-25 17:17:08.445   312   366 E ActivityManager: 33% TOTAL: 21% user + 11% kernel + 0.3% iowait");
+
+        LogcatItem logcat = new LogcatParser("2012").parse(lines);
+        assertNotNull(logcat);
+        assertEquals(parseTime("2012-04-25 17:17:08.445"), logcat.getStartTime());
+        assertEquals(parseTime("2012-04-25 17:17:08.445"), logcat.getStopTime());
+        assertEquals(1, logcat.getEvents().size());
+        assertEquals(1, logcat.getAnrs().size());
+        assertEquals(312, logcat.getAnrs().get(0).getPid().intValue());
+        assertEquals(366, logcat.getAnrs().get(0).getTid().intValue());
+        assertEquals("", logcat.getAnrs().get(0).getLastPreamble());
+        assertEquals("", logcat.getAnrs().get(0).getProcessPreamble());
+        assertEquals(parseTime("2012-04-25 17:17:08.445"), logcat.getAnrs().get(0).getEventTime());
+    }
+
+    /**
+     * Test that Java crashes can be parsed.
+     */
+    public void testParse_java_crash() throws ParseException {
+        List<String> lines = Arrays.asList(
+                "04-25 09:55:47.799  3064  3082 E AndroidRuntime: java.lang.Exception",
+                "04-25 09:55:47.799  3064  3082 E AndroidRuntime: \tat class.method1(Class.java:1)",
+                "04-25 09:55:47.799  3064  3082 E AndroidRuntime: \tat class.method2(Class.java:2)",
+                "04-25 09:55:47.799  3064  3082 E AndroidRuntime: \tat class.method3(Class.java:3)");
+
+        LogcatItem logcat = new LogcatParser("2012").parse(lines);
+        assertNotNull(logcat);
+        assertEquals(parseTime("2012-04-25 09:55:47.799"), logcat.getStartTime());
+        assertEquals(parseTime("2012-04-25 09:55:47.799"), logcat.getStopTime());
+        assertEquals(1, logcat.getEvents().size());
+        assertEquals(1, logcat.getJavaCrashes().size());
+        assertEquals(3064, logcat.getJavaCrashes().get(0).getPid().intValue());
+        assertEquals(3082, logcat.getJavaCrashes().get(0).getTid().intValue());
+        assertEquals("", logcat.getJavaCrashes().get(0).getLastPreamble());
+        assertEquals("", logcat.getJavaCrashes().get(0).getProcessPreamble());
+        assertEquals(parseTime("2012-04-25 09:55:47.799"),
+                logcat.getJavaCrashes().get(0).getEventTime());
+    }
+
+    /**
+     * Test that native crashes can be parsed.
+     */
+    public void testParse_native_crash() throws ParseException {
+        List<String> lines = Arrays.asList(
+                "04-25 18:33:27.273   115   115 I DEBUG   : *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***",
+                "04-25 18:33:27.273   115   115 I DEBUG   : Build fingerprint: 'product:build:target'",
+                "04-25 18:33:27.273   115   115 I DEBUG   : pid: 3112, tid: 3112  >>> com.google.android.browser <<<",
+                "04-25 18:33:27.273   115   115 I DEBUG   : signal 11 (SIGSEGV), code 1 (SEGV_MAPERR), fault addr 00000000");
+
+        LogcatItem logcat = new LogcatParser("2012").parse(lines);
+        assertNotNull(logcat);
+        assertEquals(parseTime("2012-04-25 18:33:27.273"), logcat.getStartTime());
+        assertEquals(parseTime("2012-04-25 18:33:27.273"), logcat.getStopTime());
+        assertEquals(1, logcat.getEvents().size());
+        assertEquals(1, logcat.getNativeCrashes().size());
+        assertEquals(115, logcat.getNativeCrashes().get(0).getPid().intValue());
+        assertEquals(115, logcat.getNativeCrashes().get(0).getTid().intValue());
+        assertEquals("", logcat.getNativeCrashes().get(0).getLastPreamble());
+        assertEquals("", logcat.getNativeCrashes().get(0).getProcessPreamble());
+        assertEquals(parseTime("2012-04-25 18:33:27.273"),
+                logcat.getNativeCrashes().get(0).getEventTime());
+    }
+
+    /**
+     * Test that multiple events can be parsed.
+     */
+    public void testParse_multiple_events() throws ParseException {
+        List<String> lines = Arrays.asList(
+                "04-25 09:55:47.799  3064  3082 E AndroidRuntime: java.lang.Exception",
+                "04-25 09:55:47.799  3064  3082 E AndroidRuntime: \tat class.method1(Class.java:1)",
+                "04-25 09:55:47.799  3064  3082 E AndroidRuntime: \tat class.method2(Class.java:2)",
+                "04-25 09:55:47.799  3064  3082 E AndroidRuntime: \tat class.method3(Class.java:3)",
+                "04-25 09:55:47.799  3065  3090 E AndroidRuntime: java.lang.Exception",
+                "04-25 09:55:47.799  3065  3090 E AndroidRuntime: \tat class.method1(Class.java:1)",
+                "04-25 09:55:47.799  3065  3090 E AndroidRuntime: \tat class.method2(Class.java:2)",
+                "04-25 09:55:47.799  3065  3090 E AndroidRuntime: \tat class.method3(Class.java:3)",
+                "04-25 17:17:08.445   312   366 E ActivityManager: ANR (application not responding) in process: com.android.package",
+                "04-25 17:17:08.445   312   366 E ActivityManager: Reason: keyDispatchingTimedOut",
+                "04-25 17:17:08.445   312   366 E ActivityManager: Load: 0.71 / 0.83 / 0.51",
+                "04-25 17:17:08.445   312   366 E ActivityManager: 33% TOTAL: 21% user + 11% kernel + 0.3% iowait",
+                "04-25 17:17:08.445   312   366 E ActivityManager: ANR (application not responding) in process: com.android.package",
+                "04-25 17:17:08.445   312   366 E ActivityManager: Reason: keyDispatchingTimedOut",
+                "04-25 17:17:08.445   312   366 E ActivityManager: Load: 0.71 / 0.83 / 0.51",
+                "04-25 17:17:08.445   312   366 E ActivityManager: 33% TOTAL: 21% user + 11% kernel + 0.3% iowait",
+                "04-25 18:33:27.273   115   115 I DEBUG   : *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***",
+                "04-25 18:33:27.273   115   115 I DEBUG   : Build fingerprint: 'product:build:target'",
+                "04-25 18:33:27.273   115   115 I DEBUG   : pid: 3112, tid: 3112  >>> com.google.android.browser <<<",
+                "04-25 18:33:27.273   115   115 I DEBUG   : signal 11 (SIGSEGV), code 1 (SEGV_MAPERR), fault addr 00000000",
+                "04-25 18:33:27.273   117   117 I DEBUG   : *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***",
+                "04-25 18:33:27.273   117   117 I DEBUG   : Build fingerprint: 'product:build:target'",
+                "04-25 18:33:27.273   117   117 I DEBUG   : pid: 3112, tid: 3112  >>> com.google.android.browser <<<",
+                "04-25 18:33:27.273   117   117 I DEBUG   : signal 11 (SIGSEGV), code 1 (SEGV_MAPERR), fault addr 00000000");
+
+
+        LogcatItem logcat = new LogcatParser("2012").parse(lines);
+        assertNotNull(logcat);
+        assertEquals(parseTime("2012-04-25 09:55:47.799"), logcat.getStartTime());
+        assertEquals(parseTime("2012-04-25 18:33:27.273"), logcat.getStopTime());
+        assertEquals(6, logcat.getEvents().size());
+        assertEquals(2, logcat.getAnrs().size());
+        assertEquals(2, logcat.getJavaCrashes().size());
+        assertEquals(2, logcat.getNativeCrashes().size());
+
+        assertEquals(312, logcat.getAnrs().get(0).getPid().intValue());
+        assertEquals(366, logcat.getAnrs().get(0).getTid().intValue());
+        assertEquals(parseTime("2012-04-25 17:17:08.445"), logcat.getAnrs().get(0).getEventTime());
+
+        assertEquals(312, logcat.getAnrs().get(1).getPid().intValue());
+        assertEquals(366, logcat.getAnrs().get(1).getTid().intValue());
+        assertEquals(parseTime("2012-04-25 17:17:08.445"), logcat.getAnrs().get(1).getEventTime());
+
+        assertEquals(3064, logcat.getJavaCrashes().get(0).getPid().intValue());
+        assertEquals(3082, logcat.getJavaCrashes().get(0).getTid().intValue());
+        assertEquals(parseTime("2012-04-25 09:55:47.799"),
+                logcat.getJavaCrashes().get(0).getEventTime());
+
+        assertEquals(3065, logcat.getJavaCrashes().get(1).getPid().intValue());
+        assertEquals(3090, logcat.getJavaCrashes().get(1).getTid().intValue());
+        assertEquals(parseTime("2012-04-25 09:55:47.799"),
+                logcat.getJavaCrashes().get(1).getEventTime());
+
+        assertEquals(115, logcat.getNativeCrashes().get(0).getPid().intValue());
+        assertEquals(115, logcat.getNativeCrashes().get(0).getTid().intValue());
+        assertEquals(parseTime("2012-04-25 18:33:27.273"),
+                logcat.getNativeCrashes().get(0).getEventTime());
+
+        assertEquals(117, logcat.getNativeCrashes().get(1).getPid().intValue());
+        assertEquals(117, logcat.getNativeCrashes().get(1).getTid().intValue());
+        assertEquals(parseTime("2012-04-25 18:33:27.273"),
+                logcat.getNativeCrashes().get(1).getEventTime());
+    }
+
+    /**
+     * Test that multiple java crashes and native crashes can be parsed even when interleaved.
+     */
+    public void testParse_multiple_events_interleaved() throws ParseException {
+        List<String> lines = Arrays.asList(
+                "04-25 09:55:47.799  3064  3082 E AndroidRuntime: java.lang.Exception",
+                "04-25 09:55:47.799  3065  3090 E AndroidRuntime: java.lang.Exception",
+                "04-25 09:55:47.799   115   115 I DEBUG   : *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***",
+                "04-25 09:55:47.799   117   117 I DEBUG   : *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***",
+                "04-25 09:55:47.799  3064  3082 E AndroidRuntime: \tat class.method1(Class.java:1)",
+                "04-25 09:55:47.799  3065  3090 E AndroidRuntime: \tat class.method1(Class.java:1)",
+                "04-25 09:55:47.799   115   115 I DEBUG   : Build fingerprint: 'product:build:target'",
+                "04-25 09:55:47.799   117   117 I DEBUG   : Build fingerprint: 'product:build:target'",
+                "04-25 09:55:47.799  3064  3082 E AndroidRuntime: \tat class.method2(Class.java:2)",
+                "04-25 09:55:47.799  3065  3090 E AndroidRuntime: \tat class.method2(Class.java:2)",
+                "04-25 09:55:47.799   115   115 I DEBUG   : pid: 3112, tid: 3112  >>> com.google.android.browser <<<",
+                "04-25 09:55:47.799   117   117 I DEBUG   : pid: 3112, tid: 3112  >>> com.google.android.browser <<<",
+                "04-25 09:55:47.799  3064  3082 E AndroidRuntime: \tat class.method3(Class.java:3)",
+                "04-25 09:55:47.799  3065  3090 E AndroidRuntime: \tat class.method3(Class.java:3)",
+                "04-25 09:55:47.799   115   115 I DEBUG   : signal 11 (SIGSEGV), code 1 (SEGV_MAPERR), fault addr 00000000",
+                "04-25 09:55:47.799   117   117 I DEBUG   : signal 11 (SIGSEGV), code 1 (SEGV_MAPERR), fault addr 00000000");
+
+        LogcatItem logcat = new LogcatParser("2012").parse(lines);
+        assertNotNull(logcat);
+        assertEquals(parseTime("2012-04-25 09:55:47.799"), logcat.getStartTime());
+        assertEquals(parseTime("2012-04-25 09:55:47.799"), logcat.getStopTime());
+        assertEquals(4, logcat.getEvents().size());
+        assertEquals(0, logcat.getAnrs().size());
+        assertEquals(2, logcat.getJavaCrashes().size());
+        assertEquals(2, logcat.getNativeCrashes().size());
+
+        assertEquals(3064, logcat.getJavaCrashes().get(0).getPid().intValue());
+        assertEquals(3082, logcat.getJavaCrashes().get(0).getTid().intValue());
+        assertEquals(parseTime("2012-04-25 09:55:47.799"),
+                logcat.getJavaCrashes().get(0).getEventTime());
+
+        assertEquals(3065, logcat.getJavaCrashes().get(1).getPid().intValue());
+        assertEquals(3090, logcat.getJavaCrashes().get(1).getTid().intValue());
+        assertEquals(parseTime("2012-04-25 09:55:47.799"),
+                logcat.getJavaCrashes().get(1).getEventTime());
+
+        assertEquals(115, logcat.getNativeCrashes().get(0).getPid().intValue());
+        assertEquals(115, logcat.getNativeCrashes().get(0).getTid().intValue());
+        assertEquals(parseTime("2012-04-25 09:55:47.799"),
+                logcat.getNativeCrashes().get(0).getEventTime());
+
+        assertEquals(117, logcat.getNativeCrashes().get(1).getPid().intValue());
+        assertEquals(117, logcat.getNativeCrashes().get(1).getTid().intValue());
+        assertEquals(parseTime("2012-04-25 09:55:47.799"),
+                logcat.getNativeCrashes().get(1).getEventTime());
+    }
+
+    /**
+     * Test that the preambles are set correctly if there's only partial preambles.
+     */
+    public void testParse_partial_preambles() throws ParseException {
+        List<String> lines = Arrays.asList(
+                "04-25 09:15:47.799   123  3082 I tag: message 1",
+                "04-25 09:20:47.799  3064  3082 I tag: message 2",
+                "04-25 09:25:47.799   345  3082 I tag: message 3",
+                "04-25 09:30:47.799  3064  3082 I tag: message 4",
+                "04-25 09:35:47.799   456  3082 I tag: message 5",
+                "04-25 09:40:47.799  3064  3082 I tag: message 6",
+                "04-25 09:45:47.799   567  3082 I tag: message 7",
+                "04-25 09:50:47.799  3064  3082 I tag: message 8",
+                "04-25 09:55:47.799  3064  3082 E AndroidRuntime: java.lang.Exception",
+                "04-25 09:55:47.799  3064  3082 E AndroidRuntime: \tat class.method1(Class.java:1)",
+                "04-25 09:55:47.799  3064  3082 E AndroidRuntime: \tat class.method2(Class.java:2)",
+                "04-25 09:55:47.799  3064  3082 E AndroidRuntime: \tat class.method3(Class.java:3)");
+
+        List<String> expectedLastPreamble = Arrays.asList(
+                "04-25 09:15:47.799   123  3082 I tag: message 1",
+                "04-25 09:20:47.799  3064  3082 I tag: message 2",
+                "04-25 09:25:47.799   345  3082 I tag: message 3",
+                "04-25 09:30:47.799  3064  3082 I tag: message 4",
+                "04-25 09:35:47.799   456  3082 I tag: message 5",
+                "04-25 09:40:47.799  3064  3082 I tag: message 6",
+                "04-25 09:45:47.799   567  3082 I tag: message 7",
+                "04-25 09:50:47.799  3064  3082 I tag: message 8");
+
+        List<String> expectedProcPreamble = Arrays.asList(
+                "04-25 09:20:47.799  3064  3082 I tag: message 2",
+                "04-25 09:30:47.799  3064  3082 I tag: message 4",
+                "04-25 09:40:47.799  3064  3082 I tag: message 6",
+                "04-25 09:50:47.799  3064  3082 I tag: message 8");
+
+        LogcatItem logcat = new LogcatParser("2012").parse(lines);
+        assertNotNull(logcat);
+        assertEquals(parseTime("2012-04-25 09:15:47.799"), logcat.getStartTime());
+        assertEquals(parseTime("2012-04-25 09:55:47.799"), logcat.getStopTime());
+        assertEquals(1, logcat.getEvents().size());
+        assertEquals(1, logcat.getJavaCrashes().size());
+        assertEquals(3064, logcat.getJavaCrashes().get(0).getPid().intValue());
+        assertEquals(3082, logcat.getJavaCrashes().get(0).getTid().intValue());
+        assertEquals(ArrayUtil.join("\n", expectedLastPreamble),
+                logcat.getJavaCrashes().get(0).getLastPreamble());
+        assertEquals(ArrayUtil.join("\n", expectedProcPreamble),
+                logcat.getJavaCrashes().get(0).getProcessPreamble());
+        assertEquals(parseTime("2012-04-25 09:55:47.799"),
+                logcat.getJavaCrashes().get(0).getEventTime());
+    }
+
+    /**
+     * Test that the preambles are set correctly if there's only full preambles.
+     */
+    public void testParse_preambles() throws ParseException {
+        List<String> lines = Arrays.asList(
+                "04-25 09:43:47.799  3064  3082 I tag: message 1",
+                "04-25 09:44:47.799   123  3082 I tag: message 2",
+                "04-25 09:45:47.799  3064  3082 I tag: message 3",
+                "04-25 09:46:47.799   234  3082 I tag: message 4",
+                "04-25 09:47:47.799  3064  3082 I tag: message 5",
+                "04-25 09:48:47.799   345  3082 I tag: message 6",
+                "04-25 09:49:47.799  3064  3082 I tag: message 7",
+                "04-25 09:50:47.799   456  3082 I tag: message 8",
+                "04-25 09:55:47.799  3064  3082 E AndroidRuntime: java.lang.Exception",
+                "04-25 09:55:47.799  3064  3082 E AndroidRuntime: \tat class.method1(Class.java:1)",
+                "04-25 09:55:47.799  3064  3082 E AndroidRuntime: \tat class.method2(Class.java:2)",
+                "04-25 09:55:47.799  3064  3082 E AndroidRuntime: \tat class.method3(Class.java:3)");
+
+        List<String> expectedLastPreamble = Arrays.asList(
+                "04-25 09:48:47.799   345  3082 I tag: message 6",
+                "04-25 09:49:47.799  3064  3082 I tag: message 7",
+                "04-25 09:50:47.799   456  3082 I tag: message 8");
+
+        List<String> expectedProcPreamble = Arrays.asList(
+                "04-25 09:45:47.799  3064  3082 I tag: message 3",
+                "04-25 09:47:47.799  3064  3082 I tag: message 5",
+                "04-25 09:49:47.799  3064  3082 I tag: message 7");
+
+        LogcatItem logcat = new LogcatParser("2012") {
+            @Override
+            int getLastPreambleSize() {
+                return 3;
+            }
+
+            @Override
+            int getProcPreambleSize() {
+                return 3;
+            }
+        }.parse(lines);
+
+        assertNotNull(logcat);
+        assertEquals(parseTime("2012-04-25 09:43:47.799"), logcat.getStartTime());
+        assertEquals(parseTime("2012-04-25 09:55:47.799"), logcat.getStopTime());
+        assertEquals(1, logcat.getEvents().size());
+        assertEquals(1, logcat.getJavaCrashes().size());
+        assertEquals(3064, logcat.getJavaCrashes().get(0).getPid().intValue());
+        assertEquals(3082, logcat.getJavaCrashes().get(0).getTid().intValue());
+        assertEquals(ArrayUtil.join("\n", expectedLastPreamble),
+                logcat.getJavaCrashes().get(0).getLastPreamble());
+        assertEquals(ArrayUtil.join("\n", expectedProcPreamble),
+                logcat.getJavaCrashes().get(0).getProcessPreamble());
+        assertEquals(parseTime("2012-04-25 09:55:47.799"),
+                logcat.getJavaCrashes().get(0).getEventTime());
+    }
+
+    /**
+     * Test that the time logcat format can be parsed.
+     */
+    public void testParse_time() throws ParseException {
+        List<String> lines = Arrays.asList(
+                "04-25 09:55:47.799  E/AndroidRuntime(3064): java.lang.Exception",
+                "04-25 09:55:47.799  E/AndroidRuntime(3064): \tat class.method1(Class.java:1)",
+                "04-25 09:55:47.799  E/AndroidRuntime(3064): \tat class.method2(Class.java:2)",
+                "04-25 09:55:47.799  E/AndroidRuntime(3064): \tat class.method3(Class.java:3)");
+
+        LogcatItem logcat = new LogcatParser("2012").parse(lines);
+        assertNotNull(logcat);
+        assertEquals(parseTime("2012-04-25 09:55:47.799"), logcat.getStartTime());
+        assertEquals(parseTime("2012-04-25 09:55:47.799"), logcat.getStopTime());
+        assertEquals(1, logcat.getEvents().size());
+        assertEquals(1, logcat.getJavaCrashes().size());
+        assertEquals(3064, logcat.getJavaCrashes().get(0).getPid().intValue());
+        assertNull(logcat.getJavaCrashes().get(0).getTid());
+        assertEquals(parseTime("2012-04-25 09:55:47.799"),
+                logcat.getJavaCrashes().get(0).getEventTime());
+    }
+
+    private Date parseTime(String timeStr) throws ParseException {
+        DateFormat formatter = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
+        return formatter.parse(timeStr);
+    }
+}
diff --git a/tests/src/com/android/loganalysis/parser/MemInfoParserTest.java b/tests/src/com/android/loganalysis/parser/MemInfoParserTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..2069d12ecedfac10d4cae3fec841abfe126d9683
--- /dev/null
+++ b/tests/src/com/android/loganalysis/parser/MemInfoParserTest.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2011 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.loganalysis.parser;
+
+import com.android.loganalysis.item.MemInfoItem;
+
+import junit.framework.TestCase;
+
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * Unit tests for {@link MemInfoParser}
+ */
+public class MemInfoParserTest extends TestCase {
+    public void testMemInfoParser() {
+        List<String> inputBlock = Arrays.asList(
+                "MemTotal:         353332 kB",
+                "MemFree:           65420 kB",
+                "Buffers:           20800 kB",
+                "Cached:            86204 kB",
+                "SwapCached:            0 kB");
+        MemInfoParser parser = new MemInfoParser();
+        MemInfoItem output = parser.parse(inputBlock);
+
+        assertEquals(5, output.size());
+        assertEquals((Integer)353332, output.get("MemTotal"));
+        assertEquals((Integer)65420, output.get("MemFree"));
+        assertEquals((Integer)20800, output.get("Buffers"));
+        assertEquals((Integer)86204, output.get("Cached"));
+        assertEquals((Integer)0, output.get("SwapCached"));
+    }
+}
diff --git a/tests/src/com/android/loganalysis/parser/MonkeyLogParserFuncTest.java b/tests/src/com/android/loganalysis/parser/MonkeyLogParserFuncTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..58fcebe4faebd873de828080b251397b7b742bb5
--- /dev/null
+++ b/tests/src/com/android/loganalysis/parser/MonkeyLogParserFuncTest.java
@@ -0,0 +1,121 @@
+/*
+ * Copyright (C) 2011 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.loganalysis.parser;
+
+import com.android.loganalysis.item.AnrItem;
+import com.android.loganalysis.item.JavaCrashItem;
+import com.android.loganalysis.item.MonkeyLogItem;
+import com.android.loganalysis.item.MonkeyLogItem.DroppedCategory;
+
+import junit.framework.TestCase;
+
+import java.io.BufferedReader;
+import java.io.FileNotFoundException;
+import java.io.FileReader;
+import java.io.IOException;
+
+/**
+ * Functional tests for {@link MonkeyLogParser}
+ */
+public class MonkeyLogParserFuncTest extends TestCase {
+    // FIXME: Make monkey log file configurable.
+    private static final String MONKEY_LOG_PATH = "/tmp/monkey_log.txt";
+
+    /**
+     * A test that is intended to force Brillopad to parse a monkey log. The purpose of this is to
+     * assist a developer in checking why a given monkey log file might not be parsed correctly by
+     * Brillopad.
+     */
+    public void testParse() {
+        BufferedReader monkeyLogReader = null;
+        try {
+            monkeyLogReader = new BufferedReader(new FileReader(MONKEY_LOG_PATH));
+        } catch (FileNotFoundException e) {
+            fail(String.format("File not found at %s", MONKEY_LOG_PATH));
+        }
+        MonkeyLogItem monkeyLog = null;
+        try {
+            long start = System.currentTimeMillis();
+            monkeyLog = new MonkeyLogParser().parse(monkeyLogReader);
+            long stop = System.currentTimeMillis();
+            System.out.println(String.format("Monkey log took %d ms to parse.", stop - start));
+        } catch (IOException e) {
+            fail(String.format("IOException: %s", e.toString()));
+        } finally {
+            if (monkeyLogReader != null) {
+                try {
+                    monkeyLogReader.close();
+                } catch (IOException e) {
+                    // Ignore
+                }
+            }        }
+
+        assertNotNull(monkeyLog);
+        assertNotNull(monkeyLog.getStartTime());
+        assertNotNull(monkeyLog.getStopTime());
+        assertNotNull(monkeyLog.getTargetCount());
+        assertNotNull(monkeyLog.getThrottle());
+        assertNotNull(monkeyLog.getSeed());
+        assertNotNull(monkeyLog.getIgnoreSecurityExceptions());
+        assertTrue(monkeyLog.getPackages().size() > 0);
+        assertTrue(monkeyLog.getCategories().size() > 0);
+        assertNotNull(monkeyLog.getIsFinished());
+        assertNotNull(monkeyLog.getIntermediateCount());
+        assertNotNull(monkeyLog.getTotalDuration());
+        assertNotNull(monkeyLog.getStartUptimeDuration());
+        assertNotNull(monkeyLog.getStopUptimeDuration());
+
+
+        StringBuffer sb = new StringBuffer();
+        sb.append("Stats for monkey log:\n");
+        sb.append(String.format("  Start time: %s\n", monkeyLog.getStartTime()));
+        sb.append(String.format("  Stop time: %s\n", monkeyLog.getStopTime()));
+        sb.append(String.format("  Parameters: target-count=%d, throttle=%d, seed=%d, " +
+                "ignore-security-exceptions=%b\n",
+                monkeyLog.getTargetCount(), monkeyLog.getThrottle(), monkeyLog.getSeed(),
+                monkeyLog.getIgnoreSecurityExceptions()));
+        sb.append(String.format("  Packages: %s\n", monkeyLog.getPackages()));
+        sb.append(String.format("  Categories: %s\n", monkeyLog.getCategories()));
+        if (monkeyLog.getNoActivities()) {
+            sb.append("  Status: no-activities=true\n");
+        } else {
+            sb.append(String.format("  Status: finished=%b, final-count=%d, " +
+                    "intermediate-count=%d\n", monkeyLog.getIsFinished(), monkeyLog.getFinalCount(),
+                    monkeyLog.getIntermediateCount()));
+
+            sb.append("  Dropped events:");
+            for (DroppedCategory drop : DroppedCategory.values()) {
+                sb.append(String.format(" %s=%d,", drop.toString(),
+                        monkeyLog.getDroppedCount(drop)));
+            }
+            sb.deleteCharAt(sb.length()-1);
+            sb.append("\n");
+        }
+        sb.append(String.format("  Run time: duration=%d ms, delta-uptime=%d (%d - %d) ms\n",
+                monkeyLog.getTotalDuration(),
+                monkeyLog.getStopUptimeDuration() - monkeyLog.getStartUptimeDuration(),
+                monkeyLog.getStopUptimeDuration(), monkeyLog.getStartUptimeDuration()));
+
+        if (monkeyLog.getCrash() != null && monkeyLog.getCrash() instanceof AnrItem) {
+            sb.append(String.format("  Stopped due to ANR\n"));
+        }
+        if (monkeyLog.getCrash() != null && monkeyLog.getCrash() instanceof JavaCrashItem) {
+            sb.append(String.format("  Stopped due to Java crash\n"));
+        }
+        System.out.println(sb.toString());
+    }
+}
+
diff --git a/tests/src/com/android/loganalysis/parser/MonkeyLogParserTest.java b/tests/src/com/android/loganalysis/parser/MonkeyLogParserTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..d5aef867bff0c5a5162ca1e9bbba23dafbb490ad
--- /dev/null
+++ b/tests/src/com/android/loganalysis/parser/MonkeyLogParserTest.java
@@ -0,0 +1,352 @@
+/*
+ * Copyright (C) 2012 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.loganalysis.parser;
+
+import com.android.loganalysis.item.AnrItem;
+import com.android.loganalysis.item.JavaCrashItem;
+import com.android.loganalysis.item.MonkeyLogItem;
+import com.android.loganalysis.item.MonkeyLogItem.DroppedCategory;
+import com.android.loganalysis.util.ArrayUtil;
+
+import junit.framework.TestCase;
+
+import java.text.DateFormat;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.Arrays;
+import java.util.Date;
+import java.util.List;
+
+/**
+ * Unit tests for {@link MonkeyLogParser}
+ */
+public class MonkeyLogParserTest extends TestCase {
+
+    /**
+     * Test that a monkey can be parsed if there are no crashes.
+     */
+    public void testParse_success() throws ParseException {
+        List<String> lines = Arrays.asList(
+                "# Wednesday, 04/25/2012 01:37:12 AM - device uptime = 242.13: Monkey command used for this test:",
+                "adb shell monkey -p com.google.android.browser  -c android.intent.category.SAMPLE_CODE -c android.intent.category.CAR_DOCK -c android.intent.category.LAUNCHER -c android.intent.category.MONKEY -c android.intent.category.INFO  --ignore-security-exceptions --throttle 100  -s 528 -v -v -v 10000 ",
+                "",
+                ":Monkey: seed=528 count=10000",
+                ":AllowPackage: com.google.android.browser",
+                ":IncludeCategory: android.intent.category.LAUNCHER",
+                ":Switch: #Intent;action=android.intent.action.MAIN;category=android.intent.category.LAUNCHER;launchFlags=0x10200000;component=com.google.android.browser/com.android.browser.BrowserActivity;end",
+                "    // Allowing start of Intent { act=android.intent.action.MAIN cat=[android.intent.category.LAUNCHER] cmp=com.google.android.browser/com.android.browser.BrowserActivity } in package com.google.android.browser",
+                "Sleeping for 100 milliseconds",
+                ":Sending Key (ACTION_DOWN): 23    // KEYCODE_DPAD_CENTER",
+                ":Sending Key (ACTION_UP): 23    // KEYCODE_DPAD_CENTER",
+                "Sleeping for 100 milliseconds",
+                ":Sending Trackball (ACTION_MOVE): 0:(-5.0,3.0)",
+                ":Sending Trackball (ACTION_MOVE): 0:(3.0,3.0)",
+                ":Sending Trackball (ACTION_MOVE): 0:(-1.0,3.0)",
+                ":Sending Trackball (ACTION_MOVE): 0:(4.0,-2.0)",
+                ":Sending Trackball (ACTION_MOVE): 0:(1.0,4.0)",
+                ":Sending Trackball (ACTION_MOVE): 0:(-4.0,2.0)",
+                "    //[calendar_time:2012-04-25 01:42:20.140  system_uptime:535179]",
+                "    // Sending event #9900",
+                ":Sending Trackball (ACTION_MOVE): 0:(2.0,-4.0)",
+                ":Sending Trackball (ACTION_MOVE): 0:(-2.0,0.0)",
+                ":Sending Trackball (ACTION_MOVE): 0:(2.0,2.0)",
+                ":Sending Trackball (ACTION_MOVE): 0:(-5.0,4.0)",
+                "Events injected: 10000",
+                ":Dropped: keys=5 pointers=6 trackballs=7 flips=8 rotations=9",
+                "// Monkey finished",
+                "",
+                "# Wednesday, 04/25/2012 01:42:09 AM - device uptime = 539.21: Monkey command ran for: 04:57 (mm:ss)",
+                "",
+                "----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------");
+
+        MonkeyLogItem monkeyLog = new MonkeyLogParser().parse(lines);
+        assertNotNull(monkeyLog);
+        // FIXME: Add test back once time situation has been worked out.
+        // assertEquals(parseTime("2012-04-25 01:37:12"), monkeyLog.getStartTime());
+        // assertEquals(parseTime("2012-04-25 01:42:09"), monkeyLog.getStopTime());
+        assertEquals(1, monkeyLog.getPackages().size());
+        assertTrue(monkeyLog.getPackages().contains("com.google.android.browser"));
+        assertEquals(1, monkeyLog.getCategories().size());
+        assertTrue(monkeyLog.getCategories().contains("android.intent.category.LAUNCHER"));
+        assertEquals(100, monkeyLog.getThrottle());
+        assertEquals(528, monkeyLog.getSeed().intValue());
+        assertEquals(10000, monkeyLog.getTargetCount().intValue());
+        assertTrue(monkeyLog.getIgnoreSecurityExceptions());
+        assertEquals(4 * 60 * 1000 + 57 * 1000, monkeyLog.getTotalDuration().longValue());
+        assertEquals(242130, monkeyLog.getStartUptimeDuration().longValue());
+        assertEquals(539210, monkeyLog.getStopUptimeDuration().longValue());
+        assertTrue(monkeyLog.getIsFinished());
+        assertFalse(monkeyLog.getNoActivities());
+        assertEquals(9900, monkeyLog.getIntermediateCount());
+        assertEquals(10000, monkeyLog.getFinalCount().intValue());
+        assertEquals(5, monkeyLog.getDroppedCount(DroppedCategory.KEYS).intValue());
+        assertEquals(6, monkeyLog.getDroppedCount(DroppedCategory.POINTERS).intValue());
+        assertEquals(7, monkeyLog.getDroppedCount(DroppedCategory.TRACKBALLS).intValue());
+        assertEquals(8, monkeyLog.getDroppedCount(DroppedCategory.FLIPS).intValue());
+        assertEquals(9, monkeyLog.getDroppedCount(DroppedCategory.ROTATIONS).intValue());
+        assertNull(monkeyLog.getCrash());
+    }
+
+    /**
+     * Test that a monkey can be parsed if there is an ANR.
+     */
+    public void testParse_anr() throws ParseException {
+        List<String> lines = Arrays.asList(
+                "# Tuesday, 04/24/2012 05:23:30 PM - device uptime = 216.48: Monkey command used for this test:",
+                "adb shell monkey -p com.google.android.youtube  -c android.intent.category.SAMPLE_CODE -c android.intent.category.CAR_DOCK -c android.intent.category.LAUNCHER -c android.intent.category.MONKEY -c android.intent.category.INFO  --ignore-security-exceptions --throttle 100  -s 993 -v -v -v 10000 ",
+                "",
+                ":Monkey: seed=993 count=10000",
+                ":AllowPackage: com.google.android.youtube",
+                ":IncludeCategory: android.intent.category.LAUNCHER",
+                ":Switch: #Intent;action=android.intent.action.MAIN;category=android.intent.category.LAUNCHER;launchFlags=0x10200000;component=com.google.android.youtube/.app.honeycomb.Shell%24HomeActivity;end",
+                "    // Allowing start of Intent { act=android.intent.action.MAIN cat=[android.intent.category.LAUNCHER] cmp=com.google.android.youtube/.app.honeycomb.Shell$HomeActivity } in package com.google.android.youtube",
+                "Sleeping for 100 milliseconds",
+                ":Sending Key (ACTION_UP): 21    // KEYCODE_DPAD_LEFT",
+                "Sleeping for 100 milliseconds",
+                ":Sending Key (ACTION_DOWN): 22    // KEYCODE_DPAD_RIGHT",
+                ":Sending Key (ACTION_UP): 22    // KEYCODE_DPAD_RIGHT",
+                "    //[calendar_time:2012-04-25 00:27:27.155  system_uptime:454996]",
+                "    // Sending event #5300",
+                ":Sending Key (ACTION_UP): 19    // KEYCODE_DPAD_UP",
+                "Sleeping for 100 milliseconds",
+                ":Sending Trackball (ACTION_MOVE): 0:(4.0,3.0)",
+                ":Sending Key (ACTION_DOWN): 20    // KEYCODE_DPAD_DOWN",
+                ":Sending Key (ACTION_UP): 20    // KEYCODE_DPAD_DOWN",
+                "// NOT RESPONDING: com.google.android.youtube (pid 3301)",
+                "ANR in com.google.android.youtube (com.google.android.youtube/.app.honeycomb.phone.WatchActivity)",
+                "Reason: keyDispatchingTimedOut",
+                "Load: 1.0 / 1.05 / 0.6",
+                "CPU usage from 4794ms to -1502ms ago with 99% awake:",
+                "  18% 3301/com.google.android.youtube: 16% user + 2.3% kernel / faults: 268 minor 9 major",
+                "  13% 313/system_server: 9.2% user + 4.4% kernel / faults: 906 minor 3 major",
+                "  10% 117/surfaceflinger: 4.9% user + 5.5% kernel / faults: 1 minor",
+                "  10% 120/mediaserver: 6.8% user + 3.6% kernel / faults: 1189 minor",
+                "34% TOTAL: 19% user + 13% kernel + 0.2% iowait + 1% softirq",
+                "",
+                "procrank:",
+                "// procrank status was 0",
+                "anr traces:",
+                "",
+                "",
+                "----- pid 2887 at 2012-04-25 17:17:08 -----",
+                "Cmd line: com.google.android.youtube",
+                "",
+                "DALVIK THREADS:",
+                "(mutexes: tll=0 tsl=0 tscl=0 ghl=0)",
+                "",
+                "\"main\" prio=5 tid=1 SUSPENDED",
+                "  | group=\"main\" sCount=1 dsCount=0 obj=0x00000001 self=0x00000001",
+                "  | sysTid=2887 nice=0 sched=0/0 cgrp=foreground handle=0000000001",
+                "  | schedstat=( 0 0 0 ) utm=5954 stm=1017 core=0",
+                "  at class.method1(Class.java:1)",
+                "  at class.method2(Class.java:2)",
+                "  at class.method2(Class.java:2)",
+                "",
+                "----- end 2887 -----",
+                "// anr traces status was 0",
+                "** Monkey aborted due to error.",
+                "Events injected: 5322",
+                ":Sending rotation degree=0, persist=false",
+                ":Dropped: keys=1 pointers=0 trackballs=0 flips=0 rotations=0",
+                "## Network stats: elapsed time=252942ms (0ms mobile, 252942ms wifi, 0ms not connected)",
+                "** System appears to have crashed at event 5322 of 10000 using seed 993",
+                "",
+                "# Tuesday, 04/24/2012 05:27:44 PM - device uptime = 471.37: Monkey command ran for: 04:14 (mm:ss)",
+                "",
+                "----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------",
+                "");
+
+        List<String> expectedStack = Arrays.asList(
+                "\"main\" prio=5 tid=1 SUSPENDED",
+                "  | group=\"main\" sCount=1 dsCount=0 obj=0x00000001 self=0x00000001",
+                "  | sysTid=2887 nice=0 sched=0/0 cgrp=foreground handle=0000000001",
+                "  | schedstat=( 0 0 0 ) utm=5954 stm=1017 core=0",
+                "  at class.method1(Class.java:1)",
+                "  at class.method2(Class.java:2)",
+                "  at class.method2(Class.java:2)");
+
+        MonkeyLogItem monkeyLog = new MonkeyLogParser().parse(lines);
+        assertNotNull(monkeyLog);
+        // FIXME: Add test back once time situation has been worked out.
+        // assertEquals(parseTime("2012-04-24 17:23:30"), monkeyLog.getStartTime());
+        // assertEquals(parseTime("2012-04-24 17:27:44"), monkeyLog.getStopTime());
+        assertEquals(1, monkeyLog.getPackages().size());
+        assertTrue(monkeyLog.getPackages().contains("com.google.android.youtube"));
+        assertEquals(1, monkeyLog.getCategories().size());
+        assertTrue(monkeyLog.getCategories().contains("android.intent.category.LAUNCHER"));
+        assertEquals(100, monkeyLog.getThrottle());
+        assertEquals(993, monkeyLog.getSeed().intValue());
+        assertEquals(10000, monkeyLog.getTargetCount().intValue());
+        assertTrue(monkeyLog.getIgnoreSecurityExceptions());
+        assertEquals(4 * 60 * 1000 + 14 * 1000, monkeyLog.getTotalDuration().longValue());
+        assertEquals(216480, monkeyLog.getStartUptimeDuration().longValue());
+        assertEquals(471370, monkeyLog.getStopUptimeDuration().longValue());
+        assertFalse(monkeyLog.getIsFinished());
+        assertFalse(monkeyLog.getNoActivities());
+        assertEquals(5300, monkeyLog.getIntermediateCount());
+        assertEquals(5322, monkeyLog.getFinalCount().intValue());
+        assertNotNull(monkeyLog.getCrash());
+        assertTrue(monkeyLog.getCrash() instanceof AnrItem);
+        assertEquals("com.google.android.youtube", monkeyLog.getCrash().getApp());
+        assertEquals(3301, monkeyLog.getCrash().getPid().intValue());
+        assertEquals("keyDispatchingTimedOut", ((AnrItem) monkeyLog.getCrash()).getReason());
+        assertEquals(ArrayUtil.join("\n", expectedStack),
+                ((AnrItem) monkeyLog.getCrash()).getTrace());
+    }
+
+    /**
+     * Test that a monkey can be parsed if there is a JavaCrash.
+     */
+    public void testParse_java_crash() throws ParseException {
+        List<String> lines = Arrays.asList(
+                "# Tuesday, 04/24/2012 05:05:50 PM - device uptime = 232.65: Monkey command used for this test:",
+                "adb shell monkey -p com.google.android.apps.maps  -c android.intent.category.SAMPLE_CODE -c android.intent.category.CAR_DOCK -c android.intent.category.LAUNCHER -c android.intent.category.MONKEY -c android.intent.category.INFO  --ignore-security-exceptions --throttle 100  -s 501 -v -v -v 10000 ",
+                "",
+                ":Monkey: seed=501 count=10000",
+                ":AllowPackage: com.google.android.apps.maps",
+                ":IncludeCategory: android.intent.category.LAUNCHER",
+                ":Switch: #Intent;action=android.intent.action.MAIN;category=android.intent.category.LAUNCHER;launchFlags=0x10200000;component=com.google.android.apps.maps/com.google.android.maps.LatitudeActivity;end",
+                "    // Allowing start of Intent { act=android.intent.action.MAIN cat=[android.intent.category.LAUNCHER] cmp=com.google.android.apps.maps/com.google.android.maps.LatitudeActivity } in package com.google.android.apps.maps",
+                "Sleeping for 100 milliseconds",
+                ":Sending Touch (ACTION_DOWN): 0:(332.0,70.0)",
+                ":Sending Touch (ACTION_UP): 0:(332.55292,76.54678)",
+                "    //[calendar_time:2012-04-25 00:06:38.419  system_uptime:280799]",
+                "    // Sending event #1600",
+                ":Sending Touch (ACTION_MOVE): 0:(1052.2666,677.64594)",
+                ":Sending Touch (ACTION_UP): 0:(1054.7593,687.3757)",
+                "Sleeping for 100 milliseconds",
+                "// CRASH: com.google.android.apps.maps (pid 3161)",
+                "// Short Msg: java.lang.Exception",
+                "// Long Msg: java.lang.Exception: This is the message",
+                "// Build Label: google/yakju/maguro:JellyBean/JRN24B/338896:userdebug/dev-keys",
+                "// Build Changelist: 338896",
+                "// Build Time: 1335309051000",
+                "// java.lang.Exception: This is the message",
+                "// \tat class.method1(Class.java:1)",
+                "// \tat class.method2(Class.java:2)",
+                "// \tat class.method3(Class.java:3)",
+                "// ",
+                "** Monkey aborted due to error.",
+                "Events injected: 1649",
+                ":Sending rotation degree=0, persist=false",
+                ":Dropped: keys=0 pointers=0 trackballs=0 flips=0 rotations=0",
+                "## Network stats: elapsed time=48897ms (0ms mobile, 48897ms wifi, 0ms not connected)",
+                "** System appears to have crashed at event 1649 of 10000 using seed 501",
+                "",
+                "# Tuesday, 04/24/2012 05:06:40 PM - device uptime = 282.53: Monkey command ran for: 00:49 (mm:ss)",
+                "",
+                "----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------",
+                "");
+
+        MonkeyLogItem monkeyLog = new MonkeyLogParser().parse(lines);
+        assertNotNull(monkeyLog);
+        // FIXME: Add test back once time situation has been worked out.
+        // assertEquals(parseTime("2012-04-24 17:05:50"), monkeyLog.getStartTime());
+        // assertEquals(parseTime("2012-04-24 17:06:40"), monkeyLog.getStopTime());
+        assertEquals(1, monkeyLog.getPackages().size());
+        assertTrue(monkeyLog.getPackages().contains("com.google.android.apps.maps"));
+        assertEquals(1, monkeyLog.getCategories().size());
+        assertTrue(monkeyLog.getCategories().contains("android.intent.category.LAUNCHER"));
+        assertEquals(100, monkeyLog.getThrottle());
+        assertEquals(501, monkeyLog.getSeed().intValue());
+        assertEquals(10000, monkeyLog.getTargetCount().intValue());
+        assertTrue(monkeyLog.getIgnoreSecurityExceptions());
+        assertEquals(49 * 1000, monkeyLog.getTotalDuration().longValue());
+        assertEquals(232650, monkeyLog.getStartUptimeDuration().longValue());
+        assertEquals(282530, monkeyLog.getStopUptimeDuration().longValue());
+        assertFalse(monkeyLog.getIsFinished());
+        assertFalse(monkeyLog.getNoActivities());
+        assertEquals(1600, monkeyLog.getIntermediateCount());
+        assertEquals(1649, monkeyLog.getFinalCount().intValue());
+        assertNotNull(monkeyLog.getCrash());
+        assertTrue(monkeyLog.getCrash() instanceof JavaCrashItem);
+        assertEquals("com.google.android.apps.maps", monkeyLog.getCrash().getApp());
+        assertEquals(3161, monkeyLog.getCrash().getPid().intValue());
+        assertEquals("java.lang.Exception", ((JavaCrashItem) monkeyLog.getCrash()).getException());
+    }
+
+    /**
+     * Test that a monkey can be parsed if there are no activities to run.
+     */
+    public void testParse_no_activities() throws ParseException {
+        List<String> lines = Arrays.asList(
+                "# Wednesday, 04/25/2012 01:37:12 AM - device uptime = 242.13: Monkey command used for this test:",
+                "adb shell monkey -p com.google.android.browser  -c android.intent.category.SAMPLE_CODE -c android.intent.category.CAR_DOCK -c android.intent.category.LAUNCHER -c android.intent.category.MONKEY -c android.intent.category.INFO  --ignore-security-exceptions --throttle 100  -s 528 -v -v -v 10000 ",
+                "",
+                ":Monkey: seed=528 count=10000",
+                ":AllowPackage: com.google.android.browser",
+                ":IncludeCategory: android.intent.category.LAUNCHER",
+                "** No activities found to run, monkey aborted.",
+                "",
+                "# Wednesday, 04/25/2012 01:42:09 AM - device uptime = 539.21: Monkey command ran for: 04:57 (mm:ss)",
+                "",
+                "----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------");
+
+        MonkeyLogItem monkeyLog = new MonkeyLogParser().parse(lines);
+        assertNotNull(monkeyLog);
+        // FIXME: Add test back once time situation has been worked out.
+        // assertEquals(parseTime("2012-04-25 01:37:12"), monkeyLog.getStartTime());
+        // assertEquals(parseTime("2012-04-25 01:42:09"), monkeyLog.getStopTime());
+        assertEquals(1, monkeyLog.getPackages().size());
+        assertTrue(monkeyLog.getPackages().contains("com.google.android.browser"));
+        assertEquals(1, monkeyLog.getCategories().size());
+        assertTrue(monkeyLog.getCategories().contains("android.intent.category.LAUNCHER"));
+        assertEquals(100, monkeyLog.getThrottle());
+        assertEquals(528, monkeyLog.getSeed().intValue());
+        assertEquals(10000, monkeyLog.getTargetCount().intValue());
+        assertTrue(monkeyLog.getIgnoreSecurityExceptions());
+        assertEquals(4 * 60 * 1000 + 57 * 1000, monkeyLog.getTotalDuration().longValue());
+        assertEquals(242130, monkeyLog.getStartUptimeDuration().longValue());
+        assertEquals(539210, monkeyLog.getStopUptimeDuration().longValue());
+        assertFalse(monkeyLog.getIsFinished());
+        assertTrue(monkeyLog.getNoActivities());
+        assertEquals(0, monkeyLog.getIntermediateCount());
+        assertNull(monkeyLog.getFinalCount());
+        assertNull(monkeyLog.getDroppedCount(DroppedCategory.KEYS));
+        assertNull(monkeyLog.getDroppedCount(DroppedCategory.POINTERS));
+        assertNull(monkeyLog.getDroppedCount(DroppedCategory.TRACKBALLS));
+        assertNull(monkeyLog.getDroppedCount(DroppedCategory.FLIPS));
+        assertNull(monkeyLog.getDroppedCount(DroppedCategory.ROTATIONS));
+        assertNull(monkeyLog.getCrash());
+    }
+
+    /**
+     * Test that the other date format can be parsed.
+     */
+    public void testAlternateDateFormat() throws ParseException {
+        List<String> lines = Arrays.asList(
+                "# Tue Apr 24 17:05:50 PST 2012 - device uptime = 232.65: Monkey command used for this test:",
+                "adb shell monkey -p com.google.android.apps.maps  -c android.intent.category.SAMPLE_CODE -c android.intent.category.CAR_DOCK -c android.intent.category.LAUNCHER -c android.intent.category.MONKEY -c android.intent.category.INFO  --ignore-security-exceptions --throttle 100  -s 501 -v -v -v 10000 ",
+                "",
+                "# Tue Apr 24 17:06:40 PST 2012 - device uptime = 282.53: Monkey command ran for: 00:49 (mm:ss)",
+                "",
+                "----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------",
+                "");
+
+        MonkeyLogItem monkeyLog = new MonkeyLogParser().parse(lines);
+        assertNotNull(monkeyLog);
+        // FIXME: Add test back once time situation has been worked out.
+        // assertEquals(parseTime("2012-04-24 17:05:50"), monkeyLog.getStartTime());
+        // assertEquals(parseTime("2012-04-24 17:06:40"), monkeyLog.getStopTime());
+    }
+
+    @SuppressWarnings("unused")
+    private Date parseTime(String timeStr) throws ParseException {
+        DateFormat formatter = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
+        return formatter.parse(timeStr);
+    }
+}
+
diff --git a/tests/src/com/android/loganalysis/parser/NativeCrashParserTest.java b/tests/src/com/android/loganalysis/parser/NativeCrashParserTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..fa6349910fbb7ae16b3d13e6695c7bdbc5b6d35f
--- /dev/null
+++ b/tests/src/com/android/loganalysis/parser/NativeCrashParserTest.java
@@ -0,0 +1,173 @@
+/*
+ * Copyright (C) 2012 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.loganalysis.parser;
+
+import com.android.loganalysis.item.NativeCrashItem;
+import com.android.loganalysis.util.ArrayUtil;
+
+import junit.framework.TestCase;
+
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * Unit tests for {@link NativeCrashParser}.
+ */
+public class NativeCrashParserTest extends TestCase {
+
+    /**
+     * Test that native crashes are parsed.
+     */
+    public void testParseage() {
+        List<String> lines = Arrays.asList(
+                "*** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***",
+                "Build fingerprint: 'google/soju/crespo:4.0.4/IMM76D/299849:userdebug/test-keys'",
+                "pid: 2058, tid: 2523  >>> com.google.android.browser <<<",
+                "signal 11 (SIGSEGV), code 1 (SEGV_MAPERR), fault addr 00000000",
+                " r0 00000000  r1 007d9064  r2 007d9063  r3 00000004",
+                " r4 006bf518  r5 0091e3b0  r6 00000000  r7 9e3779b9",
+                " r8 000006c1  r9 000006c3  10 00000000  fp 67d246c1",
+                " ip d2363b58  sp 50ed71d8  lr 4edfc89b  pc 4edfc6a0  cpsr 20000030",
+                " d0  00640065005f0065  d1  0072006f00740069",
+                " d2  00730075006e006b  d3  0066006900670000",
+                " d4  00e6d48800e6d3b8  d5  02d517a000e6d518",
+                " d6  0000270f02d51860  d7  0000000002d51a80",
+                " d8  41d3dc5261e7893b  d9  3fa999999999999a",
+                " d10 0000000000000000  d11 0000000000000000",
+                " d12 0000000000000000  d13 0000000000000000",
+                " d14 0000000000000000  d15 0000000000000000",
+                " d16 4070000000000000  d17 40c3878000000000",
+                " d18 412310f000000000  d19 3f91800dedacf040",
+                " d20 0000000000000000  d21 0000000000000000",
+                " d22 4010000000000000  d23 0000000000000000",
+                " d24 3ff0000000000000  d25 0000000000000000",
+                " d26 0000000000000000  d27 8000000000000000",
+                " d28 0000000000000000  d29 3ff0000000000000",
+                " d30 0000000000000000  d31 3ff0000000000000",
+                " scr 20000013",
+                "",
+                "         #00  pc 001236a0  /system/lib/libwebcore.so",
+                "         #01  pc 00123896  /system/lib/libwebcore.so",
+                "         #02  pc 00123932  /system/lib/libwebcore.so",
+                "         #03  pc 00123e3a  /system/lib/libwebcore.so",
+                "         #04  pc 00123e84  /system/lib/libwebcore.so",
+                "         #05  pc 003db92a  /system/lib/libwebcore.so",
+                "         #06  pc 003dd01c  /system/lib/libwebcore.so",
+                "         #07  pc 002ffb92  /system/lib/libwebcore.so",
+                "         #08  pc 0031c120  /system/lib/libwebcore.so",
+                "         #09  pc 0031c134  /system/lib/libwebcore.so",
+                "         #10  pc 0013fb98  /system/lib/libwebcore.so",
+                "         #11  pc 0015b026  /system/lib/libwebcore.so",
+                "         #12  pc 0015b164  /system/lib/libwebcore.so",
+                "         #13  pc 0015f4cc  /system/lib/libwebcore.so",
+                "         #14  pc 00170472  /system/lib/libwebcore.so",
+                "         #15  pc 0016ecb6  /system/lib/libwebcore.so",
+                "         #16  pc 0027120e  /system/lib/libwebcore.so",
+                "         #17  pc 0026efec  /system/lib/libwebcore.so",
+                "         #18  pc 0026fcd8  /system/lib/libwebcore.so",
+                "         #19  pc 00122efa  /system/lib/libwebcore.so",
+                "",
+                "code around pc:",
+                "4edfc680 4a14b5f7 0601f001 23000849 3004f88d  ...J....I..#...0",
+                "4edfc690 460a9200 3006f8ad e00e4603 3a019f00  ...F...0.F.....:",
+                "4edfc6a0 5c04f833 f83319ed 042c7c02 2cc7ea84  3..\\..3..|,....,",
+                "4edfc6b0 0405ea8c 24d4eb04 33049400 d1ed2a00  .......$...3.*..",
+                "4edfc6c0 f830b126 46681021 ff72f7ff f7ff4668  &.0.!.hF..r.hF..",
+                "",
+                "code around lr:",
+                "4edfc878 f9caf7ff 60209e03 9605e037 5b04f856  ...... `7...V..[",
+                "4edfc888 d0302d00 d13b1c6b 68a8e02d f7ff6869  .-0.k.;.-..hih..",
+                "4edfc898 6128fef3 b010f8d5 99022500 ea0146aa  ..(a.....%...F..",
+                "4edfc8a8 9b01080b 0788eb03 3028f853 b9bdb90b  ........S.(0....",
+                "4edfc8b8 3301e015 4638d005 f7ff9905 b970ff15  ...3..8F......p.",
+                "",
+                "stack:",
+                "    50ed7198  01d02c08  [heap]",
+                "    50ed719c  40045881  /system/lib/libc.so",
+                "    50ed71a0  400784c8",
+                "    50ed71a4  400784c8",
+                "    50ed71a8  02b40c68  [heap]",
+                "    50ed71ac  02b40c90  [heap]",
+                "    50ed71b0  50ed7290",
+                "    50ed71b4  006bf518  [heap]",
+                "    50ed71b8  00010000",
+                "    50ed71bc  50ed72a4",
+                "    50ed71c0  7da5a695",
+                "    50ed71c4  50ed7290",
+                "    50ed71c8  00000000",
+                "    50ed71cc  00000008",
+                "    50ed71d0  df0027ad",
+                "    50ed71d4  00000000",
+                "#00 50ed71d8  9e3779b9",
+                "    50ed71dc  00002000",
+                "    50ed71e0  00004000",
+                "    50ed71e4  006bf518  [heap]",
+                "    50ed71e8  0091e3b0  [heap]",
+                "    50ed71ec  01d72588  [heap]",
+                "    50ed71f0  00000000",
+                "    50ed71f4  4edfc89b  /system/lib/libwebcore.so",
+                "#01 50ed71f8  01d70a78  [heap]",
+                "    50ed71fc  02b6afa8  [heap]",
+                "    50ed7200  00003fff",
+                "    50ed7204  01d70a78  [heap]",
+                "    50ed7208  00004000",
+                "    50ed720c  01d72584  [heap]",
+                "    50ed7210  00000000",
+                "    50ed7214  00000006",
+                "    50ed7218  006bf518  [heap]",
+                "    50ed721c  50ed72a4",
+                "    50ed7220  7da5a695",
+                "    50ed7224  50ed7290",
+                "    50ed7228  000016b8",
+                "    50ed722c  00000008",
+                "    50ed7230  01d70a78  [heap]",
+                "    50ed7234  4edfc937  /system/lib/libwebcore.so",
+                "debuggerd committing suicide to free the zombie!",
+                "debuggerd");
+
+        NativeCrashItem nc = new NativeCrashParser().parse(lines);
+        assertNotNull(nc);
+        assertEquals("com.google.android.browser", nc.getApp());
+        assertEquals("google/soju/crespo:4.0.4/IMM76D/299849:userdebug/test-keys",
+                nc.getFingerprint());
+        assertEquals(ArrayUtil.join("\n", lines), nc.getStack());
+    }
+
+    /**
+     * Test that both types of native crash app lines are parsed.
+     */
+    public void testParseApp() {
+        List<String> lines = Arrays.asList(
+                "*** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***",
+                "Build fingerprint: 'google/soju/crespo:4.0.4/IMM76D/299849:userdebug/test-keys'",
+                "pid: 2058, tid: 2523  >>> com.google.android.browser <<<",
+                "signal 11 (SIGSEGV), code 1 (SEGV_MAPERR), fault addr 00000000");
+
+        NativeCrashItem nc = new NativeCrashParser().parse(lines);
+        assertNotNull(nc);
+        assertEquals("com.google.android.browser", nc.getApp());
+
+        lines = Arrays.asList(
+                "*** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***",
+                "Build fingerprint: 'google/soju/crespo:4.0.4/IMM76D/299849:userdebug/test-keys'",
+                "pid: 2058, tid: 2523, name: com.google.android.browser  >>> com.google.android.browser <<<",
+                "signal 11 (SIGSEGV), code 1 (SEGV_MAPERR), fault addr 00000000");
+
+        nc = new NativeCrashParser().parse(lines);
+        assertNotNull(nc);
+        assertEquals("com.google.android.browser", nc.getApp());
+    }
+}
diff --git a/tests/src/com/android/loganalysis/parser/ProcrankParserTest.java b/tests/src/com/android/loganalysis/parser/ProcrankParserTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..a6c66b78554466528f65f2cc3abac3b7f7f8113f
--- /dev/null
+++ b/tests/src/com/android/loganalysis/parser/ProcrankParserTest.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2011 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.loganalysis.parser;
+
+import com.android.loganalysis.item.ProcrankItem;
+
+import junit.framework.TestCase;
+
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * Unit tests for {@link ProcrankParser}
+ */
+public class ProcrankParserTest extends TestCase {
+    public void testProcRankParser() {
+        List<String> inputBlock = Arrays.asList(
+                "  PID      Vss      Rss      Pss      Uss  cmdline",
+                "  178   87136K   81684K   52829K   50012K  system_server",
+                " 1313   78128K   77996K   48603K   45812K  com.google.android.apps.maps",
+                " 3247   61652K   61492K   33122K   30972K  com.android.browser",
+                "  334   55740K   55572K   29629K   28360K  com.android.launcher",
+                " 2072   51348K   51172K   24263K   22812K  android.process.acore",
+                " 1236   51440K   51312K   22911K   20608K  com.android.settings",
+                "                 51312K   22911K   20608K  invalid.format",
+                "                          ------   ------  ------",
+                "                          203624K  163604K  TOTAL",
+                "RAM: 731448K total, 415804K free, 9016K buffers, 108548K cached",
+                "[procrank: 1.6s elapsed]");
+        ProcrankParser parser = new ProcrankParser();
+        ProcrankItem procrank = parser.parse(inputBlock);
+
+        // Ensures that only valid lines are parsed. Only 6 of the 11 lines under the header are
+        // valid.
+        assertEquals(6, procrank.getPids().size());
+
+        // Make sure all expected rows are present, and do a diagonal check of values
+        assertEquals((Integer) 87136, procrank.getVss(178));
+        assertEquals((Integer) 77996, procrank.getRss(1313));
+        assertEquals((Integer) 33122, procrank.getPss(3247));
+        assertEquals((Integer) 28360, procrank.getUss(334));
+        assertEquals("android.process.acore", procrank.getProcessName(2072));
+    }
+}
+
diff --git a/tests/src/com/android/loganalysis/parser/SystemPropsParserTest.java b/tests/src/com/android/loganalysis/parser/SystemPropsParserTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..a34ee865508f99e81ee42c73bfcdffd82df92ace
--- /dev/null
+++ b/tests/src/com/android/loganalysis/parser/SystemPropsParserTest.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2011 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.loganalysis.parser;
+
+import com.android.loganalysis.item.SystemPropsItem;
+
+import junit.framework.TestCase;
+
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * Unit tests for {@link SystemPropsParser}
+ */
+public class SystemPropsParserTest extends TestCase {
+    public void testSimpleParse() {
+        List<String> inputBlock = Arrays.asList(
+                "[dalvik.vm.dexopt-flags]: [m=y]",
+                "[dalvik.vm.heapgrowthlimit]: [48m]",
+                "[dalvik.vm.heapsize]: [256m]",
+                "[gsm.version.ril-impl]: [android moto-ril-multimode 1.0]");
+        SystemPropsParser parser = new SystemPropsParser();
+        SystemPropsItem map = parser.parse(inputBlock);
+
+        assertEquals(4, map.size());
+        assertEquals("m=y", map.get("dalvik.vm.dexopt-flags"));
+        assertEquals("48m", map.get("dalvik.vm.heapgrowthlimit"));
+        assertEquals("256m", map.get("dalvik.vm.heapsize"));
+        assertEquals("android moto-ril-multimode 1.0", map.get("gsm.version.ril-impl"));
+    }
+
+    /**
+     * Make sure that a parse error on one line doesn't prevent the rest of the lines from being
+     * parsed
+     */
+    public void testParseError() {
+        List<String> inputBlock = Arrays.asList(
+                "[dalvik.vm.dexopt-flags]: [m=y]",
+                "[ends with newline]: [yup",
+                "]",
+                "[dalvik.vm.heapsize]: [256m]");
+        SystemPropsParser parser = new SystemPropsParser();
+        SystemPropsItem map = parser.parse(inputBlock);
+
+        assertEquals(2, map.size());
+        assertEquals("m=y", map.get("dalvik.vm.dexopt-flags"));
+        assertEquals("256m", map.get("dalvik.vm.heapsize"));
+    }
+}
+
diff --git a/tests/src/com/android/loganalysis/parser/TracesParserTest.java b/tests/src/com/android/loganalysis/parser/TracesParserTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..b5636e040f8d5271cfed695df355818759661a84
--- /dev/null
+++ b/tests/src/com/android/loganalysis/parser/TracesParserTest.java
@@ -0,0 +1,107 @@
+/*
+ * Copyright (C) 2011 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.loganalysis.parser;
+
+import com.android.loganalysis.item.TracesItem;
+import com.android.loganalysis.util.ArrayUtil;
+
+import junit.framework.TestCase;
+
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * Unit tests for {@link TracesParser}
+ */
+public class TracesParserTest extends TestCase {
+
+    /**
+     * Test that the parser parses the correct stack.
+     */
+    public void testTracesParser() {
+        List<String> lines = Arrays.asList(
+                "",
+                "",
+                "----- pid 2887 at 2012-05-02 16:43:41 -----",
+                "Cmd line: com.android.package",
+                "",
+                "DALVIK THREADS:",
+                "(mutexes: tll=0 tsl=0 tscl=0 ghl=0)",
+                "",
+                "\"main\" prio=5 tid=1 SUSPENDED",
+                "  | group=\"main\" sCount=1 dsCount=0 obj=0x00000001 self=0x00000001",
+                "  | sysTid=2887 nice=0 sched=0/0 cgrp=foreground handle=0000000001",
+                "  | schedstat=( 0 0 0 ) utm=5954 stm=1017 core=0",
+                "  at class.method1(Class.java:1)",
+                "  at class.method2(Class.java:2)",
+                "  at class.method2(Class.java:2)",
+                "",
+                "\"Task_1\" prio=5 tid=27 WAIT",
+                "  | group=\"main\" sCount=1 dsCount=0 obj=0x00000001 self=0x00000001",
+                "  | sysTid=4789 nice=10 sched=0/0 cgrp=bg_non_interactive handle=0000000001",
+                "  | schedstat=( 0 0 0 ) utm=0 stm=0 core=0",
+                "  at class.method1(Class.java:1)",
+                "  - waiting on <0x00000001> (a java.lang.Thread) held by tid=27 (Task_1)",
+                "  at class.method2(Class.java:2)",
+                "  at class.method2(Class.java:2)",
+                "",
+                "\"Task_2\" prio=5 tid=26 NATIVE",
+                "  | group=\"main\" sCount=1 dsCount=0 obj=0x00000001 self=0x00000001",
+                "  | sysTid=4343 nice=0 sched=0/0 cgrp=foreground handle=0000000001",
+                "  | schedstat=( 0 0 0 ) utm=6 stm=3 core=0",
+                "  #00  pc 00001234  /system/lib/lib.so (addr+8)",
+                "  #01  pc 00001235  /system/lib/lib.so (addr+16)",
+                "  #02  pc 00001236  /system/lib/lib.so (addr+24)",
+                "  at class.method1(Class.java:1)",
+                "",
+                "----- end 2887 -----",
+                "",
+                "",
+                "----- pid 256 at 2012-05-02 16:43:41 -----",
+                "Cmd line: system",
+                "",
+                "DALVIK THREADS:",
+                "(mutexes: tll=0 tsl=0 tscl=0 ghl=0)",
+                "",
+                "\"main\" prio=5 tid=1 NATIVE",
+                "  | group=\"main\" sCount=1 dsCount=0 obj=0x00000001 self=0x00000001",
+                "  | sysTid=256 nice=0 sched=0/0 cgrp=foreground handle=0000000001",
+                "  | schedstat=( 0 0 0 ) utm=175 stm=41 core=0",
+                "  #00  pc 00001234  /system/lib/lib.so (addr+8)",
+                "  #01  pc 00001235  /system/lib/lib.so (addr+16)",
+                "  #02  pc 00001236  /system/lib/lib.so (addr+24)",
+                "  at class.method1(Class.java:1)",
+                "  at class.method2(Class.java:2)",
+                "  at class.method2(Class.java:2)",
+                "",
+                "----- end 256 -----",
+                "");
+
+        List<String> expectedStack = Arrays.asList(
+                "\"main\" prio=5 tid=1 SUSPENDED",
+                "  | group=\"main\" sCount=1 dsCount=0 obj=0x00000001 self=0x00000001",
+                "  | sysTid=2887 nice=0 sched=0/0 cgrp=foreground handle=0000000001",
+                "  | schedstat=( 0 0 0 ) utm=5954 stm=1017 core=0",
+                "  at class.method1(Class.java:1)",
+                "  at class.method2(Class.java:2)",
+                "  at class.method2(Class.java:2)");
+
+        TracesItem traces = new TracesParser().parse(lines);
+        assertEquals(2887, traces.getPid().intValue());
+        assertEquals("com.android.package", traces.getApp());
+        assertEquals(ArrayUtil.join("\n", expectedStack), traces.getStack());
+    }
+}
diff --git a/tests/src/com/android/loganalysis/util/ArrayUtilTest.java b/tests/src/com/android/loganalysis/util/ArrayUtilTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..9c682935aeaa8303180737be113a63403dcfdf74
--- /dev/null
+++ b/tests/src/com/android/loganalysis/util/ArrayUtilTest.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright (C) 2011 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.loganalysis.util;
+
+import junit.framework.TestCase;
+
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * Unit tests for {@link ArrayUtil}
+ */
+public class ArrayUtilTest extends TestCase {
+
+    /**
+     * Simple test for {@link ArrayUtil#buildArray(String[]...)}
+     */
+    public void testBuildArray_arrays() {
+        String[] newArray = ArrayUtil.buildArray(new String[] {"1", "2"}, new String[] {"3"},
+                new String[] {"4"});
+        assertEquals(4, newArray.length);
+        for (int i = 0; i < 4; i++) {
+            assertEquals(Integer.toString(i+1), newArray[i]);
+        }
+    }
+
+    /**
+     * Make sure that Collections aren't double-wrapped
+     */
+    public void testJoinCollection() {
+        List<String> list = Arrays.asList("alpha", "beta", "gamma");
+        final String expected = "alpha, beta, gamma";
+        String str = ArrayUtil.join(", ", list);
+        assertEquals(expected, str);
+    }
+
+    /**
+     * Make sure that Arrays aren't double-wrapped
+     */
+    public void testJoinArray() {
+        String[] ary = new String[] {"alpha", "beta", "gamma"};
+        final String expected = "alpha, beta, gamma";
+        String str = ArrayUtil.join(", ", (Object[]) ary);
+        assertEquals(expected, str);
+    }
+
+    /**
+     * Make sure that join on varargs arrays work as expected
+     */
+    public void testJoinNormal() {
+        final String expected = "alpha, beta, gamma";
+        String str = ArrayUtil.join(", ", "alpha", "beta", "gamma");
+        assertEquals(expected, str);
+    }
+}
diff --git a/tests/src/com/android/loganalysis/util/RegexTrieTest.java b/tests/src/com/android/loganalysis/util/RegexTrieTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..a124ac2d951d2a06397bf6231e74488ead167f10
--- /dev/null
+++ b/tests/src/com/android/loganalysis/util/RegexTrieTest.java
@@ -0,0 +1,272 @@
+/*
+ * 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.loganalysis.util;
+
+import com.android.loganalysis.util.RegexTrie.CompPattern;
+
+import junit.framework.TestCase;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.regex.Pattern;
+
+/**
+ * Set of unit tests to verify the behavior of the RegexTrie
+ */
+public class RegexTrieTest extends TestCase {
+    private RegexTrie<Integer> mTrie = null;
+    private static final Integer STORED_VAL = 42;
+    private static final List<String> NULL_LIST = Arrays.asList((String)null);
+
+    @Override
+    public void setUp() throws Exception {
+        mTrie = new RegexTrie<Integer>();
+    }
+
+    private void dumpTrie(RegexTrie trie) {
+        System.err.format("Trie is '%s'\n", trie.toString());
+    }
+
+    public void testStringPattern() {
+        mTrie.put(STORED_VAL, "[p]art1", "[p]art2", "[p]art3");
+        Integer retrieved = mTrie.retrieve("part1", "part2", "part3");
+        assertEquals(STORED_VAL, retrieved);
+    }
+
+    public void testAlternation_single() {
+        mTrie.put(STORED_VAL, "alpha|beta");
+        Integer retrieved;
+        retrieved = mTrie.retrieve("alpha");
+        assertEquals(STORED_VAL, retrieved);
+        retrieved = mTrie.retrieve("beta");
+        assertEquals(STORED_VAL, retrieved);
+        retrieved = mTrie.retrieve("alpha|beta");
+        assertNull(retrieved);
+        retrieved = mTrie.retrieve("gamma");
+        assertNull(retrieved);
+        retrieved = mTrie.retrieve("alph");
+        assertNull(retrieved);
+    }
+
+    public void testAlternation_multiple() {
+        mTrie.put(STORED_VAL, "a|alpha", "b|beta");
+        Integer retrieved;
+        retrieved = mTrie.retrieve("a", "b");
+        assertEquals(STORED_VAL, retrieved);
+        retrieved = mTrie.retrieve("a", "beta");
+        assertEquals(STORED_VAL, retrieved);
+        retrieved = mTrie.retrieve("alpha", "b");
+        assertEquals(STORED_VAL, retrieved);
+        retrieved = mTrie.retrieve("alpha", "beta");
+        assertEquals(STORED_VAL, retrieved);
+
+        retrieved = mTrie.retrieve("alpha");
+        assertNull(retrieved);
+        retrieved = mTrie.retrieve("beta");
+        assertNull(retrieved);
+        retrieved = mTrie.retrieve("alpha", "bet");
+        assertNull(retrieved);
+    }
+
+    public void testGroups_fullMatch() {
+        mTrie.put(STORED_VAL, "a|(alpha)", "b|(beta)");
+        Integer retrieved;
+        List<List<String>> groups = new ArrayList<List<String>>();
+
+        retrieved = mTrie.retrieve(groups, "a", "b");
+        assertEquals(STORED_VAL, retrieved);
+        assertEquals(2, groups.size());
+        assertEquals(NULL_LIST, groups.get(0));
+        assertEquals(NULL_LIST, groups.get(1));
+
+        retrieved = mTrie.retrieve(groups, "a", "beta");
+        assertEquals(STORED_VAL, retrieved);
+        assertEquals(2, groups.size());
+        assertEquals(NULL_LIST, groups.get(0));
+        assertEquals(Arrays.asList("beta"), groups.get(1));
+
+        retrieved = mTrie.retrieve(groups, "alpha", "b");
+        assertEquals(STORED_VAL, retrieved);
+        assertEquals(2, groups.size());
+        assertEquals(Arrays.asList("alpha"), groups.get(0));
+        assertEquals(NULL_LIST, groups.get(1));
+
+        retrieved = mTrie.retrieve(groups, "alpha", "beta");
+        assertEquals(STORED_VAL, retrieved);
+        assertEquals(2, groups.size());
+        assertEquals(Arrays.asList("alpha"), groups.get(0));
+        assertEquals(Arrays.asList("beta"), groups.get(1));
+    }
+
+    public void testGroups_partialMatch() {
+        mTrie.put(STORED_VAL, "a|(alpha)", "b|(beta)");
+        Integer retrieved;
+        List<List<String>> groups = new ArrayList<List<String>>();
+
+        retrieved = mTrie.retrieve(groups, "alpha");
+        assertNull(retrieved);
+        assertEquals(1, groups.size());
+        assertEquals(Arrays.asList("alpha"), groups.get(0));
+
+        retrieved = mTrie.retrieve(groups, "beta");
+        assertNull(retrieved);
+        assertEquals(0, groups.size());
+
+        retrieved = mTrie.retrieve(groups, "alpha", "bet");
+        assertNull(retrieved);
+        assertEquals(1, groups.size());
+        assertEquals(Arrays.asList("alpha"), groups.get(0));
+
+        retrieved = mTrie.retrieve(groups, "alpha", "betar");
+        assertNull(retrieved);
+        assertEquals(1, groups.size());
+        assertEquals(Arrays.asList("alpha"), groups.get(0));
+
+        retrieved = mTrie.retrieve(groups, "alpha", "beta", "gamma");
+        assertNull(retrieved);
+        assertEquals(2, groups.size());
+        assertEquals(Arrays.asList("alpha"), groups.get(0));
+        assertEquals(Arrays.asList("beta"), groups.get(1));
+    }
+
+    /**
+     * Make sure that the wildcard functionality works
+     */
+    public void testWildcard() {
+        mTrie.put(STORED_VAL, "a", null);
+        Integer retrieved;
+        List<List<String>> groups = new ArrayList<List<String>>();
+
+        retrieved = mTrie.retrieve(groups, "a", "b", "c");
+        assertEquals(STORED_VAL, retrieved);
+        assertEquals(3, groups.size());
+        assertTrue(groups.get(0).isEmpty());
+        assertEquals(Arrays.asList("b"), groups.get(1));
+        assertEquals(Arrays.asList("c"), groups.get(2));
+
+        retrieved = mTrie.retrieve(groups, "a");
+        assertNull(retrieved);
+        assertEquals(1, groups.size());
+        assertTrue(groups.get(0).isEmpty());
+    }
+
+    /**
+     * Make sure that if a wildcard and a more specific match could both match, that the more
+     * specific match takes precedence
+     */
+    public void testWildcard_precedence() {
+        // Do one before and one after the wildcard to check for ordering effects
+        mTrie.put(STORED_VAL + 1, "a", "(b)");
+        mTrie.put(STORED_VAL, "a", null);
+        mTrie.put(STORED_VAL + 2, "a", "(c)");
+        Integer retrieved;
+        List<List<String>> groups = new ArrayList<List<String>>();
+
+        retrieved = mTrie.retrieve(groups, "a", "d");
+        assertEquals(STORED_VAL, retrieved);
+        assertEquals(2, groups.size());
+        assertTrue(groups.get(0).isEmpty());
+        assertEquals(Arrays.asList("d"), groups.get(1));
+
+        retrieved = mTrie.retrieve(groups, "a", "b");
+        assertEquals((Integer)(STORED_VAL + 1), retrieved);
+        assertEquals(2, groups.size());
+        assertTrue(groups.get(0).isEmpty());
+        assertEquals(Arrays.asList("b"), groups.get(1));
+
+        retrieved = mTrie.retrieve(groups, "a", "c");
+        assertEquals((Integer)(STORED_VAL + 2), retrieved);
+        assertEquals(2, groups.size());
+        assertTrue(groups.get(0).isEmpty());
+        assertEquals(Arrays.asList("c"), groups.get(1));
+    }
+
+    /**
+     * Verify a bugfix: make sure that no NPE results from calling #retrieve with a wildcard but
+     * without a place to retrieve captures.
+     */
+    public void testWildcard_noCapture() throws NullPointerException {
+        mTrie.put(STORED_VAL, "a", null);
+        String[] key = new String[] {"a", "b", "c"};
+
+        mTrie.retrieve(key);
+        mTrie.retrieve(null, key);
+        // test passes if no exceptions were thrown
+    }
+
+    public void testMultiChild() {
+        mTrie.put(STORED_VAL + 1, "a", "b");
+        mTrie.put(STORED_VAL + 2, "a", "c");
+        dumpTrie(mTrie);
+
+        Object retrieved;
+        retrieved = mTrie.retrieve("a", "b");
+        assertEquals(STORED_VAL + 1, retrieved);
+        retrieved = mTrie.retrieve("a", "c");
+        assertEquals(STORED_VAL + 2, retrieved);
+    }
+
+    /**
+     * Make sure that {@link CompPattern#equals} works as expected.  Shake a proverbial fist at Java
+     */
+    public void testCompPattern_equality() {
+        String regex = "regex";
+        Pattern p1 = Pattern.compile(regex);
+        Pattern p2 = Pattern.compile(regex);
+        Pattern pOther = Pattern.compile("other");
+        CompPattern cp1 = new CompPattern(p1);
+        CompPattern cp2 = new CompPattern(p2);
+        CompPattern cpOther = new CompPattern(pOther);
+
+        // This is the problem with Pattern as implemented
+        assertFalse(p1.equals(p2));
+        assertFalse(p2.equals(p1));
+
+        // Make sure that wrapped patterns with the same regex are considered equivalent
+        assertTrue(cp2.equals(p1));
+        assertTrue(cp2.equals(p2));
+        assertTrue(cp2.equals(cp1));
+
+        // And make sure that wrapped patterns with different regexen are still considered different
+        assertFalse(cp2.equals(pOther));
+        assertFalse(cp2.equals(cpOther));
+    }
+
+    public void testCompPattern_hashmap() {
+        HashMap<CompPattern, Integer> map = new HashMap<CompPattern, Integer>();
+        String regex = "regex";
+        Pattern p1 = Pattern.compile(regex);
+        Pattern p2 = Pattern.compile(regex);
+        Pattern pOther = Pattern.compile("other");
+        CompPattern cp1 = new CompPattern(p1);
+        CompPattern cp2 = new CompPattern(p2);
+        CompPattern cpOther = new CompPattern(pOther);
+
+        map.put(cp1, STORED_VAL);
+        assertTrue(map.containsKey(cp1));
+        assertTrue(map.containsKey(cp2));
+        assertFalse(map.containsKey(cpOther));
+
+        map.put(cpOther, STORED_VAL);
+        assertEquals(map.size(), 2);
+        assertTrue(map.containsKey(cp1));
+        assertTrue(map.containsKey(cp2));
+        assertTrue(map.containsKey(cpOther));
+    }
+}
+