Initial commit of PowerStatsService

Bug: 164465661
Bug: 164466995
Bug: 167280723
Test: Verified PowerStatsService is functional on targets that
have ODPM data.

Change-Id: Ic555b380c566ea26bc2214374f142c5448ea2ee7
diff --git a/core/java/android/content/Context.java b/core/java/android/content/Context.java
index 98f7887..bd11bc8 100644
--- a/core/java/android/content/Context.java
+++ b/core/java/android/content/Context.java
@@ -3384,6 +3384,7 @@
     /** @hide */
     @StringDef(suffix = { "_SERVICE" }, value = {
             POWER_SERVICE,
+            //@hide: POWER_STATS_SERVICE,
             WINDOW_SERVICE,
             LAYOUT_INFLATER_SERVICE,
             ACCOUNT_SERVICE,
@@ -3726,6 +3727,16 @@
 
     /**
      * Use with {@link #getSystemService(String)} to retrieve a
+     * {@link android.os.PowerStatsService} for accessing power stats
+     * service.
+     *
+     * @see #getSystemService(String)
+     * @hide
+     */
+    public static final String POWER_STATS_SERVICE = "power_stats";
+
+    /**
+     * Use with {@link #getSystemService(String)} to retrieve a
      * {@link android.os.RecoverySystem} for accessing the recovery system
      * service.
      *
diff --git a/core/proto/android/os/incident.proto b/core/proto/android/os/incident.proto
index 5609b36..9fed1b9 100644
--- a/core/proto/android/os/incident.proto
+++ b/core/proto/android/os/incident.proto
@@ -40,6 +40,7 @@
 import "frameworks/base/core/proto/android/server/jobscheduler.proto";
 import "frameworks/base/core/proto/android/server/location/context_hub.proto";
 import "frameworks/base/core/proto/android/server/powermanagerservice.proto";
+import "frameworks/base/core/proto/android/server/powerstatsservice.proto";
 import "frameworks/base/core/proto/android/server/rolemanagerservice.proto";
 import "frameworks/base/core/proto/android/server/windowmanagerservice.proto";
 import "frameworks/base/core/proto/android/service/appwidget.proto";
@@ -510,6 +511,11 @@
         (section).args = "sensorservice --proto"
     ];
 
+    optional com.android.server.powerstats.PowerStatsServiceProto powerstats = 3054 [
+        (section).type = SECTION_DUMPSYS,
+        (section).args = "power_stats --proto"
+    ];
+
     // Dumps in text format (on userdebug and eng builds only): 4000 ~ 4999
     optional android.util.TextDumpProto textdump_wifi = 4000 [
         (section).type = SECTION_TEXT_DUMPSYS,
diff --git a/core/proto/android/server/powerstatsservice.proto b/core/proto/android/server/powerstatsservice.proto
new file mode 100644
index 0000000..c805244
--- /dev/null
+++ b/core/proto/android/server/powerstatsservice.proto
@@ -0,0 +1,67 @@
+/*
+ * Copyright (C) 2020 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.
+ */
+
+syntax = "proto2";
+
+package com.android.server.powerstats;
+
+option java_multiple_files = true;
+
+message IncidentReportProto {
+    /** Section number matches that in incident.proto */
+    optional PowerStatsServiceProto incident_report = 3054;
+}
+
+message PowerStatsServiceProto {
+    repeated RailInfoProto rail_info = 1;
+    repeated EnergyDataProto energy_data = 2;
+}
+
+/**
+ * Rail information:
+ * Reports information related to the rails being monitored.
+ */
+message RailInfoProto {
+    /** Index corresponding to the rail */
+    optional int32 index = 1;
+
+    /** Name of the rail (opaque to the framework) */
+    optional string rail_name = 2;
+
+    /** Name of the subsystem to which this rail belongs (opaque to the framework) */
+    optional string subsys_name = 3;
+
+    /** Hardware sampling rate */
+    optional int32 sampling_rate = 4;
+}
+
+/**
+ * Rail level energy measurements:
+ * Reports accumulated energy since boot on each rail.
+ */
+message EnergyDataProto {
+    /**
+     * Index corresponding to the rail. This index matches
+     * the index returned in RailInfo
+     */
+    optional int32 index = 1;
+
+    /** Time since device boot(CLOCK_BOOTTIME) in milli-seconds */
+    optional int64 timestamp_ms = 2;
+
+    /** Accumulated energy since device boot in microwatt-seconds (uWs) */
+    optional int64 energy_uws = 3;
+}
diff --git a/services/core/java/com/android/server/powerstats/BatteryTrigger.java b/services/core/java/com/android/server/powerstats/BatteryTrigger.java
new file mode 100644
index 0000000..c1f97f2
--- /dev/null
+++ b/services/core/java/com/android/server/powerstats/BatteryTrigger.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2020 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.server.powerstats;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.os.BatteryManager;
+import android.util.Log;
+
+/**
+ * BatteryTrigger instantiates a BroadcastReceiver that listens for changes
+ * to the battery.  When the battery level drops by 1% a message is sent to
+ * the PowerStatsLogger to log the rail energy data to on-device storage.
+ */
+public final class BatteryTrigger extends PowerStatsLogTrigger {
+    private static final String TAG = BatteryTrigger.class.getSimpleName();
+    private static final boolean DEBUG = false;
+
+    private int mBatteryLevel = 0;
+
+    private final BroadcastReceiver mBatteryLevelReceiver = new BroadcastReceiver() {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            switch (intent.getAction()) {
+                case Intent.ACTION_BATTERY_CHANGED:
+                    int newBatteryLevel = intent.getIntExtra(BatteryManager.EXTRA_LEVEL, 0);
+
+                    if (newBatteryLevel < mBatteryLevel) {
+                        if (DEBUG) Log.d(TAG, "Battery level dropped.  Log rail data");
+                        logPowerStatsData();
+                    }
+
+                    mBatteryLevel = newBatteryLevel;
+                    break;
+            }
+        }
+    };
+
+    public BatteryTrigger(Context context, PowerStatsLogger powerStatsLogger,
+            boolean triggerEnabled) {
+        super(context, powerStatsLogger);
+
+        if (triggerEnabled) {
+            IntentFilter filter = new IntentFilter(Intent.ACTION_BATTERY_CHANGED);
+            Intent batteryStatus = mContext.registerReceiver(mBatteryLevelReceiver, filter);
+            mBatteryLevel = batteryStatus.getIntExtra(BatteryManager.EXTRA_LEVEL, 0);
+        }
+    }
+}
diff --git a/services/core/java/com/android/server/powerstats/PowerStatsData.java b/services/core/java/com/android/server/powerstats/PowerStatsData.java
new file mode 100644
index 0000000..755bd5f
--- /dev/null
+++ b/services/core/java/com/android/server/powerstats/PowerStatsData.java
@@ -0,0 +1,286 @@
+/*
+ * Copyright (C) 2020 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.server.powerstats;
+
+import android.util.Log;
+import android.util.proto.ProtoInputStream;
+import android.util.proto.ProtoOutputStream;
+import android.util.proto.ProtoUtils;
+import android.util.proto.WireTypeMismatchException;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * PowerStatsData is a class that performs two operations:
+ * 1) Unpacks serialized protobuf byte arrays, as defined in powerstatsservice.proto,
+ *    into RailInfo or EnergyData object arrays.
+ *
+ * 2) Packs RailInfo or EnergyData object arrays in protobuf byte arrays as
+ *    defined in powerstatsservice.proto.
+ *
+ * Inside frameworks, proto source is generated with the genstream option
+ * and therefore the getter/setter helper functions are not available.
+ * The protos need to be packed/unpacked in a more manual way using
+ * ProtoOutputStream/ProtoInputStream.
+ */
+public class PowerStatsData {
+    private static final String TAG = PowerStatsData.class.getSimpleName();
+
+    private List<Data> mDataList;
+
+    public PowerStatsData(ProtoInputStream pis) throws IOException {
+        mDataList = new ArrayList<Data>();
+        unpackProto(pis);
+    }
+
+    public PowerStatsData(Data[] data) {
+        mDataList = new ArrayList<Data>(Arrays.asList(data));
+    }
+
+    private void unpackProto(ProtoInputStream pis) throws IOException {
+        long token;
+
+        while (true) {
+            try {
+                switch (pis.nextField()) {
+                    case (int) PowerStatsServiceProto.RAIL_INFO:
+                        token = pis.start(PowerStatsServiceProto.RAIL_INFO);
+                        mDataList.add(new RailInfo(pis));
+                        pis.end(token);
+                        break;
+
+                    case (int) PowerStatsServiceProto.ENERGY_DATA:
+                        token = pis.start(PowerStatsServiceProto.ENERGY_DATA);
+                        mDataList.add(new EnergyData(pis));
+                        pis.end(token);
+                        break;
+
+                    case ProtoInputStream.NO_MORE_FIELDS:
+                        return;
+
+                    default:
+                        Log.e(TAG, "Unhandled field in proto: "
+                                + ProtoUtils.currentFieldToString(pis));
+                        break;
+                }
+            } catch (WireTypeMismatchException wtme) {
+                Log.e(TAG, "Wire Type mismatch in proto: " + ProtoUtils.currentFieldToString(pis));
+            }
+        }
+    }
+
+    /**
+     * Write this object to an output stream in protobuf format.
+     *
+     * @param pos ProtoOutputStream of file where data is to be written.  Data is
+     *            written in protobuf format as defined by powerstatsservice.proto.
+     */
+    public void toProto(ProtoOutputStream pos) {
+        long token;
+
+        for (Data data : mDataList) {
+            if (data instanceof RailInfo) {
+                token = pos.start(PowerStatsServiceProto.RAIL_INFO);
+            } else {
+                token = pos.start(PowerStatsServiceProto.ENERGY_DATA);
+            }
+            data.toProto(pos);
+            pos.end(token);
+        }
+    }
+
+    /**
+     * Convert mDataList to proto format and return the serialized byte array.
+     *
+     * @return byte array containing a serialized protobuf of mDataList.
+     */
+    public byte[] getProtoBytes() {
+        ProtoOutputStream pos = new ProtoOutputStream();
+        long token;
+
+        for (Data data : mDataList) {
+            if (data instanceof RailInfo) {
+                token = pos.start(PowerStatsServiceProto.RAIL_INFO);
+            } else {
+                token = pos.start(PowerStatsServiceProto.ENERGY_DATA);
+            }
+            data.toProto(pos);
+            pos.end(token);
+        }
+        return pos.getBytes();
+    }
+
+    /**
+     * Print this object to logcat.
+     */
+    public void print() {
+        for (Data data : mDataList) {
+            Log.d(TAG, data.toString());
+        }
+    }
+
+    /**
+     * RailInfo is a class that stores a description for an individual ODPM
+     * rail.  It provides functionality to unpack a RailInfo object from a
+     * serialized protobuf byte array, and to pack a RailInfo object into
+     * a ProtoOutputStream.
+     */
+    public static class RailInfo extends Data {
+        public String mRailName;
+        public String mSubSysName;
+        public long mSamplingRate;
+
+        public RailInfo(ProtoInputStream pis) throws IOException {
+            unpackProto(pis);
+        }
+
+        public RailInfo(long index, String railName, String subSysName, long samplingRate) {
+            mIndex = index;
+            mRailName = railName;
+            mSubSysName = subSysName;
+            mSamplingRate = samplingRate;
+        }
+
+        @Override
+        protected void unpackProto(ProtoInputStream pis) throws IOException {
+            while (true) {
+                try {
+                    switch (pis.nextField()) {
+                        case (int) RailInfoProto.INDEX:
+                            mIndex = pis.readInt(RailInfoProto.INDEX);
+                            break;
+
+                        case (int) RailInfoProto.RAIL_NAME:
+                            mRailName = pis.readString(RailInfoProto.RAIL_NAME);
+                            break;
+
+                        case (int) RailInfoProto.SUBSYS_NAME:
+                            mSubSysName = pis.readString(RailInfoProto.SUBSYS_NAME);
+                            break;
+
+                        case (int) RailInfoProto.SAMPLING_RATE:
+                            mSamplingRate = pis.readInt(RailInfoProto.SAMPLING_RATE);
+                            break;
+
+                        case ProtoInputStream.NO_MORE_FIELDS:
+                            return;
+
+                        default:
+                            Log.e(TAG, "Unhandled field in RailInfoProto: "
+                                    + ProtoUtils.currentFieldToString(pis));
+                            break;
+                    }
+                } catch (WireTypeMismatchException wtme) {
+                    Log.e(TAG, "Wire Type mismatch in RailInfoProto: "
+                            + ProtoUtils.currentFieldToString(pis));
+                }
+            }
+        }
+
+        @Override
+        public void toProto(ProtoOutputStream pos) {
+            pos.write(RailInfoProto.INDEX, mIndex);
+            pos.write(RailInfoProto.RAIL_NAME, mRailName);
+            pos.write(RailInfoProto.SUBSYS_NAME, mSubSysName);
+            pos.write(RailInfoProto.SAMPLING_RATE, mSamplingRate);
+        }
+
+        @Override
+        public String toString() {
+            return String.format("Index = " + mIndex
+                + ", RailName = " + mRailName
+                + ", SubSysName = " + mSubSysName
+                + ", SamplingRate = " + mSamplingRate);
+        }
+    }
+
+    /**
+     * EnergyData is a class that stores an energy (uWs) data reading for an
+     * individual ODPM rail.  It provides functionality to unpack an EnergyData
+     * object from a serialized protobuf byte array, and to pack an EnergyData
+     * object into a ProtoOutputStream.
+     */
+    public static class EnergyData extends Data {
+        public long mTimestampMs;
+        public long mEnergyUWs;
+
+        public EnergyData(ProtoInputStream pis) throws IOException {
+            unpackProto(pis);
+        }
+
+        public EnergyData(long index, long timestampMs, long energyUWs) {
+            mIndex = index;
+            mTimestampMs = timestampMs;
+            mEnergyUWs = energyUWs;
+        }
+
+        @Override
+        protected void unpackProto(ProtoInputStream pis) throws IOException {
+            while (true) {
+                try {
+                    switch (pis.nextField()) {
+                        case (int) EnergyDataProto.INDEX:
+                            mIndex = pis.readInt(EnergyDataProto.INDEX);
+                            break;
+
+                        case (int) EnergyDataProto.TIMESTAMP_MS:
+                            mTimestampMs = pis.readLong(EnergyDataProto.TIMESTAMP_MS);
+                            break;
+
+                        case (int) EnergyDataProto.ENERGY_UWS:
+                            mEnergyUWs = pis.readLong(EnergyDataProto.ENERGY_UWS);
+                            break;
+
+                        case ProtoInputStream.NO_MORE_FIELDS:
+                            return;
+
+                        default:
+                            Log.e(TAG, "Unhandled field in EnergyDataProto: "
+                                    + ProtoUtils.currentFieldToString(pis));
+                            break;
+                    }
+                } catch (WireTypeMismatchException wtme) {
+                    Log.e(TAG, "Wire Type mismatch in EnergyDataProto: "
+                            + ProtoUtils.currentFieldToString(pis));
+                }
+            }
+        }
+
+        @Override
+        protected void toProto(ProtoOutputStream pos) {
+            pos.write(EnergyDataProto.INDEX, mIndex);
+            pos.write(EnergyDataProto.TIMESTAMP_MS, mTimestampMs);
+            pos.write(EnergyDataProto.ENERGY_UWS, mEnergyUWs);
+        }
+
+        @Override
+        public String toString() {
+            return String.format("Index = " + mIndex
+                + ", Timestamp (ms) = " + mTimestampMs
+                + ", Energy (uWs) = " + mEnergyUWs);
+        }
+    }
+
+    private abstract static class Data {
+        public long mIndex;
+        protected abstract void unpackProto(ProtoInputStream pis) throws IOException;
+        protected abstract void toProto(ProtoOutputStream pos);
+    }
+}
diff --git a/services/core/java/com/android/server/powerstats/PowerStatsDataStorage.java b/services/core/java/com/android/server/powerstats/PowerStatsDataStorage.java
new file mode 100644
index 0000000..84a6fc9
--- /dev/null
+++ b/services/core/java/com/android/server/powerstats/PowerStatsDataStorage.java
@@ -0,0 +1,223 @@
+/*
+ * Copyright (C) 2020 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.server.powerstats;
+
+import android.content.Context;
+import android.util.Log;
+
+import com.android.internal.util.FileRotator;
+
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.nio.ByteBuffer;
+import java.util.concurrent.locks.ReentrantLock;
+
+/**
+ * PowerStatsDataStorage implements the on-device storage cache for energy
+ * data.  This data must be persisted across boot cycles so we store it
+ * on-device.  Versioning of this data is handled by deleting any data that
+ * does not match the current version.  The cache is implemented as a circular
+ * buffer using the FileRotator class in android.util.  We maintain 48 hours
+ * worth of logs in 12 files (4 hours each).
+ */
+public class PowerStatsDataStorage {
+    private static final String TAG = PowerStatsDataStorage.class.getSimpleName();
+
+    private static final long MILLISECONDS_PER_HOUR = 1000 * 60 * 60;
+    // Rotate files every 4 hours.
+    private static final long ROTATE_AGE_MILLIS = 4 * MILLISECONDS_PER_HOUR;
+    // Store 48 hours worth of data.
+    private static final long DELETE_AGE_MILLIS = 48 * MILLISECONDS_PER_HOUR;
+
+    private final ReentrantLock mLock = new ReentrantLock();
+    private File mDataStorageDir;
+    private final FileRotator mFileRotator;
+
+    private static class DataElement {
+        private static final int LENGTH_FIELD_WIDTH = 4;
+        private static final int MAX_DATA_ELEMENT_SIZE = 1000;
+
+        private byte[] mData;
+
+        private byte[] toByteArray() throws IOException {
+            ByteArrayOutputStream data = new ByteArrayOutputStream();
+            data.write(ByteBuffer.allocate(LENGTH_FIELD_WIDTH).putInt(mData.length).array());
+            data.write(mData);
+            return data.toByteArray();
+        }
+
+        protected byte[] getData() {
+            return mData;
+        }
+
+        private DataElement(byte[] data) {
+            mData = data;
+        }
+
+        private DataElement(InputStream in) throws IOException {
+            byte[] lengthBytes = new byte[LENGTH_FIELD_WIDTH];
+            int bytesRead = in.read(lengthBytes);
+            mData = new byte[0];
+
+            if (bytesRead == LENGTH_FIELD_WIDTH) {
+                int length = ByteBuffer.wrap(lengthBytes).getInt();
+
+                if (0 < length && length < MAX_DATA_ELEMENT_SIZE) {
+                    mData = new byte[length];
+                    bytesRead = in.read(mData);
+
+                    if (bytesRead != length) {
+                        throw new IOException("Invalid bytes read, expected: " + length
+                            + ", actual: " + bytesRead);
+                    }
+                } else {
+                    throw new IOException("DataElement size is invalid: " + length);
+                }
+            } else {
+                throw new IOException("Did not read " + LENGTH_FIELD_WIDTH + " bytes (" + bytesRead
+                    + ")");
+            }
+        }
+    }
+
+    /**
+     * Used by external classes to read DataElements from on-device storage.
+     * This callback is passed in to the read() function and is called for
+     * each DataElement read from on-device storage.
+     */
+    public interface DataElementReadCallback {
+        /**
+         * When performing a read of the on-device storage this callback
+         * must be passed in to the read function.  The function will be
+         * called for each DataElement read from on-device storage.
+         *
+         * @param data Byte array containing a DataElement payload.
+         */
+        void onReadDataElement(byte[] data);
+    }
+
+    private static class DataReader implements FileRotator.Reader {
+        private DataElementReadCallback mCallback;
+
+        DataReader(DataElementReadCallback callback) {
+            mCallback = callback;
+        }
+
+        @Override
+        public void read(InputStream in) throws IOException {
+            while (in.available() > 0) {
+                try {
+                    DataElement dataElement = new DataElement(in);
+                    mCallback.onReadDataElement(dataElement.getData());
+                } catch (IOException e) {
+                    Log.e(TAG, "Failed to read from storage. " + e.getMessage());
+                }
+            }
+        }
+    }
+
+    private static class DataRewriter implements FileRotator.Rewriter {
+        byte[] mActiveFileData;
+        byte[] mNewData;
+
+        DataRewriter(byte[] data) {
+            mActiveFileData = new byte[0];
+            mNewData = data;
+        }
+
+        @Override
+        public void reset() {
+            // ignored
+        }
+
+        @Override
+        public void read(InputStream in) throws IOException {
+            mActiveFileData = new byte[in.available()];
+            in.read(mActiveFileData);
+        }
+
+        @Override
+        public boolean shouldWrite() {
+            return true;
+        }
+
+        @Override
+        public void write(OutputStream out) throws IOException {
+            out.write(mActiveFileData);
+            out.write(mNewData);
+        }
+    }
+
+    public PowerStatsDataStorage(Context context, File dataStoragePath,
+            String dataStorageFilename) {
+        mDataStorageDir = dataStoragePath;
+
+        if (!mDataStorageDir.exists() && !mDataStorageDir.mkdirs()) {
+            Log.wtf(TAG, "mDataStorageDir does not exist: " + mDataStorageDir.getPath());
+            mFileRotator = null;
+        } else {
+            // Delete files written with an old version number.  The version is included in the
+            // filename, so any files that don't match the current version number can be deleted.
+            File[] files = mDataStorageDir.listFiles();
+            for (int i = 0; i < files.length; i++) {
+                if (!files[i].getName().matches(dataStorageFilename + "(.*)")) {
+                    files[i].delete();
+                }
+            }
+
+            mFileRotator = new FileRotator(mDataStorageDir,
+                                           dataStorageFilename,
+                                           ROTATE_AGE_MILLIS,
+                                           DELETE_AGE_MILLIS);
+        }
+    }
+
+    /**
+     * Writes data stored in PowerStatsDataStorage to a file descriptor.
+     *
+     * @param data Byte array to write to on-device storage.  Byte array is
+     *             converted to a DataElement which prefixes the payload with
+     *             the data length.  The DataElement is then converted to a byte
+     *             array and written to on-device storage.
+     */
+    public void write(byte[] data) {
+        mLock.lock();
+
+        long currentTimeMillis = System.currentTimeMillis();
+        try {
+            DataElement dataElement = new DataElement(data);
+            mFileRotator.rewriteActive(new DataRewriter(dataElement.toByteArray()),
+                    currentTimeMillis);
+            mFileRotator.maybeRotate(currentTimeMillis);
+        } catch (IOException e) {
+            Log.e(TAG, "Failed to write to on-device storage: " + e);
+        }
+
+        mLock.unlock();
+    }
+
+    /**
+     * Reads all DataElements stored in on-device storage.  For each
+     * DataElement retrieved from on-device storage, callback is called.
+     */
+    public void read(DataElementReadCallback callback) throws IOException {
+        mFileRotator.readMatching(new DataReader(callback), Long.MIN_VALUE, Long.MAX_VALUE);
+    }
+}
diff --git a/services/core/java/com/android/server/powerstats/PowerStatsHALWrapper.java b/services/core/java/com/android/server/powerstats/PowerStatsHALWrapper.java
new file mode 100644
index 0000000..dc996a3
--- /dev/null
+++ b/services/core/java/com/android/server/powerstats/PowerStatsHALWrapper.java
@@ -0,0 +1,105 @@
+/*
+ * Copyright (C) 2020 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.server.powerstats;
+
+/**
+ * PowerStatsHALWrapper is a wrapper class for the PowerStats HAL API calls.
+ */
+public final class PowerStatsHALWrapper {
+    private static final String TAG = PowerStatsHALWrapper.class.getSimpleName();
+
+    /**
+     * IPowerStatsHALWrapper defines the interface to the PowerStatsHAL.
+     */
+    public interface IPowerStatsHALWrapper {
+        /**
+         * Returns rail info for all available ODPM rails.
+         *
+         * @return array of RailInfo objects containing rail info for all
+         *         available rails.
+         */
+        PowerStatsData.RailInfo[] readRailInfo();
+
+        /**
+         * Returns energy data for all available ODPM rails.  Available rails can
+         *         be retrieved by calling nativeGetRailInfo.  Energy data and
+         *         rail info can be linked through the index field.
+         *
+         * @return array of EnergyData objects containing energy data for all
+         *         available rails.
+         */
+        PowerStatsData.EnergyData[] readEnergyData();
+
+        /**
+         * Returns boolean indicating if connection to power stats HAL was
+         *         established.
+         *
+         * @return true if connection to power stats HAL was correctly established
+         *         and that energy data and rail info can be read from the interface.
+         *         false otherwise
+         */
+        boolean initialize();
+    }
+
+    /**
+     * PowerStatsHALWrapperImpl is the implementation of the IPowerStatsHALWrapper
+     * used by the PowerStatsService.  Other implementations will be used by the testing
+     * framework and will be passed into the PowerStatsService through an injector.
+     */
+    public static final class PowerStatsHALWrapperImpl implements IPowerStatsHALWrapper {
+        private static native boolean nativeInit();
+        private static native PowerStatsData.RailInfo[] nativeGetRailInfo();
+        private static native PowerStatsData.EnergyData[] nativeGetEnergyData();
+
+        /**
+         * Returns rail info for all available ODPM rails.
+         *
+         * @return array of RailInfo objects containing rail info for all
+         *         available rails.
+         */
+        @Override
+        public PowerStatsData.RailInfo[] readRailInfo() {
+            return nativeGetRailInfo();
+        }
+
+        /**
+         * Returns energy data for all available ODPM rails.  Available rails can
+         *         be retrieved by calling nativeGetRailInfo.  Energy data and
+         *         rail info can be linked through the index field.
+         *
+         * @return array of EnergyData objects containing energy data for all
+         *         available rails.
+         */
+        @Override
+        public PowerStatsData.EnergyData[] readEnergyData() {
+            return nativeGetEnergyData();
+        }
+
+        /**
+         * Returns boolean indicating if connection to power stats HAL was
+         *         established.
+         *
+         * @return true if connection to power stats HAL was correctly established
+         *         and that energy data and rail info can be read from the interface.
+         *         false otherwise
+         */
+        @Override
+        public boolean initialize() {
+            return nativeInit();
+        }
+    }
+}
diff --git a/services/core/java/com/android/server/powerstats/PowerStatsLogTrigger.java b/services/core/java/com/android/server/powerstats/PowerStatsLogTrigger.java
new file mode 100644
index 0000000..1754185
--- /dev/null
+++ b/services/core/java/com/android/server/powerstats/PowerStatsLogTrigger.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2020 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.server.powerstats;
+
+import android.content.Context;
+import android.os.Message;
+
+/**
+ * PowerStatsLogTrigger is the base class for other trigger classes.
+ * It provides the logPowerStatsData() function which sends a message
+ * to the PowerStatsLogger to read the rail energy data and log it to
+ * on-device storage.  This class is abstract and cannot be instantiated.
+ */
+public abstract class PowerStatsLogTrigger {
+    private static final String TAG = PowerStatsLogTrigger.class.getSimpleName();
+
+    protected Context mContext;
+    private PowerStatsLogger mPowerStatsLogger;
+
+    protected void logPowerStatsData() {
+        Message.obtain(
+            mPowerStatsLogger,
+            PowerStatsLogger.MSG_LOG_TO_DATA_STORAGE).sendToTarget();
+    }
+
+    public PowerStatsLogTrigger(Context context, PowerStatsLogger powerStatsLogger) {
+        mContext = context;
+        mPowerStatsLogger = powerStatsLogger;
+    }
+}
diff --git a/services/core/java/com/android/server/powerstats/PowerStatsLogger.java b/services/core/java/com/android/server/powerstats/PowerStatsLogger.java
new file mode 100644
index 0000000..71a34a4
--- /dev/null
+++ b/services/core/java/com/android/server/powerstats/PowerStatsLogger.java
@@ -0,0 +1,109 @@
+/*
+ * Copyright (C) 2020 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.server.powerstats;
+
+import android.content.Context;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.util.Log;
+import android.util.proto.ProtoInputStream;
+import android.util.proto.ProtoOutputStream;
+
+import com.android.server.powerstats.PowerStatsHALWrapper.IPowerStatsHALWrapper;
+
+import java.io.ByteArrayInputStream;
+import java.io.File;
+import java.io.FileDescriptor;
+import java.io.IOException;
+
+/**
+ * PowerStatsLogger is responsible for logging energy data to on-device
+ * storage.  Messages are sent to its message handler to request that energy
+ * data be logged, at which time it queries the PowerStats HAL and logs the
+ * data to on-device storage.  The on-device storage is dumped to file by
+ * calling writeToFile with a file descriptor that points to the output file.
+ */
+public final class PowerStatsLogger extends Handler {
+    private static final String TAG = PowerStatsLogger.class.getSimpleName();
+    private static final boolean DEBUG = false;
+    protected static final int MSG_LOG_TO_DATA_STORAGE = 0;
+
+    private final PowerStatsDataStorage mPowerStatsDataStorage;
+    private final IPowerStatsHALWrapper mPowerStatsHALWrapper;
+
+    @Override
+    public void handleMessage(Message msg) {
+        switch (msg.what) {
+            case MSG_LOG_TO_DATA_STORAGE:
+                if (DEBUG) Log.d(TAG, "Logging to data storage");
+                PowerStatsData energyData =
+                        new PowerStatsData(mPowerStatsHALWrapper.readEnergyData());
+                mPowerStatsDataStorage.write(energyData.getProtoBytes());
+                break;
+        }
+    }
+
+    /**
+     * Writes data stored in PowerStatsDataStorage to a file descriptor.
+     *
+     * @param fd FileDescriptor where data stored in PowerStatsDataStorage is
+     *           written.  Data is written in protobuf format as defined by
+     *           powerstatsservice.proto.
+     */
+    public void writeToFile(FileDescriptor fd) {
+        if (DEBUG) Log.d(TAG, "Writing to file");
+
+        final ProtoOutputStream pos = new ProtoOutputStream(fd);
+
+        try {
+            PowerStatsData railInfo = new PowerStatsData(mPowerStatsHALWrapper.readRailInfo());
+            railInfo.toProto(pos);
+            if (DEBUG) railInfo.print();
+
+            mPowerStatsDataStorage.read(new PowerStatsDataStorage.DataElementReadCallback() {
+                @Override
+                public void onReadDataElement(byte[] data) {
+                    try {
+                        final ProtoInputStream pis =
+                                new ProtoInputStream(new ByteArrayInputStream(data));
+                        // TODO(b/166535853): ProtoOutputStream doesn't provide a method to write
+                        // a byte array that already contains a serialized proto, so I have to
+                        // deserialize, then re-serialize.  This is computationally inefficient.
+                        final PowerStatsData energyData = new PowerStatsData(pis);
+                        energyData.toProto(pos);
+                        if (DEBUG) energyData.print();
+                    } catch (IOException e) {
+                        Log.e(TAG, "Failed to write energy data to incident report.");
+                    }
+                }
+            });
+        } catch (IOException e) {
+            Log.e(TAG, "Failed to write rail info to incident report.");
+        }
+
+        pos.flush();
+    }
+
+    public PowerStatsLogger(Context context, File dataStoragePath, String dataStorageFilename,
+            IPowerStatsHALWrapper powerStatsHALWrapper) {
+        super(Looper.getMainLooper());
+        mPowerStatsHALWrapper = powerStatsHALWrapper;
+        mPowerStatsDataStorage = new PowerStatsDataStorage(context, dataStoragePath,
+            dataStorageFilename);
+    }
+}
diff --git a/services/core/java/com/android/server/powerstats/PowerStatsService.java b/services/core/java/com/android/server/powerstats/PowerStatsService.java
new file mode 100644
index 0000000..fd609c1
--- /dev/null
+++ b/services/core/java/com/android/server/powerstats/PowerStatsService.java
@@ -0,0 +1,135 @@
+/*
+ * Copyright (C) 2020 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.server.powerstats;
+
+import android.content.Context;
+import android.os.Binder;
+import android.os.Environment;
+import android.os.UserHandle;
+import android.util.Log;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.util.DumpUtils;
+import com.android.server.SystemService;
+import com.android.server.powerstats.PowerStatsHALWrapper.IPowerStatsHALWrapper;
+import com.android.server.powerstats.PowerStatsHALWrapper.PowerStatsHALWrapperImpl;
+
+import java.io.File;
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+
+/**
+ * This class provides a system service that estimates system power usage
+ * per subsystem (modem, wifi, gps, display, etc) and provides those power
+ * estimates to subscribers.
+ */
+public class PowerStatsService extends SystemService {
+    private static final String TAG = PowerStatsService.class.getSimpleName();
+    private static final boolean DEBUG = false;
+    private static final String DATA_STORAGE_SUBDIR = "powerstats";
+    private static final int DATA_STORAGE_VERSION = 0;
+    private static final String DATA_STORAGE_FILENAME = "log.powerstats." + DATA_STORAGE_VERSION;
+
+    private final Injector mInjector;
+
+    private Context mContext;
+    private IPowerStatsHALWrapper mPowerStatsHALWrapper;
+    private PowerStatsLogger mPowerStatsLogger;
+    private BatteryTrigger mBatteryTrigger;
+    private TimerTrigger mTimerTrigger;
+
+    @VisibleForTesting
+    static class Injector {
+        File createDataStoragePath() {
+            return new File(Environment.getDataSystemDeDirectory(UserHandle.USER_SYSTEM),
+                DATA_STORAGE_SUBDIR);
+        }
+
+        String createDataStorageFilename() {
+            return DATA_STORAGE_FILENAME;
+        }
+
+        IPowerStatsHALWrapper createPowerStatsHALWrapperImpl() {
+            return new PowerStatsHALWrapperImpl();
+        }
+
+        PowerStatsLogger createPowerStatsLogger(Context context, File dataStoragePath,
+                String dataStorageFilename, IPowerStatsHALWrapper powerStatsHALWrapper) {
+            return new PowerStatsLogger(context, dataStoragePath, dataStorageFilename,
+                powerStatsHALWrapper);
+        }
+
+        BatteryTrigger createBatteryTrigger(Context context, PowerStatsLogger powerStatsLogger) {
+            return new BatteryTrigger(context, powerStatsLogger, true /* trigger enabled */);
+        }
+
+        TimerTrigger createTimerTrigger(Context context, PowerStatsLogger powerStatsLogger) {
+            return new TimerTrigger(context, powerStatsLogger, true /* trigger enabled */);
+        }
+    }
+
+    private final class BinderService extends Binder {
+        @Override
+        protected void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
+            if (!DumpUtils.checkDumpPermission(mContext, TAG, pw)) return;
+
+            if (args.length > 0 && "--proto".equals(args[0])) {
+                mPowerStatsLogger.writeToFile(fd);
+            }
+        }
+    }
+
+    @Override
+    public void onBootPhase(int phase) {
+        if (phase == SystemService.PHASE_BOOT_COMPLETED) {
+            onSystemServiceReady();
+        }
+    }
+
+    @Override
+    public void onStart() {
+        publishBinderService(Context.POWER_STATS_SERVICE, new BinderService());
+    }
+
+    private void onSystemServiceReady() {
+        mPowerStatsHALWrapper = mInjector.createPowerStatsHALWrapperImpl();
+
+        if (mPowerStatsHALWrapper.initialize()) {
+            if (DEBUG) Log.d(TAG, "Starting PowerStatsService");
+
+            // Only start logger and triggers if initialization is successful.
+            mPowerStatsLogger = mInjector.createPowerStatsLogger(mContext,
+                mInjector.createDataStoragePath(), mInjector.createDataStorageFilename(),
+                mPowerStatsHALWrapper);
+            mBatteryTrigger = mInjector.createBatteryTrigger(mContext, mPowerStatsLogger);
+            mTimerTrigger = mInjector.createTimerTrigger(mContext, mPowerStatsLogger);
+        } else {
+            Log.e(TAG, "Initialization of PowerStatsHAL wrapper failed");
+        }
+    }
+
+    public PowerStatsService(Context context) {
+        this(context, new Injector());
+    }
+
+    @VisibleForTesting
+    public PowerStatsService(Context context, Injector injector) {
+        super(context);
+        mContext = context;
+        mInjector = injector;
+    }
+}
diff --git a/services/core/java/com/android/server/powerstats/TEST_MAPPING b/services/core/java/com/android/server/powerstats/TEST_MAPPING
new file mode 100644
index 0000000..79224a5
--- /dev/null
+++ b/services/core/java/com/android/server/powerstats/TEST_MAPPING
@@ -0,0 +1,12 @@
+{
+  "presubmit": [
+    {
+      "name": "FrameworksServicesTests",
+      "options": [
+        {
+          "include-filter": "com.android.server.powerstats"
+        }
+      ]
+    }
+  ]
+}
diff --git a/services/core/java/com/android/server/powerstats/TimerTrigger.java b/services/core/java/com/android/server/powerstats/TimerTrigger.java
new file mode 100644
index 0000000..a9bee8b
--- /dev/null
+++ b/services/core/java/com/android/server/powerstats/TimerTrigger.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2020 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.server.powerstats;
+
+import android.content.Context;
+import android.os.Handler;
+import android.util.Log;
+
+/**
+ * TimerTrigger sets a 60 second opportunistic timer using postDelayed.
+ * When the timer expires a message is sent to the PowerStatsLogger to
+ * read the rail energy data and log it to on-device storage.
+ */
+public final class TimerTrigger extends PowerStatsLogTrigger {
+    private static final String TAG = TimerTrigger.class.getSimpleName();
+    private static final boolean DEBUG = false;
+    // TODO(b/166689029): Make configurable through global settings.
+    private static final long LOG_PERIOD_MS = 60 * 1000;
+
+    private final Handler mHandler;
+
+    private Runnable mLogData = new Runnable() {
+        @Override
+        public void run() {
+            // Do not wake the device for these messages.  Opportunistically log rail data every
+            // LOG_PERIOD_MS.
+            mHandler.postDelayed(mLogData, LOG_PERIOD_MS);
+            if (DEBUG) Log.d(TAG, "Received delayed message.  Log rail data");
+            logPowerStatsData();
+        }
+    };
+
+    public TimerTrigger(Context context, PowerStatsLogger powerStatsLogger,
+            boolean triggerEnabled) {
+        super(context, powerStatsLogger);
+        mHandler = mContext.getMainThreadHandler();
+
+        if (triggerEnabled) {
+            mLogData.run();
+        }
+    }
+}
diff --git a/services/core/jni/Android.bp b/services/core/jni/Android.bp
index d84f9d1..1b649fd 100644
--- a/services/core/jni/Android.bp
+++ b/services/core/jni/Android.bp
@@ -37,6 +37,7 @@
         "com_android_server_locksettings_SyntheticPasswordManager.cpp",
         "com_android_server_net_NetworkStatsService.cpp",
         "com_android_server_power_PowerManagerService.cpp",
+        "com_android_server_powerstats_PowerStatsService.cpp",
         "com_android_server_security_VerityUtils.cpp",
         "com_android_server_SerialService.cpp",
         "com_android_server_soundtrigger_middleware_AudioSessionProviderImpl.cpp",
diff --git a/services/core/jni/com_android_server_powerstats_PowerStatsService.cpp b/services/core/jni/com_android_server_powerstats_PowerStatsService.cpp
new file mode 100644
index 0000000..5eb6b73
--- /dev/null
+++ b/services/core/jni/com_android_server_powerstats_PowerStatsService.cpp
@@ -0,0 +1,222 @@
+/*
+ * Copyright (C) 2020 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.
+ */
+
+#define LOG_TAG "PowerStatsService"
+
+#include <android/hardware/power/stats/1.0/IPowerStats.h>
+#include <jni.h>
+#include <nativehelper/JNIHelp.h>
+
+#include <log/log.h>
+
+using android::hardware::hidl_vec;
+using android::hardware::Return;
+using android::hardware::power::stats::V1_0::EnergyData;
+using android::hardware::power::stats::V1_0::RailInfo;
+using android::hardware::power::stats::V1_0::Status;
+
+static jclass class_railInfo;
+static jmethodID method_railInfoInit;
+static jclass class_energyData;
+static jmethodID method_energyDataInit;
+
+namespace android {
+
+static std::mutex gPowerStatsHalMutex;
+static sp<android::hardware::power::stats::V1_0::IPowerStats> gPowerStatsHalV1_0_ptr = nullptr;
+
+static void deinitPowerStats() {
+    gPowerStatsHalV1_0_ptr = nullptr;
+}
+
+struct PowerStatsHalDeathRecipient : virtual public hardware::hidl_death_recipient {
+    virtual void serviceDied(uint64_t cookie,
+                             const wp<android::hidl::base::V1_0::IBase> &who) override {
+        // The HAL just died. Reset all handles to HAL services.
+        std::lock_guard<std::mutex> lock(gPowerStatsHalMutex);
+        deinitPowerStats();
+    }
+};
+
+sp<PowerStatsHalDeathRecipient> gPowerStatsHalDeathRecipient = new PowerStatsHalDeathRecipient();
+
+static bool connectToPowerStatsHal() {
+    if (gPowerStatsHalV1_0_ptr == nullptr) {
+        gPowerStatsHalV1_0_ptr = android::hardware::power::stats::V1_0::IPowerStats::getService();
+
+        if (gPowerStatsHalV1_0_ptr == nullptr) {
+            ALOGE("Unable to get power.stats HAL service.");
+            return false;
+        }
+
+        // Link death recipient to power.stats service handle
+        hardware::Return<bool> linked =
+                gPowerStatsHalV1_0_ptr->linkToDeath(gPowerStatsHalDeathRecipient, 0);
+        if (!linked.isOk()) {
+            ALOGE("Transaction error in linking to power.stats HAL death: %s",
+                  linked.description().c_str());
+            deinitPowerStats();
+            return false;
+        } else if (!linked) {
+            ALOGW("Unable to link to power.stats HAL death notifications");
+            return false;
+        }
+    }
+    return true;
+}
+
+static bool checkResult(const Return<void> &ret, const char *function) {
+    if (!ret.isOk()) {
+        ALOGE("%s failed: requested HAL service not available. Description: %s", function,
+              ret.description().c_str());
+        if (ret.isDeadObject()) {
+            deinitPowerStats();
+        }
+        return false;
+    }
+    return true;
+}
+
+static jobjectArray nativeGetRailInfo(JNIEnv *env, jclass clazz) {
+    std::lock_guard<std::mutex> lock(gPowerStatsHalMutex);
+
+    if (!connectToPowerStatsHal()) {
+        ALOGE("nativeGetRailInfo failed to connect to power.stats HAL");
+        return nullptr;
+    }
+
+    hidl_vec<RailInfo> list;
+    Return<void> ret = gPowerStatsHalV1_0_ptr->getRailInfo([&list](auto rails, auto status) {
+        if (status != Status::SUCCESS) {
+            ALOGW("Rail information is not available");
+        } else {
+            list = std::move(rails);
+        }
+    });
+
+    if (!checkResult(ret, __func__)) {
+        ALOGE("getRailInfo failed");
+        return nullptr;
+    } else {
+        jobjectArray railInfoArray = env->NewObjectArray(list.size(), class_railInfo, nullptr);
+        for (int i = 0; i < list.size(); i++) {
+            jstring railName = env->NewStringUTF(list[i].railName.c_str());
+            jstring subsysName = env->NewStringUTF(list[i].subsysName.c_str());
+            jobject railInfo = env->NewObject(class_railInfo, method_railInfoInit, list[i].index,
+                                              railName, subsysName, list[i].samplingRate);
+            env->SetObjectArrayElement(railInfoArray, i, railInfo);
+            env->DeleteLocalRef(railName);
+            env->DeleteLocalRef(subsysName);
+            env->DeleteLocalRef(railInfo);
+        }
+        return railInfoArray;
+    }
+}
+
+static jobjectArray nativeGetEnergyData(JNIEnv *env, jclass clazz) {
+    std::lock_guard<std::mutex> lock(gPowerStatsHalMutex);
+
+    if (!connectToPowerStatsHal()) {
+        ALOGE("nativeGetEnergy failed to connect to power.stats HAL");
+    }
+
+    hidl_vec<EnergyData> list;
+    Return<void> ret =
+            gPowerStatsHalV1_0_ptr->getEnergyData({}, [&list](auto energyData, auto status) {
+                if (status != Status::SUCCESS) {
+                    ALOGW("getEnergyData is not supported");
+                } else {
+                    list = std::move(energyData);
+                }
+            });
+
+    if (!checkResult(ret, __func__)) {
+        ALOGE("getEnergyData failed");
+        return nullptr;
+    } else {
+        jobjectArray energyDataArray = env->NewObjectArray(list.size(), class_energyData, nullptr);
+        for (int i = 0; i < list.size(); i++) {
+            jobject energyData = env->NewObject(class_energyData, method_energyDataInit,
+                                                list[i].index, list[i].timestamp, list[i].energy);
+            env->SetObjectArrayElement(energyDataArray, i, energyData);
+            env->DeleteLocalRef(energyData);
+        }
+        return energyDataArray;
+    }
+}
+
+static jboolean nativeInit(JNIEnv *env, jclass clazz) {
+    std::lock_guard<std::mutex> lock(gPowerStatsHalMutex);
+
+    jclass temp = env->FindClass("com/android/server/powerstats/PowerStatsData$RailInfo");
+    if (temp == nullptr) return false;
+
+    class_railInfo = (jclass)env->NewGlobalRef(temp);
+    if (class_railInfo == nullptr) return false;
+
+    method_railInfoInit =
+            env->GetMethodID(class_railInfo, "<init>", "(JLjava/lang/String;Ljava/lang/String;J)V");
+    if (method_railInfoInit == nullptr) return false;
+
+    temp = env->FindClass("com/android/server/powerstats/PowerStatsData$EnergyData");
+    if (temp == nullptr) return false;
+
+    class_energyData = (jclass)env->NewGlobalRef(temp);
+    if (class_energyData == nullptr) return false;
+
+    method_energyDataInit = env->GetMethodID(class_energyData, "<init>", "(JJJ)V");
+    if (method_energyDataInit == nullptr) return false;
+
+    bool rv = true;
+
+    if (!connectToPowerStatsHal()) {
+        ALOGE("nativeInit failed to connect to power.stats HAL");
+        rv = false;
+    } else {
+        Return<void> ret = gPowerStatsHalV1_0_ptr->getRailInfo([&rv](auto rails, auto status) {
+            if (status != Status::SUCCESS) {
+                ALOGE("nativeInit RailInfo is unavailable");
+                rv = false;
+            }
+        });
+
+        ret = gPowerStatsHalV1_0_ptr->getEnergyData({}, [&rv](auto energyData, auto status) {
+            if (status != Status::SUCCESS) {
+                ALOGE("nativeInit EnergyData is unavailable");
+                rv = false;
+            }
+        });
+    }
+
+    return rv;
+}
+
+static const JNINativeMethod method_table[] = {
+        {"nativeInit", "()Z", (void *)nativeInit},
+        {"nativeGetRailInfo", "()[Lcom/android/server/powerstats/PowerStatsData$RailInfo;",
+         (void *)nativeGetRailInfo},
+        {"nativeGetEnergyData", "()[Lcom/android/server/powerstats/PowerStatsData$EnergyData;",
+         (void *)nativeGetEnergyData},
+};
+
+int register_android_server_PowerStatsService(JNIEnv *env) {
+    return jniRegisterNativeMethods(env,
+                                    "com/android/server/powerstats/"
+                                    "PowerStatsHALWrapper$PowerStatsHALWrapperImpl",
+                                    method_table, NELEM(method_table));
+}
+
+}; // namespace android
diff --git a/services/core/jni/onload.cpp b/services/core/jni/onload.cpp
index 5df1ada..e7f6db9 100644
--- a/services/core/jni/onload.cpp
+++ b/services/core/jni/onload.cpp
@@ -29,6 +29,7 @@
 int register_android_server_InputManager(JNIEnv* env);
 int register_android_server_LightsService(JNIEnv* env);
 int register_android_server_PowerManagerService(JNIEnv* env);
+int register_android_server_PowerStatsService(JNIEnv* env);
 int register_android_server_storage_AppFuse(JNIEnv* env);
 int register_android_server_SerialService(JNIEnv* env);
 int register_android_server_SystemServer(JNIEnv* env);
@@ -82,6 +83,7 @@
     register_android_server_broadcastradio_BroadcastRadioService(env);
     register_android_server_broadcastradio_Tuner(vm, env);
     register_android_server_PowerManagerService(env);
+    register_android_server_PowerStatsService(env);
     register_android_server_SerialService(env);
     register_android_server_InputManager(env);
     register_android_server_LightsService(env);
diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java
index 97ae505..ddd2377 100644
--- a/services/java/com/android/server/SystemServer.java
+++ b/services/java/com/android/server/SystemServer.java
@@ -150,6 +150,7 @@
 import com.android.server.power.PowerManagerService;
 import com.android.server.power.ShutdownThread;
 import com.android.server.power.ThermalManagerService;
+import com.android.server.powerstats.PowerStatsService;
 import com.android.server.profcollect.ProfcollectForwardingService;
 import com.android.server.recoverysystem.RecoverySystemService;
 import com.android.server.restrictions.RestrictionsManagerService;
@@ -761,6 +762,11 @@
         mSystemServiceManager.startService(UriGrantsManagerService.Lifecycle.class);
         t.traceEnd();
 
+        t.traceBegin("StartPowerStatsService");
+        // Tracks rail data to be used for power statistics.
+        mSystemServiceManager.startService(PowerStatsService.class);
+        t.traceEnd();
+
         // Activity manager runs the show.
         t.traceBegin("StartActivityManager");
         // TODO: Might need to move after migration to WM.
diff --git a/services/tests/servicestests/src/com/android/server/powerstats/PowerStatsServiceTest.java b/services/tests/servicestests/src/com/android/server/powerstats/PowerStatsServiceTest.java
new file mode 100644
index 0000000..3221a4d
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/powerstats/PowerStatsServiceTest.java
@@ -0,0 +1,271 @@
+/*
+ * Copyright (C) 2020 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.server.powerstats;
+
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import android.content.Context;
+
+import androidx.test.InstrumentationRegistry;
+
+import com.android.server.SystemService;
+import com.android.server.powerstats.PowerStatsHALWrapper.IPowerStatsHALWrapper;
+import com.android.server.powerstats.nano.PowerStatsServiceProto;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.file.Files;
+import java.util.Random;
+
+/**
+ * Tests for {@link com.android.server.powerstats.PowerStatsService}.
+ *
+ * Build/Install/Run:
+ *  atest FrameworksServicesTests:PowerStatsServiceTest
+ */
+public class PowerStatsServiceTest {
+    private static final String TAG = PowerStatsServiceTest.class.getSimpleName();
+    private static final String DATA_STORAGE_SUBDIR = "powerstatstest";
+    private static final String DATA_STORAGE_FILENAME = "test";
+    private static final String PROTO_OUTPUT_FILENAME = "powerstats.proto";
+    private static final String RAIL_NAME = "railname";
+    private static final String SUBSYS_NAME = "subsysname";
+    private static final int POWER_RAIL_COUNT = 8;
+
+    private final Context mContext = InstrumentationRegistry.getInstrumentation().getContext();
+    private PowerStatsService mService;
+    private File mDataStorageDir;
+    private TimerTrigger mTimerTrigger;
+    private PowerStatsLogger mPowerStatsLogger;
+
+    private final PowerStatsService.Injector mInjector = new PowerStatsService.Injector() {
+        @Override
+        File createDataStoragePath() {
+            mDataStorageDir = null;
+
+            try {
+                mDataStorageDir = Files.createTempDirectory(DATA_STORAGE_SUBDIR).toFile();
+            } catch (IOException e) {
+                fail("Could not create temp directory.");
+            }
+
+            return mDataStorageDir;
+        }
+
+        @Override
+        String createDataStorageFilename() {
+            return DATA_STORAGE_FILENAME;
+        }
+
+        @Override
+        IPowerStatsHALWrapper createPowerStatsHALWrapperImpl() {
+            return new TestPowerStatsHALWrapper();
+        }
+
+        @Override
+        PowerStatsLogger createPowerStatsLogger(Context context, File dataStoragePath,
+                String dataStorageFilename, IPowerStatsHALWrapper powerStatsHALWrapper) {
+            mPowerStatsLogger = new PowerStatsLogger(context, dataStoragePath, dataStorageFilename,
+                powerStatsHALWrapper);
+            return mPowerStatsLogger;
+        }
+
+        @Override
+        BatteryTrigger createBatteryTrigger(Context context, PowerStatsLogger powerStatsLogger) {
+            return new BatteryTrigger(context, powerStatsLogger, false /* trigger enabled */);
+        }
+
+        @Override
+        TimerTrigger createTimerTrigger(Context context, PowerStatsLogger powerStatsLogger) {
+            mTimerTrigger = new TimerTrigger(context, powerStatsLogger,
+                false /* trigger enabled */);
+            return mTimerTrigger;
+        }
+    };
+
+    public static final class TestPowerStatsHALWrapper implements IPowerStatsHALWrapper {
+        @Override
+        public PowerStatsData.RailInfo[] readRailInfo() {
+            PowerStatsData.RailInfo[] railInfoArray = new PowerStatsData.RailInfo[POWER_RAIL_COUNT];
+            for (int i = 0; i < POWER_RAIL_COUNT; i++) {
+                railInfoArray[i] = new PowerStatsData.RailInfo(i, RAIL_NAME + i, SUBSYS_NAME + i,
+                    i);
+            }
+            return railInfoArray;
+        }
+
+        @Override
+        public PowerStatsData.EnergyData[] readEnergyData() {
+            PowerStatsData.EnergyData[] energyDataArray =
+              new PowerStatsData.EnergyData[POWER_RAIL_COUNT];
+            for (int i = 0; i < POWER_RAIL_COUNT; i++) {
+                energyDataArray[i] = new PowerStatsData.EnergyData(i, i, i);
+            }
+            return energyDataArray;
+        }
+
+        @Override
+        public boolean initialize() {
+            return true;
+        }
+    }
+
+    @Before
+    public void setUp() {
+        mService = new PowerStatsService(mContext, mInjector);
+    }
+
+    @Test
+    public void testWrittenPowerStatsHALDataMatchesReadIncidentReportData()
+            throws InterruptedException, IOException {
+        mService.onBootPhase(SystemService.PHASE_BOOT_COMPLETED);
+
+        // Write data to on-device storage.
+        mTimerTrigger.logPowerStatsData();
+
+        // The above call puts a message on a handler.  Wait for
+        // it to be processed.
+        Thread.sleep(100);
+
+        // Write on-device storage to an incident report.
+        File incidentReport = new File(mDataStorageDir, PROTO_OUTPUT_FILENAME);
+        FileOutputStream fos = new FileOutputStream(incidentReport);
+        mPowerStatsLogger.writeToFile(fos.getFD());
+
+        // Read the incident report in to a byte array.
+        FileInputStream fis = new FileInputStream(incidentReport);
+        byte[] fileContent = new byte[(int) incidentReport.length()];
+        fis.read(fileContent);
+
+        // Parse the incident data into a PowerStatsServiceProto object.
+        PowerStatsServiceProto pssProto = PowerStatsServiceProto.parseFrom(fileContent);
+
+        // Validate the railInfo array matches what was written to on-device storage.
+        assertTrue(pssProto.railInfo.length == POWER_RAIL_COUNT);
+        for (int i = 0; i < pssProto.railInfo.length; i++) {
+            assertTrue(pssProto.railInfo[i].index == i);
+            assertTrue(pssProto.railInfo[i].railName.equals(RAIL_NAME + i));
+            assertTrue(pssProto.railInfo[i].subsysName.equals(SUBSYS_NAME + i));
+            assertTrue(pssProto.railInfo[i].samplingRate == i);
+        }
+
+        // Validate the energyData array matches what was written to on-device storage.
+        assertTrue(pssProto.energyData.length == POWER_RAIL_COUNT);
+        for (int i = 0; i < pssProto.energyData.length; i++) {
+            assertTrue(pssProto.energyData[i].index == i);
+            assertTrue(pssProto.energyData[i].timestampMs == i);
+            assertTrue(pssProto.energyData[i].energyUws == i);
+        }
+    }
+
+    @Test
+    public void testCorruptOnDeviceStorage() throws IOException {
+        mService.onBootPhase(SystemService.PHASE_BOOT_COMPLETED);
+
+        // Generate random array of bytes to emulate corrupt data.
+        Random rd = new Random();
+        byte[] bytes = new byte[100];
+        rd.nextBytes(bytes);
+
+        // Store corrupt data in on-device storage.  Add fake timestamp to filename
+        // to match format expected by FileRotator.
+        File onDeviceStorageFile = new File(mDataStorageDir, DATA_STORAGE_FILENAME + ".1234-2234");
+        FileOutputStream onDeviceStorageFos = new FileOutputStream(onDeviceStorageFile);
+        onDeviceStorageFos.write(bytes);
+        onDeviceStorageFos.close();
+
+        // Write on-device storage to an incident report.
+        File incidentReport = new File(mDataStorageDir, PROTO_OUTPUT_FILENAME);
+        FileOutputStream incidentReportFos = new FileOutputStream(incidentReport);
+        mPowerStatsLogger.writeToFile(incidentReportFos.getFD());
+
+        // Read the incident report in to a byte array.
+        FileInputStream fis = new FileInputStream(incidentReport);
+        byte[] fileContent = new byte[(int) incidentReport.length()];
+        fis.read(fileContent);
+
+        // Parse the incident data into a PowerStatsServiceProto object.
+        PowerStatsServiceProto pssProto = PowerStatsServiceProto.parseFrom(fileContent);
+
+        // Valid railInfo data is written to the incident report in the call to
+        // mPowerStatsLogger.writeToFile().
+        assertTrue(pssProto.railInfo.length == POWER_RAIL_COUNT);
+        for (int i = 0; i < pssProto.railInfo.length; i++) {
+            assertTrue(pssProto.railInfo[i].index == i);
+            assertTrue(pssProto.railInfo[i].railName.equals(RAIL_NAME + i));
+            assertTrue(pssProto.railInfo[i].subsysName.equals(SUBSYS_NAME + i));
+            assertTrue(pssProto.railInfo[i].samplingRate == i);
+        }
+
+        // No energyData should be written to the incident report since it
+        // is all corrupt (random bytes generated above).
+        assertTrue(pssProto.energyData.length == 0);
+    }
+
+    @Test
+    public void testNotEnoughBytesAfterLengthField() throws IOException {
+        mService.onBootPhase(SystemService.PHASE_BOOT_COMPLETED);
+
+        // Create corrupt data.
+        // Length field is correct, but there is no data following the length.
+        ByteArrayOutputStream data = new ByteArrayOutputStream();
+        data.write(ByteBuffer.allocate(4).putInt(50).array());
+        byte[] test = data.toByteArray();
+
+        // Store corrupt data in on-device storage.  Add fake timestamp to filename
+        // to match format expected by FileRotator.
+        File onDeviceStorageFile = new File(mDataStorageDir, DATA_STORAGE_FILENAME + ".1234-2234");
+        FileOutputStream onDeviceStorageFos = new FileOutputStream(onDeviceStorageFile);
+        onDeviceStorageFos.write(data.toByteArray());
+        onDeviceStorageFos.close();
+
+        // Write on-device storage to an incident report.
+        File incidentReport = new File(mDataStorageDir, PROTO_OUTPUT_FILENAME);
+        FileOutputStream incidentReportFos = new FileOutputStream(incidentReport);
+        mPowerStatsLogger.writeToFile(incidentReportFos.getFD());
+
+        // Read the incident report in to a byte array.
+        FileInputStream fis = new FileInputStream(incidentReport);
+        byte[] fileContent = new byte[(int) incidentReport.length()];
+        fis.read(fileContent);
+
+        // Parse the incident data into a PowerStatsServiceProto object.
+        PowerStatsServiceProto pssProto = PowerStatsServiceProto.parseFrom(fileContent);
+
+        // Valid railInfo data is written to the incident report in the call to
+        // mPowerStatsLogger.writeToFile().
+        assertTrue(pssProto.railInfo.length == POWER_RAIL_COUNT);
+        for (int i = 0; i < pssProto.railInfo.length; i++) {
+            assertTrue(pssProto.railInfo[i].index == i);
+            assertTrue(pssProto.railInfo[i].railName.equals(RAIL_NAME + i));
+            assertTrue(pssProto.railInfo[i].subsysName.equals(SUBSYS_NAME + i));
+            assertTrue(pssProto.railInfo[i].samplingRate == i);
+        }
+
+        // No energyData should be written to the incident report since the
+        // input buffer had only length and no data.
+        assertTrue(pssProto.energyData.length == 0);
+    }
+}
diff --git a/tools/powerstats/Android.bp b/tools/powerstats/Android.bp
new file mode 100644
index 0000000..af41144
--- /dev/null
+++ b/tools/powerstats/Android.bp
@@ -0,0 +1,10 @@
+java_binary_host {
+    name: "PowerStatsServiceProtoParser",
+    manifest: "PowerStatsServiceProtoParser_manifest.txt",
+    srcs: [
+        "*.java",
+    ],
+    static_libs: [
+        "platformprotos",
+    ],
+}
diff --git a/tools/powerstats/PowerStatsServiceProtoParser.java b/tools/powerstats/PowerStatsServiceProtoParser.java
new file mode 100644
index 0000000..8ab302a
--- /dev/null
+++ b/tools/powerstats/PowerStatsServiceProtoParser.java
@@ -0,0 +1,95 @@
+/*
+ * Copyright (C) 2020 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.server.powerstats;
+
+import java.io.FileInputStream;
+import java.io.IOException;
+
+/**
+ * This class implements a utility to parse ODPM data out
+ * of incident reports contained in bugreports.  The data
+ * is output to STDOUT in csv format.
+ */
+public class PowerStatsServiceProtoParser {
+    private static void printRailInfo(PowerStatsServiceProto proto) {
+        String csvHeader = new String();
+        for (int i = 0; i < proto.getRailInfoCount(); i++) {
+            RailInfoProto railInfo = proto.getRailInfo(i);
+            csvHeader += "Index" + ","
+                + "Timestamp" + ","
+                + railInfo.getRailName() + "/" + railInfo.getSubsysName() + ",";
+        }
+        System.out.println(csvHeader);
+    }
+
+    private static void printEnergyData(PowerStatsServiceProto proto) {
+        int railInfoCount = proto.getRailInfoCount();
+
+        if (railInfoCount > 0) {
+            int energyDataCount = proto.getEnergyDataCount();
+            int energyDataSetCount = energyDataCount / railInfoCount;
+
+            for (int i = 0; i < energyDataSetCount; i++) {
+                String csvRow = new String();
+                for (int j = 0; j < railInfoCount; j++) {
+                    EnergyDataProto energyData = proto.getEnergyData(i * railInfoCount + j);
+                    csvRow += energyData.getIndex() + ","
+                        + energyData.getTimestampMs() + ","
+                        + energyData.getEnergyUws() + ",";
+                }
+                System.out.println(csvRow);
+            }
+        } else {
+            System.out.println("Error:  railInfoCount is zero");
+        }
+    }
+
+    private static void generateCsvFile(String pathToIncidentReport) {
+        try {
+            IncidentReportProto irProto =
+                    IncidentReportProto.parseFrom(new FileInputStream(pathToIncidentReport));
+
+            if (irProto.hasIncidentReport()) {
+                PowerStatsServiceProto pssProto = irProto.getIncidentReport();
+                printRailInfo(pssProto);
+                printEnergyData(pssProto);
+            } else {
+                System.out.println("Incident report not found.  Exiting.");
+            }
+        } catch (IOException e) {
+            System.out.println("Unable to open incident report file: " + pathToIncidentReport);
+            System.out.println(e);
+        }
+    }
+
+    /**
+     * This is the entry point to parse the ODPM data out of incident reports.
+     * It requires one argument which is the path to the incident_report.proto
+     * file captured in a bugreport.
+     *
+     * @param args Path to incident_report.proto passed in from command line.
+     */
+    public static void main(String[] args) {
+        if (args.length > 0) {
+            generateCsvFile(args[0]);
+        } else {
+            System.err.println("Usage: PowerStatsServiceProtoParser <incident_report.proto>");
+            System.err.println("Missing path to incident_report.proto.  Exiting.");
+            System.exit(1);
+        }
+    }
+}
diff --git a/tools/powerstats/PowerStatsServiceProtoParser_manifest.txt b/tools/powerstats/PowerStatsServiceProtoParser_manifest.txt
new file mode 100644
index 0000000..5df1211
--- /dev/null
+++ b/tools/powerstats/PowerStatsServiceProtoParser_manifest.txt
@@ -0,0 +1 @@
+Main-class: com.android.server.powerstats.PowerStatsServiceProtoParser