/*
 * Copyright (C) 2020 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package android.os;

import android.annotation.IntDef;
import android.annotation.NonNull;
import android.util.Range;
import android.util.SparseArray;
import android.util.TypedXmlPullParser;
import android.util.TypedXmlSerializer;
import android.util.proto.ProtoOutputStream;

import com.android.internal.os.BatteryStatsHistory;
import com.android.internal.os.BatteryStatsHistoryIterator;
import com.android.internal.os.PowerCalculator;

import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;

import java.io.IOException;
import java.io.PrintWriter;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;

/**
 * Contains a snapshot of battery attribution data, on a per-subsystem and per-UID basis.
 * <p>
 * The totals for the entire device are returned as AggregateBatteryConsumers, which can be
 * obtained by calling {@link #getAggregateBatteryConsumer(int)}.
 * <p>
 * Power attributed to individual apps is returned as UidBatteryConsumers, see
 * {@link #getUidBatteryConsumers()}.
 *
 * @hide
 */
public final class BatteryUsageStats implements Parcelable {

    /**
     * Scope of battery stats included in a BatteryConsumer: the entire device, just
     * the apps, etc.
     *
     * @hide
     */
    @IntDef(prefix = {"AGGREGATE_BATTERY_CONSUMER_SCOPE_"}, value = {
            AGGREGATE_BATTERY_CONSUMER_SCOPE_DEVICE,
            AGGREGATE_BATTERY_CONSUMER_SCOPE_ALL_APPS,
    })
    @Retention(RetentionPolicy.SOURCE)
    public static @interface AggregateBatteryConsumerScope {
    }

    /**
     * Power consumption by the entire device, since last charge.  The power usage in this
     * scope includes both the power attributed to apps and the power unattributed to any
     * apps.
     */
    public static final int AGGREGATE_BATTERY_CONSUMER_SCOPE_DEVICE = 0;

    /**
     * Aggregated power consumed by all applications, combined, since last charge. This is
     * the sum of power reported in UidBatteryConsumers.
     */
    public static final int AGGREGATE_BATTERY_CONSUMER_SCOPE_ALL_APPS = 1;

    public static final int AGGREGATE_BATTERY_CONSUMER_SCOPE_COUNT = 2;

    private static final int STATSD_PULL_ATOM_MAX_BYTES = 45000;

    // XML tags and attributes for BatteryUsageStats persistence
    static final String XML_TAG_BATTERY_USAGE_STATS = "battery_usage_stats";
    static final String XML_TAG_AGGREGATE = "aggregate";
    static final String XML_TAG_UID = "uid";
    static final String XML_TAG_USER = "user";
    static final String XML_TAG_POWER_COMPONENTS = "power_components";
    static final String XML_TAG_COMPONENT = "component";
    static final String XML_TAG_CUSTOM_COMPONENT = "custom_component";
    static final String XML_ATTR_ID = "id";
    static final String XML_ATTR_UID = "uid";
    static final String XML_ATTR_USER_ID = "user_id";
    static final String XML_ATTR_SCOPE = "scope";
    static final String XML_ATTR_PREFIX_CUSTOM_COMPONENT = "custom_component_";
    static final String XML_ATTR_START_TIMESTAMP = "start_timestamp";
    static final String XML_ATTR_END_TIMESTAMP = "end_timestamp";
    static final String XML_ATTR_POWER = "power";
    static final String XML_ATTR_DURATION = "duration";
    static final String XML_ATTR_MODEL = "model";
    static final String XML_ATTR_BATTERY_CAPACITY = "battery_capacity";
    static final String XML_ATTR_DISCHARGE_PERCENT = "discharge_pct";
    static final String XML_ATTR_DISCHARGE_LOWER = "discharge_lower";
    static final String XML_ATTR_DISCHARGE_UPPER = "discharge_upper";
    static final String XML_ATTR_BATTERY_REMAINING = "battery_remaining";
    static final String XML_ATTR_CHARGE_REMAINING = "charge_remaining";
    static final String XML_ATTR_HIGHEST_DRAIN_PACKAGE = "highest_drain_package";
    static final String XML_ATTR_TIME_IN_FOREGROUND = "time_in_foreground";
    static final String XML_ATTR_TIME_IN_BACKGROUND = "time_in_background";

    private final int mDischargePercentage;
    private final double mBatteryCapacityMah;
    private final long mStatsStartTimestampMs;
    private final long mStatsEndTimestampMs;
    private final long mStatsDurationMs;
    private final double mDischargedPowerLowerBound;
    private final double mDischargedPowerUpperBound;
    private final long mBatteryTimeRemainingMs;
    private final long mChargeTimeRemainingMs;
    private final String[] mCustomPowerComponentNames;
    private final List<UidBatteryConsumer> mUidBatteryConsumers;
    private final List<UserBatteryConsumer> mUserBatteryConsumers;
    private final AggregateBatteryConsumer[] mAggregateBatteryConsumers;
    private final Parcel mHistoryBuffer;
    private final List<BatteryStats.HistoryTag> mHistoryTagPool;

    private BatteryUsageStats(@NonNull Builder builder) {
        mStatsStartTimestampMs = builder.mStatsStartTimestampMs;
        mStatsEndTimestampMs = builder.mStatsEndTimestampMs;
        mStatsDurationMs = builder.getStatsDuration();
        mBatteryCapacityMah = builder.mBatteryCapacityMah;
        mDischargePercentage = builder.mDischargePercentage;
        mDischargedPowerLowerBound = builder.mDischargedPowerLowerBoundMah;
        mDischargedPowerUpperBound = builder.mDischargedPowerUpperBoundMah;
        mHistoryBuffer = builder.mHistoryBuffer;
        mHistoryTagPool = builder.mHistoryTagPool;
        mBatteryTimeRemainingMs = builder.mBatteryTimeRemainingMs;
        mChargeTimeRemainingMs = builder.mChargeTimeRemainingMs;
        mCustomPowerComponentNames = builder.mCustomPowerComponentNames;

        double totalPowerMah = 0;
        final int uidBatteryConsumerCount = builder.mUidBatteryConsumerBuilders.size();
        mUidBatteryConsumers = new ArrayList<>(uidBatteryConsumerCount);
        for (int i = 0; i < uidBatteryConsumerCount; i++) {
            final UidBatteryConsumer.Builder uidBatteryConsumerBuilder =
                    builder.mUidBatteryConsumerBuilders.valueAt(i);
            if (!uidBatteryConsumerBuilder.isExcludedFromBatteryUsageStats()) {
                final UidBatteryConsumer consumer = uidBatteryConsumerBuilder.build();
                totalPowerMah += consumer.getConsumedPower();
                mUidBatteryConsumers.add(consumer);
            }
        }

        final int userBatteryConsumerCount = builder.mUserBatteryConsumerBuilders.size();
        mUserBatteryConsumers = new ArrayList<>(userBatteryConsumerCount);
        for (int i = 0; i < userBatteryConsumerCount; i++) {
            final UserBatteryConsumer consumer =
                    builder.mUserBatteryConsumerBuilders.valueAt(i).build();
            totalPowerMah += consumer.getConsumedPower();
            mUserBatteryConsumers.add(consumer);
        }

        builder.getAggregateBatteryConsumerBuilder(AGGREGATE_BATTERY_CONSUMER_SCOPE_ALL_APPS)
                .setConsumedPower(totalPowerMah);

        mAggregateBatteryConsumers =
                new AggregateBatteryConsumer[AGGREGATE_BATTERY_CONSUMER_SCOPE_COUNT];
        for (int i = 0; i < AGGREGATE_BATTERY_CONSUMER_SCOPE_COUNT; i++) {
            mAggregateBatteryConsumers[i] = builder.mAggregateBatteryConsumersBuilders[i].build();
        }
    }

    /**
     * Timestamp (as returned by System.currentTimeMillis()) of the latest battery stats reset, in
     * milliseconds.
     */
    public long getStatsStartTimestamp() {
        return mStatsStartTimestampMs;
    }

    /**
     * Timestamp (as returned by System.currentTimeMillis()) of when the stats snapshot was taken,
     * in milliseconds.
     */
    public long getStatsEndTimestamp() {
        return mStatsEndTimestampMs;
    }

    /**
     * Returns the duration of the stats session captured by this BatteryUsageStats.
     * In rare cases, statsDuration != statsEndTimestamp - statsStartTimestamp.  This may
     * happen when BatteryUsageStats represents an accumulation of data across multiple
     * non-contiguous sessions.
     */
    public long getStatsDuration() {
        return mStatsDurationMs;
    }

    /**
     * Total amount of battery charge drained since BatteryStats reset (e.g. due to being fully
     * charged), in mAh
     */
    public double getConsumedPower() {
        return mAggregateBatteryConsumers[AGGREGATE_BATTERY_CONSUMER_SCOPE_DEVICE]
                .getConsumedPower();
    }

    /**
     * Returns battery capacity in milli-amp-hours.
     */
    public double getBatteryCapacity() {
        return mBatteryCapacityMah;
    }

    /**
     * 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]. May exceed 100 if
     * the device repeatedly charged and discharged prior to the reset.
     */
    public int getDischargePercentage() {
        return mDischargePercentage;
    }

    /**
     * Returns the discharged power since BatteryStats were last reset, in mAh as an estimated
     * range.
     */
    public Range<Double> getDischargedPowerRange() {
        return Range.create(mDischargedPowerLowerBound, mDischargedPowerUpperBound);
    }

    /**
     * Returns an approximation for how much run time (in milliseconds) is remaining on
     * the battery.  Returns -1 if no time can be computed: either there is not
     * enough current data to make a decision, or the battery is currently
     * charging.
     */
    public long getBatteryTimeRemainingMs() {
        return mBatteryTimeRemainingMs;
    }

    /**
     * Returns an approximation for how much time (in milliseconds) remains until the battery
     * is fully charged.  Returns -1 if no time can be computed: either there is not
     * enough current data to make a decision, or the battery is currently discharging.
     */
    public long getChargeTimeRemainingMs() {
        return mChargeTimeRemainingMs;
    }

    /**
     * Returns a battery consumer for the specified battery consumer type.
     */
    public BatteryConsumer getAggregateBatteryConsumer(
            @AggregateBatteryConsumerScope int scope) {
        return mAggregateBatteryConsumers[scope];
    }

    @NonNull
    public List<UidBatteryConsumer> getUidBatteryConsumers() {
        return mUidBatteryConsumers;
    }

    @NonNull
    public List<UserBatteryConsumer> getUserBatteryConsumers() {
        return mUserBatteryConsumers;
    }

    /**
     * Returns the names of custom power components in order, so the first name in the array
     * corresponds to the custom componentId
     * {@link BatteryConsumer.FIRST_CUSTOM_POWER_COMPONENT_ID}.
     */
    @NonNull
    public String[] getCustomPowerComponentNames() {
        return mCustomPowerComponentNames;
    }

    /**
     * Returns an iterator for {@link android.os.BatteryStats.HistoryItem}'s.
     */
    @NonNull
    public BatteryStatsHistoryIterator iterateBatteryStatsHistory() {
        if (mHistoryBuffer == null) {
            throw new IllegalStateException(
                    "Battery history was not requested in the BatteryUsageStatsQuery");
        }
        return new BatteryStatsHistoryIterator(new BatteryStatsHistory(mHistoryBuffer),
                mHistoryTagPool);
    }

    @Override
    public int describeContents() {
        return 0;
    }

    private BatteryUsageStats(@NonNull Parcel source) {
        mStatsStartTimestampMs = source.readLong();
        mStatsEndTimestampMs = source.readLong();
        mStatsDurationMs = source.readLong();
        mBatteryCapacityMah = source.readDouble();
        mDischargePercentage = source.readInt();
        mDischargedPowerLowerBound = source.readDouble();
        mDischargedPowerUpperBound = source.readDouble();
        mBatteryTimeRemainingMs = source.readLong();
        mChargeTimeRemainingMs = source.readLong();
        mCustomPowerComponentNames = source.readStringArray();
        mAggregateBatteryConsumers =
                new AggregateBatteryConsumer[AGGREGATE_BATTERY_CONSUMER_SCOPE_COUNT];
        for (int i = 0; i < AGGREGATE_BATTERY_CONSUMER_SCOPE_COUNT; i++) {
            mAggregateBatteryConsumers[i] =
                    AggregateBatteryConsumer.CREATOR.createFromParcel(source);
            mAggregateBatteryConsumers[i].setCustomPowerComponentNames(mCustomPowerComponentNames);
        }

        // UidBatteryConsumers are included as a blob to avoid a TransactionTooLargeException
        final Parcel blob = Parcel.obtain();
        final byte[] bytes = source.readBlob();
        blob.unmarshall(bytes, 0, bytes.length);
        blob.setDataPosition(0);

        final int uidCount = blob.readInt();
        mUidBatteryConsumers = new ArrayList<>(uidCount);
        for (int i = 0; i < uidCount; i++) {
            final UidBatteryConsumer consumer =
                    UidBatteryConsumer.CREATOR.createFromParcel(blob);
            consumer.setCustomPowerComponentNames(mCustomPowerComponentNames);
            mUidBatteryConsumers.add(consumer);
        }
        blob.recycle();

        int userCount = source.readInt();
        mUserBatteryConsumers = new ArrayList<>(userCount);
        for (int i = 0; i < userCount; i++) {
            final UserBatteryConsumer consumer =
                    UserBatteryConsumer.CREATOR.createFromParcel(source);
            consumer.setCustomPowerComponentNames(mCustomPowerComponentNames);
            mUserBatteryConsumers.add(consumer);
        }
        if (source.readBoolean()) {
            final byte[] historyBlob = source.readBlob();

            mHistoryBuffer = Parcel.obtain();
            mHistoryBuffer.unmarshall(historyBlob, 0, historyBlob.length);

            int historyTagCount = source.readInt();
            mHistoryTagPool = new ArrayList<>(historyTagCount);
            for (int i = 0; i < historyTagCount; i++) {
                BatteryStats.HistoryTag tag = new BatteryStats.HistoryTag();
                tag.string = source.readString();
                tag.uid = source.readInt();
                tag.poolIdx = source.readInt();
                mHistoryTagPool.add(tag);
            }
        } else {
            mHistoryBuffer = null;
            mHistoryTagPool = null;
        }
    }

    @Override
    public void writeToParcel(@NonNull Parcel dest, int flags) {
        dest.writeLong(mStatsStartTimestampMs);
        dest.writeLong(mStatsEndTimestampMs);
        dest.writeLong(mStatsDurationMs);
        dest.writeDouble(mBatteryCapacityMah);
        dest.writeInt(mDischargePercentage);
        dest.writeDouble(mDischargedPowerLowerBound);
        dest.writeDouble(mDischargedPowerUpperBound);
        dest.writeLong(mBatteryTimeRemainingMs);
        dest.writeLong(mChargeTimeRemainingMs);
        dest.writeStringArray(mCustomPowerComponentNames);
        for (int i = 0; i < AGGREGATE_BATTERY_CONSUMER_SCOPE_COUNT; i++) {
            mAggregateBatteryConsumers[i].writeToParcel(dest, flags);
        }

        // UidBatteryConsumers are included as a blob, because each UidBatteryConsumer
        // takes > 300 bytes, so a typical number of UIDs in the system, 300 would result
        // in a 90 kB Parcel, which is not safe to pass in a binder transaction because
        // of the possibility of TransactionTooLargeException
        final Parcel blob = Parcel.obtain();
        blob.writeInt(mUidBatteryConsumers.size());
        for (int i = mUidBatteryConsumers.size() - 1; i >= 0; i--) {
            mUidBatteryConsumers.get(i).writeToParcel(blob, flags);
        }
        dest.writeBlob(blob.marshall());
        blob.recycle();

        dest.writeInt(mUserBatteryConsumers.size());
        for (int i = mUserBatteryConsumers.size() - 1; i >= 0; i--) {
            mUserBatteryConsumers.get(i).writeToParcel(dest, flags);
        }
        if (mHistoryBuffer != null) {
            dest.writeBoolean(true);
            dest.writeBlob(mHistoryBuffer.marshall());
            dest.writeInt(mHistoryTagPool.size());
            for (int i = mHistoryTagPool.size() - 1; i >= 0; i--) {
                final BatteryStats.HistoryTag tag = mHistoryTagPool.get(i);
                dest.writeString(tag.string);
                dest.writeInt(tag.uid);
                dest.writeInt(tag.poolIdx);
            }
        } else {
            dest.writeBoolean(false);
        }
    }

    @NonNull
    public static final Creator<BatteryUsageStats> CREATOR = new Creator<BatteryUsageStats>() {
        public BatteryUsageStats createFromParcel(@NonNull Parcel source) {
            return new BatteryUsageStats(source);
        }

        public BatteryUsageStats[] newArray(int size) {
            return new BatteryUsageStats[size];
        }
    };

    /** Returns a proto (as used for atoms.proto) corresponding to this BatteryUsageStats. */
    public byte[] getStatsProto() {
        // ProtoOutputStream.getRawSize() returns the buffer size before compaction.
        // BatteryUsageStats contains a lot of integers, so compaction of integers to
        // varint reduces the size of the proto buffer by as much as 50%.
        int maxRawSize = (int) (STATSD_PULL_ATOM_MAX_BYTES * 1.75);
        // Limit the number of attempts in order to prevent an infinite loop
        for (int i = 0; i < 3; i++) {
            final ProtoOutputStream proto = new ProtoOutputStream();
            writeStatsProto(proto, maxRawSize);

            final int rawSize = proto.getRawSize();
            final byte[] protoOutput = proto.getBytes();

            if (protoOutput.length <= STATSD_PULL_ATOM_MAX_BYTES) {
                return protoOutput;
            }

            // Adjust maxRawSize proportionately and try again.
            maxRawSize =
                    (int) ((long) STATSD_PULL_ATOM_MAX_BYTES * rawSize / protoOutput.length - 1024);
        }

        // Fallback: if we have failed to generate a proto smaller than STATSD_PULL_ATOM_MAX_BYTES,
        // just generate a proto with the _rawSize_ of STATSD_PULL_ATOM_MAX_BYTES, which is
        // guaranteed to produce a compacted proto (significantly) smaller than
        // STATSD_PULL_ATOM_MAX_BYTES.
        final ProtoOutputStream proto = new ProtoOutputStream();
        writeStatsProto(proto, STATSD_PULL_ATOM_MAX_BYTES);
        return proto.getBytes();
    }

    @NonNull
    private void writeStatsProto(ProtoOutputStream proto, int maxRawSize) {
        final BatteryConsumer deviceBatteryConsumer = getAggregateBatteryConsumer(
                AGGREGATE_BATTERY_CONSUMER_SCOPE_DEVICE);

        proto.write(BatteryUsageStatsAtomsProto.SESSION_START_MILLIS, getStatsStartTimestamp());
        proto.write(BatteryUsageStatsAtomsProto.SESSION_END_MILLIS, getStatsEndTimestamp());
        proto.write(BatteryUsageStatsAtomsProto.SESSION_DURATION_MILLIS, getStatsDuration());
        proto.write(BatteryUsageStatsAtomsProto.SESSION_DISCHARGE_PERCENTAGE,
                getDischargePercentage());
        deviceBatteryConsumer.writeStatsProto(proto,
                BatteryUsageStatsAtomsProto.DEVICE_BATTERY_CONSUMER);
        writeUidBatteryConsumersProto(proto, maxRawSize);
    }

    /**
     * Writes the UidBatteryConsumers data, held by this BatteryUsageStats, to the proto (as used
     * for atoms.proto).
     */
    private void writeUidBatteryConsumersProto(ProtoOutputStream proto, int maxRawSize) {
        final List<UidBatteryConsumer> consumers = getUidBatteryConsumers();
        // Order consumers by descending weight (a combination of consumed power and usage time)
        consumers.sort(Comparator.comparingDouble(this::getUidBatteryConsumerWeight).reversed());

        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);

            if (proto.getRawSize() >= maxRawSize) {
                break;
            }
        }
    }

    private static final double WEIGHT_CONSUMED_POWER = 1;
    // Weight one hour in foreground the same as 100 mAh of power drain
    private static final double WEIGHT_FOREGROUND_STATE = 100.0 / (1 * 60 * 60 * 1000);
    // Weight one hour in background the same as 300 mAh of power drain
    private static final double WEIGHT_BACKGROUND_STATE = 300.0 / (1 * 60 * 60 * 1000);

    /**
     * Computes the weight associated with a UidBatteryConsumer, which is used for sorting.
     * We want applications with the largest consumed power as well as applications
     * with the highest usage time to be included in the statsd atom.
     */
    private double getUidBatteryConsumerWeight(UidBatteryConsumer uidBatteryConsumer) {
        final double consumedPower = uidBatteryConsumer.getConsumedPower();
        final long timeInForeground =
                uidBatteryConsumer.getTimeInStateMs(UidBatteryConsumer.STATE_FOREGROUND);
        final long timeInBackground =
                uidBatteryConsumer.getTimeInStateMs(UidBatteryConsumer.STATE_BACKGROUND);
        return consumedPower * WEIGHT_CONSUMED_POWER
                + timeInForeground * WEIGHT_FOREGROUND_STATE
                + timeInBackground * WEIGHT_BACKGROUND_STATE;
    }

    /**
     * Prints the stats in a human-readable format.
     */
    public void dump(PrintWriter pw, String prefix) {
        pw.print(prefix);
        pw.println("  Estimated power use (mAh):");
        pw.print(prefix);
        pw.print("    Capacity: ");
        PowerCalculator.printPowerMah(pw, getBatteryCapacity());
        pw.print(", Computed drain: ");
        PowerCalculator.printPowerMah(pw, getConsumedPower());
        final Range<Double> dischargedPowerRange = getDischargedPowerRange();
        pw.print(", actual drain: ");
        PowerCalculator.printPowerMah(pw, dischargedPowerRange.getLower());
        if (!dischargedPowerRange.getLower().equals(dischargedPowerRange.getUpper())) {
            pw.print("-");
            PowerCalculator.printPowerMah(pw, dischargedPowerRange.getUpper());
        }
        pw.println();

        pw.println("    Global");
        final BatteryConsumer deviceConsumer = getAggregateBatteryConsumer(
                AGGREGATE_BATTERY_CONSUMER_SCOPE_DEVICE);
        final BatteryConsumer appsConsumer = getAggregateBatteryConsumer(
                AGGREGATE_BATTERY_CONSUMER_SCOPE_ALL_APPS);

        for (int componentId = 0; componentId < BatteryConsumer.POWER_COMPONENT_COUNT;
                componentId++) {
            final double devicePowerMah = deviceConsumer.getConsumedPower(componentId);
            final double appsPowerMah = appsConsumer.getConsumedPower(componentId);
            if (devicePowerMah == 0 && appsPowerMah == 0) {
                continue;
            }

            final String componentName = BatteryConsumer.powerComponentIdToString(componentId);
            printPowerComponent(pw, prefix, componentName, devicePowerMah, appsPowerMah,
                    deviceConsumer.getPowerModel(componentId),
                    deviceConsumer.getUsageDurationMillis(componentId));
        }

        for (int componentId = BatteryConsumer.FIRST_CUSTOM_POWER_COMPONENT_ID;
                componentId < BatteryConsumer.FIRST_CUSTOM_POWER_COMPONENT_ID
                        + mCustomPowerComponentNames.length;
                componentId++) {
            final double devicePowerMah =
                    deviceConsumer.getConsumedPowerForCustomComponent(componentId);
            final double appsPowerMah =
                    appsConsumer.getConsumedPowerForCustomComponent(componentId);
            if (devicePowerMah == 0 && appsPowerMah == 0) {
                continue;
            }

            printPowerComponent(pw, prefix, deviceConsumer.getCustomPowerComponentName(componentId),
                    devicePowerMah, appsPowerMah,
                    BatteryConsumer.POWER_MODEL_UNDEFINED,
                    deviceConsumer.getUsageDurationForCustomComponentMillis(componentId));
        }

        dumpSortedBatteryConsumers(pw, prefix, getUidBatteryConsumers());
        dumpSortedBatteryConsumers(pw, prefix, getUserBatteryConsumers());
    }

    private void printPowerComponent(PrintWriter pw, String prefix, String componentName,
            double devicePowerMah, double appsPowerMah, int powerModel, long durationMs) {
        StringBuilder sb = new StringBuilder();
        sb.append(prefix).append("      ").append(componentName).append(": ")
                .append(PowerCalculator.formatCharge(devicePowerMah));
        if (powerModel != BatteryConsumer.POWER_MODEL_UNDEFINED
                && powerModel != BatteryConsumer.POWER_MODEL_POWER_PROFILE) {
            sb.append(" [");
            sb.append(BatteryConsumer.powerModelToString(powerModel));
            sb.append("]");
        }
        sb.append(" apps: ").append(PowerCalculator.formatCharge(appsPowerMah));
        if (durationMs != 0) {
            sb.append(" duration: ");
            BatteryStats.formatTimeMs(sb, durationMs);
        }

        pw.println(sb.toString());
    }

    private void dumpSortedBatteryConsumers(PrintWriter pw, String prefix,
            List<? extends BatteryConsumer> batteryConsumers) {
        batteryConsumers.sort(
                Comparator.<BatteryConsumer>comparingDouble(BatteryConsumer::getConsumedPower)
                        .reversed());
        for (BatteryConsumer consumer : batteryConsumers) {
            if (consumer.getConsumedPower() == 0) {
                continue;
            }
            pw.print(prefix);
            pw.print("    ");
            consumer.dump(pw);
            pw.println();
        }
    }

    /** Serializes this object to XML */
    public void writeXml(TypedXmlSerializer serializer) throws IOException {
        serializer.startTag(null, XML_TAG_BATTERY_USAGE_STATS);

        for (int i = 0; i < mCustomPowerComponentNames.length; i++) {
            serializer.attribute(null, XML_ATTR_PREFIX_CUSTOM_COMPONENT + i,
                    mCustomPowerComponentNames[i]);
        }

        serializer.attributeLong(null, XML_ATTR_START_TIMESTAMP, mStatsStartTimestampMs);
        serializer.attributeLong(null, XML_ATTR_END_TIMESTAMP, mStatsEndTimestampMs);
        serializer.attributeLong(null, XML_ATTR_DURATION, mStatsDurationMs);
        serializer.attributeDouble(null, XML_ATTR_BATTERY_CAPACITY, mBatteryCapacityMah);
        serializer.attributeInt(null, XML_ATTR_DISCHARGE_PERCENT, mDischargePercentage);
        serializer.attributeDouble(null, XML_ATTR_DISCHARGE_LOWER, mDischargedPowerLowerBound);
        serializer.attributeDouble(null, XML_ATTR_DISCHARGE_UPPER, mDischargedPowerUpperBound);
        serializer.attributeLong(null, XML_ATTR_BATTERY_REMAINING, mBatteryTimeRemainingMs);
        serializer.attributeLong(null, XML_ATTR_CHARGE_REMAINING, mChargeTimeRemainingMs);

        for (int scope = 0; scope < BatteryUsageStats.AGGREGATE_BATTERY_CONSUMER_SCOPE_COUNT;
                scope++) {
            mAggregateBatteryConsumers[scope].writeToXml(serializer, scope);
        }
        for (UidBatteryConsumer consumer : mUidBatteryConsumers) {
            consumer.writeToXml(serializer);
        }
        for (UserBatteryConsumer consumer : mUserBatteryConsumers) {
            consumer.writeToXml(serializer);
        }
        serializer.endTag(null, XML_TAG_BATTERY_USAGE_STATS);
    }

    /** Parses an XML representation of BatteryUsageStats */
    public static BatteryUsageStats createFromXml(TypedXmlPullParser parser)
            throws XmlPullParserException, IOException {
        Builder builder = null;
        int eventType = parser.getEventType();
        while (eventType != XmlPullParser.END_DOCUMENT) {
            if (eventType == XmlPullParser.START_TAG
                    && parser.getName().equals(XML_TAG_BATTERY_USAGE_STATS)) {
                List<String> customComponentNames = new ArrayList<>();
                int i = 0;
                while (true) {
                    int index = parser.getAttributeIndex(null,
                            XML_ATTR_PREFIX_CUSTOM_COMPONENT + i);
                    if (index == -1) {
                        break;
                    }
                    customComponentNames.add(parser.getAttributeValue(index));
                    i++;
                }

                builder = new Builder(
                        customComponentNames.toArray(new String[0]), true);

                builder.setStatsStartTimestamp(
                        parser.getAttributeLong(null, XML_ATTR_START_TIMESTAMP));
                builder.setStatsEndTimestamp(
                        parser.getAttributeLong(null, XML_ATTR_END_TIMESTAMP));
                builder.setStatsDuration(
                        parser.getAttributeLong(null, XML_ATTR_DURATION));
                builder.setBatteryCapacity(
                        parser.getAttributeDouble(null, XML_ATTR_BATTERY_CAPACITY));
                builder.setDischargePercentage(
                        parser.getAttributeInt(null, XML_ATTR_DISCHARGE_PERCENT));
                builder.setDischargedPowerRange(
                        parser.getAttributeDouble(null, XML_ATTR_DISCHARGE_LOWER),
                        parser.getAttributeDouble(null, XML_ATTR_DISCHARGE_UPPER));
                builder.setBatteryTimeRemainingMs(
                        parser.getAttributeLong(null, XML_ATTR_BATTERY_REMAINING));
                builder.setChargeTimeRemainingMs(
                        parser.getAttributeLong(null, XML_ATTR_CHARGE_REMAINING));

                eventType = parser.next();
                break;
            }
            eventType = parser.next();
        }

        if (builder == null) {
            throw new XmlPullParserException("No root element");
        }

        while (eventType != XmlPullParser.END_DOCUMENT) {
            if (eventType == XmlPullParser.START_TAG) {
                switch (parser.getName()) {
                    case XML_TAG_AGGREGATE:
                        AggregateBatteryConsumer.parseXml(parser, builder);
                        break;
                    case XML_TAG_UID:
                        UidBatteryConsumer.createFromXml(parser, builder);
                        break;
                    case XML_TAG_USER:
                        UserBatteryConsumer.createFromXml(parser, builder);
                        break;
                }
            }
            eventType = parser.next();
        }

        return builder.build();
    }

    /**
     * Builder for BatteryUsageStats.
     */
    public static final class Builder {
        @NonNull
        private final String[] mCustomPowerComponentNames;
        private final boolean mIncludePowerModels;
        private long mStatsStartTimestampMs;
        private long mStatsEndTimestampMs;
        private long mStatsDurationMs = -1;
        private double mBatteryCapacityMah;
        private int mDischargePercentage;
        private double mDischargedPowerLowerBoundMah;
        private double mDischargedPowerUpperBoundMah;
        private long mBatteryTimeRemainingMs = -1;
        private long mChargeTimeRemainingMs = -1;
        private final AggregateBatteryConsumer.Builder[] mAggregateBatteryConsumersBuilders =
                new AggregateBatteryConsumer.Builder[AGGREGATE_BATTERY_CONSUMER_SCOPE_COUNT];
        private final SparseArray<UidBatteryConsumer.Builder> mUidBatteryConsumerBuilders =
                new SparseArray<>();
        private final SparseArray<UserBatteryConsumer.Builder> mUserBatteryConsumerBuilders =
                new SparseArray<>();
        private Parcel mHistoryBuffer;
        private List<BatteryStats.HistoryTag> mHistoryTagPool;

        public Builder(@NonNull String[] customPowerComponentNames) {
            this(customPowerComponentNames, false);
        }

        public Builder(@NonNull String[] customPowerComponentNames,  boolean includePowerModels) {
            mCustomPowerComponentNames = customPowerComponentNames;
            mIncludePowerModels = includePowerModels;
            for (int i = 0; i < AGGREGATE_BATTERY_CONSUMER_SCOPE_COUNT; i++) {
                mAggregateBatteryConsumersBuilders[i] = new AggregateBatteryConsumer.Builder(
                        customPowerComponentNames, includePowerModels);
            }
        }

        /**
         * Constructs a read-only object using the Builder values.
         */
        @NonNull
        public BatteryUsageStats build() {
            return new BatteryUsageStats(this);
        }

        /**
         * Sets the battery capacity in milli-amp-hours.
         */
        public Builder setBatteryCapacity(double batteryCapacityMah) {
            mBatteryCapacityMah = batteryCapacityMah;
            return this;
        }

        /**
         * Sets the timestamp of the latest battery stats reset, in milliseconds.
         */
        public Builder setStatsStartTimestamp(long statsStartTimestampMs) {
            mStatsStartTimestampMs = statsStartTimestampMs;
            return this;
        }

        /**
         * Sets the timestamp of when the battery stats snapshot was taken, in milliseconds.
         */
        public Builder setStatsEndTimestamp(long statsEndTimestampMs) {
            mStatsEndTimestampMs = statsEndTimestampMs;
            return this;
        }

        /**
         * Sets the duration of the stats session.  The default value of this field is
         * statsEndTimestamp - statsStartTimestamp.
         */
        public Builder setStatsDuration(long statsDurationMs) {
            mStatsDurationMs = statsDurationMs;
            return this;
        }

        private long getStatsDuration() {
            if (mStatsDurationMs != -1) {
                return mStatsDurationMs;
            } else {
                return mStatsEndTimestampMs - mStatsStartTimestampMs;
            }
        }

        /**
         * Sets the battery discharge amount since BatteryStats reset as percentage of the full
         * charge.
         */
        @NonNull
        public Builder setDischargePercentage(int dischargePercentage) {
            mDischargePercentage = dischargePercentage;
            return this;
        }

        /**
         * Sets the estimated battery discharge range.
         */
        @NonNull
        public Builder setDischargedPowerRange(double dischargedPowerLowerBoundMah,
                double dischargedPowerUpperBoundMah) {
            mDischargedPowerLowerBoundMah = dischargedPowerLowerBoundMah;
            mDischargedPowerUpperBoundMah = dischargedPowerUpperBoundMah;
            return this;
        }

        /**
         * Sets an approximation for how much time (in milliseconds) remains until the battery
         * is fully discharged.
         */
        @NonNull
        public Builder setBatteryTimeRemainingMs(long batteryTimeRemainingMs) {
            mBatteryTimeRemainingMs = batteryTimeRemainingMs;
            return this;
        }

        /**
         * Sets an approximation for how much time (in milliseconds) remains until the battery
         * is fully charged.
         */
        @NonNull
        public Builder setChargeTimeRemainingMs(long chargeTimeRemainingMs) {
            mChargeTimeRemainingMs = chargeTimeRemainingMs;
            return this;
        }

        /**
         * Sets the parceled recent history.
         */
        @NonNull
        public Builder setBatteryHistory(Parcel historyBuffer,
                List<BatteryStats.HistoryTag> historyTagPool) {
            mHistoryBuffer = historyBuffer;
            mHistoryTagPool = historyTagPool;
            return this;
        }

        /**
         * Creates or returns an AggregateBatteryConsumer builder, which represents aggregate
         * battery consumption data for the specified scope.
         */
        @NonNull
        public AggregateBatteryConsumer.Builder getAggregateBatteryConsumerBuilder(
                @AggregateBatteryConsumerScope int scope) {
            return mAggregateBatteryConsumersBuilders[scope];
        }

        /**
         * Creates or returns a UidBatteryConsumer, which represents battery attribution
         * data for an individual UID.
         */
        @NonNull
        public UidBatteryConsumer.Builder getOrCreateUidBatteryConsumerBuilder(
                @NonNull BatteryStats.Uid batteryStatsUid) {
            int uid = batteryStatsUid.getUid();
            UidBatteryConsumer.Builder builder = mUidBatteryConsumerBuilders.get(uid);
            if (builder == null) {
                builder = new UidBatteryConsumer.Builder(mCustomPowerComponentNames,
                        mIncludePowerModels, batteryStatsUid);
                mUidBatteryConsumerBuilders.put(uid, builder);
            }
            return builder;
        }

        /**
         * Creates or returns a UidBatteryConsumer, which represents battery attribution
         * data for an individual UID. This version of the method is not suitable for use
         * with PowerCalculators.
         */
        @NonNull
        public UidBatteryConsumer.Builder getOrCreateUidBatteryConsumerBuilder(int uid) {
            UidBatteryConsumer.Builder builder = mUidBatteryConsumerBuilders.get(uid);
            if (builder == null) {
                builder = new UidBatteryConsumer.Builder(mCustomPowerComponentNames,
                        mIncludePowerModels, uid);
                mUidBatteryConsumerBuilders.put(uid, builder);
            }
            return builder;
        }

        /**
         * Creates or returns a UserBatteryConsumer, which represents battery attribution
         * data for an individual {@link UserHandle}.
         */
        @NonNull
        public UserBatteryConsumer.Builder getOrCreateUserBatteryConsumerBuilder(int userId) {
            UserBatteryConsumer.Builder builder = mUserBatteryConsumerBuilders.get(userId);
            if (builder == null) {
                builder = new UserBatteryConsumer.Builder(mCustomPowerComponentNames,
                        mIncludePowerModels, userId);
                mUserBatteryConsumerBuilders.put(userId, builder);
            }
            return builder;
        }

        @NonNull
        public SparseArray<UidBatteryConsumer.Builder> getUidBatteryConsumerBuilders() {
            return mUidBatteryConsumerBuilders;
        }

        /**
         * Adds battery usage stats from another snapshots. The two snapshots are assumed to be
         * non-overlapping, meaning that the power consumption estimates and session durations
         * can be simply summed across the two snapshots.  This remains true even if the timestamps
         * seem to indicate that the sessions are in fact overlapping: timestamps may be off as a
         * result of realtime clock adjustments by the user or the system.
         */
        @NonNull
        public Builder add(BatteryUsageStats stats) {
            if (!Arrays.equals(mCustomPowerComponentNames, stats.mCustomPowerComponentNames)) {
                throw new IllegalArgumentException(
                        "BatteryUsageStats have different custom power components");
            }

            if (mUserBatteryConsumerBuilders.size() != 0
                    || !stats.getUserBatteryConsumers().isEmpty()) {
                throw new UnsupportedOperationException(
                        "Combining UserBatteryConsumers is not supported");
            }

            mDischargedPowerLowerBoundMah += stats.mDischargedPowerLowerBound;
            mDischargedPowerUpperBoundMah += stats.mDischargedPowerUpperBound;
            mDischargePercentage += stats.mDischargePercentage;

            mStatsDurationMs = getStatsDuration() + stats.getStatsDuration();

            if (mStatsStartTimestampMs == 0
                    || stats.mStatsStartTimestampMs < mStatsStartTimestampMs) {
                mStatsStartTimestampMs = stats.mStatsStartTimestampMs;
            }

            final boolean addingLaterSnapshot = stats.mStatsEndTimestampMs > mStatsEndTimestampMs;
            if (addingLaterSnapshot) {
                mStatsEndTimestampMs = stats.mStatsEndTimestampMs;
            }

            for (int scope = 0; scope < AGGREGATE_BATTERY_CONSUMER_SCOPE_COUNT; scope++) {
                getAggregateBatteryConsumerBuilder(scope)
                        .add(stats.mAggregateBatteryConsumers[scope]);
            }

            for (UidBatteryConsumer consumer : stats.getUidBatteryConsumers()) {
                getOrCreateUidBatteryConsumerBuilder(consumer.getUid()).add(consumer);
            }

            if (addingLaterSnapshot) {
                mBatteryCapacityMah = stats.mBatteryCapacityMah;
                mBatteryTimeRemainingMs = stats.mBatteryTimeRemainingMs;
                mChargeTimeRemainingMs = stats.mChargeTimeRemainingMs;
            }

            return this;
        }
    }
}
