Add EnergyConsumer data to battery history

Bug: 243199649
Test: atest FrameworksServicesTests:BatteryStatsTests

Change-Id: I9ed353dc313a01995d0de96415336dd5298e4aa0
diff --git a/core/java/android/os/BatteryStats.java b/core/java/android/os/BatteryStats.java
index da20626..0ad596b 100644
--- a/core/java/android/os/BatteryStats.java
+++ b/core/java/android/os/BatteryStats.java
@@ -1767,6 +1767,49 @@
     }
 
     /**
+     * Measured energy delta from the previous reading.
+     */
+    public static final class MeasuredEnergyDetails {
+        /**
+         * Description of the energy consumer, such as CPU, DISPLAY etc
+         */
+        public static final class EnergyConsumer {
+            /**
+             * See android.hardware.power.stats.EnergyConsumerType
+             */
+            public int type;
+            /**
+             * Used when there are multipe energy consumers of the same type, such
+             * as CPU clusters, multiple displays on foldable devices etc.
+             */
+            public int ordinal;
+            /**
+             * Human-readable name of the energy consumer, e.g. "CPU"
+             */
+            public String name;
+        }
+        public EnergyConsumer[] consumers;
+        public long[] chargeUC;
+
+        @Override
+        public String toString() {
+            final StringBuilder sb = new StringBuilder();
+            for (int i = 0; i < consumers.length; i++) {
+                if (chargeUC[i] == POWER_DATA_UNAVAILABLE) {
+                    continue;
+                }
+                if (sb.length() != 0) {
+                    sb.append(' ');
+                }
+                sb.append(consumers[i].name);
+                sb.append('=');
+                sb.append(chargeUC[i]);
+            }
+            return sb.toString();
+        }
+    }
+
+    /**
      * Battery history record.
      */
     public static final class HistoryItem {
@@ -1886,6 +1929,7 @@
         public static final int STATE2_BLUETOOTH_SCAN_FLAG = 1 << 20;
         public static final int STATE2_CELLULAR_HIGH_TX_POWER_FLAG = 1 << 19;
         public static final int STATE2_USB_DATA_LINK_FLAG = 1 << 18;
+        public static final int STATE2_EXTENSIONS_FLAG = 1 << 17;
 
         public static final int MOST_INTERESTING_STATES2 =
                 STATE2_POWER_SAVE_FLAG | STATE2_WIFI_ON_FLAG | STATE2_DEVICE_IDLE_MASK
@@ -1905,6 +1949,9 @@
         // Non-null when there is more detailed information at this step.
         public HistoryStepDetails stepDetails;
 
+        // Non-null when there is measured energy information
+        public MeasuredEnergyDetails measuredEnergyDetails;
+
         public static final int EVENT_FLAG_START = 0x8000;
         public static final int EVENT_FLAG_FINISH = 0x4000;
 
@@ -2113,6 +2160,7 @@
             eventCode = EVENT_NONE;
             eventTag = null;
             tagsFirstOccurrence = false;
+            measuredEnergyDetails = null;
         }
 
         @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P)
@@ -2162,6 +2210,7 @@
             }
             tagsFirstOccurrence = o.tagsFirstOccurrence;
             currentTime = o.currentTime;
+            measuredEnergyDetails = o.measuredEnergyDetails;
         }
 
         public boolean sameNonEvent(HistoryItem o) {
@@ -6951,6 +7000,14 @@
                         item.append("\"");
                     }
                 }
+                if ((rec.states2 & HistoryItem.STATE2_EXTENSIONS_FLAG) != 0) {
+                    if (!checkin) {
+                        item.append(" ext=");
+                        if (rec.measuredEnergyDetails != null) {
+                            item.append("E");
+                        }
+                    }
+                }
                 if (rec.eventCode != HistoryItem.EVENT_NONE) {
                     item.append(checkin ? "," : " ");
                     if ((rec.eventCode&HistoryItem.EVENT_FLAG_START) != 0) {
@@ -7075,6 +7132,25 @@
                         item.append("\n");
                     }
                 }
+                if (rec.measuredEnergyDetails != null) {
+                    if (!checkin) {
+                        item.append("                 Energy: ");
+                        item.append(rec.measuredEnergyDetails);
+                        item.append("\n");
+                    } else {
+                        item.append(BATTERY_STATS_CHECKIN_VERSION); item.append(',');
+                        item.append(HISTORY_DATA); item.append(",0,XE");
+                        for (int i = 0; i < rec.measuredEnergyDetails.consumers.length; i++) {
+                            if (rec.measuredEnergyDetails.chargeUC[i] != POWER_DATA_UNAVAILABLE) {
+                                item.append(',');
+                                item.append(rec.measuredEnergyDetails.consumers[i].name);
+                                item.append('=');
+                                item.append(rec.measuredEnergyDetails.chargeUC[i]);
+                            }
+                        }
+                        item.append("\n");
+                    }
+                }
                 oldState = rec.states;
                 oldState2 = rec.states2;
                 // Clear High Tx Power Flag for volta positioning
diff --git a/core/java/com/android/internal/os/BatteryStatsHistory.java b/core/java/com/android/internal/os/BatteryStatsHistory.java
index 6909965..146af6a 100644
--- a/core/java/com/android/internal/os/BatteryStatsHistory.java
+++ b/core/java/com/android/internal/os/BatteryStatsHistory.java
@@ -21,6 +21,7 @@
 import android.os.BatteryStats.HistoryItem;
 import android.os.BatteryStats.HistoryStepDetails;
 import android.os.BatteryStats.HistoryTag;
+import android.os.BatteryStats.MeasuredEnergyDetails;
 import android.os.Parcel;
 import android.os.ParcelFormatException;
 import android.os.Process;
@@ -113,6 +114,9 @@
     // therefore the tag value is written in the parcel
     static final int TAG_FIRST_OCCURRENCE_FLAG = 0x8000;
 
+    static final int EXTENSION_MEASURED_ENERGY_HEADER_FLAG = 0x00000001;
+    static final int EXTENSION_MEASURED_ENERGY_FLAG = 0x00000002;
+
     private final Parcel mHistoryBuffer;
     private final File mSystemDir;
     private final HistoryStepDetailsCalculator mStepDetailsCalculator;
@@ -183,6 +187,7 @@
     private long mTrackRunningHistoryElapsedRealtimeMs = 0;
     private long mTrackRunningHistoryUptimeMs = 0;
     private long mHistoryBaseTimeMs;
+    private boolean mMeasuredEnergyHeaderWritten = false;
 
     private byte mLastHistoryStepLevel = 0;
 
@@ -293,6 +298,7 @@
         mLastHistoryElapsedRealtimeMs = 0;
         mTrackRunningHistoryElapsedRealtimeMs = 0;
         mTrackRunningHistoryUptimeMs = 0;
+        mMeasuredEnergyHeaderWritten = false;
 
         mHistoryBuffer.setDataSize(0);
         mHistoryBuffer.setDataPosition(0);
@@ -933,6 +939,16 @@
     }
 
     /**
+     * Records measured energy data.
+     */
+    public void recordMeasuredEnergyDetails(long elapsedRealtimeMs, long uptimeMs,
+            MeasuredEnergyDetails measuredEnergyDetails) {
+        mHistoryCur.measuredEnergyDetails = measuredEnergyDetails;
+        mHistoryCur.states2 |= HistoryItem.STATE2_EXTENSIONS_FLAG;
+        writeHistoryItem(elapsedRealtimeMs, uptimeMs);
+    }
+
+    /**
      * Records a history item with the amount of charge consumed by WiFi.  Used on certain devices
      * equipped with on-device power metering.
      */
@@ -1256,6 +1272,7 @@
         cur.eventCode = HistoryItem.EVENT_NONE;
         cur.eventTag = null;
         cur.tagsFirstOccurrence = false;
+        cur.measuredEnergyDetails = null;
         if (DEBUG) {
             Slog.i(TAG, "Writing history buffer: was " + mHistoryBufferLastPos
                     + " now " + mHistoryBuffer.dataPosition()
@@ -1348,6 +1365,7 @@
             return;
         }
 
+        int extensionFlags = 0;
         final long deltaTime = cur.time - last.time;
         final int lastBatteryLevelInt = buildBatteryLevelInt(last);
         final int lastStateInt = buildStateInt(last);
@@ -1374,6 +1392,15 @@
         if (stateIntChanged) {
             firstToken |= BatteryStatsHistory.DELTA_STATE_FLAG;
         }
+        if (cur.measuredEnergyDetails != null) {
+            extensionFlags |= BatteryStatsHistory.EXTENSION_MEASURED_ENERGY_FLAG;
+            if (!mMeasuredEnergyHeaderWritten) {
+                extensionFlags |= BatteryStatsHistory.EXTENSION_MEASURED_ENERGY_HEADER_FLAG;
+            }
+        }
+        if (extensionFlags != 0) {
+            cur.states2 |= HistoryItem.STATE2_EXTENSIONS_FLAG;
+        }
         final boolean state2IntChanged = cur.states2 != last.states2;
         if (state2IntChanged) {
             firstToken |= BatteryStatsHistory.DELTA_STATE2_FLAG;
@@ -1491,6 +1518,28 @@
         }
         dest.writeDouble(cur.modemRailChargeMah);
         dest.writeDouble(cur.wifiRailChargeMah);
+        if (extensionFlags != 0) {
+            dest.writeInt(extensionFlags);
+            if (cur.measuredEnergyDetails != null) {
+                if (DEBUG) {
+                    Slog.i(TAG, "WRITE DELTA: measuredEnergyDetails=" + cur.measuredEnergyDetails);
+                }
+                if (!mMeasuredEnergyHeaderWritten) {
+                    MeasuredEnergyDetails.EnergyConsumer[] consumers =
+                            cur.measuredEnergyDetails.consumers;
+                    dest.writeInt(consumers.length);
+                    for (MeasuredEnergyDetails.EnergyConsumer consumer : consumers) {
+                        dest.writeInt(consumer.type);
+                        dest.writeInt(consumer.ordinal);
+                        dest.writeString(consumer.name);
+                    }
+                    mMeasuredEnergyHeaderWritten = true;
+                }
+                for (long chargeUC : cur.measuredEnergyDetails.chargeUC) {
+                    dest.writeLong(chargeUC);
+                }
+            }
+        }
     }
 
     private int buildBatteryLevelInt(HistoryItem h) {
diff --git a/core/java/com/android/internal/os/BatteryStatsHistoryIterator.java b/core/java/com/android/internal/os/BatteryStatsHistoryIterator.java
index 1bf878cb..ee3d15b 100644
--- a/core/java/com/android/internal/os/BatteryStatsHistoryIterator.java
+++ b/core/java/com/android/internal/os/BatteryStatsHistoryIterator.java
@@ -33,6 +33,7 @@
     private final BatteryStats.HistoryStepDetails mReadHistoryStepDetails =
             new BatteryStats.HistoryStepDetails();
     private final SparseArray<BatteryStats.HistoryTag> mHistoryTags = new SparseArray<>();
+    private BatteryStats.MeasuredEnergyDetails mMeasuredEnergyDetails;
 
     public BatteryStatsHistoryIterator(@NonNull BatteryStatsHistory history) {
         mBatteryStatsHistory = history;
@@ -198,6 +199,40 @@
         }
         cur.modemRailChargeMah = src.readDouble();
         cur.wifiRailChargeMah = src.readDouble();
+        if ((cur.states2 & BatteryStats.HistoryItem.STATE2_EXTENSIONS_FLAG) != 0) {
+            final int extensionFlags = src.readInt();
+            if ((extensionFlags & BatteryStatsHistory.EXTENSION_MEASURED_ENERGY_HEADER_FLAG) != 0) {
+                if (mMeasuredEnergyDetails == null) {
+                    mMeasuredEnergyDetails = new BatteryStats.MeasuredEnergyDetails();
+                }
+
+                final int consumerCount = src.readInt();
+                mMeasuredEnergyDetails.consumers =
+                        new BatteryStats.MeasuredEnergyDetails.EnergyConsumer[consumerCount];
+                mMeasuredEnergyDetails.chargeUC = new long[consumerCount];
+                for (int i = 0; i < consumerCount; i++) {
+                    BatteryStats.MeasuredEnergyDetails.EnergyConsumer consumer =
+                            new BatteryStats.MeasuredEnergyDetails.EnergyConsumer();
+                    consumer.type = src.readInt();
+                    consumer.ordinal = src.readInt();
+                    consumer.name = src.readString();
+                    mMeasuredEnergyDetails.consumers[i] = consumer;
+                }
+            }
+
+            if ((extensionFlags & BatteryStatsHistory.EXTENSION_MEASURED_ENERGY_FLAG) != 0) {
+                if (mMeasuredEnergyDetails == null) {
+                    throw new IllegalStateException("MeasuredEnergyDetails without a header");
+                }
+
+                for (int i = 0; i < mMeasuredEnergyDetails.chargeUC.length; i++) {
+                    mMeasuredEnergyDetails.chargeUC[i] = src.readLong();
+                }
+                cur.measuredEnergyDetails = mMeasuredEnergyDetails;
+            }
+        } else {
+            cur.measuredEnergyDetails = null;
+        }
     }
 
     private boolean readHistoryTag(Parcel src, int index, BatteryStats.HistoryTag outTag) {
diff --git a/services/core/java/com/android/server/power/stats/BatteryExternalStatsWorker.java b/services/core/java/com/android/server/power/stats/BatteryExternalStatsWorker.java
index df902c2..49ac559 100644
--- a/services/core/java/com/android/server/power/stats/BatteryExternalStatsWorker.java
+++ b/services/core/java/com/android/server/power/stats/BatteryExternalStatsWorker.java
@@ -663,6 +663,11 @@
                     BatteryStats.HistoryItem.EVENT_COLLECT_EXTERNAL_STATS,
                     reason, 0);
 
+            if (measuredEnergyDeltas != null && !measuredEnergyDeltas.isEmpty()) {
+                mStats.recordMeasuredEnergyDetailsLocked(elapsedRealtime, uptime,
+                        mMeasuredEnergySnapshot.getMeasuredEnergyDetails(measuredEnergyDeltas));
+            }
+
             if ((updateFlags & UPDATE_CPU) != 0) {
                 if (useLatestStates) {
                     onBattery = mStats.isOnBatteryLocked();
diff --git a/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java b/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java
index 968f916..5fdd006 100644
--- a/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java
+++ b/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java
@@ -7404,6 +7404,16 @@
         return names;
     }
 
+    /**
+     * Adds measured energy delta to battery history.
+     */
+    @GuardedBy("this")
+    public void recordMeasuredEnergyDetailsLocked(long elapsedRealtimeMs,
+            long uptimeMs, MeasuredEnergyDetails measuredEnergyDetails) {
+        mHistory.recordMeasuredEnergyDetails(elapsedRealtimeMs, uptimeMs,
+                measuredEnergyDetails);
+    }
+
     @GuardedBy("this")
     @Override public long getStartClockTime() {
         final long currentTimeMs = mClock.currentTimeMillis();
diff --git a/services/core/java/com/android/server/power/stats/MeasuredEnergySnapshot.java b/services/core/java/com/android/server/power/stats/MeasuredEnergySnapshot.java
index b55c799..c8b4e36 100644
--- a/services/core/java/com/android/server/power/stats/MeasuredEnergySnapshot.java
+++ b/services/core/java/com/android/server/power/stats/MeasuredEnergySnapshot.java
@@ -23,19 +23,17 @@
 import android.hardware.power.stats.EnergyConsumerAttribution;
 import android.hardware.power.stats.EnergyConsumerResult;
 import android.hardware.power.stats.EnergyConsumerType;
+import android.os.BatteryStats.MeasuredEnergyDetails;
 import android.util.Slog;
 import android.util.SparseArray;
 import android.util.SparseIntArray;
 import android.util.SparseLongArray;
 
-import com.android.internal.annotations.VisibleForTesting;
-
 import java.io.PrintWriter;
 
 /**
  * Keeps snapshots of data from previously pulled EnergyConsumerResults.
  */
-@VisibleForTesting
 public class MeasuredEnergySnapshot {
     private static final String TAG = "MeasuredEnergySnapshot";
 
@@ -87,6 +85,8 @@
      */
     private final SparseArray<SparseLongArray> mAttributionSnapshots;
 
+    private MeasuredEnergyDetails mMeasuredEnergyDetails;
+
     /**
      * Constructor that initializes to the given id->EnergyConsumer map, indicating which consumers
      * exist and what their details are.
@@ -128,6 +128,28 @@
 
         /** Map of {@link EnergyConsumerType#OTHER} ordinals to their {uid->chargeUC} maps. */
         public @Nullable SparseLongArray[] otherUidChargesUC = null;
+
+        boolean isEmpty() {
+            return bluetoothChargeUC <= 0
+                    && isEmpty(cpuClusterChargeUC)
+                    && isEmpty(displayChargeUC)
+                    && gnssChargeUC <= 0
+                    && mobileRadioChargeUC <= 0
+                    && wifiChargeUC <= 0
+                    && isEmpty(otherTotalChargeUC);
+        }
+
+        private boolean isEmpty(long[] values) {
+            if (values == null) {
+                return true;
+            }
+            for (long value: values) {
+                if (value > 0) {
+                    return false;
+                }
+            }
+            return true;
+        }
     }
 
     /**
@@ -394,4 +416,119 @@
         // since the last snapshot. Round off to the nearest whole long.
         return (deltaEnergyUJ * MILLIVOLTS_PER_VOLT + (avgVoltageMV / 2)) / avgVoltageMV;
     }
+
+    /**
+     * Converts the MeasuredEnergyDeltaData object to MeasuredEnergyDetails, which can
+     * be saved in battery history.
+     */
+    MeasuredEnergyDetails getMeasuredEnergyDetails(
+            MeasuredEnergySnapshot.MeasuredEnergyDeltaData delta) {
+        if (mMeasuredEnergyDetails == null) {
+            mMeasuredEnergyDetails = createMeasuredEnergyDetails();
+        }
+
+        final long[] chargeUC = mMeasuredEnergyDetails.chargeUC;
+        for (int i = 0; i < mMeasuredEnergyDetails.consumers.length; i++) {
+            MeasuredEnergyDetails.EnergyConsumer energyConsumer =
+                    mMeasuredEnergyDetails.consumers[i];
+            switch (energyConsumer.type) {
+                case EnergyConsumerType.BLUETOOTH:
+                    chargeUC[i] = delta.bluetoothChargeUC;
+                    break;
+                case EnergyConsumerType.CPU_CLUSTER:
+                    if (delta.cpuClusterChargeUC != null) {
+                        chargeUC[i] = delta.cpuClusterChargeUC[energyConsumer.ordinal];
+                    } else {
+                        chargeUC[i] = UNAVAILABLE;
+                    }
+                    break;
+                case EnergyConsumerType.DISPLAY:
+                    if (delta.displayChargeUC != null) {
+                        chargeUC[i] = delta.displayChargeUC[energyConsumer.ordinal];
+                    } else {
+                        chargeUC[i] = UNAVAILABLE;
+                    }
+                    break;
+                case EnergyConsumerType.GNSS:
+                    chargeUC[i] = delta.gnssChargeUC;
+                    break;
+                case EnergyConsumerType.MOBILE_RADIO:
+                    chargeUC[i] = delta.mobileRadioChargeUC;
+                    break;
+                case EnergyConsumerType.WIFI:
+                    chargeUC[i] = delta.wifiChargeUC;
+                    break;
+                case EnergyConsumerType.OTHER:
+                    if (delta.otherTotalChargeUC != null) {
+                        chargeUC[i] = delta.otherTotalChargeUC[energyConsumer.ordinal];
+                    } else {
+                        chargeUC[i] = UNAVAILABLE;
+                    }
+                    break;
+                default:
+                    chargeUC[i] = UNAVAILABLE;
+                    break;
+            }
+        }
+        return mMeasuredEnergyDetails;
+    }
+
+    private MeasuredEnergyDetails createMeasuredEnergyDetails() {
+        MeasuredEnergyDetails details = new MeasuredEnergyDetails();
+        details.consumers =
+                new MeasuredEnergyDetails.EnergyConsumer[mEnergyConsumers.size()];
+        for (int i = 0; i < mEnergyConsumers.size(); i++) {
+            EnergyConsumer energyConsumer = mEnergyConsumers.valueAt(i);
+            MeasuredEnergyDetails.EnergyConsumer consumer =
+                    new MeasuredEnergyDetails.EnergyConsumer();
+            consumer.type = energyConsumer.type;
+            consumer.ordinal = energyConsumer.ordinal;
+            switch (consumer.type) {
+                case EnergyConsumerType.BLUETOOTH:
+                    consumer.name = "BLUETOOTH";
+                    break;
+                case EnergyConsumerType.CPU_CLUSTER:
+                    consumer.name = "CPU";
+                    break;
+                case EnergyConsumerType.DISPLAY:
+                    consumer.name = "DISPLAY";
+                    break;
+                case EnergyConsumerType.GNSS:
+                    consumer.name = "GNSS";
+                    break;
+                case EnergyConsumerType.MOBILE_RADIO:
+                    consumer.name = "MOBILE_RADIO";
+                    break;
+                case EnergyConsumerType.WIFI:
+                    consumer.name = "WIFI";
+                    break;
+                case EnergyConsumerType.OTHER:
+                    consumer.name = sanitizeCustomBucketName(energyConsumer.name);
+                    break;
+                default:
+                    consumer.name = "UNKNOWN";
+                    break;
+            }
+            if (consumer.type != EnergyConsumerType.OTHER) {
+                boolean hasOrdinal = consumer.ordinal != 0;
+                if (!hasOrdinal) {
+                    // See if any other EnergyConsumer of the same type has an ordinal
+                    for (int j = 0; j < mEnergyConsumers.size(); j++) {
+                        EnergyConsumer aConsumer = mEnergyConsumers.valueAt(j);
+                        if (aConsumer.type == consumer.type && aConsumer.ordinal != 0) {
+                            hasOrdinal = true;
+                            break;
+                        }
+                    }
+                }
+                if (hasOrdinal) {
+                    consumer.name = consumer.name + "/" + energyConsumer.ordinal;
+                }
+            }
+            details.consumers[i] = consumer;
+        }
+
+        details.chargeUC = new long[details.consumers.length];
+        return details;
+    }
 }
diff --git a/services/tests/servicestests/src/com/android/server/power/stats/BatteryStatsHistoryTest.java b/services/tests/servicestests/src/com/android/server/power/stats/BatteryStatsHistoryTest.java
index 5c934852..047fcd6 100644
--- a/services/tests/servicestests/src/com/android/server/power/stats/BatteryStatsHistoryTest.java
+++ b/services/tests/servicestests/src/com/android/server/power/stats/BatteryStatsHistoryTest.java
@@ -16,11 +16,17 @@
 
 package com.android.server.power.stats;
 
+import static com.google.common.truth.Truth.assertThat;
+
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.when;
 
 import android.content.Context;
+import android.os.BatteryManager;
+import android.os.BatteryStats;
+import android.os.BatteryStats.MeasuredEnergyDetails;
 import android.os.Parcel;
 import android.util.Log;
 
@@ -28,15 +34,19 @@
 import androidx.test.runner.AndroidJUnit4;
 
 import com.android.internal.os.BatteryStatsHistory;
+import com.android.internal.os.BatteryStatsHistoryIterator;
 import com.android.internal.os.Clock;
 
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
+import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 
 import java.io.File;
 import java.io.IOException;
+import java.io.PrintWriter;
+import java.io.StringWriter;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
@@ -51,6 +61,9 @@
     private File mSystemDir;
     private File mHistoryDir;
     private final Clock mClock = new MockClock();
+    private BatteryStatsHistory mHistory;
+    @Mock
+    private BatteryStatsHistory.HistoryStepDetailsCalculator mStepDetailsCalculator;
 
     @Before
     public void setUp() {
@@ -65,55 +78,56 @@
             }
         }
         mHistoryDir.delete();
+        mHistory = new BatteryStatsHistory(mHistoryBuffer, mSystemDir, 32, 1024,
+                mStepDetailsCalculator, mClock);
+
+        when(mStepDetailsCalculator.getHistoryStepDetails())
+                .thenReturn(new BatteryStats.HistoryStepDetails());
     }
 
     @Test
     public void testConstruct() {
-        BatteryStatsHistory history = new BatteryStatsHistory(mHistoryBuffer, mSystemDir, 32, 1024,
-                null, mClock);
-        createActiveFile(history);
-        verifyFileNumbers(history, Arrays.asList(0));
-        verifyActiveFile(history, "0.bin");
+        createActiveFile(mHistory);
+        verifyFileNumbers(mHistory, Arrays.asList(0));
+        verifyActiveFile(mHistory, "0.bin");
     }
 
     @Test
     public void testStartNextFile() {
-        BatteryStatsHistory history = new BatteryStatsHistory(mHistoryBuffer, mSystemDir, 32, 1024,
-                null, mClock);
         List<Integer> fileList = new ArrayList<>();
         fileList.add(0);
-        createActiveFile(history);
+        createActiveFile(mHistory);
 
         // create file 1 to 31.
         for (int i = 1; i < 32; i++) {
             fileList.add(i);
-            history.startNextFile();
-            createActiveFile(history);
-            verifyFileNumbers(history, fileList);
-            verifyActiveFile(history, i + ".bin");
+            mHistory.startNextFile();
+            createActiveFile(mHistory);
+            verifyFileNumbers(mHistory, fileList);
+            verifyActiveFile(mHistory, i + ".bin");
         }
 
         // create file 32
-        history.startNextFile();
-        createActiveFile(history);
+        mHistory.startNextFile();
+        createActiveFile(mHistory);
         fileList.add(32);
         fileList.remove(0);
         // verify file 0 is deleted.
         verifyFileDeleted("0.bin");
-        verifyFileNumbers(history, fileList);
-        verifyActiveFile(history, "32.bin");
+        verifyFileNumbers(mHistory, fileList);
+        verifyActiveFile(mHistory, "32.bin");
 
         // create file 33
-        history.startNextFile();
-        createActiveFile(history);
+        mHistory.startNextFile();
+        createActiveFile(mHistory);
         // verify file 1 is deleted
         fileList.add(33);
         fileList.remove(0);
         verifyFileDeleted("1.bin");
-        verifyFileNumbers(history, fileList);
-        verifyActiveFile(history, "33.bin");
+        verifyFileNumbers(mHistory, fileList);
+        verifyActiveFile(mHistory, "33.bin");
 
-        assertEquals(0, history.getHistoryUsedSize());
+        assertEquals(0, mHistory.getHistoryUsedSize());
 
         // create a new BatteryStatsHistory object, it will pick up existing history files.
         BatteryStatsHistory history2 = new BatteryStatsHistory(mHistoryBuffer, mSystemDir, 32, 1024,
@@ -168,4 +182,74 @@
             Log.e(TAG, "Error creating history file " + file.getPath(), e);
         }
     }
+
+    @Test
+    public void testRecordMeasuredEnergyDetails() {
+        mHistory.forceRecordAllHistory();
+        mHistory.startRecordingHistory(0, 0, /* reset */ true);
+        mHistory.setBatteryState(true /* charging */, BatteryManager.BATTERY_STATUS_CHARGING, 80,
+                1234);
+
+        MeasuredEnergyDetails details = new MeasuredEnergyDetails();
+        MeasuredEnergyDetails.EnergyConsumer consumer1 =
+                new MeasuredEnergyDetails.EnergyConsumer();
+        consumer1.type = 42;
+        consumer1.ordinal = 0;
+        consumer1.name = "A";
+
+        MeasuredEnergyDetails.EnergyConsumer consumer2 =
+                new MeasuredEnergyDetails.EnergyConsumer();
+        consumer2.type = 777;
+        consumer2.ordinal = 0;
+        consumer2.name = "B/0";
+
+        MeasuredEnergyDetails.EnergyConsumer consumer3 =
+                new MeasuredEnergyDetails.EnergyConsumer();
+        consumer3.type = 777;
+        consumer3.ordinal = 1;
+        consumer3.name = "B/1";
+
+        MeasuredEnergyDetails.EnergyConsumer consumer4 =
+                new MeasuredEnergyDetails.EnergyConsumer();
+        consumer4.type = 314;
+        consumer4.ordinal = 1;
+        consumer4.name = "C";
+
+        details.consumers =
+                new MeasuredEnergyDetails.EnergyConsumer[]{consumer1, consumer2, consumer3,
+                        consumer4};
+        details.chargeUC = new long[details.consumers.length];
+        for (int i = 0; i < details.chargeUC.length; i++) {
+            details.chargeUC[i] = 100L * i;
+        }
+        details.chargeUC[3] = BatteryStats.POWER_DATA_UNAVAILABLE;
+
+        mHistory.recordMeasuredEnergyDetails(200, 200, details);
+
+        BatteryStatsHistoryIterator iterator = mHistory.iterate();
+        BatteryStats.HistoryItem item = new BatteryStats.HistoryItem();
+        assertThat(iterator.next(item)).isTrue(); // First item contains current time only
+
+        assertThat(iterator.next(item)).isTrue();
+
+        String dump = toString(item, /* checkin */ false);
+        assertThat(dump).contains("+200ms");
+        assertThat(dump).contains("ext=E");
+        assertThat(dump).contains("Energy: A=0 B/0=100 B/1=200");
+        assertThat(dump).doesNotContain("C=");
+
+        String checkin = toString(item, /* checkin */ true);
+        assertThat(checkin).contains("XE");
+        assertThat(checkin).contains("A=0,B/0=100,B/1=200");
+        assertThat(checkin).doesNotContain("C=");
+    }
+
+    private String toString(BatteryStats.HistoryItem item, boolean checkin) {
+        BatteryStats.HistoryPrinter printer = new BatteryStats.HistoryPrinter();
+        StringWriter writer = new StringWriter();
+        PrintWriter pw = new PrintWriter(writer);
+        printer.printNextItem(pw, item, 0, checkin, /* verbose */ true);
+        pw.flush();
+        return writer.toString();
+    }
 }
diff --git a/services/tests/servicestests/src/com/android/server/power/stats/MeasuredEnergySnapshotTest.java b/services/tests/servicestests/src/com/android/server/power/stats/MeasuredEnergySnapshotTest.java
index 8a0f924..122f7eb 100644
--- a/services/tests/servicestests/src/com/android/server/power/stats/MeasuredEnergySnapshotTest.java
+++ b/services/tests/servicestests/src/com/android/server/power/stats/MeasuredEnergySnapshotTest.java
@@ -26,6 +26,7 @@
 import android.hardware.power.stats.EnergyConsumerAttribution;
 import android.hardware.power.stats.EnergyConsumerResult;
 import android.hardware.power.stats.EnergyConsumerType;
+import android.os.BatteryStats;
 import android.util.SparseArray;
 import android.util.SparseLongArray;
 
@@ -236,6 +237,17 @@
         assertEquals(0, snapshot.getOtherOrdinalNames().length);
     }
 
+    @Test
+    public void getMeasuredEnergyDetails() {
+        final MeasuredEnergySnapshot snapshot = new MeasuredEnergySnapshot(ALL_ID_CONSUMER_MAP);
+        snapshot.updateAndGetDelta(RESULTS_0, VOLTAGE_0);
+        MeasuredEnergyDeltaData delta = snapshot.updateAndGetDelta(RESULTS_1, VOLTAGE_1);
+        BatteryStats.MeasuredEnergyDetails details = snapshot.getMeasuredEnergyDetails(delta);
+        assertThat(details.consumers).hasLength(4);
+        assertThat(details.chargeUC).isEqualTo(new long[]{2667, 3200000, 0, 0});
+        assertThat(details.toString()).isEqualTo("DISPLAY=2667 HPU=3200000 GPU=0 IPU &_=0");
+    }
+
     private static EnergyConsumer createEnergyConsumer(int id, int ord, byte type, String name) {
         final EnergyConsumer ec = new EnergyConsumer();
         ec.id = id;