Merge changes Ibc9ada6f,I2c5fce16

* changes:
  Power model calculation based on batterystats data.
  Parse the raw batterystats into an ActivityReport object.
diff --git a/tools/powermodel/src/com/android/powermodel/ActivityReport.java b/tools/powermodel/src/com/android/powermodel/ActivityReport.java
new file mode 100644
index 0000000..4a8f633
--- /dev/null
+++ b/tools/powermodel/src/com/android/powermodel/ActivityReport.java
@@ -0,0 +1,92 @@
+/*
+ * Copyright (C) 2018 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.powermodel;
+
+import java.util.Collection;
+import java.util.EnumSet;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+import com.google.common.collect.ImmutableList;
+
+/**
+ * ActivityReport contains the summary of the activity that consumes power
+ * as reported by batterystats or statsd.
+ */
+public class ActivityReport {
+    private AppList<AppActivity> mApps;
+
+    public ImmutableList<AppActivity> getAllApps() {
+        return mApps.getAllApps();
+    }
+
+    public ImmutableList<AppActivity> getRegularApps() {
+        return mApps.getRegularApps();
+    }
+
+    public List<AppActivity> findApp(String pkg) {
+        return mApps.findApp(pkg);
+    }
+
+    public AppActivity findApp(SpecialApp specialApp) {
+        return mApps.findApp(specialApp);
+    }
+
+    /**
+     * Find a component in the GLOBAL app.
+     * <p>
+     * Returns null if either the global app doesn't exist (bad data?) or the component
+     * doesn't exist in the global app.
+     */
+    public ComponentActivity findGlobalComponent(Component component) {
+         final AppActivity global = mApps.findApp(SpecialApp.GLOBAL);
+         if (global == null) {
+             return null;
+         }
+         return global.getComponentActivity(component);
+    }
+
+    public static class Builder {
+        private AppList.Builder<AppActivity,AppActivity.Builder> mApps = new AppList.Builder();
+
+        public Builder() {
+        }
+
+        public ActivityReport build() {
+            final ActivityReport result = new ActivityReport();
+            result.mApps = mApps.build();
+            return result;
+        }
+
+        public void addActivity(Component component, Collection<ComponentActivity> activities) {
+            for (final ComponentActivity activity: activities) {
+                addActivity(component, activity);
+            }
+        }
+
+        public void addActivity(Component component, ComponentActivity activity) {
+            AppActivity.Builder app = mApps.get(activity.attribution);
+            if (app == null) {
+                app = new AppActivity.Builder();
+                app.setAttribution(activity.attribution);
+                mApps.put(activity.attribution, app);
+            }
+            app.addComponentActivity(component, activity);
+        }
+    }
+}
diff --git a/tools/powermodel/src/com/android/powermodel/AppActivity.java b/tools/powermodel/src/com/android/powermodel/AppActivity.java
new file mode 100644
index 0000000..b87426c
--- /dev/null
+++ b/tools/powermodel/src/com/android/powermodel/AppActivity.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright (C) 2018 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.powermodel;
+
+import java.util.HashMap;
+
+import com.google.common.collect.ImmutableCollection;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+
+public class AppActivity extends AppInfo {
+
+    private ImmutableMap<Component, ComponentActivity> mComponents;
+    // TODO: power rails
+    // private ImmutableMap<Component, PowerRailActivity> mRails;
+
+    private AppActivity() {
+    }
+
+    /**
+     * Returns the {@link ComponentActivity} for the {@link Component} provided,
+     * or null if this AppActivity does not have that component.
+     * @more
+     * If there is no ComponentActivity for a particular Component, then
+     * there was no usage associated with that app for the app in question.
+     */
+    public ComponentActivity getComponentActivity(Component component) {
+        return mComponents.get(component);
+    }
+
+    public ImmutableSet<Component> getComponents() {
+        return mComponents.keySet();
+    }
+
+    public ImmutableMap<Component,ComponentActivity> getComponentActivities() {
+        return mComponents;
+    }
+
+    // TODO: power rails
+    // public ComponentActivity getPowerRail(Component component) {
+    //     return mComponents.get(component);
+    // }
+    //
+    // public Set<Component> getPowerRails() {
+    //     return mComponents.keySet();
+    // }
+
+    public static class Builder extends AppInfo.Builder<AppActivity> {
+        private HashMap<Component, ComponentActivity> mComponents = new HashMap();
+        // TODO power rails.
+        
+        public Builder() {
+        }
+
+        public AppActivity build() {
+            final AppActivity result = new AppActivity();
+            init(result);
+            result.mComponents = ImmutableMap.copyOf(mComponents);
+            return result;
+        }
+
+        public void addComponentActivity(Component component, ComponentActivity activity) {
+            mComponents.put(component, activity);
+        }
+    }
+}
diff --git a/tools/powermodel/src/com/android/powermodel/AppInfo.java b/tools/powermodel/src/com/android/powermodel/AppInfo.java
new file mode 100644
index 0000000..208339e
--- /dev/null
+++ b/tools/powermodel/src/com/android/powermodel/AppInfo.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2018 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.powermodel;
+
+class AppInfo {
+    private AttributionKey mAttribution;
+
+    protected AppInfo() {
+    }
+
+    public boolean hasPackage(String pkg) {
+        return mAttribution.hasPackage(pkg);
+    }
+
+    public AttributionKey getAttribution() {
+        return mAttribution;
+    }
+
+    abstract static class Builder<APP extends AppInfo> {
+        private AttributionKey mAttribution;
+
+        public Builder() {
+        }
+
+        public abstract APP build();
+
+        protected void init(AppInfo app) {
+            if (mAttribution == null) {
+                throw new RuntimeException("setAttribution(AttributionKey attribution) not called");
+            }
+            app.mAttribution = mAttribution;
+        }
+
+        public void setAttribution(AttributionKey attribution) {
+            mAttribution = attribution;
+        }
+
+        public AttributionKey getAttribution() {
+            return mAttribution;
+        }
+    }
+}
diff --git a/tools/powermodel/src/com/android/powermodel/AppList.java b/tools/powermodel/src/com/android/powermodel/AppList.java
new file mode 100644
index 0000000..19572fc
--- /dev/null
+++ b/tools/powermodel/src/com/android/powermodel/AppList.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright (C) 2018 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.powermodel;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+
+class AppList<APP extends AppInfo> {
+    private ImmutableList<APP> mAllApps;
+    private ImmutableList<APP> mRegularApps;
+    private ImmutableMap<SpecialApp,APP> mSpecialApps;
+
+    private AppList() {
+    }
+
+    public ImmutableList<APP> getAllApps() {
+        return mAllApps;
+    }
+
+    public ImmutableList<APP> getRegularApps() {
+        return mRegularApps;
+    }
+
+    public List<APP> findApp(String pkg) {
+        List<APP> result = new ArrayList();
+        for (APP app: mRegularApps) {
+            if (app.hasPackage(pkg)) {
+                result.add(app);
+            }
+        }
+        return result;
+    }
+
+    public APP findApp(SpecialApp specialApp) {
+        return mSpecialApps.get(specialApp);
+    }
+
+    public static class Builder<APP extends AppInfo, BUILDER extends AppInfo.Builder<APP>> {
+        private final HashMap<AttributionKey,BUILDER> mApps = new HashMap();
+
+        public Builder() {
+        }
+
+        public AppList<APP> build() {
+            final AppList<APP> result = new AppList();
+            final ArrayList<APP> allApps = new ArrayList();
+            final ArrayList<APP> regularApps = new ArrayList();
+            final HashMap<SpecialApp,APP> specialApps = new HashMap();
+            for (AppInfo.Builder<APP> app: mApps.values()) {
+                final AttributionKey attribution = app.getAttribution();
+                final APP appActivity = app.build();
+                allApps.add(appActivity);
+                if (attribution.isSpecialApp()) {
+                    specialApps.put(attribution.getSpecialApp(), appActivity);
+                } else {
+                    regularApps.add(appActivity);
+                }
+            }
+            result.mAllApps = ImmutableList.copyOf(allApps);
+            result.mRegularApps = ImmutableList.copyOf(regularApps);
+            result.mSpecialApps = ImmutableMap.copyOf(specialApps);
+            return result;
+        }
+
+        public BUILDER get(AttributionKey attribution) {
+            return mApps.get(attribution);
+        }
+
+        public BUILDER put(AttributionKey attribution, BUILDER app) {
+            return mApps.put(attribution, app);
+        }
+    }
+}
diff --git a/tools/powermodel/src/com/android/powermodel/AppPower.java b/tools/powermodel/src/com/android/powermodel/AppPower.java
new file mode 100644
index 0000000..283982b
--- /dev/null
+++ b/tools/powermodel/src/com/android/powermodel/AppPower.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright (C) 2018 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.powermodel;
+
+import java.util.HashMap;
+import java.util.Set;
+
+import com.google.common.collect.ImmutableMap;
+
+public class AppPower extends AppInfo {
+    private ImmutableMap<Component, ComponentPower> mComponents;
+
+    private double mAppPowerMah;
+
+
+    private AppPower() {
+    }
+
+    /**
+     * Returns the {@link ComponentPower} for the {@link Component} provided,
+     * or null if this AppPower does not have that component.
+     * @more
+     * If the component was in the power profile for this device, there
+     * will be a component for it, even if there was no power used
+     * by that component. In that case, the
+     * {@link ComponentPower.getUsage() ComponentPower.getUsage()}
+     * method will return 0.
+     */
+    public ComponentPower getComponentPower(Component component) {
+        return mComponents.get(component);
+    }
+
+    public Set<Component> getComponents() {
+        return mComponents.keySet();
+    }
+
+    /**
+     * Return the total power used by this app.
+     */
+    public double getAppPowerMah() {
+        return mAppPowerMah;
+    }
+
+    /**
+     * Builder class for {@link AppPower}
+     */
+    public static class Builder extends AppInfo.Builder<AppPower> {
+        private HashMap<Component, ComponentPower> mComponents = new HashMap();
+
+        public Builder() {
+        }
+
+        public AppPower build() {
+            final AppPower result = new AppPower();
+            init(result);
+            result.mComponents = ImmutableMap.copyOf(mComponents);
+
+            // Add up the components
+            double appPowerMah = 0;
+            for (final ComponentPower componentPower: mComponents.values()) {
+                appPowerMah += componentPower.powerMah;
+            }
+            result.mAppPowerMah = appPowerMah;
+
+            return result;
+        }
+
+        public void addComponentPower(Component component, ComponentPower componentPower) {
+            mComponents.put(component, componentPower);
+        }
+    }
+}
diff --git a/tools/powermodel/src/com/android/powermodel/BatteryStatsReader.java b/tools/powermodel/src/com/android/powermodel/BatteryStatsReader.java
new file mode 100644
index 0000000..595c661
--- /dev/null
+++ b/tools/powermodel/src/com/android/powermodel/BatteryStatsReader.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright (C) 2018 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.powermodel;
+
+import java.io.InputStream;
+import java.io.IOException;
+import com.android.powermodel.component.ModemBatteryStatsReader;
+
+public class BatteryStatsReader {
+    /**
+     * Construct a reader.
+     */
+    public BatteryStatsReader() {
+    }
+
+    /**
+     * Parse a powermodel.xml file and return a PowerProfile object.
+     *
+     * @param stream An InputStream containing the batterystats output.
+     *
+     * @throws ParseException Thrown when the xml file can not be parsed.
+     * @throws IOException When there is a problem reading the stream.
+     */
+    public static ActivityReport parse(InputStream stream) throws ParseException, IOException {
+        final Parser parser = new Parser(stream);
+        return parser.parse();
+    }
+
+    /**
+     * Implements the reading and power model logic.
+     */
+    private static class Parser {
+        final InputStream mStream;
+        final ActivityReport mResult;
+        RawBatteryStats mBs;
+
+        /**
+         * Constructor to capture the parameters to read.
+         */
+        Parser(InputStream stream) {
+            mStream = stream;
+            mResult = new ActivityReport();
+        }
+
+        /**
+         * Read the stream, parse it, and apply the power model.
+         * Do not call this more than once.
+         */
+        ActivityReport parse() throws ParseException, IOException {
+            mBs = RawBatteryStats.parse(mStream);
+
+            final ActivityReport.Builder report = new ActivityReport.Builder();
+
+            report.addActivity(Component.MODEM, ModemBatteryStatsReader.createActivities(mBs));
+
+            return report.build();
+        }
+    }
+}
+
diff --git a/tools/powermodel/src/com/android/powermodel/ComponentActivity.java b/tools/powermodel/src/com/android/powermodel/ComponentActivity.java
new file mode 100644
index 0000000..c1e2662
--- /dev/null
+++ b/tools/powermodel/src/com/android/powermodel/ComponentActivity.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2018 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.powermodel;
+
+
+/**
+ * Encapsulates the work done by an app (including synthetic apps) that costs power.
+ */
+public class ComponentActivity {
+    public AttributionKey attribution;
+
+    protected ComponentActivity(AttributionKey attribution) {
+        this.attribution = attribution;
+    }
+
+    // TODO: Can we refactor what goes into the activities so this function
+    // doesn't need the global state?
+    /**
+     * Apply the power profile for this component.  Subclasses should implement this
+     * to do the per-component calculatinos.  The default implementation returns null.
+     * If this method returns null, then there will be no power associated for this
+     * component, which, for example is true with some of the GLOBAL activities.
+     */
+    public ComponentPower applyProfile(ActivityReport activityReport, PowerProfile profile) {
+        return null;
+    }
+}
+
diff --git a/tools/powermodel/src/com/android/powermodel/ComponentPower.java b/tools/powermodel/src/com/android/powermodel/ComponentPower.java
new file mode 100644
index 0000000..b22ff87
--- /dev/null
+++ b/tools/powermodel/src/com/android/powermodel/ComponentPower.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2018 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.powermodel;
+
+/**
+ * The hardware component that uses power on a device.
+ * <p>
+ * This base class contains the total power used by each Component in an app.
+ * Subclasses may add more detail, which is a drill-down, but is not to be
+ * <i>added</i> to {@link #powerMah}.
+ */
+public abstract class ComponentPower<ACTIVITY extends ComponentActivity> {
+    /**
+     * The app associated with this ComponentPower.
+     */
+    public AttributionKey attribution;
+
+    /**
+     * The app activity that resulted in the power usage for this component.
+     */
+    public ACTIVITY activity;
+
+    /**
+     * The total power used by this component in this app.
+     */
+    public double powerMah;
+}
diff --git a/tools/powermodel/src/com/android/powermodel/PowerReport.java b/tools/powermodel/src/com/android/powermodel/PowerReport.java
new file mode 100644
index 0000000..76ba672
--- /dev/null
+++ b/tools/powermodel/src/com/android/powermodel/PowerReport.java
@@ -0,0 +1,101 @@
+/*
+ * Copyright (C) 2018 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.powermodel;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
+
+import com.google.common.collect.ImmutableMap;
+
+/**
+ * PowerReport contains the summary of all power used on a device
+ * as reported by batterystats or statsd, based on the power profile.
+ */
+public class PowerReport {
+    private AppList<AppPower> mApps;
+    private double mTotalPowerMah;
+
+    private PowerReport() {
+    }
+
+    /**
+     * The total power used by this device for this PowerReport.
+     */
+    public double getTotalPowerMah() {
+        return mTotalPowerMah;
+    }
+
+    public List<AppPower> getAllApps() {
+        return mApps.getAllApps();
+    }
+
+    public List<AppPower> getRegularApps() {
+        return mApps.getRegularApps();
+    }
+
+    public List<AppPower> findApp(String pkg) {
+        return mApps.findApp(pkg);
+    }
+
+    public AppPower findApp(SpecialApp specialApp) {
+        return mApps.findApp(specialApp);
+    }
+
+    public static PowerReport createReport(PowerProfile profile, ActivityReport activityReport) {
+        final PowerReport.Builder powerReport = new PowerReport.Builder();
+        for (final AppActivity appActivity: activityReport.getAllApps()) {
+            final AppPower.Builder appPower = new AppPower.Builder();
+            appPower.setAttribution(appActivity.getAttribution());
+
+            for (final ImmutableMap.Entry<Component,ComponentActivity> entry:
+                    appActivity.getComponentActivities().entrySet()) {
+                final ComponentPower componentPower = entry.getValue()
+                        .applyProfile(activityReport, profile);
+                if (componentPower != null) {
+                    appPower.addComponentPower(entry.getKey(), componentPower);
+                }
+            }
+
+            powerReport.add(appPower);
+        }
+        return powerReport.build();
+    }
+
+    private static class Builder {
+        private AppList.Builder mApps = new AppList.Builder();
+
+        public Builder() {
+        }
+
+        public PowerReport build() {
+            final PowerReport report = new PowerReport();
+
+            report.mApps = mApps.build();
+
+            for (AppPower app: report.mApps.getAllApps()) {
+                report.mTotalPowerMah += app.getAppPowerMah();
+            }
+
+            return report;
+        }
+
+        public void add(AppPower.Builder app) {
+            mApps.put(app.getAttribution(), app);
+        }
+    }
+}
diff --git a/tools/powermodel/src/com/android/powermodel/RawBatteryStats.java b/tools/powermodel/src/com/android/powermodel/RawBatteryStats.java
index d0c1790..76c0482 100644
--- a/tools/powermodel/src/com/android/powermodel/RawBatteryStats.java
+++ b/tools/powermodel/src/com/android/powermodel/RawBatteryStats.java
@@ -168,6 +168,80 @@
         public String lineType;
     }
 
+    @Line(tag="bt", scope=Scope.SYSTEM, count=Count.SINGLE)
+    public static class Battery extends Record {
+        // If which != STATS_SINCE_CHARGED, the csv will be "N/A" and we will get
+        // a parsing warning.  Nobody uses anything other than STATS_SINCE_CHARGED.
+        @Field(index=0)
+        public int startCount;
+
+        @Field(index=1)
+        public long whichBatteryRealtimeMs;
+
+        @Field(index=2)
+        public long whichBatteryUptimeMs;
+
+        @Field(index=3)
+        public long totalRealtimeMs;
+
+        @Field(index=4)
+        public long totalUptimeMs;
+
+        @Field(index=5)
+        public long getStartClockTimeMs;
+
+        @Field(index=6)
+        public long whichBatteryScreenOffRealtimeMs;
+
+        @Field(index=7)
+        public long whichBatteryScreenOffUptimeMs;
+
+        @Field(index=8)
+        public long estimatedBatteryCapacityMah;
+
+        @Field(index=9)
+        public long minLearnedBatteryCapacityMah;
+
+        @Field(index=10)
+        public long maxLearnedBatteryCapacityMah;
+
+        @Field(index=11)
+        public long screenDozeTimeMs;
+    }
+
+    @Line(tag="gn", scope=Scope.SYSTEM, count=Count.SINGLE)
+    public static class GlobalNetwork extends Record {
+        @Field(index=0)
+        public long mobileRxTotalBytes;
+
+        @Field(index=1)
+        public long mobileTxTotalBytes;
+
+        @Field(index=2)
+        public long wifiRxTotalBytes;
+
+        @Field(index=3)
+        public long wifiTxTotalBytes;
+
+        @Field(index=4)
+        public long mobileRxTotalPackets;
+
+        @Field(index=5)
+        public long mobileTxTotalPackets;
+
+        @Field(index=6)
+        public long wifiRxTotalPackets;
+
+        @Field(index=7)
+        public long wifiTxTotalPackets;
+
+        @Field(index=8)
+        public long btRxTotalBytes;
+
+        @Field(index=9)
+        public long btTxTotalBytes;
+    }
+
     @Line(tag="gmcd", scope=Scope.SYSTEM, count=Count.SINGLE)
     public static class GlobalModemController extends Record {
         @Field(index=0)
@@ -183,6 +257,154 @@
         public long[] txTimeMs;
     }
 
+    @Line(tag="m", scope=Scope.SYSTEM, count=Count.SINGLE)
+    public static class Misc extends Record {
+        @Field(index=0)
+        public long screenOnTimeMs;
+
+        @Field(index=1)
+        public long phoneOnTimeMs;
+
+        @Field(index=2)
+        public long fullWakeLockTimeTotalMs;
+
+        @Field(index=3)
+        public long partialWakeLockTimeTotalMs;
+
+        @Field(index=4)
+        public long mobileRadioActiveTimeMs;
+
+        @Field(index=5)
+        public long mobileRadioActiveAdjustedTimeMs;
+
+        @Field(index=6)
+        public long interactiveTimeMs;
+
+        @Field(index=7)
+        public long powerSaveModeEnabledTimeMs;
+
+        @Field(index=8)
+        public int connectivityChangeCount;
+
+        @Field(index=9)
+        public long deepDeviceIdleModeTimeMs;
+
+        @Field(index=10)
+        public long deepDeviceIdleModeCount;
+
+        @Field(index=11)
+        public long deepDeviceIdlingTimeMs;
+
+        @Field(index=12)
+        public long deepDeviceIdlingCount;
+
+        @Field(index=13)
+        public long mobileRadioActiveCount;
+
+        @Field(index=14)
+        public long mobileRadioActiveUnknownTimeMs;
+
+        @Field(index=15)
+        public long lightDeviceIdleModeTimeMs;
+
+        @Field(index=16)
+        public long lightDeviceIdleModeCount;
+
+        @Field(index=17)
+        public long lightDeviceIdlingTimeMs;
+
+        @Field(index=18)
+        public long lightDeviceIdlingCount;
+
+        @Field(index=19)
+        public long lightLongestDeviceIdleModeTimeMs;
+
+        @Field(index=20)
+        public long deepLongestDeviceIdleModeTimeMs;
+    }
+
+    @Line(tag="nt", scope=Scope.UID, count=Count.SINGLE)
+    public static class Network extends Record {
+        @Field(index=0)
+        public long mobileRxBytes;
+
+        @Field(index=1)
+        public long mobileTxBytes;
+
+        @Field(index=2)
+        public long wifiRxBytes;
+
+        @Field(index=3)
+        public long wifiTxBytes;
+
+        @Field(index=4)
+        public long mobileRxPackets;
+
+        @Field(index=5)
+        public long mobileTxPackets;
+
+        @Field(index=6)
+        public long wifiRxPackets;
+
+        @Field(index=7)
+        public long wifiTxPackets;
+
+        // This is microseconds, because... batterystats.
+        @Field(index=8)
+        public long mobileRadioActiveTimeUs;
+
+        @Field(index=9)
+        public long mobileRadioActiveCount;
+
+        @Field(index=10)
+        public long btRxBytes;
+
+        @Field(index=11)
+        public long btTxBytes;
+
+        @Field(index=12)
+        public long mobileWakeupCount;
+
+        @Field(index=13)
+        public long wifiWakeupCount;
+
+        @Field(index=14)
+        public long mobileBgRxBytes;
+
+        @Field(index=15)
+        public long mobileBgTxBytes;
+
+        @Field(index=16)
+        public long wifiBgRxBytes;
+
+        @Field(index=17)
+        public long wifiBgTxBytes;
+
+        @Field(index=18)
+        public long mobileBgRxPackets;
+
+        @Field(index=19)
+        public long mobileBgTxPackets;
+
+        @Field(index=20)
+        public long wifiBgRxPackets;
+
+        @Field(index=21)
+        public long wifiBgTxPackets;
+    }
+
+    @Line(tag="sgt", scope=Scope.SYSTEM, count=Count.SINGLE)
+    public static class SignalStrengthTime extends Record {
+        @Field(index=0)
+        public long[] phoneSignalStrengthTimeMs;
+    }
+
+    @Line(tag="sst", scope=Scope.SYSTEM, count=Count.SINGLE)
+    public static class SignalScanningTime extends Record {
+        @Field(index=0)
+        public long phoneSignalScanningTimeMs;
+    }
+
     @Line(tag="uid", scope=Scope.UID, count=Count.MULTIPLE)
     public static class Uid extends Record {
         @Field(index=0)
diff --git a/tools/powermodel/src/com/android/powermodel/component/ModemAppActivity.java b/tools/powermodel/src/com/android/powermodel/component/ModemAppActivity.java
new file mode 100644
index 0000000..cb70051
--- /dev/null
+++ b/tools/powermodel/src/com/android/powermodel/component/ModemAppActivity.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright (C) 2018 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.powermodel.component;
+
+import com.android.powermodel.ActivityReport;
+import com.android.powermodel.AttributionKey;
+import com.android.powermodel.Component;
+import com.android.powermodel.ComponentActivity;
+import com.android.powermodel.PowerProfile;
+import com.android.powermodel.util.Conversion;
+
+/**
+ * Encapsulates the work done by the celluar modem on behalf of an app.
+ */
+public class ModemAppActivity extends ComponentActivity {
+    /**
+     * Construct a new ModemAppActivity.
+     */
+    public ModemAppActivity(AttributionKey attribution) {
+        super(attribution);
+    }
+
+    /**
+     * The number of packets received by the app.
+     */
+    public long rxPacketCount;
+
+    /**
+     * The number of packets sent by the app.
+     */
+    public long txPacketCount;
+
+    @Override
+    public ModemAppPower applyProfile(ActivityReport activityReport, PowerProfile profile) {
+        // Profile
+        final ModemProfile modemProfile = (ModemProfile)profile.getComponent(Component.MODEM);
+        if (modemProfile == null) {
+            // TODO: This is kind of a big problem...  Should this throw instead?
+            return null;
+        }
+
+        // Activity
+        final ModemGlobalActivity global
+                = (ModemGlobalActivity)activityReport.findGlobalComponent(Component.MODEM);
+        if (global == null) {
+            return null;
+        }
+
+        final double averageModemPowerMa = getAverageModemPowerMa(modemProfile);
+        final long totalPacketCount = global.rxPacketCount + global.txPacketCount;
+        final long appPacketCount = this.rxPacketCount + this.txPacketCount;
+
+        final ModemAppPower result = new ModemAppPower();
+        result.attribution = this.attribution;
+        result.activity = this;
+        result.powerMah = Conversion.msToHr(
+                (totalPacketCount > 0 ? (appPacketCount / (double)totalPacketCount) : 0)
+                * global.totalActiveTimeMs
+                * averageModemPowerMa);
+        return result;
+    }
+
+    static final double getAverageModemPowerMa(ModemProfile profile) {
+        double sumMa = profile.getRxMa();
+        for (float powerAtTxLevelMa: profile.getTxMa()) {
+            sumMa += powerAtTxLevelMa;
+        }
+        return sumMa / (profile.getTxMa().length + 1);
+    }
+}
+
diff --git a/tools/powermodel/src/com/android/powermodel/component/ModemAppPower.java b/tools/powermodel/src/com/android/powermodel/component/ModemAppPower.java
new file mode 100644
index 0000000..f553127
--- /dev/null
+++ b/tools/powermodel/src/com/android/powermodel/component/ModemAppPower.java
@@ -0,0 +1,24 @@
+/*
+ * Copyright (C) 2018 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.powermodel.component;
+
+import com.android.powermodel.Component;
+import com.android.powermodel.ComponentPower;
+
+public class ModemAppPower extends ComponentPower<ModemAppActivity> {
+}
+
diff --git a/tools/powermodel/src/com/android/powermodel/component/ModemBatteryStatsReader.java b/tools/powermodel/src/com/android/powermodel/component/ModemBatteryStatsReader.java
new file mode 100644
index 0000000..6dbfbc2
--- /dev/null
+++ b/tools/powermodel/src/com/android/powermodel/component/ModemBatteryStatsReader.java
@@ -0,0 +1,110 @@
+/*
+ * Copyright (C) 2018 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.powermodel.component;
+
+import java.util.ArrayList;
+import java.util.List;
+import com.android.powermodel.AttributionKey;
+import com.android.powermodel.ComponentActivity;
+import com.android.powermodel.RawBatteryStats;
+import com.android.powermodel.SpecialApp;
+
+public class ModemBatteryStatsReader {
+    private ModemBatteryStatsReader() {
+    }
+
+    public static List<ComponentActivity> createActivities(RawBatteryStats bs) {
+        final List<ComponentActivity> result = new ArrayList<ComponentActivity>();
+
+        // The whole system
+        createGlobal(result, bs);
+
+        // The apps
+        createApps(result, bs);
+
+        // The synthetic "cell" app.
+        createRemainder(result, bs);
+
+        return result;
+    }
+
+    private static void createGlobal(List<ComponentActivity> result, RawBatteryStats bs) {
+        final ModemGlobalActivity global
+                = new ModemGlobalActivity(new AttributionKey(SpecialApp.GLOBAL));
+
+        final RawBatteryStats.GlobalNetwork gn = bs.getSingle(RawBatteryStats.GlobalNetwork.class);
+        final RawBatteryStats.Misc misc = bs.getSingle(RawBatteryStats.Misc.class);
+
+        // Null here just means no network activity.
+        if (gn != null && misc != null) {
+            global.rxPacketCount = gn.mobileRxTotalPackets;
+            global.txPacketCount = gn.mobileTxTotalPackets;
+
+            global.totalActiveTimeMs = misc.mobileRadioActiveTimeMs;
+        }
+
+        result.add(global);
+    }
+
+    private static void createApps(List<ComponentActivity> result, RawBatteryStats bs) {
+        for (AttributionKey key: bs.getApps()) {
+            final int uid = key.getUid();
+            final RawBatteryStats.Network network
+                    = bs.getSingle(RawBatteryStats.Network.class, uid);
+
+            // Null here just means no network activity.
+            if (network != null) {
+                final ModemAppActivity app = new ModemAppActivity(key);
+
+                app.rxPacketCount = network.mobileRxPackets;
+                app.txPacketCount = network.mobileTxPackets;
+
+                result.add(app);
+            }
+        }
+    }
+
+    private static void createRemainder(List<ComponentActivity> result, RawBatteryStats bs) {
+        final RawBatteryStats.SignalStrengthTime strength
+                = bs.getSingle(RawBatteryStats.SignalStrengthTime.class);
+        final RawBatteryStats.SignalScanningTime scanning
+                = bs.getSingle(RawBatteryStats.SignalScanningTime.class);
+        final RawBatteryStats.Misc misc = bs.getSingle(RawBatteryStats.Misc.class);
+
+        if (strength != null && scanning != null && misc != null) {
+            final ModemRemainderActivity remainder
+                    = new ModemRemainderActivity(new AttributionKey(SpecialApp.REMAINDER));
+
+            // Signal strength buckets
+            remainder.strengthTimeMs = strength.phoneSignalStrengthTimeMs;
+
+            // Time spent scanning
+            remainder.scanningTimeMs = scanning.phoneSignalScanningTimeMs;
+
+            // Unaccounted for active time
+            final long totalActiveTimeMs = misc.mobileRadioActiveTimeMs;
+            long appActiveTimeMs = 0;
+            for (RawBatteryStats.Network nw: bs.getMultiple(RawBatteryStats.Network.class)) {
+                appActiveTimeMs += nw.mobileRadioActiveTimeUs / 1000;
+            }
+            remainder.activeTimeMs = totalActiveTimeMs - appActiveTimeMs;
+
+            result.add(remainder);
+        }
+    }
+}
+
diff --git a/tools/powermodel/src/com/android/powermodel/component/ModemGlobalActivity.java b/tools/powermodel/src/com/android/powermodel/component/ModemGlobalActivity.java
new file mode 100644
index 0000000..a53b53e
--- /dev/null
+++ b/tools/powermodel/src/com/android/powermodel/component/ModemGlobalActivity.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2018 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.powermodel.component;
+
+import com.android.powermodel.ActivityReport;
+import com.android.powermodel.AttributionKey;
+import com.android.powermodel.ComponentActivity;
+import com.android.powermodel.ComponentPower;
+import com.android.powermodel.PowerProfile;
+
+/**
+ * Encapsulates total work done by the modem for the device.
+ */
+public class ModemGlobalActivity extends ComponentActivity {
+    /**
+     * Construct a new ModemGlobalActivity.
+     */
+    public ModemGlobalActivity(AttributionKey attribution) {
+        super(attribution);
+    }
+
+    /**
+     * Returns the total number of packets received in the whole device.
+     */
+    public long rxPacketCount;
+
+    /**
+     * Returns the total number of packets sent in the whole device.
+     */
+    public long txPacketCount;
+
+    /**
+     * Returns the total time the radio was active in the whole device.
+     */
+    public long totalActiveTimeMs;
+}
+
diff --git a/tools/powermodel/src/com/android/powermodel/component/ModemRemainderActivity.java b/tools/powermodel/src/com/android/powermodel/component/ModemRemainderActivity.java
new file mode 100644
index 0000000..0e268c2
--- /dev/null
+++ b/tools/powermodel/src/com/android/powermodel/component/ModemRemainderActivity.java
@@ -0,0 +1,87 @@
+/*
+ * Copyright (C) 2018 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.powermodel.component;
+
+import com.android.powermodel.ActivityReport;
+import com.android.powermodel.AttributionKey;
+import com.android.powermodel.Component;
+import com.android.powermodel.ComponentActivity;
+import com.android.powermodel.PowerProfile;
+import com.android.powermodel.util.Conversion;
+
+/**
+ * Encapsulates the work done by the remaining 
+ */
+public class ModemRemainderActivity extends ComponentActivity {
+    /**
+     * Construct a new ModemRemainderActivity.
+     */
+    public ModemRemainderActivity(AttributionKey attribution) {
+        super(attribution);
+    }
+
+    /**
+     * Number of milliseconds spent at each of the signal strengths.
+     */
+    public long[] strengthTimeMs;
+
+    /**
+     * Number of milliseconds spent scanning for a network.
+     */
+    public long scanningTimeMs;
+
+    /**
+     * Number of milliseconds that the radio is active for reasons other
+     * than an app transmitting and receiving data.
+     */
+    public long activeTimeMs;
+
+    @Override
+    public ModemRemainderPower applyProfile(ActivityReport activityReport, PowerProfile profile) {
+        // Profile
+        final ModemProfile modemProfile = (ModemProfile)profile.getComponent(Component.MODEM);
+        if (modemProfile == null) {
+            return null;
+        }
+
+        // Activity
+        final ModemRemainderPower result = new ModemRemainderPower();
+        result.attribution = this.attribution;
+        result.activity = this;
+
+        // strengthMah
+        // TODO: If the array lengths don't match... then?
+        result.strengthMah = new double[this.strengthTimeMs.length];
+        for (int i=0; i<this.strengthTimeMs.length; i++) {
+            result.strengthMah[i] = Conversion.msToHr(
+                    this.strengthTimeMs[i] * modemProfile.getTxMa()[i]);
+            result.powerMah += result.strengthMah[i];
+        }
+
+        // scanningMah
+        result.scanningMah = Conversion.msToHr(this.scanningTimeMs * modemProfile.getScanningMa());
+        result.powerMah += result.scanningMah;
+
+        // activeMah
+        result.activeMah = Conversion.msToHr(
+                this.activeTimeMs * ModemAppActivity.getAverageModemPowerMa(modemProfile));
+        result.powerMah += result.activeMah;
+
+        return result;
+    }
+}
+
diff --git a/tools/powermodel/src/com/android/powermodel/component/ModemRemainderPower.java b/tools/powermodel/src/com/android/powermodel/component/ModemRemainderPower.java
new file mode 100644
index 0000000..7f38cd3
--- /dev/null
+++ b/tools/powermodel/src/com/android/powermodel/component/ModemRemainderPower.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2018 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.powermodel.component;
+
+import com.android.powermodel.Component;
+import com.android.powermodel.ComponentPower;
+
+public class ModemRemainderPower extends ComponentPower<ModemRemainderActivity> {
+
+    public double[] strengthMah;
+
+    public double scanningMah;
+
+    public double activeMah;
+}
+
diff --git a/tools/powermodel/src/com/android/powermodel/util/Conversion.java b/tools/powermodel/src/com/android/powermodel/util/Conversion.java
index 9a79a2d..e556c25 100644
--- a/tools/powermodel/src/com/android/powermodel/util/Conversion.java
+++ b/tools/powermodel/src/com/android/powermodel/util/Conversion.java
@@ -35,6 +35,10 @@
         return result;
     }
     
+    public static double msToHr(double ms) {
+        return ms / 3600.0 / 1000.0;
+    }
+
     /**
      * No public constructor.
      */
diff --git a/tools/powermodel/test-resource/bs.csv b/tools/powermodel/test-resource/bs.csv
new file mode 100644
index 0000000..6e84120
--- /dev/null
+++ b/tools/powermodel/test-resource/bs.csv
@@ -0,0 +1,7 @@
+9,0,i,vers,32,177,PPR1.180326.002,PQ1A.181105.015
+9,0,i,uid,10139,com.google.android.gm
+9,0,l,gn,108060756,17293456,4896592,3290614,97840,72941,6903,8107,390,105
+9,0,l,m,2590630,0,384554,3943868,5113727,265,2565483,0,16,0,0,0,0,192,25331,3472068,17,3543323,14,614050,0
+9,10139,l,nt,13688501,534571,13842,7792,9925,5577,30,67,190051799,27,0,0,5,3,126020,42343,13842,7792,207,167,30,67
+9,0,l,sgt,3066958,0,34678,1643364,7045084
+9,0,l,sst,2443805
diff --git a/tools/powermodel/test/com/android/powermodel/BatteryStatsReaderTest.java b/tools/powermodel/test/com/android/powermodel/BatteryStatsReaderTest.java
new file mode 100644
index 0000000..e7b2c37
--- /dev/null
+++ b/tools/powermodel/test/com/android/powermodel/BatteryStatsReaderTest.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright (C) 2018 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.powermodel;
+
+import java.io.ByteArrayInputStream;
+import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
+import java.util.List;
+import org.junit.Test;
+import org.junit.Assert;
+
+import com.android.powermodel.component.ModemAppActivity;
+import com.android.powermodel.component.ModemGlobalActivity;
+import com.android.powermodel.component.ModemRemainderActivity;
+
+/**
+ * Tests {@link BatteryStatsReader}.
+ */
+public class BatteryStatsReaderTest {
+    private static InputStream loadCsvStream() {
+        return BatteryStatsReaderTest.class.getResourceAsStream("/bs.csv");
+    }
+
+    @Test public void testModemGlobal() throws Exception {
+        final ActivityReport report = BatteryStatsReader.parse(loadCsvStream());
+
+        final AppActivity global = report.findApp(SpecialApp.GLOBAL);
+        Assert.assertNotNull(global);
+
+        final ModemGlobalActivity modem
+                = (ModemGlobalActivity)global.getComponentActivity(Component.MODEM);
+        Assert.assertNotNull(modem);
+        Assert.assertEquals(97840, modem.rxPacketCount);
+        Assert.assertEquals(72941, modem.txPacketCount);
+        Assert.assertEquals(5113727, modem.totalActiveTimeMs);
+    }
+
+    @Test public void testModemApp() throws Exception {
+        final ActivityReport report = BatteryStatsReader.parse(loadCsvStream());
+
+        final List<AppActivity> gmailList = report.findApp("com.google.android.gm");
+        Assert.assertEquals(1, gmailList.size());
+        final AppActivity gmail = gmailList.get(0);
+
+        final ModemAppActivity modem
+                = (ModemAppActivity)gmail.getComponentActivity(Component.MODEM);
+        Assert.assertNotNull(modem);
+        Assert.assertEquals(9925, modem.rxPacketCount);
+        Assert.assertEquals(5577, modem.txPacketCount);
+    }
+
+    @Test public void testModemRemainder() throws Exception {
+        final ActivityReport report = BatteryStatsReader.parse(loadCsvStream());
+
+        final AppActivity remainder = report.findApp(SpecialApp.REMAINDER);
+        Assert.assertNotNull(remainder);
+
+        final ModemRemainderActivity modem
+                = (ModemRemainderActivity)remainder.getComponentActivity(Component.MODEM);
+        Assert.assertNotNull(modem);
+        Assert.assertArrayEquals(new long[] { 3066958, 0, 34678, 1643364, 7045084 },
+                modem.strengthTimeMs);
+        Assert.assertEquals(2443805, modem.scanningTimeMs);
+        Assert.assertEquals(4923676, modem.activeTimeMs);
+    }
+}
diff --git a/tools/powermodel/test/com/android/powermodel/PowerReportTest.java b/tools/powermodel/test/com/android/powermodel/PowerReportTest.java
new file mode 100644
index 0000000..1a61737
--- /dev/null
+++ b/tools/powermodel/test/com/android/powermodel/PowerReportTest.java
@@ -0,0 +1,128 @@
+/*
+ * Copyright (C) 2018 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.powermodel;
+
+import java.io.ByteArrayInputStream;
+import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
+import java.util.List;
+import org.junit.Test;
+import org.junit.Assert;
+
+import com.android.powermodel.component.ModemAppPower;
+import com.android.powermodel.component.ModemRemainderPower;
+
+/**
+ * Tests {@link PowerReport}.
+ */
+public class PowerReportTest {
+    private static final double EPSILON = 0.001;
+    private static final double MS_PER_HR = 3600000.0;
+
+    private static final double AVERAGE_MODEM_POWER = ((11+16+19+22+73+132) / 6.0);
+    private static final double GMAIL_MODEM_MAH = ((9925+5577) / (double)(97840+72941))
+            * 5113727 * AVERAGE_MODEM_POWER * (1.0 / 3600 / 1000);
+    private static final double GMAIL_MAH
+            = GMAIL_MODEM_MAH;
+
+    private static final double REMAINDER_MODEM_MAH
+            =  (1.0 / 3600 / 1000)
+            * ((3066958 * 16) + (0 * 19) + (34678 * 22) + (1643364 * 73) + (7045084 * 132)
+                + (2443805 * 12)
+                + (4923676 * AVERAGE_MODEM_POWER));
+    private static final double REMAINDER_MAH
+            = REMAINDER_MODEM_MAH;
+
+    private static final double TOTAL_MAH
+            = GMAIL_MAH
+            + REMAINDER_MAH;
+
+    private static InputStream loadPowerProfileStream() {
+        return PowerProfileTest.class.getResourceAsStream("/power_profile.xml");
+    }
+
+    private static InputStream loadCsvStream() {
+        return BatteryStatsReaderTest.class.getResourceAsStream("/bs.csv");
+    }
+
+    private static PowerReport loadPowerReport() throws Exception {
+        final PowerProfile profile = PowerProfile.parse(loadPowerProfileStream());
+        final ActivityReport activity = BatteryStatsReader.parse(loadCsvStream());
+        return PowerReport.createReport(profile, activity);
+    }
+
+    @Test public void testModemApp() throws Exception {
+        final PowerReport report = loadPowerReport();
+
+        final List<AppPower> gmailList = report.findApp("com.google.android.gm");
+        Assert.assertEquals(1, gmailList.size());
+        final AppPower gmail = gmailList.get(0);
+
+        final ModemAppPower modem = (ModemAppPower)gmail.getComponentPower(Component.MODEM);
+        Assert.assertNotNull(modem);
+        Assert.assertEquals(GMAIL_MODEM_MAH, modem.powerMah, EPSILON);
+    }
+
+    @Test public void testModemRemainder() throws Exception {
+        final PowerReport report = loadPowerReport();
+
+        final AppPower remainder = report.findApp(SpecialApp.REMAINDER);
+        Assert.assertNotNull(remainder);
+
+        final ModemRemainderPower modem
+                = (ModemRemainderPower)remainder.getComponentPower(Component.MODEM);
+        Assert.assertNotNull(modem);
+
+        Assert.assertArrayEquals(new double[] {
+                    3066958 * 16.0 / MS_PER_HR,
+                    0 * 19.0 / MS_PER_HR,
+                    34678 * 22.0 / MS_PER_HR,
+                    1643364 * 73.0 / MS_PER_HR,
+                    7045084 * 132.0 / MS_PER_HR },
+                modem.strengthMah, EPSILON);
+        Assert.assertEquals(2443805 * 12 / MS_PER_HR, modem.scanningMah, EPSILON);
+        Assert.assertEquals(4923676 * AVERAGE_MODEM_POWER / MS_PER_HR, modem.activeMah, EPSILON);
+
+        Assert.assertEquals(REMAINDER_MODEM_MAH, modem.powerMah, EPSILON);
+    }
+
+    @Test public void testAppTotal() throws Exception {
+        final PowerReport report = loadPowerReport();
+
+        final List<AppPower> gmailList = report.findApp("com.google.android.gm");
+        Assert.assertEquals(1, gmailList.size());
+        final AppPower gmail = gmailList.get(0);
+
+        Assert.assertEquals(GMAIL_MAH, gmail.getAppPowerMah(), EPSILON);
+    }
+
+    @Test public void testRemainderTotal() throws Exception {
+        final PowerReport report = loadPowerReport();
+
+        final AppPower remainder = report.findApp(SpecialApp.REMAINDER);
+        Assert.assertNotNull(remainder);
+
+        Assert.assertEquals(REMAINDER_MAH, remainder.getAppPowerMah(), EPSILON);
+    }
+
+    @Test public void testTotal() throws Exception {
+        final PowerReport report = loadPowerReport();
+
+        Assert.assertEquals(TOTAL_MAH, report.getTotalPowerMah(), EPSILON);
+    }
+}
+