Add state residency logging to power stats

Test: atest FrameworksServicesTests:PowerStatsServiceTest
Bug: 175724197

Change-Id: I4ba7af3b9a895ecdc9f8c16cd371e5582458d212
diff --git a/core/proto/android/os/incident.proto b/core/proto/android/os/incident.proto
index 8de30f8..944edb0 100644
--- a/core/proto/android/os/incident.proto
+++ b/core/proto/android/os/incident.proto
@@ -521,6 +521,11 @@
         (section).args = "power_stats --proto model"
     ];
 
+    optional com.android.server.powerstats.PowerStatsServiceResidencyProto powerstats_residency = 3056 [
+        (section).type = SECTION_DUMPSYS,
+        (section).args = "power_stats --proto residency"
+    ];
+
     // 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
index 9a7ed7c..30c4274 100644
--- a/core/proto/android/server/powerstatsservice.proto
+++ b/core/proto/android/server/powerstatsservice.proto
@@ -41,6 +41,16 @@
 }
 
 /**
+ * IncidentReportResidencyProto is used only in the parsing tool located
+ * in frameworks/base/tools which is used to parse this data out of
+ * incident reports.
+ */
+message IncidentReportResidencyProto {
+    /** Section number matches that in incident.proto */
+    optional PowerStatsServiceResidencyProto incident_report = 3056;
+}
+
+/**
  * EnergyConsumer (model) data is exposed by the PowerStats HAL.  This data
  * represents modeled energy consumption estimates and is provided per
  * subsystem.  The default subsystems are defined in EnergyConsumerId.aidl.
@@ -63,6 +73,99 @@
 }
 
 /**
+ * A PowerEntity is defined as a platform subsystem, peripheral, or power domain
+ * that impacts the total device power consumption.  PowerEntityInfo is
+ * information related to each power entity.  Each PowerEntity may reside in one
+ * of multiple states. It may also transition from one state to another.
+ * StateResidency is defined as an accumulation of time that a PowerEntity
+ * resided in each of its possible states, the number of times that each state
+ * was entered, and a timestamp corresponding to the last time that state was
+ * entered.
+ */
+message PowerStatsServiceResidencyProto {
+    repeated PowerEntityInfoProto power_entity_info = 1;
+    repeated StateResidencyResultProto state_residency_result = 2;
+}
+
+/**
+ * Information about the possible states for a particular PowerEntity.
+ */
+message StateInfoProto {
+    /**
+     * Unique (for a given PowerEntityInfo) ID of this StateInfo
+     */
+    optional int32 state_id = 1;
+    /**
+     * Unique (for a given PowerEntityInfo) name of the state. Vendor/device specific.
+     * Opaque to framework
+     */
+    optional string state_name = 2;
+}
+
+/**
+ * A PowerEntity is defined as a platform subsystem, peripheral, or power domain
+ * that impacts the total device power consumption.  PowerEntityInfo is
+ * information about a PowerEntity.  It includes an array of information about
+ * each possible state of the PowerEntity.
+ */
+message PowerEntityInfoProto {
+    /**
+     * Unique ID of this PowerEntityInfo
+     */
+    optional int32 power_entity_id = 1;
+    /**
+     * Unique name of the PowerEntity. Vendor/device specific. Opaque to framework
+     */
+    optional string power_entity_name = 2;
+    /**
+     * List of states that the PowerEntity may reside in
+     */
+    repeated StateInfoProto states = 3;
+}
+
+/**
+ * StateResidency is defined as an accumulation of time that a PowerEntity
+ * resided in each of its possible states, the number of times that each state
+ * was entered, and a timestamp corresponding to the last time that state was
+ * entered.  Data is accumulated starting at device boot.
+ */
+message StateResidencyProto {
+    /**
+     * ID of the state associated with this residency
+     */
+    optional int32 state_id = 1;
+    /**
+     * Total time in milliseconds that the corresponding PowerEntity resided
+     * in this state since boot
+     */
+    optional int64 total_time_in_state_ms = 2;
+    /**
+     * Total number of times that the state was entered since boot
+     */
+    optional int64 total_state_entry_count = 3;
+    /**
+     * Last time this state was entered. Time in milliseconds since boot
+     */
+    optional int64 last_entry_timestamp_ms = 4;
+}
+
+/**
+ * A StateResidencyResult is an array of StateResidencies for a particular
+ * PowerEntity.  The StateResidencyResult can be matched to its corresponding
+ * PowerEntityInfo through the power_entity_id field.
+ */
+message StateResidencyResultProto {
+    /**
+     * ID of the PowerEntity associated with this result
+     */
+    optional int32 power_entity_id = 1;
+    /**
+     * Residency for each state in the PowerEntity's state space
+     */
+    repeated StateResidencyProto state_residency_data = 2;
+}
+
+/**
  * Energy consumer ID:
  * A list of default subsystems for which energy consumption estimates
  * may be provided (hardware dependent).
diff --git a/services/core/java/com/android/server/powerstats/PowerStatsDataStorage.java b/services/core/java/com/android/server/powerstats/PowerStatsDataStorage.java
index c9595c2..6d9cb75 100644
--- a/services/core/java/com/android/server/powerstats/PowerStatsDataStorage.java
+++ b/services/core/java/com/android/server/powerstats/PowerStatsDataStorage.java
@@ -218,7 +218,7 @@
      *             array and written to on-device storage.
      */
     public void write(byte[] data) {
-        if (data.length > 0) {
+        if (data != null && data.length > 0) {
             mLock.lock();
 
             long currentTimeMillis = System.currentTimeMillis();
diff --git a/services/core/java/com/android/server/powerstats/PowerStatsLogger.java b/services/core/java/com/android/server/powerstats/PowerStatsLogger.java
index 409cd82..4e86cd8 100644
--- a/services/core/java/com/android/server/powerstats/PowerStatsLogger.java
+++ b/services/core/java/com/android/server/powerstats/PowerStatsLogger.java
@@ -20,6 +20,8 @@
 import android.hardware.power.stats.ChannelInfo;
 import android.hardware.power.stats.EnergyConsumerResult;
 import android.hardware.power.stats.EnergyMeasurement;
+import android.hardware.power.stats.PowerEntityInfo;
+import android.hardware.power.stats.StateResidencyResult;
 import android.os.Handler;
 import android.os.Looper;
 import android.os.Message;
@@ -32,6 +34,8 @@
 import com.android.server.powerstats.ProtoStreamUtils.EnergyConsumerIdUtils;
 import com.android.server.powerstats.ProtoStreamUtils.EnergyConsumerResultUtils;
 import com.android.server.powerstats.ProtoStreamUtils.EnergyMeasurementUtils;
+import com.android.server.powerstats.ProtoStreamUtils.PowerEntityInfoUtils;
+import com.android.server.powerstats.ProtoStreamUtils.StateResidencyResultUtils;
 
 import java.io.ByteArrayInputStream;
 import java.io.File;
@@ -42,8 +46,8 @@
  * PowerStatsLogger is responsible for logging model and meter 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 writeModelDataToFile or writeMeterDataToFile with a file descriptor
- * that points to the output file.
+ * dumped to file by calling writeModelDataToFile, writeMeterDataToFile, or writeResidencyDataToFile
+ * with a file descriptor that points to the output file.
  */
 public final class PowerStatsLogger extends Handler {
     private static final String TAG = PowerStatsLogger.class.getSimpleName();
@@ -52,6 +56,7 @@
 
     private final PowerStatsDataStorage mPowerStatsMeterStorage;
     private final PowerStatsDataStorage mPowerStatsModelStorage;
+    private final PowerStatsDataStorage mPowerStatsResidencyStorage;
     private final IPowerStatsHALWrapper mPowerStatsHALWrapper;
 
     @Override
@@ -73,6 +78,13 @@
                 mPowerStatsModelStorage.write(
                         EnergyConsumerResultUtils.getProtoBytes(energyConsumerResults));
                 if (DEBUG) EnergyConsumerResultUtils.print(energyConsumerResults);
+
+                // Log state residency data.
+                StateResidencyResult[] stateResidencyResults =
+                    mPowerStatsHALWrapper.getStateResidency(new int[0]);
+                mPowerStatsResidencyStorage.write(
+                        StateResidencyResultUtils.getProtoBytes(stateResidencyResults));
+                if (DEBUG) StateResidencyResultUtils.print(stateResidencyResults);
                 break;
         }
     }
@@ -159,13 +171,57 @@
         pos.flush();
     }
 
+    /**
+     * Writes residency data stored in PowerStatsDataStorage to a file descriptor.
+     *
+     * @param fd FileDescriptor where residency data stored in PowerStatsDataStorage is written.
+     *           Data is written in protobuf format as defined by powerstatsservice.proto.
+     */
+    public void writeResidencyDataToFile(FileDescriptor fd) {
+        if (DEBUG) Slog.d(TAG, "Writing residency data to file");
+
+        final ProtoOutputStream pos = new ProtoOutputStream(fd);
+
+        try {
+            PowerEntityInfo[] powerEntityInfo = mPowerStatsHALWrapper.getPowerEntityInfo();
+            PowerEntityInfoUtils.packProtoMessage(powerEntityInfo, pos);
+            if (DEBUG) PowerEntityInfoUtils.print(powerEntityInfo);
+
+            mPowerStatsResidencyStorage.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.
+                        StateResidencyResult[] stateResidencyResult =
+                            StateResidencyResultUtils.unpackProtoMessage(data);
+                        StateResidencyResultUtils.packProtoMessage(stateResidencyResult, pos);
+                        if (DEBUG) StateResidencyResultUtils.print(stateResidencyResult);
+                    } catch (IOException e) {
+                        Slog.e(TAG, "Failed to write residency data to incident report.");
+                    }
+                }
+            });
+        } catch (IOException e) {
+            Slog.e(TAG, "Failed to write residency data to incident report.");
+        }
+
+        pos.flush();
+    }
+
     public PowerStatsLogger(Context context, File dataStoragePath, String meterFilename,
-            String modelFilename, IPowerStatsHALWrapper powerStatsHALWrapper) {
+            String modelFilename, String residencyFilename,
+            IPowerStatsHALWrapper powerStatsHALWrapper) {
         super(Looper.getMainLooper());
         mPowerStatsHALWrapper = powerStatsHALWrapper;
         mPowerStatsMeterStorage = new PowerStatsDataStorage(context, dataStoragePath,
             meterFilename);
         mPowerStatsModelStorage = new PowerStatsDataStorage(context, dataStoragePath,
             modelFilename);
+        mPowerStatsResidencyStorage = new PowerStatsDataStorage(context, dataStoragePath,
+            residencyFilename);
     }
 }
diff --git a/services/core/java/com/android/server/powerstats/PowerStatsService.java b/services/core/java/com/android/server/powerstats/PowerStatsService.java
index ce50e583..7778572 100644
--- a/services/core/java/com/android/server/powerstats/PowerStatsService.java
+++ b/services/core/java/com/android/server/powerstats/PowerStatsService.java
@@ -49,6 +49,8 @@
     private static final int DATA_STORAGE_VERSION = 0;
     private static final String METER_FILENAME = "log.powerstats.meter." + DATA_STORAGE_VERSION;
     private static final String MODEL_FILENAME = "log.powerstats.model." + DATA_STORAGE_VERSION;
+    private static final String RESIDENCY_FILENAME =
+            "log.powerstats.residency." + DATA_STORAGE_VERSION;
 
     private final Injector mInjector;
 
@@ -76,15 +78,19 @@
             return MODEL_FILENAME;
         }
 
+        String createResidencyFilename() {
+            return RESIDENCY_FILENAME;
+        }
+
         IPowerStatsHALWrapper createPowerStatsHALWrapperImpl() {
             return PowerStatsHALWrapper.getPowerStatsHalImpl();
         }
 
         PowerStatsLogger createPowerStatsLogger(Context context, File dataStoragePath,
-                String meterFilename, String modelFilename,
+                String meterFilename, String modelFilename, String residencyFilename,
                 IPowerStatsHALWrapper powerStatsHALWrapper) {
             return new PowerStatsLogger(context, dataStoragePath, meterFilename,
-                modelFilename, powerStatsHALWrapper);
+                modelFilename, residencyFilename, powerStatsHALWrapper);
         }
 
         BatteryTrigger createBatteryTrigger(Context context, PowerStatsLogger powerStatsLogger) {
@@ -109,6 +115,8 @@
                         mPowerStatsLogger.writeModelDataToFile(fd);
                     } else if ("meter".equals(args[1])) {
                         mPowerStatsLogger.writeMeterDataToFile(fd);
+                    } else if ("residency".equals(args[1])) {
+                        mPowerStatsLogger.writeResidencyDataToFile(fd);
                     }
                 } else if (args.length == 0) {
                     pw.println("PowerStatsService dumpsys: available PowerEntityInfos");
@@ -148,7 +156,8 @@
             // Only start logger and triggers if initialization is successful.
             mPowerStatsLogger = mInjector.createPowerStatsLogger(mContext,
                 mInjector.createDataStoragePath(), mInjector.createMeterFilename(),
-                mInjector.createModelFilename(), mPowerStatsHALWrapper);
+                mInjector.createModelFilename(), mInjector.createResidencyFilename(),
+                mPowerStatsHALWrapper);
             mBatteryTrigger = mInjector.createBatteryTrigger(mContext, mPowerStatsLogger);
             mTimerTrigger = mInjector.createTimerTrigger(mContext, mPowerStatsLogger);
         } else {
diff --git a/services/core/java/com/android/server/powerstats/ProtoStreamUtils.java b/services/core/java/com/android/server/powerstats/ProtoStreamUtils.java
index 5e23b86..ab9b3e0 100644
--- a/services/core/java/com/android/server/powerstats/ProtoStreamUtils.java
+++ b/services/core/java/com/android/server/powerstats/ProtoStreamUtils.java
@@ -20,6 +20,8 @@
 import android.hardware.power.stats.EnergyConsumerResult;
 import android.hardware.power.stats.EnergyMeasurement;
 import android.hardware.power.stats.PowerEntityInfo;
+import android.hardware.power.stats.StateInfo;
+import android.hardware.power.stats.StateResidency;
 import android.hardware.power.stats.StateResidencyResult;
 import android.util.Slog;
 import android.util.proto.ProtoInputStream;
@@ -45,6 +47,29 @@
     private static final String TAG = ProtoStreamUtils.class.getSimpleName();
 
     static class PowerEntityInfoUtils {
+        public static void packProtoMessage(PowerEntityInfo[] powerEntityInfo,
+                ProtoOutputStream pos) {
+            if (powerEntityInfo == null) return;
+
+            for (int i = 0; i < powerEntityInfo.length; i++) {
+                long peiToken = pos.start(PowerStatsServiceResidencyProto.POWER_ENTITY_INFO);
+                pos.write(PowerEntityInfoProto.POWER_ENTITY_ID, powerEntityInfo[i].powerEntityId);
+                pos.write(PowerEntityInfoProto.POWER_ENTITY_NAME,
+                        powerEntityInfo[i].powerEntityName);
+                if (powerEntityInfo[i].states != null) {
+                    final int statesLength = powerEntityInfo[i].states.length;
+                    for (int j = 0; j < statesLength; j++) {
+                        final StateInfo state = powerEntityInfo[i].states[j];
+                        long siToken = pos.start(PowerEntityInfoProto.STATES);
+                        pos.write(StateInfoProto.STATE_ID, state.stateId);
+                        pos.write(StateInfoProto.STATE_NAME, state.stateName);
+                        pos.end(siToken);
+                    }
+                }
+                pos.end(peiToken);
+            }
+        }
+
         public static void print(PowerEntityInfo[] powerEntityInfo) {
             if (powerEntityInfo == null) return;
 
@@ -77,6 +102,144 @@
     }
 
     static class StateResidencyResultUtils {
+        public static byte[] getProtoBytes(StateResidencyResult[] stateResidencyResult) {
+            ProtoOutputStream pos = new ProtoOutputStream();
+            packProtoMessage(stateResidencyResult, pos);
+            return pos.getBytes();
+        }
+
+        public static void packProtoMessage(StateResidencyResult[] stateResidencyResult,
+                ProtoOutputStream pos) {
+            if (stateResidencyResult == null) return;
+
+            for (int i = 0; i < stateResidencyResult.length; i++) {
+                final int stateLength = stateResidencyResult[i].stateResidencyData.length;
+                long srrToken = pos.start(PowerStatsServiceResidencyProto.STATE_RESIDENCY_RESULT);
+                pos.write(StateResidencyResultProto.POWER_ENTITY_ID,
+                        stateResidencyResult[i].powerEntityId);
+                for (int j = 0; j < stateLength; j++) {
+                    final StateResidency stateResidencyData =
+                            stateResidencyResult[i].stateResidencyData[j];
+                    long srdToken = pos.start(StateResidencyResultProto.STATE_RESIDENCY_DATA);
+                    pos.write(StateResidencyProto.STATE_ID, stateResidencyData.stateId);
+                    pos.write(StateResidencyProto.TOTAL_TIME_IN_STATE_MS,
+                            stateResidencyData.totalTimeInStateMs);
+                    pos.write(StateResidencyProto.TOTAL_STATE_ENTRY_COUNT,
+                            stateResidencyData.totalStateEntryCount);
+                    pos.write(StateResidencyProto.LAST_ENTRY_TIMESTAMP_MS,
+                            stateResidencyData.lastEntryTimestampMs);
+                    pos.end(srdToken);
+                }
+                pos.end(srrToken);
+            }
+        }
+
+        public static StateResidencyResult[] unpackProtoMessage(byte[] data) throws IOException {
+            final ProtoInputStream pis = new ProtoInputStream(new ByteArrayInputStream(data));
+            List<StateResidencyResult> stateResidencyResultList =
+                    new ArrayList<StateResidencyResult>();
+            while (true) {
+                try {
+                    int nextField = pis.nextField();
+                    StateResidencyResult stateResidencyResult = new StateResidencyResult();
+
+                    if (nextField == (int) PowerStatsServiceResidencyProto.STATE_RESIDENCY_RESULT) {
+                        long token =
+                                pis.start(PowerStatsServiceResidencyProto.STATE_RESIDENCY_RESULT);
+                        stateResidencyResultList.add(unpackStateResidencyResultProto(pis));
+                        pis.end(token);
+                    } else if (nextField == ProtoInputStream.NO_MORE_FIELDS) {
+                        return stateResidencyResultList.toArray(
+                            new StateResidencyResult[stateResidencyResultList.size()]);
+                    } else {
+                        Slog.e(TAG, "Unhandled field in PowerStatsServiceResidencyProto: "
+                                + ProtoUtils.currentFieldToString(pis));
+                    }
+                } catch (WireTypeMismatchException wtme) {
+                    Slog.e(TAG, "Wire Type mismatch in PowerStatsServiceResidencyProto: "
+                            + ProtoUtils.currentFieldToString(pis));
+                }
+            }
+        }
+
+        private static StateResidencyResult unpackStateResidencyResultProto(ProtoInputStream pis)
+                throws IOException {
+            StateResidencyResult stateResidencyResult = new StateResidencyResult();
+            List<StateResidency> stateResidencyList = new ArrayList<StateResidency>();
+
+            while (true) {
+                try {
+                    switch (pis.nextField()) {
+                        case (int) StateResidencyResultProto.POWER_ENTITY_ID:
+                            stateResidencyResult.powerEntityId =
+                                pis.readInt(StateResidencyResultProto.POWER_ENTITY_ID);
+                            break;
+
+                        case (int) StateResidencyResultProto.STATE_RESIDENCY_DATA:
+                            long token = pis.start(StateResidencyResultProto.STATE_RESIDENCY_DATA);
+                            stateResidencyList.add(unpackStateResidencyProto(pis));
+                            pis.end(token);
+                            break;
+
+                        case ProtoInputStream.NO_MORE_FIELDS:
+                            stateResidencyResult.stateResidencyData = stateResidencyList.toArray(
+                                new StateResidency[stateResidencyList.size()]);
+                            return stateResidencyResult;
+
+                        default:
+                            Slog.e(TAG, "Unhandled field in StateResidencyResultProto: "
+                                    + ProtoUtils.currentFieldToString(pis));
+                            break;
+                    }
+                } catch (WireTypeMismatchException wtme) {
+                    Slog.e(TAG, "Wire Type mismatch in StateResidencyResultProto: "
+                            + ProtoUtils.currentFieldToString(pis));
+                }
+            }
+        }
+
+        private static StateResidency unpackStateResidencyProto(ProtoInputStream pis)
+                throws IOException {
+            StateResidency stateResidency = new StateResidency();
+
+            while (true) {
+                try {
+                    switch (pis.nextField()) {
+                        case (int) StateResidencyProto.STATE_ID:
+                            stateResidency.stateId = pis.readInt(StateResidencyProto.STATE_ID);
+                            break;
+
+                        case (int) StateResidencyProto.TOTAL_TIME_IN_STATE_MS:
+                            stateResidency.totalTimeInStateMs =
+                                pis.readLong(StateResidencyProto.TOTAL_TIME_IN_STATE_MS);
+                            break;
+
+                        case (int) StateResidencyProto.TOTAL_STATE_ENTRY_COUNT:
+                            stateResidency.totalStateEntryCount =
+                                pis.readLong(StateResidencyProto.TOTAL_STATE_ENTRY_COUNT);
+                            break;
+
+                        case (int) StateResidencyProto.LAST_ENTRY_TIMESTAMP_MS:
+                            stateResidency.lastEntryTimestampMs =
+                                pis.readLong(StateResidencyProto.LAST_ENTRY_TIMESTAMP_MS);
+                            break;
+
+                        case ProtoInputStream.NO_MORE_FIELDS:
+                            return stateResidency;
+
+                        default:
+                            Slog.e(TAG, "Unhandled field in StateResidencyProto: "
+                                    + ProtoUtils.currentFieldToString(pis));
+                            break;
+
+                    }
+                } catch (WireTypeMismatchException wtme) {
+                    Slog.e(TAG, "Wire Type mismatch in StateResidencyProto: "
+                            + ProtoUtils.currentFieldToString(pis));
+                }
+            }
+        }
+
         public static void print(StateResidencyResult[] stateResidencyResult) {
             if (stateResidencyResult == null) return;
 
@@ -98,17 +261,14 @@
 
     static class ChannelInfoUtils {
         public static void packProtoMessage(ChannelInfo[] channelInfo, ProtoOutputStream pos) {
-            long token;
-
             if (channelInfo == null) return;
 
             for (int i = 0; i < channelInfo.length; i++) {
-                token = pos.start(PowerStatsServiceMeterProto.CHANNEL_INFO);
+                long token = pos.start(PowerStatsServiceMeterProto.CHANNEL_INFO);
                 pos.write(ChannelInfoProto.CHANNEL_ID, channelInfo[i].channelId);
                 pos.write(ChannelInfoProto.CHANNEL_NAME, channelInfo[i].channelName);
                 pos.end(token);
             }
-
         }
 
         public static void print(ChannelInfo[] channelInfo) {
@@ -139,12 +299,10 @@
 
         public static void packProtoMessage(EnergyMeasurement[] energyMeasurement,
                 ProtoOutputStream pos) {
-            long token;
-
             if (energyMeasurement == null) return;
 
             for (int i = 0; i < energyMeasurement.length; i++) {
-                token = pos.start(PowerStatsServiceMeterProto.ENERGY_MEASUREMENT);
+                long token = pos.start(PowerStatsServiceMeterProto.ENERGY_MEASUREMENT);
                 pos.write(EnergyMeasurementProto.CHANNEL_ID, energyMeasurement[i].channelId);
                 pos.write(EnergyMeasurementProto.TIMESTAMP_MS, energyMeasurement[i].timestampMs);
                 pos.write(EnergyMeasurementProto.ENERGY_UWS, energyMeasurement[i].energyUWs);
@@ -155,7 +313,6 @@
         public static EnergyMeasurement[] unpackProtoMessage(byte[] data) throws IOException {
             final ProtoInputStream pis = new ProtoInputStream(new ByteArrayInputStream(data));
             List<EnergyMeasurement> energyMeasurementList = new ArrayList<EnergyMeasurement>();
-            long token;
 
             while (true) {
                 try {
@@ -163,8 +320,8 @@
                     EnergyMeasurement energyMeasurement = new EnergyMeasurement();
 
                     if (nextField == (int) PowerStatsServiceMeterProto.ENERGY_MEASUREMENT) {
-                        token = pis.start(PowerStatsServiceMeterProto.ENERGY_MEASUREMENT);
-                        energyMeasurementList.add(unpackProtoMessage(pis));
+                        long token = pis.start(PowerStatsServiceMeterProto.ENERGY_MEASUREMENT);
+                        energyMeasurementList.add(unpackEnergyMeasurementProto(pis));
                         pis.end(token);
                     } else if (nextField == ProtoInputStream.NO_MORE_FIELDS) {
                         return energyMeasurementList.toArray(
@@ -180,7 +337,7 @@
             }
         }
 
-        private static EnergyMeasurement unpackProtoMessage(ProtoInputStream pis)
+        private static EnergyMeasurement unpackEnergyMeasurementProto(ProtoInputStream pis)
                 throws IOException {
             EnergyMeasurement energyMeasurement = new EnergyMeasurement();
 
@@ -230,12 +387,10 @@
 
     static class EnergyConsumerIdUtils {
         public static void packProtoMessage(int[] energyConsumerId, ProtoOutputStream pos) {
-            long token;
-
             if (energyConsumerId == null) return;
 
             for (int i = 0; i < energyConsumerId.length; i++) {
-                token = pos.start(PowerStatsServiceModelProto.ENERGY_CONSUMER_ID);
+                long token = pos.start(PowerStatsServiceModelProto.ENERGY_CONSUMER_ID);
                 pos.write(EnergyConsumerIdProto.ENERGY_CONSUMER_ID, energyConsumerId[i]);
                 pos.end(token);
             }
@@ -267,12 +422,10 @@
 
         public static void packProtoMessage(EnergyConsumerResult[] energyConsumerResult,
                 ProtoOutputStream pos) {
-            long token;
-
             if (energyConsumerResult == null) return;
 
             for (int i = 0; i < energyConsumerResult.length; i++) {
-                token = pos.start(PowerStatsServiceModelProto.ENERGY_CONSUMER_RESULT);
+                long token = pos.start(PowerStatsServiceModelProto.ENERGY_CONSUMER_RESULT);
                 pos.write(EnergyConsumerResultProto.ENERGY_CONSUMER_ID,
                         energyConsumerResult[i].energyConsumerId);
                 pos.write(EnergyConsumerResultProto.TIMESTAMP_MS,
@@ -286,16 +439,14 @@
             final ProtoInputStream pis = new ProtoInputStream(new ByteArrayInputStream(data));
             List<EnergyConsumerResult> energyConsumerResultList =
                     new ArrayList<EnergyConsumerResult>();
-            long token;
-
             while (true) {
                 try {
                     int nextField = pis.nextField();
                     EnergyConsumerResult energyConsumerResult = new EnergyConsumerResult();
 
                     if (nextField == (int) PowerStatsServiceModelProto.ENERGY_CONSUMER_RESULT) {
-                        token = pis.start(PowerStatsServiceModelProto.ENERGY_CONSUMER_RESULT);
-                        energyConsumerResultList.add(unpackProtoMessage(pis));
+                        long token = pis.start(PowerStatsServiceModelProto.ENERGY_CONSUMER_RESULT);
+                        energyConsumerResultList.add(unpackEnergyConsumerResultProto(pis));
                         pis.end(token);
                     } else if (nextField == ProtoInputStream.NO_MORE_FIELDS) {
                         return energyConsumerResultList.toArray(
@@ -311,7 +462,7 @@
             }
         }
 
-        private static EnergyConsumerResult unpackProtoMessage(ProtoInputStream pis)
+        private static EnergyConsumerResult unpackEnergyConsumerResultProto(ProtoInputStream pis)
                 throws IOException {
             EnergyConsumerResult energyConsumerResult = new EnergyConsumerResult();
 
diff --git a/services/tests/servicestests/src/com/android/server/powerstats/PowerStatsServiceTest.java b/services/tests/servicestests/src/com/android/server/powerstats/PowerStatsServiceTest.java
index 08d4caa..9fe3c0d 100644
--- a/services/tests/servicestests/src/com/android/server/powerstats/PowerStatsServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/powerstats/PowerStatsServiceTest.java
@@ -32,8 +32,13 @@
 
 import com.android.server.SystemService;
 import com.android.server.powerstats.PowerStatsHALWrapper.IPowerStatsHALWrapper;
+import com.android.server.powerstats.nano.PowerEntityInfoProto;
 import com.android.server.powerstats.nano.PowerStatsServiceMeterProto;
 import com.android.server.powerstats.nano.PowerStatsServiceModelProto;
+import com.android.server.powerstats.nano.PowerStatsServiceResidencyProto;
+import com.android.server.powerstats.nano.StateInfoProto;
+import com.android.server.powerstats.nano.StateResidencyProto;
+import com.android.server.powerstats.nano.StateResidencyResultProto;
 
 import org.junit.Before;
 import org.junit.Test;
@@ -58,6 +63,7 @@
     private static final String DATA_STORAGE_SUBDIR = "powerstatstest";
     private static final String METER_FILENAME = "metertest";
     private static final String MODEL_FILENAME = "modeltest";
+    private static final String RESIDENCY_FILENAME = "residencytest";
     private static final String PROTO_OUTPUT_FILENAME = "powerstats.proto";
     private static final String CHANNEL_NAME = "channelname";
     private static final String POWER_ENTITY_NAME = "powerentityinfo";
@@ -99,16 +105,21 @@
         }
 
         @Override
+        String createResidencyFilename() {
+            return RESIDENCY_FILENAME;
+        }
+
+        @Override
         IPowerStatsHALWrapper createPowerStatsHALWrapperImpl() {
             return new TestPowerStatsHALWrapper();
         }
 
         @Override
         PowerStatsLogger createPowerStatsLogger(Context context, File dataStoragePath,
-                String meterFilename, String modelFilename,
+                String meterFilename, String modelFilename, String residencyFilename,
                 IPowerStatsHALWrapper powerStatsHALWrapper) {
             mPowerStatsLogger = new PowerStatsLogger(context, dataStoragePath, meterFilename,
-                modelFilename, powerStatsHALWrapper);
+                modelFilename, residencyFilename, powerStatsHALWrapper);
             return mPowerStatsLogger;
         }
 
@@ -137,7 +148,7 @@
                 for (int j = 0; j < powerEntityInfoList[i].states.length; j++) {
                     powerEntityInfoList[i].states[j] = new StateInfo();
                     powerEntityInfoList[i].states[j].stateId = j;
-                    powerEntityInfoList[i].states[j].stateName = new String(STATE_NAME + i);
+                    powerEntityInfoList[i].states[j].stateName = new String(STATE_NAME + j);
                 }
             }
             return powerEntityInfoList;
@@ -154,6 +165,7 @@
                     new StateResidency[STATE_RESIDENCY_COUNT];
                 for (int j = 0; j < stateResidencyResultList[i].stateResidencyData.length; j++) {
                     stateResidencyResultList[i].stateResidencyData[j] = new StateResidency();
+                    stateResidencyResultList[i].stateResidencyData[j].stateId = j;
                     stateResidencyResultList[i].stateResidencyData[j].totalTimeInStateMs = j;
                     stateResidencyResultList[i].stateResidencyData[j].totalStateEntryCount = j;
                     stateResidencyResultList[i].stateResidencyData[j].lastEntryTimestampMs = j;
@@ -301,6 +313,61 @@
     }
 
     @Test
+    public void testWrittenResidencyDataMatchesReadIncidentReportData()
+            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.writeResidencyDataToFile(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 PowerStatsServiceResidencyProto object.
+        PowerStatsServiceResidencyProto pssProto =
+                PowerStatsServiceResidencyProto.parseFrom(fileContent);
+
+        // Validate the powerEntityInfo array matches what was written to on-device storage.
+        assertTrue(pssProto.powerEntityInfo.length == POWER_ENTITY_COUNT);
+        for (int i = 0; i < pssProto.powerEntityInfo.length; i++) {
+            PowerEntityInfoProto powerEntityInfo = pssProto.powerEntityInfo[i];
+            assertTrue(powerEntityInfo.powerEntityId == i);
+            assertTrue(powerEntityInfo.powerEntityName.equals(POWER_ENTITY_NAME + i));
+            for (int j = 0; j < powerEntityInfo.states.length; j++) {
+                StateInfoProto stateInfo = powerEntityInfo.states[j];
+                assertTrue(stateInfo.stateId == j);
+                assertTrue(stateInfo.stateName.equals(STATE_NAME + j));
+            }
+        }
+
+        // Validate the stateResidencyResult array matches what was written to on-device storage.
+        assertTrue(pssProto.stateResidencyResult.length == POWER_ENTITY_COUNT);
+        for (int i = 0; i < pssProto.stateResidencyResult.length; i++) {
+            StateResidencyResultProto stateResidencyResult = pssProto.stateResidencyResult[i];
+            assertTrue(stateResidencyResult.powerEntityId == i);
+            assertTrue(stateResidencyResult.stateResidencyData.length == STATE_RESIDENCY_COUNT);
+            for (int j = 0; j < stateResidencyResult.stateResidencyData.length; j++) {
+                StateResidencyProto stateResidency = stateResidencyResult.stateResidencyData[j];
+                assertTrue(stateResidency.stateId == j);
+                assertTrue(stateResidency.totalTimeInStateMs == j);
+                assertTrue(stateResidency.totalStateEntryCount == j);
+                assertTrue(stateResidency.lastEntryTimestampMs == j);
+            }
+        }
+    }
+
+    @Test
     public void testCorruptOnDeviceMeterStorage() throws IOException {
         mService.onBootPhase(SystemService.PHASE_BOOT_COMPLETED);
 
@@ -384,6 +451,55 @@
     }
 
     @Test
+    public void testCorruptOnDeviceResidencyStorage() 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, RESIDENCY_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.writeResidencyDataToFile(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 PowerStatsServiceResidencyProto object.
+        PowerStatsServiceResidencyProto pssProto =
+                PowerStatsServiceResidencyProto.parseFrom(fileContent);
+
+        // Valid powerEntityInfo data is written to the incident report in the call to
+        // mPowerStatsLogger.writeResidencyDataToFile().
+        assertTrue(pssProto.powerEntityInfo.length == POWER_ENTITY_COUNT);
+        for (int i = 0; i < pssProto.powerEntityInfo.length; i++) {
+            PowerEntityInfoProto powerEntityInfo = pssProto.powerEntityInfo[i];
+            assertTrue(powerEntityInfo.powerEntityId == i);
+            assertTrue(powerEntityInfo.powerEntityName.equals(POWER_ENTITY_NAME + i));
+            for (int j = 0; j < powerEntityInfo.states.length; j++) {
+                StateInfoProto stateInfo = powerEntityInfo.states[j];
+                assertTrue(stateInfo.stateId == j);
+                assertTrue(stateInfo.stateName.equals(STATE_NAME + j));
+            }
+        }
+
+        // No stateResidencyResults should be written to the incident report since it
+        // is all corrupt (random bytes generated above).
+        assertTrue(pssProto.stateResidencyResult.length == 0);
+    }
+
+    @Test
     public void testNotEnoughBytesAfterMeterLengthField() throws IOException {
         mService.onBootPhase(SystemService.PHASE_BOOT_COMPLETED);
 
@@ -467,4 +583,54 @@
         // input buffer had only length and no data.
         assertTrue(pssProto.energyConsumerResult.length == 0);
     }
+
+    @Test
+    public void testNotEnoughBytesAfterResidencyLengthField() 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, RESIDENCY_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.writeResidencyDataToFile(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 PowerStatsServiceResidencyProto object.
+        PowerStatsServiceResidencyProto pssProto =
+                PowerStatsServiceResidencyProto.parseFrom(fileContent);
+
+        // Valid powerEntityInfo data is written to the incident report in the call to
+        // mPowerStatsLogger.writeResidencyDataToFile().
+        assertTrue(pssProto.powerEntityInfo.length == POWER_ENTITY_COUNT);
+        for (int i = 0; i < pssProto.powerEntityInfo.length; i++) {
+            PowerEntityInfoProto powerEntityInfo = pssProto.powerEntityInfo[i];
+            assertTrue(powerEntityInfo.powerEntityId == i);
+            assertTrue(powerEntityInfo.powerEntityName.equals(POWER_ENTITY_NAME + i));
+            for (int j = 0; j < powerEntityInfo.states.length; j++) {
+                StateInfoProto stateInfo = powerEntityInfo.states[j];
+                assertTrue(stateInfo.stateId == j);
+                assertTrue(stateInfo.stateName.equals(STATE_NAME + j));
+            }
+        }
+
+        // No stateResidencyResults should be written to the incident report since the
+        // input buffer had only length and no data.
+        assertTrue(pssProto.stateResidencyResult.length == 0);
+    }
 }
diff --git a/tools/powerstats/PowerStatsServiceProtoParser.java b/tools/powerstats/PowerStatsServiceProtoParser.java
index 76edd63..97a2a40 100644
--- a/tools/powerstats/PowerStatsServiceProtoParser.java
+++ b/tools/powerstats/PowerStatsServiceProtoParser.java
@@ -90,6 +90,38 @@
         }
     }
 
+    private static void printPowerEntityInfo(PowerStatsServiceResidencyProto proto) {
+        String csvHeader = new String();
+        for (int i = 0; i < proto.getPowerEntityInfoCount(); i++) {
+            PowerEntityInfoProto powerEntityInfo = proto.getPowerEntityInfo(i);
+            csvHeader += powerEntityInfo.getPowerEntityId() + ","
+                + powerEntityInfo.getPowerEntityName() + ",";
+            for (int j = 0; j < powerEntityInfo.getStatesCount(); j++) {
+                StateInfoProto stateInfo = powerEntityInfo.getStates(j);
+                csvHeader += stateInfo.getStateId() + "," + stateInfo.getStateName() + ",";
+            }
+        }
+        System.out.println(csvHeader);
+    }
+
+    private static void printStateResidencyResult(PowerStatsServiceResidencyProto proto) {
+        for (int i = 0; i < proto.getStateResidencyResultCount(); i++) {
+            String csvRow = new String();
+
+            StateResidencyResultProto stateResidencyResult = proto.getStateResidencyResult(i);
+            csvRow += stateResidencyResult.getPowerEntityId() + ",";
+
+            for (int j = 0; j < stateResidencyResult.getStateResidencyDataCount(); j++) {
+                StateResidencyProto stateResidency = stateResidencyResult.getStateResidencyData(j);
+                csvRow += stateResidency.getStateId() + ","
+                    + stateResidency.getTotalTimeInStateMs() + ","
+                    + stateResidency.getTotalStateEntryCount() + ","
+                    + stateResidency.getLastEntryTimestampMs() + ",";
+            }
+            System.out.println(csvRow);
+        }
+    }
+
     private static void generateCsvFile(String pathToIncidentReport) {
         try {
             // Print power meter data.
@@ -115,6 +147,21 @@
             } else {
                 System.out.println("Model incident report not found.  Exiting.");
             }
+
+            // Print state residency data.
+            IncidentReportResidencyProto irResidencyProto =
+                    IncidentReportResidencyProto.parseFrom(
+                        new FileInputStream(pathToIncidentReport));
+
+            if (irResidencyProto.hasIncidentReport()) {
+                PowerStatsServiceResidencyProto pssResidencyProto =
+                        irResidencyProto.getIncidentReport();
+                printPowerEntityInfo(pssResidencyProto);
+                printStateResidencyResult(pssResidencyProto);
+            } else {
+                System.out.println("Residency incident report not found.  Exiting.");
+            }
+
         } catch (IOException e) {
             System.out.println("Unable to open incident report file: " + pathToIncidentReport);
             System.out.println(e);