BatteryUsageStats atom - frameworks/base

Writes the BatteryUsageStats atoms.proto atoms
based on the current BatteryUsageStats data in BatteryStats.

Does NOT write the past pre-reset snapshot atoms; that is
an adventure for a future cl.

Bug: 184095105
Test: atest BatteryUsageStatsProtoTests
Test: statsd_testdrive <atomId>
Change-Id: I2fc5a983deb58d7d393c0696db2165b124c94dc2
diff --git a/core/java/android/os/BatteryConsumer.java b/core/java/android/os/BatteryConsumer.java
index edb30b0..da94d74 100644
--- a/core/java/android/os/BatteryConsumer.java
+++ b/core/java/android/os/BatteryConsumer.java
@@ -18,6 +18,8 @@
 
 import android.annotation.IntDef;
 import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.util.proto.ProtoOutputStream;
 
 import java.io.PrintWriter;
 import java.lang.annotation.Retention;
@@ -59,27 +61,32 @@
     public static @interface PowerComponent {
     }
 
-    public static final int POWER_COMPONENT_SCREEN = 0;
-    public static final int POWER_COMPONENT_CPU = 1;
-    public static final int POWER_COMPONENT_BLUETOOTH = 2;
-    public static final int POWER_COMPONENT_CAMERA = 3;
-    public static final int POWER_COMPONENT_AUDIO = 4;
-    public static final int POWER_COMPONENT_VIDEO = 5;
-    public static final int POWER_COMPONENT_FLASHLIGHT = 6;
-    public static final int POWER_COMPONENT_SYSTEM_SERVICES = 7;
-    public static final int POWER_COMPONENT_MOBILE_RADIO = 8;
-    public static final int POWER_COMPONENT_SENSORS = 9;
-    public static final int POWER_COMPONENT_GNSS = 10;
-    public static final int POWER_COMPONENT_WIFI = 11;
-    public static final int POWER_COMPONENT_WAKELOCK = 12;
-    public static final int POWER_COMPONENT_MEMORY = 13;
-    public static final int POWER_COMPONENT_PHONE = 14;
-    public static final int POWER_COMPONENT_AMBIENT_DISPLAY = 15;
-    public static final int POWER_COMPONENT_IDLE = 16;
+    public static final int POWER_COMPONENT_SCREEN = OsProtoEnums.POWER_COMPONENT_SCREEN; // 0
+    public static final int POWER_COMPONENT_CPU = OsProtoEnums.POWER_COMPONENT_CPU; // 1
+    public static final int POWER_COMPONENT_BLUETOOTH = OsProtoEnums.POWER_COMPONENT_BLUETOOTH; // 2
+    public static final int POWER_COMPONENT_CAMERA = OsProtoEnums.POWER_COMPONENT_CAMERA; // 3
+    public static final int POWER_COMPONENT_AUDIO = OsProtoEnums.POWER_COMPONENT_AUDIO; // 4
+    public static final int POWER_COMPONENT_VIDEO = OsProtoEnums.POWER_COMPONENT_VIDEO; // 5
+    public static final int POWER_COMPONENT_FLASHLIGHT =
+            OsProtoEnums.POWER_COMPONENT_FLASHLIGHT; // 6
+    public static final int POWER_COMPONENT_SYSTEM_SERVICES =
+            OsProtoEnums.POWER_COMPONENT_SYSTEM_SERVICES; // 7
+    public static final int POWER_COMPONENT_MOBILE_RADIO =
+            OsProtoEnums.POWER_COMPONENT_MOBILE_RADIO; // 8
+    public static final int POWER_COMPONENT_SENSORS = OsProtoEnums.POWER_COMPONENT_SENSORS; // 9
+    public static final int POWER_COMPONENT_GNSS = OsProtoEnums.POWER_COMPONENT_GNSS; // 10
+    public static final int POWER_COMPONENT_WIFI = OsProtoEnums.POWER_COMPONENT_WIFI; // 11
+    public static final int POWER_COMPONENT_WAKELOCK = OsProtoEnums.POWER_COMPONENT_WAKELOCK; // 12
+    public static final int POWER_COMPONENT_MEMORY = OsProtoEnums.POWER_COMPONENT_MEMORY; // 13
+    public static final int POWER_COMPONENT_PHONE = OsProtoEnums.POWER_COMPONENT_PHONE; // 14
+    public static final int POWER_COMPONENT_AMBIENT_DISPLAY =
+            OsProtoEnums.POWER_COMPONENT_AMBIENT_DISPLAY; // 15
+    public static final int POWER_COMPONENT_IDLE = OsProtoEnums.POWER_COMPONENT_IDLE; // 16
     // Power that is re-attributed to other battery consumers. For example, for System Server
     // this represents the power attributed to apps requesting system services.
     // The value should be negative or zero.
-    public static final int POWER_COMPONENT_REATTRIBUTED_TO_OTHER_CONSUMERS = 17;
+    public static final int POWER_COMPONENT_REATTRIBUTED_TO_OTHER_CONSUMERS =
+            OsProtoEnums.POWER_COMPONENT_REATTRIBUTED_TO_OTHER_CONSUMERS; // 17
 
     public static final int POWER_COMPONENT_COUNT = 18;
 
@@ -263,6 +270,54 @@
      */
     public abstract void dump(PrintWriter pw, boolean skipEmptyComponents);
 
+    /** Returns whether there are any atoms.proto BATTERY_CONSUMER_DATA data to write to a proto. */
+    boolean hasStatsProtoData() {
+        return writeStatsProtoImpl(null, /* Irrelevant fieldId: */ 0);
+    }
+
+    /** Writes the atoms.proto BATTERY_CONSUMER_DATA for this BatteryConsumer to the given proto. */
+    void writeStatsProto(@NonNull ProtoOutputStream proto, long fieldId) {
+        writeStatsProtoImpl(proto, fieldId);
+    }
+
+    /**
+     * Returns whether there are any atoms.proto BATTERY_CONSUMER_DATA data to write to a proto,
+     * and writes it to the given proto if it is non-null.
+     */
+    private boolean writeStatsProtoImpl(@Nullable ProtoOutputStream proto, long fieldId) {
+        final long totalConsumedPowerDeciCoulombs = convertMahToDeciCoulombs(getConsumedPower());
+
+        if (totalConsumedPowerDeciCoulombs == 0) {
+            // NOTE: Strictly speaking we should also check !mPowerComponents.hasStatsProtoData().
+            // However, that call is a bit expensive (a for loop). And the only way that
+            // totalConsumedPower can be 0 while mPowerComponents.hasStatsProtoData() is true is
+            // if POWER_COMPONENT_REATTRIBUTED_TO_OTHER_CONSUMERS (which is the only negative
+            // allowed) happens to exactly equal the sum of all other components, which
+            // can't really happen in practice.
+            // So we'll just adopt the rule "if total==0, don't write any details".
+            // If negative values are used for other things in the future, this can be revisited.
+            return false;
+        }
+        if (proto == null) {
+            // We're just asked whether there is data, not to actually write it. And there is.
+            return true;
+        }
+
+        final long token = proto.start(fieldId);
+        proto.write(
+                BatteryUsageStatsAtomsProto.BatteryConsumerData.TOTAL_CONSUMED_POWER_DECI_COULOMBS,
+                totalConsumedPowerDeciCoulombs);
+        mPowerComponents.writeStatsProto(proto);
+        proto.end(token);
+
+        return true;
+    }
+
+    /** Converts charge from milliamp hours (mAh) to decicoulombs (dC). */
+    static long convertMahToDeciCoulombs(double powerMah) {
+        return (long) (powerMah * (10 * 3600 / 1000) + 0.5);
+    }
+
     protected abstract static class BaseBuilder<T extends BaseBuilder<?>> {
         final PowerComponents.Builder mPowerComponentsBuilder;
 
diff --git a/core/java/android/os/BatteryUsageStats.java b/core/java/android/os/BatteryUsageStats.java
index 3225667..bd31992 100644
--- a/core/java/android/os/BatteryUsageStats.java
+++ b/core/java/android/os/BatteryUsageStats.java
@@ -20,6 +20,7 @@
 import android.annotation.NonNull;
 import android.util.Range;
 import android.util.SparseArray;
+import android.util.proto.ProtoOutputStream;
 
 import com.android.internal.os.BatteryStatsHistory;
 import com.android.internal.os.BatteryStatsHistoryIterator;
@@ -158,7 +159,8 @@
 
     /**
      * Portion of battery charge drained since BatteryStats reset (e.g. due to being fully
-     * charged), as percentage of the full charge in the range [0:100]
+     * charged), as percentage of the full charge in the range [0:100]. May exceed 100 if
+     * the device repeatedly charged and discharged prior to the reset.
      */
     public int getDischargePercentage() {
         return mDischargePercentage;
@@ -335,6 +337,70 @@
         }
     };
 
+    /** Returns a proto (as used for atoms.proto) corresponding to this BatteryUsageStats. */
+    public byte[] getStatsProto(long sessionEndTimestampMs) {
+
+        final long sessionStartMillis = getStatsStartTimestamp();
+        // TODO(b/187223764): Use the getStatsEndTimestamp() instead, once that is added.
+        final long sessionEndMillis = sessionEndTimestampMs;
+        final long sessionDurationMillis = sessionEndTimestampMs - getStatsStartTimestamp();
+
+        final BatteryConsumer deviceBatteryConsumer = getAggregateBatteryConsumer(
+                AGGREGATE_BATTERY_CONSUMER_SCOPE_DEVICE);
+
+        final int sessionDischargePercentage = getDischargePercentage();
+
+        final ProtoOutputStream proto = new ProtoOutputStream();
+        proto.write(BatteryUsageStatsAtomsProto.SESSION_START_MILLIS, sessionStartMillis);
+        proto.write(BatteryUsageStatsAtomsProto.SESSION_END_MILLIS, sessionEndMillis);
+        proto.write(BatteryUsageStatsAtomsProto.SESSION_DURATION_MILLIS, sessionDurationMillis);
+        deviceBatteryConsumer.writeStatsProto(proto,
+                BatteryUsageStatsAtomsProto.DEVICE_BATTERY_CONSUMER);
+        writeUidBatteryConsumersProto(proto);
+        proto.write(BatteryUsageStatsAtomsProto.SESSION_DISCHARGE_PERCENTAGE,
+                sessionDischargePercentage);
+        return proto.getBytes();
+    }
+
+    /**
+     * Writes the UidBatteryConsumers data, held by this BatteryUsageStats, to the proto (as used
+     * for atoms.proto).
+     */
+    private void writeUidBatteryConsumersProto(ProtoOutputStream proto) {
+        final List<UidBatteryConsumer> consumers = getUidBatteryConsumers();
+
+        // TODO: Sort the list by power consumption. If during the for, proto.getRawSize() > 45kb,
+        //       truncate the remainder of the list.
+        final int size = consumers.size();
+        for (int i = 0; i < size; i++) {
+            final UidBatteryConsumer consumer = consumers.get(i);
+
+            final long fgMs = consumer.getTimeInStateMs(UidBatteryConsumer.STATE_FOREGROUND);
+            final long bgMs = consumer.getTimeInStateMs(UidBatteryConsumer.STATE_BACKGROUND);
+            final boolean hasBaseData = consumer.hasStatsProtoData();
+
+            if (fgMs == 0 && bgMs == 0 && !hasBaseData) {
+                continue;
+            }
+
+            final long token = proto.start(BatteryUsageStatsAtomsProto.UID_BATTERY_CONSUMERS);
+            proto.write(
+                    BatteryUsageStatsAtomsProto.UidBatteryConsumer.UID,
+                    consumer.getUid());
+            if (hasBaseData) {
+                consumer.writeStatsProto(proto,
+                        BatteryUsageStatsAtomsProto.UidBatteryConsumer.BATTERY_CONSUMER_DATA);
+            }
+            proto.write(
+                    BatteryUsageStatsAtomsProto.UidBatteryConsumer.TIME_IN_FOREGROUND_MILLIS,
+                    fgMs);
+            proto.write(
+                    BatteryUsageStatsAtomsProto.UidBatteryConsumer.TIME_IN_BACKGROUND_MILLIS,
+                    bgMs);
+            proto.end(token);
+        }
+    }
+
     /**
      * Prints the stats in a human-readable format.
      */
diff --git a/core/java/android/os/PowerComponents.java b/core/java/android/os/PowerComponents.java
index 964f1b6..a90ed20 100644
--- a/core/java/android/os/PowerComponents.java
+++ b/core/java/android/os/PowerComponents.java
@@ -15,7 +15,11 @@
  */
 package android.os;
 
+import static android.os.BatteryConsumer.convertMahToDeciCoulombs;
+
 import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.util.proto.ProtoOutputStream;
 
 import com.android.internal.os.PowerCalculator;
 
@@ -237,6 +241,59 @@
         }
     }
 
+    /** Returns whether there are any atoms.proto POWER_COMPONENTS data to write to a proto. */
+    boolean hasStatsProtoData() {
+        return writeStatsProtoImpl(null);
+    }
+
+    /** Writes all atoms.proto POWER_COMPONENTS for this PowerComponents to the given proto. */
+    void writeStatsProto(@NonNull ProtoOutputStream proto) {
+        writeStatsProtoImpl(proto);
+    }
+
+    /**
+     * Returns whether there are any atoms.proto POWER_COMPONENTS data to write to a proto,
+     * and writes it to the given proto if it is non-null.
+     */
+    private boolean writeStatsProtoImpl(@Nullable ProtoOutputStream proto) {
+        boolean interestingData = false;
+
+        for (int idx = 0; idx < mPowerComponentsMah.length; idx++) {
+            final int componentId = idx < BatteryConsumer.POWER_COMPONENT_COUNT ?
+                    idx : idx - CUSTOM_POWER_COMPONENT_OFFSET;
+            final long powerDeciCoulombs = convertMahToDeciCoulombs(mPowerComponentsMah[idx]);
+            final long durationMs = mUsageDurationsMs[idx];
+
+            if (powerDeciCoulombs == 0 && durationMs == 0) {
+                // No interesting data. Make sure not to even write the COMPONENT int.
+                continue;
+            }
+
+            interestingData = true;
+            if (proto == null) {
+                // We're just asked whether there is data, not to actually write it. And there is.
+                return true;
+            }
+
+            final long token =
+                    proto.start(BatteryUsageStatsAtomsProto.BatteryConsumerData.POWER_COMPONENTS);
+            proto.write(
+                    BatteryUsageStatsAtomsProto.BatteryConsumerData.PowerComponentUsage
+                            .COMPONENT,
+                    componentId);
+            proto.write(
+                    BatteryUsageStatsAtomsProto.BatteryConsumerData.PowerComponentUsage
+                            .POWER_DECI_COULOMBS,
+                    powerDeciCoulombs);
+            proto.write(
+                    BatteryUsageStatsAtomsProto.BatteryConsumerData.PowerComponentUsage
+                            .DURATION_MILLIS,
+                    durationMs);
+            proto.end(token);
+        }
+        return interestingData;
+    }
+
     /**
      * Builder for PowerComponents.
      */
diff --git a/core/proto/android/os/batteryusagestats.proto b/core/proto/android/os/batteryusagestats.proto
new file mode 100644
index 0000000..bcce784
--- /dev/null
+++ b/core/proto/android/os/batteryusagestats.proto
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2021 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 android.os;
+
+option java_multiple_files = true;
+
+import "frameworks/proto_logging/stats/enums/os/enums.proto";
+
+// This message is used for statsd logging and should be kept in sync with
+// frameworks/proto_logging/stats/atoms.proto
+/**
+ * Represents a device's BatteryUsageStats, with power usage information about the device
+ * and each app.
+ */
+message BatteryUsageStatsAtomsProto {
+
+    // The session start timestamp in UTC milliseconds since January 1, 1970, per Date#getTime().
+    // All data is no older than this time.
+    optional int64 session_start_millis = 1;
+
+    // The session end timestamp in UTC milliseconds since January 1, 1970, per Date#getTime().
+    // All data is no more recent than this time.
+    optional int64 session_end_millis = 2;
+
+    // Length that the reported data covered. This usually will be equal to the entire session,
+    // session_end_millis - session_start_millis, but may not be if some data during this time frame
+    // is missing.
+    optional int64 session_duration_millis = 3;
+
+    // Represents usage of a consumer, storing all of its power component usage.
+    message BatteryConsumerData {
+        // Total power consumed by this BatteryConsumer (including all of its PowerComponents).
+        // May not equal the sum of the PowerComponentUsage due to under- or over-estimations.
+        // Multiply by 1/36 to obtain mAh.
+        optional int64 total_consumed_power_deci_coulombs = 1;
+
+        // Represents power and time usage of a particular power component.
+        message PowerComponentUsage {
+            // Holds android.os.PowerComponentEnum, or custom component value between 1000 and 9999.
+            // Evidently, if one attempts to write an int to an enum field that is out of range, it
+            // is treated as 0, so we must make this an int32.
+            optional int32 component = 1;
+
+            // Power consumed by this component. Multiply by 1/36 to obtain mAh.
+            optional int64 power_deci_coulombs = 2;
+
+            optional int64 duration_millis = 3;
+        }
+        repeated PowerComponentUsage power_components = 2;
+    }
+
+    // Total power usage for the device during this session.
+    optional BatteryConsumerData device_battery_consumer = 4;
+
+    // Power usage by a uid during this session.
+    message UidBatteryConsumer {
+        optional int32 uid = 1;
+        optional BatteryConsumerData battery_consumer_data = 2;
+        optional int64 time_in_foreground_millis = 3;
+        optional int64 time_in_background_millis = 4;
+    }
+    repeated UidBatteryConsumer uid_battery_consumers = 5;
+
+    // Sum of all discharge percentage point drops during the reported session.
+    optional int32 session_discharge_percentage = 6;
+}
\ No newline at end of file
diff --git a/core/tests/batterystatstests/BatteryUsageStatsProtoTests/Android.bp b/core/tests/batterystatstests/BatteryUsageStatsProtoTests/Android.bp
new file mode 100644
index 0000000..36cb554
--- /dev/null
+++ b/core/tests/batterystatstests/BatteryUsageStatsProtoTests/Android.bp
@@ -0,0 +1,28 @@
+package {
+    // See: http://go/android-license-faq
+    // A large-scale-change added 'default_applicable_licenses' to import
+    // all of the 'license_kinds' from "frameworks_base_license"
+    // to get the below license kinds:
+    //   SPDX-license-identifier-Apache-2.0
+    default_applicable_licenses: ["frameworks_base_license"],
+}
+
+android_test {
+    name: "BatteryUsageStatsProtoTests",
+    srcs: ["src/**/*.java"],
+
+    static_libs: [
+        "androidx.test.rules",
+        "junit",
+        "platform-test-annotations",
+        "platformprotosnano",
+        "statsdprotolite",
+    ],
+
+    libs: ["android.test.runner"],
+
+    platform_apis: true,
+    certificate: "platform",
+
+    test_suites: ["device-tests"],
+}
diff --git a/core/tests/batterystatstests/BatteryUsageStatsProtoTests/AndroidManifest.xml b/core/tests/batterystatstests/BatteryUsageStatsProtoTests/AndroidManifest.xml
new file mode 100644
index 0000000..9128dca
--- /dev/null
+++ b/core/tests/batterystatstests/BatteryUsageStatsProtoTests/AndroidManifest.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2021 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.
+  -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+          package="com.android.frameworks.core.batteryusagestatsprototests">
+
+    <uses-permission android:name="android.permission.BATTERY_STATS"/>
+
+    <instrumentation
+        android:name="androidx.test.runner.AndroidJUnitRunner"
+        android:targetPackage="com.android.frameworks.core.batteryusagestatsprototests"
+        android:label="BatteryUsageStats Proto Tests" />
+
+</manifest>
diff --git a/core/tests/batterystatstests/BatteryUsageStatsProtoTests/src/com/android/internal/os/BatteryUsageStatsPulledTest.java b/core/tests/batterystatstests/BatteryUsageStatsProtoTests/src/com/android/internal/os/BatteryUsageStatsPulledTest.java
new file mode 100644
index 0000000..bee0a0b
--- /dev/null
+++ b/core/tests/batterystatstests/BatteryUsageStatsProtoTests/src/com/android/internal/os/BatteryUsageStatsPulledTest.java
@@ -0,0 +1,229 @@
+/*
+ * Copyright (C) 2021 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.internal.os;
+
+import static android.os.BatteryUsageStats.AGGREGATE_BATTERY_CONSUMER_SCOPE_DEVICE;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.fail;
+
+import android.os.BatteryConsumer;
+import android.os.BatteryUsageStats;
+import android.os.nano.BatteryUsageStatsAtomsProto;
+import android.os.nano.BatteryUsageStatsAtomsProto.BatteryConsumerData.PowerComponentUsage;
+
+import androidx.test.filters.SmallTest;
+
+import com.google.protobuf.nano.InvalidProtocolBufferNanoException;
+
+import org.junit.Test;
+
+import java.util.Arrays;
+import java.util.List;
+
+
+
+@SmallTest
+public class BatteryUsageStatsPulledTest {
+
+    private static final int UID_0 = 1000;
+    private static final int UID_1 = 2000;
+    private static final int UID_2 = 3000;
+    private static final int UID_3 = 4000;
+
+    @Test
+    public void testGetStatsProto() {
+        final long sessionEndTimestampMs = 1050;
+        final BatteryUsageStats bus = buildBatteryUsageStats();
+        final byte[] bytes = bus.getStatsProto(sessionEndTimestampMs);
+        BatteryUsageStatsAtomsProto proto;
+        try {
+            proto = BatteryUsageStatsAtomsProto.parseFrom(bytes);
+        } catch (InvalidProtocolBufferNanoException e) {
+            fail("Invalid proto: " + e);
+            return;
+        }
+
+        assertEquals(bus.getStatsStartTimestamp(), proto.sessionStartMillis);
+        assertEquals(sessionEndTimestampMs, proto.sessionEndMillis);
+        assertEquals(
+                sessionEndTimestampMs - bus.getStatsStartTimestamp(),
+                proto.sessionDurationMillis);
+        assertEquals(bus.getDischargePercentage(), proto.sessionDischargePercentage);
+
+        assertEquals(3, proto.deviceBatteryConsumer.powerComponents.length); // Only 3 are non-empty
+        assertSameBatteryConsumer("For deviceBatteryConsumer",
+                bus.getAggregateBatteryConsumer(AGGREGATE_BATTERY_CONSUMER_SCOPE_DEVICE),
+                proto.deviceBatteryConsumer);
+
+        // Now for the UidBatteryConsumers.
+        final List<android.os.UidBatteryConsumer> uidConsumers = bus.getUidBatteryConsumers();
+        uidConsumers.sort((a, b) -> a.getUid() - b.getUid());
+
+        final BatteryUsageStatsAtomsProto.UidBatteryConsumer[] uidConsumersProto
+                = proto.uidBatteryConsumers;
+        Arrays.sort(uidConsumersProto, (a, b) -> a.uid - b.uid);
+
+        // UID_0 - After sorting, UID_0 should be in position 0 for both data structures
+        assertEquals(UID_0, bus.getUidBatteryConsumers().get(0).getUid());
+        assertEquals(UID_0, proto.uidBatteryConsumers[0].uid);
+        assertSameUidBatteryConsumer(
+                bus.getUidBatteryConsumers().get(0),
+                proto.uidBatteryConsumers[0],
+                false);
+
+        // UID_1 - After sorting, UID_1 should be in position 1 for both data structures
+        assertEquals(UID_1, bus.getUidBatteryConsumers().get(1).getUid());
+        assertEquals(UID_1, proto.uidBatteryConsumers[1].uid);
+        assertSameUidBatteryConsumer(
+                bus.getUidBatteryConsumers().get(1),
+                proto.uidBatteryConsumers[1],
+                true);
+
+        // UID_2 - After sorting, UID_2 should be in position 2 for both data structures
+        assertEquals(UID_2, bus.getUidBatteryConsumers().get(2).getUid());
+        assertEquals(UID_2, proto.uidBatteryConsumers[2].uid);
+        assertSameUidBatteryConsumer(
+                bus.getUidBatteryConsumers().get(2),
+                proto.uidBatteryConsumers[2],
+                false);
+
+        // UID_3 - Should be none, since no interesting data (done last for debugging convenience).
+        assertEquals(3, proto.uidBatteryConsumers.length);
+    }
+
+    private void assertSameBatteryConsumer(String message, BatteryConsumer consumer,
+            android.os.nano.BatteryUsageStatsAtomsProto.BatteryConsumerData consumerProto) {
+        assertNotNull(message, consumerProto);
+        assertEquals(
+                convertMahToDc(consumer.getConsumedPower()),
+                consumerProto.totalConsumedPowerDeciCoulombs);
+
+        for (PowerComponentUsage componentProto : consumerProto.powerComponents) {
+            final int componentId = componentProto.component;
+            if (componentId < BatteryConsumer.POWER_COMPONENT_COUNT) {
+                assertEquals(message + " for component " + componentId,
+                        convertMahToDc(consumer.getConsumedPower(componentId)),
+                        componentProto.powerDeciCoulombs);
+                assertEquals(message + " for component " + componentId,
+                        consumer.getUsageDurationMillis(componentId),
+                        componentProto.durationMillis);
+            } else {
+                assertEquals(message + " for custom component " + componentId,
+                        convertMahToDc(consumer.getConsumedPowerForCustomComponent(componentId)),
+                        componentProto.powerDeciCoulombs);
+                assertEquals(message + " for custom component " + componentId,
+                        consumer.getUsageDurationForCustomComponentMillis(componentId),
+                        componentProto.durationMillis);
+            }
+        }
+    }
+
+    private void assertSameUidBatteryConsumer(
+            android.os.UidBatteryConsumer uidConsumer,
+            BatteryUsageStatsAtomsProto.UidBatteryConsumer uidConsumerProto,
+            boolean expectNullBatteryConsumerData) {
+
+        final int uid = uidConsumerProto.uid;
+        assertEquals("Uid consumers had mismatched uids", uid, uidConsumer.getUid());
+
+        assertEquals("For uid " + uid,
+                uidConsumer.getTimeInStateMs(android.os.UidBatteryConsumer.STATE_FOREGROUND),
+                uidConsumerProto.timeInForegroundMillis);
+        assertEquals("For uid " + uid,
+                uidConsumer.getTimeInStateMs(android.os.UidBatteryConsumer.STATE_BACKGROUND),
+                uidConsumerProto.timeInBackgroundMillis);
+        if (expectNullBatteryConsumerData) {
+            assertNull("For uid " + uid, uidConsumerProto.batteryConsumerData);
+        } else {
+            assertSameBatteryConsumer("For uid " + uid,
+                    uidConsumer,
+                    uidConsumerProto.batteryConsumerData);
+        }
+    }
+
+    /** Converts charge from milliamp hours (mAh) to decicoulombs (dC). */
+    private long convertMahToDc(double powerMah) {
+        return (long) (powerMah * 36 + 0.5);
+    }
+
+    private BatteryUsageStats buildBatteryUsageStats() {
+        final BatteryStatsImpl batteryStats = new BatteryStatsImpl();
+        final BatteryStatsImpl.Uid batteryStatsUid0 = batteryStats.getUidStatsLocked(UID_0);
+        final BatteryStatsImpl.Uid batteryStatsUid1 = batteryStats.getUidStatsLocked(UID_1);
+        final BatteryStatsImpl.Uid batteryStatsUid2 = batteryStats.getUidStatsLocked(UID_2);
+        final BatteryStatsImpl.Uid batteryStatsUid3 = batteryStats.getUidStatsLocked(UID_3);
+
+        final BatteryUsageStats.Builder builder =
+                new BatteryUsageStats.Builder(new String[]{"CustomConsumer1", "CustomConsumer2"})
+                        .setDischargePercentage(20)
+                        .setDischargedPowerRange(1000, 2000)
+                        .setStatsStartTimestamp(1000);
+        builder.getOrCreateUidBatteryConsumerBuilder(batteryStatsUid0)
+                .setPackageWithHighestDrain("myPackage0")
+                .setTimeInStateMs(android.os.UidBatteryConsumer.STATE_FOREGROUND, 1000)
+                .setTimeInStateMs(android.os.UidBatteryConsumer.STATE_BACKGROUND, 2000)
+                .setConsumedPower(
+                        BatteryConsumer.POWER_COMPONENT_SCREEN, 300)
+                .setConsumedPower(
+                        BatteryConsumer.POWER_COMPONENT_CPU, 400)
+                .setConsumedPowerForCustomComponent(
+                        BatteryConsumer.FIRST_CUSTOM_POWER_COMPONENT_ID, 450)
+                .setConsumedPowerForCustomComponent(
+                        BatteryConsumer.FIRST_CUSTOM_POWER_COMPONENT_ID + 1, 500)
+                .setUsageDurationMillis(
+                        BatteryConsumer.POWER_COMPONENT_CPU, 600)
+                .setUsageDurationForCustomComponentMillis(
+                        BatteryConsumer.FIRST_CUSTOM_POWER_COMPONENT_ID + 1, 800);
+
+        builder.getOrCreateUidBatteryConsumerBuilder(batteryStatsUid1)
+                .setPackageWithHighestDrain("myPackage1")
+                .setTimeInStateMs(android.os.UidBatteryConsumer.STATE_FOREGROUND, 1234);
+
+        builder.getOrCreateUidBatteryConsumerBuilder(batteryStatsUid2)
+                .setConsumedPower(BatteryConsumer.POWER_COMPONENT_SCREEN,
+                        766);
+
+        builder.getOrCreateUidBatteryConsumerBuilder(batteryStatsUid3);
+
+        builder.getAggregateBatteryConsumerBuilder(AGGREGATE_BATTERY_CONSUMER_SCOPE_DEVICE)
+                .setConsumedPower(30000)
+                .setConsumedPower(
+                        BatteryConsumer.POWER_COMPONENT_CPU, 20100)
+                .setConsumedPower(
+                        BatteryConsumer.POWER_COMPONENT_AUDIO, 0) // Empty
+                .setConsumedPower(
+                        BatteryConsumer.POWER_COMPONENT_CAMERA, 20150)
+                .setConsumedPowerForCustomComponent(
+                        BatteryConsumer.FIRST_CUSTOM_POWER_COMPONENT_ID, 20200)
+                .setUsageDurationMillis(
+                        BatteryConsumer.POWER_COMPONENT_CPU, 20300)
+                .setUsageDurationForCustomComponentMillis(
+                        BatteryConsumer.FIRST_CUSTOM_POWER_COMPONENT_ID, 20400);
+
+        // Not used; just to make sure extraneous data doesn't mess things up.
+        builder.getAggregateBatteryConsumerBuilder(
+                BatteryUsageStats.AGGREGATE_BATTERY_CONSUMER_SCOPE_ALL_APPS)
+                .setConsumedPower(
+                        BatteryConsumer.POWER_COMPONENT_CPU, 10100)
+                .setConsumedPowerForCustomComponent(
+                        BatteryConsumer.FIRST_CUSTOM_POWER_COMPONENT_ID, 10200);
+
+        return builder.build();
+    }
+}
diff --git a/services/core/java/com/android/server/am/BatteryStatsService.java b/services/core/java/com/android/server/am/BatteryStatsService.java
index 5937a18..5da8f30 100644
--- a/services/core/java/com/android/server/am/BatteryStatsService.java
+++ b/services/core/java/com/android/server/am/BatteryStatsService.java
@@ -20,6 +20,7 @@
 import static android.os.BatteryStats.POWER_DATA_UNAVAILABLE;
 
 import android.annotation.NonNull;
+import android.app.StatsManager;
 import android.bluetooth.BluetoothActivityEnergyInfo;
 import android.content.ContentResolver;
 import android.content.Context;
@@ -69,9 +70,11 @@
 import android.telephony.SignalStrength;
 import android.telephony.TelephonyManager;
 import android.util.Slog;
+import android.util.StatsEvent;
 
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.app.IBatteryStats;
+import com.android.internal.os.BackgroundThread;
 import com.android.internal.os.BatteryStatsHelper;
 import com.android.internal.os.BatteryStatsImpl;
 import com.android.internal.os.BatteryUsageStatsProvider;
@@ -380,6 +383,8 @@
 
         final DataConnectionStats dataConnectionStats = new DataConnectionStats(mContext, mHandler);
         dataConnectionStats.startMonitoring();
+
+        registerStatsCallbacks();
     }
 
     private final class LocalService extends BatteryStatsInternal {
@@ -672,7 +677,7 @@
      * and per-UID basis.
      */
     public List<BatteryUsageStats> getBatteryUsageStats(List<BatteryUsageStatsQuery> queries) {
-        mContext.enforceCallingPermission(
+        mContext.enforceCallingOrSelfPermission(
                 android.Manifest.permission.BATTERY_STATS, null);
         awaitCompletion();
 
@@ -734,6 +739,48 @@
         }
     }
 
+    /** Register callbacks for statsd pulled atoms. */
+    private void registerStatsCallbacks() {
+        final StatsManager statsManager = mContext.getSystemService(StatsManager.class);
+        final StatsPullAtomCallbackImpl pullAtomCallback = new StatsPullAtomCallbackImpl();
+
+        statsManager.setPullAtomCallback(
+                FrameworkStatsLog.BATTERY_USAGE_STATS_SINCE_RESET,
+                null, // use default PullAtomMetadata values
+                BackgroundThread.getExecutor(), pullAtomCallback);
+        statsManager.setPullAtomCallback(
+                FrameworkStatsLog.BATTERY_USAGE_STATS_SINCE_RESET_USING_POWER_PROFILE_MODEL,
+                null, // use default PullAtomMetadata values
+                BackgroundThread.getExecutor(), pullAtomCallback);
+    }
+
+    /** StatsPullAtomCallback for pulling BatteryUsageStats data. */
+    private class StatsPullAtomCallbackImpl implements StatsManager.StatsPullAtomCallback {
+        @Override
+        public int onPullAtom(int atomTag, List<StatsEvent> data) {
+            final BatteryUsageStats bus;
+            switch (atomTag) {
+                case FrameworkStatsLog.BATTERY_USAGE_STATS_SINCE_RESET:
+                    bus = getBatteryUsageStats(List.of(BatteryUsageStatsQuery.DEFAULT)).get(0);
+                    break;
+                case FrameworkStatsLog.BATTERY_USAGE_STATS_SINCE_RESET_USING_POWER_PROFILE_MODEL:
+                    final BatteryUsageStatsQuery powerProfileQuery =
+                            new BatteryUsageStatsQuery.Builder().powerProfileModeledOnly().build();
+                    bus = getBatteryUsageStats(List.of(powerProfileQuery)).get(0);
+                    break;
+                default:
+                    throw new UnsupportedOperationException("Unknown tagId=" + atomTag);
+            }
+            // TODO(b/187223764): busTime won't be needed once end_session is a field in BUS.
+            final long busTime = System.currentTimeMillis();
+            final byte[] statsProto = bus.getStatsProto(busTime);
+
+            data.add(FrameworkStatsLog.buildStatsEvent(atomTag, statsProto));
+
+            return StatsManager.PULL_SUCCESS;
+        }
+    }
+
     public boolean isCharging() {
         synchronized (mStats) {
             return mStats.isCharging();