Add Location clients heuristics

Parse the activity service section and retrieve location usage clients

Change-Id: I71fb6331be713b3b8653834feea05c88deab68b6
diff --git a/src/com/android/loganalysis/item/ActivityServiceItem.java b/src/com/android/loganalysis/item/ActivityServiceItem.java
new file mode 100644
index 0000000..f149adf
--- /dev/null
+++ b/src/com/android/loganalysis/item/ActivityServiceItem.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2016 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 Activity Service Dumps
+ */
+public class ActivityServiceItem extends GenericItem {
+
+    /** Constant for JSON output */
+    public static final String LOCATION_DUMPS = "LOCATION";
+
+    private static final Set<String> ATTRIBUTES = new HashSet<String>(Arrays.asList(
+            LOCATION_DUMPS));
+
+    /**
+     * The constructor for {@link ActivityServiceItem}.
+     */
+    public ActivityServiceItem() {
+        super(ATTRIBUTES);
+    }
+
+    /**
+     * Get the location dump
+     */
+    public LocationDumpsItem getLocationDumps() {
+        return (LocationDumpsItem) getAttribute(LOCATION_DUMPS);
+    }
+
+    /**
+     * Set the location dump
+     */
+    public void setLocationDumps(LocationDumpsItem location) {
+        setAttribute(LOCATION_DUMPS, location);
+    }
+}
diff --git a/src/com/android/loganalysis/item/BugreportItem.java b/src/com/android/loganalysis/item/BugreportItem.java
index 969c90a..e716728 100644
--- a/src/com/android/loganalysis/item/BugreportItem.java
+++ b/src/com/android/loganalysis/item/BugreportItem.java
@@ -45,10 +45,12 @@
     public static final String SYSTEM_PROPS = "SYSTEM_PROPS";
     /** Constant for JSON output */
     public static final String DUMPSYS = "DUMPSYS";
+    /** Constant for JSON output */
+    public static final String ACTIVITY_SERVICE = "ACTIVITY_SERVICE";
 
     private static final Set<String> ATTRIBUTES = new HashSet<String>(Arrays.asList(
             TIME, COMMAND_LINE, MEM_INFO, PROCRANK, TOP, KERNEL_LOG, LAST_KMSG, SYSTEM_LOG,
-            SYSTEM_PROPS, DUMPSYS));
+            SYSTEM_PROPS, DUMPSYS, ACTIVITY_SERVICE));
 
     public static class CommandLineItem extends GenericMapItem<String> {
         private static final long serialVersionUID = 0L;
@@ -200,4 +202,17 @@
     public void setDumpsys(DumpsysItem dumpsys) {
         setAttribute(DUMPSYS, dumpsys);
     }
+    /**
+     * Get the {@link ActivityServiceItem} of the bugreport.
+     */
+    public ActivityServiceItem getActivityService() {
+        return (ActivityServiceItem) getAttribute(ACTIVITY_SERVICE);
+    }
+
+    /**
+     * Set the {@link ActivityServiceItem} of the bugreport.
+     */
+    public void setActivityService(ActivityServiceItem activityService) {
+        setAttribute(ACTIVITY_SERVICE, activityService);
+    }
 }
diff --git a/src/com/android/loganalysis/item/LocationDumpsItem.java b/src/com/android/loganalysis/item/LocationDumpsItem.java
new file mode 100644
index 0000000..580145f
--- /dev/null
+++ b/src/com/android/loganalysis/item/LocationDumpsItem.java
@@ -0,0 +1,178 @@
+/*
+ * Copyright (C) 2016 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 org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.LinkedList;
+import java.util.Set;
+
+
+/**
+ * An {@link IItem} used to store location dumps.
+ */
+public class LocationDumpsItem implements IItem {
+
+    /** Constant for JSON output */
+    public static final String LOCATION_CLIENTS = "LOCATION_CLIENTS";
+
+    private Collection<LocationInfoItem> mLocationClients =
+            new LinkedList<LocationInfoItem>();
+
+    public static class LocationInfoItem extends GenericItem {
+        /** Constant for JSON output */
+        public static final String PACKAGE = "PACKAGE";
+        /** Constant for JSON output */
+        public static final String EFFECTIVE_INTERVAL = "EFFECTIVE_INTERVAL";
+        /** Constant for JSON output */
+        public static final String MIN_INTERVAL = "MIN_INTERVAL";
+        /** Constant for JSON output */
+        public static final String MAX_INTERVAL = "MAX_INTERVAL";
+        /** Constant for JSON output */
+        public static final String REQUEST_PRIORITY = "PRIORITY";
+        /** Constant for JSON output */
+        public static final String LOCATION_DURATION = "LOCATION_DURATION";
+
+        private static final Set<String> ATTRIBUTES = new HashSet<String>(Arrays.asList(
+                PACKAGE, EFFECTIVE_INTERVAL, MIN_INTERVAL, MAX_INTERVAL, REQUEST_PRIORITY,
+                LOCATION_DURATION ));
+        /**
+         * The constructor for {@link LocationInfoItem}
+         *
+         * @param packageName The package that requests location
+         * @param effective Effective interval of location request
+         * @param min Min interval of location request
+         * @param max Max interval of location request
+         * @param priority The priority of the request
+         * @param duration Duration of the request
+         */
+        public LocationInfoItem(String packageName, int effective, int min, int max,
+                String priority, int duration) {
+            super(ATTRIBUTES);
+            setAttribute(PACKAGE, packageName);
+            setAttribute(EFFECTIVE_INTERVAL, effective);
+            setAttribute(MIN_INTERVAL, min);
+            setAttribute(MAX_INTERVAL, max);
+            setAttribute(REQUEST_PRIORITY, priority);
+            setAttribute(LOCATION_DURATION, duration);
+        }
+
+        /**
+         * Get the name of the package
+         */
+        public String getPackage() {
+            return (String) getAttribute(PACKAGE);
+        }
+
+        /**
+         * Get the effective location interval
+         */
+        public int getEffectiveInterval() {
+            return (int) getAttribute(EFFECTIVE_INTERVAL);
+        }
+
+        /**
+         * Get the min location interval
+         */
+        public int getMinInterval() {
+            return (int) getAttribute(MIN_INTERVAL);
+        }
+
+        /**
+         * Get the max location interval
+         */
+        public int getMaxInterval() {
+            return (int) getAttribute(MAX_INTERVAL);
+        }
+
+        /**
+         * Get the priority of location request
+         */
+        public String getPriority() {
+            return (String) getAttribute(REQUEST_PRIORITY);
+        }
+
+        /**
+         * Get the location duration
+         */
+        public int getDuration() {
+            return (int) getAttribute(LOCATION_DURATION);
+        }
+
+    }
+
+    /**
+     * Add a location client {@link LocationDumpsItem}.
+     *
+     * @param packageName The package that requests location
+     * @param effective Effective interval of location request
+     * @param min Min interval of location request
+     * @param max Max interval of location request
+     * @param priority The priority of the request
+     * @param duration Duration of the request
+     */
+    public void addLocationClient(String packageName, int effective, int min, int max,
+            String priority, int duration) {
+        mLocationClients.add(new LocationInfoItem(packageName, effective, min, max, priority,
+                duration));
+    }
+
+    public Collection<LocationInfoItem> getLocationClients() {
+        return mLocationClients;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public IItem merge(IItem other) throws ConflictingItemException {
+        throw new ConflictingItemException("Location dumps items cannot be merged");
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public boolean isConsistent(IItem other) {
+        return false;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public JSONObject toJson() {
+        JSONObject object = new JSONObject();
+        if (mLocationClients != null) {
+            try {
+                JSONArray locationClients = new JSONArray();
+                for (LocationInfoItem locationClient : mLocationClients) {
+                    locationClients.put(locationClient.toJson());
+                }
+                object.put(LOCATION_CLIENTS, locationClients);
+            } catch (JSONException e) {
+                // Ignore
+            }
+        }
+
+        return object;
+    }
+}
diff --git a/src/com/android/loganalysis/parser/ActivityServiceParser.java b/src/com/android/loganalysis/parser/ActivityServiceParser.java
new file mode 100644
index 0000000..c79d6e5
--- /dev/null
+++ b/src/com/android/loganalysis/parser/ActivityServiceParser.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright (C) 2016 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.LocationDumpsItem;
+import com.android.loganalysis.item.ActivityServiceItem;
+
+import java.util.List;
+
+/**
+ * A {@link IParser} to parse the activity service dump section of the bugreport
+ */
+public class ActivityServiceParser extends AbstractSectionParser {
+
+    private static final String LOCATION_SECTION_REGEX =
+            "^\\s*SERVICE com.google.android.gms/"
+            + "com.google.android.location.internal.GoogleLocationManagerService \\w+ pid=\\d+";
+
+    private static final String NOOP_SECTION_REGEX = "^\\s*SERVICE .*/.*";
+
+    private LocationServiceParser mLocationParser = new LocationServiceParser();
+
+    private ActivityServiceItem mActivityServiceItem= null;
+    private boolean mParsedInput = false;
+
+    /**
+     * {@inheritDoc}
+     *
+     * @return The {@link ActivityServiceItem}
+     */
+    @Override
+    public ActivityServiceItem parse(List<String> lines) {
+        setup();
+        for (String line : lines) {
+            if (!mParsedInput && !"".equals(line.trim())) {
+                mParsedInput = true;
+            }
+            parseLine(line);
+        }
+        commit();
+        return mActivityServiceItem;
+    }
+
+    /**
+     * Sets up the parser by adding the section parsers.
+     */
+    protected void setup() {
+        addSectionParser(mLocationParser, LOCATION_SECTION_REGEX);
+        addSectionParser(new NoopParser(), NOOP_SECTION_REGEX);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    protected void commit() {
+        // signal EOF
+        super.commit();
+        if (mParsedInput) {
+            if (mActivityServiceItem == null) {
+                mActivityServiceItem = new ActivityServiceItem();
+            }
+        }
+
+        if (mActivityServiceItem != null) {
+            mActivityServiceItem.setLocationDumps(
+                    (LocationDumpsItem) getSection(mLocationParser));
+        }
+    }
+}
diff --git a/src/com/android/loganalysis/parser/BugreportParser.java b/src/com/android/loganalysis/parser/BugreportParser.java
index ece6076..dac1f53 100644
--- a/src/com/android/loganalysis/parser/BugreportParser.java
+++ b/src/com/android/loganalysis/parser/BugreportParser.java
@@ -15,6 +15,7 @@
  */
 package com.android.loganalysis.parser;
 
+import com.android.loganalysis.item.ActivityServiceItem;
 import com.android.loganalysis.item.AnrItem;
 import com.android.loganalysis.item.BugreportItem;
 import com.android.loganalysis.item.BugreportItem.CommandLineItem;
@@ -55,6 +56,8 @@
             "------ (SYSTEM|MAIN|MAIN AND SYSTEM) LOG .*";
     private static final String ANR_TRACES_SECTION_REGEX = "------ VM TRACES AT LAST ANR .*";
     private static final String DUMPSYS_SECTION_REGEX = "------ DUMPSYS .*";
+    private static final String ACTIVITY_SERVICE_SECTION_REGEX =
+            "^------ APP SERVICES \\(dumpsys activity service all\\) ------$";
     private static final String NOOP_SECTION_REGEX = "------ .* ------";
 
     private static final String BOOTREASON_PROP = "ro.boot.bootreason";
@@ -112,6 +115,7 @@
     private KernelLogParser mLastKmsgParser = new KernelLogParser();
     private LogcatParser mLogcatParser = new LogcatParser();
     private DumpsysParser mDumpsysParser = new DumpsysParser();
+    private ActivityServiceParser mActivityServiceParser =  new ActivityServiceParser();
 
     private BugreportItem mBugreport = null;
     private CommandLineItem mCommandLine = new CommandLineItem();
@@ -175,6 +179,7 @@
         addSectionParser(mKernelLogParser, KERNEL_LOG_SECTION_REGEX);
         addSectionParser(mLastKmsgParser, LAST_KMSG_SECTION_REGEX);
         addSectionParser(mDumpsysParser, DUMPSYS_SECTION_REGEX);
+        addSectionParser(mActivityServiceParser, ACTIVITY_SERVICE_SECTION_REGEX);
         addSectionParser(new NoopParser(), NOOP_SECTION_REGEX);
         mKernelLogParser.setAddUnknownBootreason(false);
         mLastKmsgParser.setAddUnknownBootreason(false);
@@ -202,6 +207,7 @@
             mBugreport.setLastKmsg((KernelLogItem) getSection(mLastKmsgParser));
             mBugreport.setSystemProps((SystemPropsItem) getSection(mSystemPropsParser));
             mBugreport.setDumpsys((DumpsysItem) getSection(mDumpsysParser));
+            mBugreport.setActivityService((ActivityServiceItem) getSection(mActivityServiceParser));
 
             if (mBugreport.getSystemLog() != null && mBugreport.getProcrank() != null) {
                 for (IItem item : mBugreport.getSystemLog().getEvents()) {
diff --git a/src/com/android/loganalysis/parser/LocationServiceParser.java b/src/com/android/loganalysis/parser/LocationServiceParser.java
new file mode 100644
index 0000000..1f0450f
--- /dev/null
+++ b/src/com/android/loganalysis/parser/LocationServiceParser.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright (C) 2016 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.LocationDumpsItem;
+import com.android.loganalysis.util.NumberFormattingUtil;
+
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * A {@link IParser} to handle the parsing of location request information
+ */
+public class LocationServiceParser implements IParser {
+    /**
+     * Match a valid line such as:
+     * "Interval effective/min/max 1/0/0[s] Duration: 140[minutes] [com.google.android.gms,
+     * PRIORITY_NO_POWER, UserLocationProducer] Num requests: 2 Active: true"
+     */
+    private static final Pattern LOCATION_PAT = Pattern.compile(
+            "^\\s*Interval effective/min/max (\\d+)/(\\d+)/(\\d+)\\[s\\] Duration: (\\d+)"
+            + "\\[minutes\\]\\s*\\[([\\w.]+), (\\w+)(,.*)?\\] Num requests: \\d+ Active: \\w*");
+
+    private LocationDumpsItem mItem = new LocationDumpsItem();
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public LocationDumpsItem parse(List<String> lines) {
+        Matcher m = null;
+        for (String line : lines) {
+            m = LOCATION_PAT.matcher(line);
+            if (m.matches()) {
+                mItem.addLocationClient(m.group(5), NumberFormattingUtil.parseIntOrZero(m.group(1)),
+                        NumberFormattingUtil.parseIntOrZero(m.group(2)),
+                        NumberFormattingUtil.parseIntOrZero(m.group(3)), m.group(6),
+                        NumberFormattingUtil.parseIntOrZero(m.group(4)));
+                continue;
+            }
+        }
+        return mItem;
+    }
+
+    /**
+     * Get the {@link LocationDumpsItem}.
+     * <p>
+     * Exposed for unit testing.
+     * </p>
+     */
+    LocationDumpsItem getItem() {
+        return mItem;
+    }
+}
diff --git a/src/com/android/loganalysis/rule/LocationUsageRule.java b/src/com/android/loganalysis/rule/LocationUsageRule.java
new file mode 100644
index 0000000..71defa2
--- /dev/null
+++ b/src/com/android/loganalysis/rule/LocationUsageRule.java
@@ -0,0 +1,83 @@
+/*
+ * Copyright (C) 2016 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.rule;
+
+import com.android.loganalysis.item.BugreportItem;
+import com.android.loganalysis.item.LocationDumpsItem;
+import com.android.loganalysis.item.LocationDumpsItem.LocationInfoItem;
+
+import java.util.concurrent.TimeUnit;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+
+/**
+ * Rules definition for Process usage
+ */
+public class LocationUsageRule extends AbstractPowerRule {
+
+    private static final String LOCATION_USAGE_ANALYSIS = "LOCATION_USAGE_ANALYSIS";
+    private static final float LOCATION_REQUEST_DURATION_THRESHOLD = 0.1f; // 10%
+    // GSA requests for location every 285 seconds, anything more frequent is an issue
+    private static final int LOCATION_INTERVAL_THRESHOLD = 285;
+
+    private StringBuffer mAnalysisBuffer;
+
+    private BugreportItem mBugreportItem = null;
+
+    public LocationUsageRule (BugreportItem bugreportItem) {
+        super(bugreportItem);
+        mBugreportItem = bugreportItem;
+    }
+
+
+    @Override
+    public void applyRule() {
+        mAnalysisBuffer = new StringBuffer();
+        LocationDumpsItem locationDumpsItem = mBugreportItem.getActivityService()
+                .getLocationDumps();
+        final long locationRequestThresholdMs = (long) (getTimeOnBattery() *
+                LOCATION_REQUEST_DURATION_THRESHOLD);
+        if (locationDumpsItem != null) {
+            for (LocationInfoItem locationClient : locationDumpsItem.getLocationClients()) {
+                final String packageName = locationClient.getPackage();
+                final String priority = locationClient.getPriority();
+                final int effectiveIntervalSec = locationClient.getEffectiveInterval();
+                if (effectiveIntervalSec < LOCATION_INTERVAL_THRESHOLD &&
+                        !priority.equals("PRIORITY_NO_POWER") &&
+                        (TimeUnit.MINUTES.toMillis(locationClient.getDuration()) >
+                        locationRequestThresholdMs)) {
+                    mAnalysisBuffer.append(String.format("Package %s is requesting for location "
+                            + "updates every %d secs with priority %s", packageName,
+                            effectiveIntervalSec, priority));
+                }
+            }
+        }
+    }
+
+    @Override
+    public JSONObject getAnalysis() {
+        JSONObject usageAnalysis = new JSONObject();
+        try {
+            usageAnalysis.put(LOCATION_USAGE_ANALYSIS, mAnalysisBuffer.toString());
+        } catch (JSONException e) {
+          // do nothing
+        }
+        return usageAnalysis;
+    }
+}
diff --git a/src/com/android/loganalysis/rule/RuleEngine.java b/src/com/android/loganalysis/rule/RuleEngine.java
index add61e6..57daa19 100644
--- a/src/com/android/loganalysis/rule/RuleEngine.java
+++ b/src/com/android/loganalysis/rule/RuleEngine.java
@@ -66,5 +66,6 @@
     private void addPowerRules() {
         mRulesList.add(new WakelockRule(mBugreportItem));
         mRulesList.add(new ProcessUsageRule(mBugreportItem));
+        mRulesList.add(new LocationUsageRule(mBugreportItem));
     }
 }
diff --git a/tests/src/com/android/loganalysis/item/LocationDumpsItemTest.java b/tests/src/com/android/loganalysis/item/LocationDumpsItemTest.java
new file mode 100644
index 0000000..c1af8a0
--- /dev/null
+++ b/tests/src/com/android/loganalysis/item/LocationDumpsItemTest.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2016 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 com.android.loganalysis.item.LocationDumpsItem.LocationInfoItem;
+
+import junit.framework.TestCase;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+/**
+ * Unit test for {@link LocationDumpsItem}.
+ */
+public class LocationDumpsItemTest extends TestCase {
+
+    /**
+     * Test that {@link LocationDumpsItem#toJson()} returns correctly.
+     */
+    public void testToJson() throws JSONException {
+        LocationDumpsItem item = new LocationDumpsItem();
+        item.addLocationClient("com.google.android.gms", 500, 60, 1000, "PRIORITY_ACCURACY", 45);
+        item.addLocationClient("com.google.android.maps", 0, 0, 0, "PRIORITY_ACCURACY", 55);
+
+        // Convert to JSON string and back again
+        JSONObject output = new JSONObject(item.toJson().toString());
+
+        assertTrue(output.has(LocationDumpsItem.LOCATION_CLIENTS));
+        assertTrue(output.get(LocationDumpsItem.LOCATION_CLIENTS) instanceof JSONArray);
+
+        JSONArray locationClients = output.getJSONArray(LocationDumpsItem.LOCATION_CLIENTS);
+
+        assertEquals(2, locationClients.length());
+        assertTrue(locationClients.getJSONObject(0).has(LocationInfoItem.PACKAGE));
+        assertTrue(locationClients.getJSONObject(0).has(LocationInfoItem.EFFECTIVE_INTERVAL));
+        assertTrue(locationClients.getJSONObject(0).has(LocationInfoItem.MIN_INTERVAL));
+        assertTrue(locationClients.getJSONObject(0).has(LocationInfoItem.MAX_INTERVAL));
+        assertTrue(locationClients.getJSONObject(0).has(LocationInfoItem.REQUEST_PRIORITY));
+        assertTrue(locationClients.getJSONObject(0).has(LocationInfoItem.LOCATION_DURATION));
+
+        assertTrue(locationClients.getJSONObject(1).has(LocationInfoItem.PACKAGE));
+        assertTrue(locationClients.getJSONObject(1).has(LocationInfoItem.EFFECTIVE_INTERVAL));
+        assertTrue(locationClients.getJSONObject(1).has(LocationInfoItem.MIN_INTERVAL));
+        assertTrue(locationClients.getJSONObject(1).has(LocationInfoItem.MAX_INTERVAL));
+        assertTrue(locationClients.getJSONObject(1).has(LocationInfoItem.REQUEST_PRIORITY));
+        assertTrue(locationClients.getJSONObject(1).has(LocationInfoItem.LOCATION_DURATION));
+    }
+
+    /**
+     * Test that {@link LocationDumpsItem#getLocationDumps()} returns correctly.
+     */
+    public void testGetLocationDumps() {
+        LocationDumpsItem item = new LocationDumpsItem();
+        item.addLocationClient("com.google.android.gms", 500, 60, 1000, "PRIORITY_ACCURACY", 45);
+
+        assertEquals(item.getLocationClients().size(), 1);
+        LocationInfoItem client = item.getLocationClients().iterator().next();
+        assertNotNull(client);
+        assertEquals(client.getPackage(), "com.google.android.gms");
+        assertEquals(client.getEffectiveInterval(), 500);
+        assertEquals(client.getMinInterval(), 60);
+        assertEquals(client.getMaxInterval(), 1000);
+        assertEquals(client.getPriority(), "PRIORITY_ACCURACY");
+        assertEquals(client.getDuration(), 45);
+    }
+
+}
diff --git a/tests/src/com/android/loganalysis/parser/ActivityServiceParserTest.java b/tests/src/com/android/loganalysis/parser/ActivityServiceParserTest.java
new file mode 100644
index 0000000..c074f14
--- /dev/null
+++ b/tests/src/com/android/loganalysis/parser/ActivityServiceParserTest.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright (C) 2016 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.ActivityServiceItem;
+import com.android.loganalysis.item.DumpsysItem;
+
+import junit.framework.TestCase;
+
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * Unit tests for {@link ActivityServiceParser}
+ */
+public class ActivityServiceParserTest extends TestCase {
+
+    /**
+     * Test that normal input is parsed.
+     */
+    public void testActivityServiceParser() {
+        List<String> inputBlock = Arrays.asList(
+                "SERVICE com.google.android.gms/"
+                + "com.google.android.location.internal.GoogleLocationManagerService f4c9e9d "
+                + "pid=1494",
+                "Client:",
+                " nothing to dump",
+                "Location Request History By Package:",
+                "Interval effective/min/max 1/0/0[s] Duration: 140[minutes] "
+                + "[com.google.android.gms, PRIORITY_NO_POWER, UserLocationProducer] "
+                + "Num requests: 2 Active: true",
+                "Interval effective/min/max 284/285/3600[s] Duration: 140[minutes] "
+                + "[com.google.android.googlequicksearchbox, PRIORITY_BALANCED_POWER_ACCURACY] "
+                + "Num requests: 5 Active: true",
+                "FLP WakeLock Count:",
+                "SERVICE com.android.server.telecom/.components.BluetoothPhoneService 98ab pid=802",
+                "Interval effective/min/max 1/0/0[s] Duration: 140[minutes] "
+                + "[com.google.android.gms, PRIORITY_NO_POWER, UserLocationProducer] "
+                + "Num requests: 2 Active: true",
+                "");
+
+        ActivityServiceItem activityService = new ActivityServiceParser().parse(inputBlock);
+        assertNotNull(activityService.getLocationDumps());
+        assertEquals(activityService.getLocationDumps().getLocationClients().size(), 2);
+    }
+}
+
diff --git a/tests/src/com/android/loganalysis/parser/BugreportParserTest.java b/tests/src/com/android/loganalysis/parser/BugreportParserTest.java
index 6c8f7c2..b6e35b3 100644
--- a/tests/src/com/android/loganalysis/parser/BugreportParserTest.java
+++ b/tests/src/com/android/loganalysis/parser/BugreportParserTest.java
@@ -18,10 +18,17 @@
 import com.android.loganalysis.item.BugreportItem;
 import com.android.loganalysis.item.IItem;
 import com.android.loganalysis.item.MiscKernelLogItem;
+import com.android.loganalysis.rule.RuleEngine;
+import com.android.loganalysis.rule.RuleEngine.RuleType;
 import com.android.loganalysis.util.ArrayUtil;
 
 import junit.framework.TestCase;
 
+import org.json.JSONObject;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileReader;
 import java.text.DateFormat;
 import java.text.ParseException;
 import java.text.SimpleDateFormat;
@@ -138,7 +145,20 @@
                 "  All wakeup reasons:",
                 "  Wakeup reason 2:bcmsdh_sdmmc:2:qcom,smd:2:msmgio: 1m 5s 4ms (2 times) realtime",
                 "  Wakeup reason 2:qcom,smd-rpm:2:fc4c.qcom,spmi: 7m 1s 914ms (7 times) realtime",
-                "");
+                "",
+                "========================================================",
+                "== Running Application Services",
+                "========================================================",
+                "------ APP SERVICES (dumpsys activity service all) ------",
+                "SERVICE com.google.android.gms/"
+                + "com.google.android.location.internal.GoogleLocationManagerService f4c9d pid=14",
+                " Location Request History By Package:",
+                "Interval effective/min/max 1/0/0[s] Duration: 140[minutes] ["
+                + "com.google.android.gms, PRIORITY_NO_POWER, UserLocationProducer] "
+                + "Num requests: 2 Active: true",
+                "Interval effective/min/max 284/285/3600[s] Duration: 140[minutes] "
+                + "[com.google.android.googlequicksearchbox, PRIORITY_BALANCED_POWER_ACCURACY] "
+                + "Num requests: 5 Active: true");
 
         BugreportItem bugreport = new BugreportParser().parse(lines);
         assertNotNull(bugreport);
@@ -171,6 +191,11 @@
 
         assertNotNull(bugreport.getDumpsys());
         assertNotNull(bugreport.getDumpsys().getBatteryStats());
+
+        assertNotNull(bugreport.getActivityService());
+        assertNotNull(bugreport.getActivityService().getLocationDumps().getLocationClients());
+        assertEquals(bugreport.getActivityService().getLocationDumps().getLocationClients().size(),
+                2);
     }
 
     /**
diff --git a/tests/src/com/android/loganalysis/parser/LocationServiceParserTest.java b/tests/src/com/android/loganalysis/parser/LocationServiceParserTest.java
new file mode 100644
index 0000000..fc58e45
--- /dev/null
+++ b/tests/src/com/android/loganalysis/parser/LocationServiceParserTest.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright (C) 2016 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.LocationDumpsItem;
+import com.android.loganalysis.item.LocationDumpsItem.LocationInfoItem;
+
+import junit.framework.TestCase;
+
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * Unit tests for {@link LocationServiceParser}
+ */
+public class LocationServiceParserTest extends TestCase {
+
+    /**
+     * Test that normal input is parsed.
+     */
+    public void testLocationClientsSize() {
+        List<String> inputBlock = Arrays.asList(
+               " Location Request History By Package:",
+               " Interval effective/min/max 1/0/0[s] Duration: 140[minutes] "
+               + "[com.google.android.gms, PRIORITY_NO_POWER, UserLocationProducer] "
+               + "Num requests: 2 Active: true",
+               " Interval effective/min/max 284/285/3600[s] Duration: 140[minutes] "
+               + "[com.google.android.googlequicksearchbox, PRIORITY_BALANCED_POWER_ACCURACY] "
+               + "Num requests: 5 Active: true",
+               "  Interval effective/min/max 0/0/0[s] Duration: 0[minutes] "
+               + "[com.google.android.apps.walletnfcrel, PRIORITY_BALANCED_POWER_ACCURACY] "
+               + "Num requests: 1 Active: false",
+               "  ",
+               " FLP WakeLock Count");
+
+        LocationDumpsItem locationClients = new LocationServiceParser().parse(inputBlock);
+        assertNotNull(locationClients.getLocationClients());
+        assertEquals(locationClients.getLocationClients().size(), 3);
+    }
+
+    /**
+     * Test that normal input is parsed.
+     */
+    public void testLocationClientParser() {
+        List<String> inputBlock = Arrays.asList(
+               " Location Request History By Package:",
+               " Interval effective/min/max 1/0/0[s] Duration: 140[minutes] "
+               + "[com.google.android.gms, PRIORITY_NO_POWER, UserLocationProducer] "
+               + "Num requests: 2 Active: true");
+
+        LocationDumpsItem locationClients = new LocationServiceParser().parse(inputBlock);
+        assertNotNull(locationClients.getLocationClients());
+        LocationInfoItem client = locationClients.getLocationClients().iterator().next();
+        assertEquals(client.getPackage(), "com.google.android.gms");
+        assertEquals(client.getEffectiveInterval(), 1);
+        assertEquals(client.getMinInterval(), 0);
+        assertEquals(client.getMaxInterval(), 0);
+        assertEquals(client.getPriority(), "PRIORITY_NO_POWER");
+        assertEquals(client.getDuration(), 140);
+    }
+
+    /**
+     * Test that invalid input is parsed.
+     */
+    public void testLocationClientParserInvalidInput() {
+        List<String> inputBlock = Arrays.asList(
+                " Location Request History By Package:",
+                " Interval effective/min/max 1/0/0[s] Duration: 140[minutes] "
+                + "[com.google.android.gms PRIORITY_NO_POWER UserLocationProducer] "
+                + "Num requests: 2 Active: true");
+        LocationDumpsItem locationClients = new LocationServiceParser().parse(inputBlock);
+        assertEquals(locationClients.getLocationClients().size(), 0);
+    }
+
+}
+