/*
 * 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.database.Cursor;
import android.database.CursorWindow;
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;
import com.android.modules.utils.TypedXmlPullParser;
import com.android.modules.utils.TypedXmlSerializer;

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

import java.io.Closeable;
import java.io.FileDescriptor;
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, Closeable {

    /**
     * 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;

    // 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_PREFIX_INCLUDES_PROC_STATE_DATA = "includes_proc_state_data";
    static final String XML_ATTR_START_TIMESTAMP = "start_timestamp";
    static final String XML_ATTR_END_TIMESTAMP = "end_timestamp";
    static final String XML_ATTR_PROCESS_STATE = "process_state";
    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_DISCHARGE_DURATION = "discharge_duration";
    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";

    // We need about 700 bytes per UID
    private static final long BATTERY_CONSUMER_CURSOR_WINDOW_SIZE = 5_000 * 700;

    private static final int STATSD_PULL_ATOM_MAX_BYTES = 45000;

    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 mDischargeDurationMs;
    private final long mBatteryTimeRemainingMs;
    private final long mChargeTimeRemainingMs;
    private final String[] mCustomPowerComponentNames;
    private final boolean mIncludesPowerModels;
    private final boolean mIncludesProcessStateData;
    private final List<UidBatteryConsumer> mUidBatteryConsumers;
    private final List<UserBatteryConsumer> mUserBatteryConsumers;
    private final AggregateBatteryConsumer[] mAggregateBatteryConsumers;
    private final BatteryStatsHistory mBatteryStatsHistory;
    private CursorWindow mBatteryConsumersCursorWindow;

    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;
        mDischargeDurationMs = builder.mDischargeDurationMs;
        mBatteryStatsHistory = builder.mBatteryStatsHistory;
        mBatteryTimeRemainingMs = builder.mBatteryTimeRemainingMs;
        mChargeTimeRemainingMs = builder.mChargeTimeRemainingMs;
        mCustomPowerComponentNames = builder.mCustomPowerComponentNames;
        mIncludesPowerModels = builder.mIncludePowerModels;
        mIncludesProcessStateData = builder.mIncludesProcessStateData;
        mBatteryConsumersCursorWindow = builder.mBatteryConsumersCursorWindow;

        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 the total amount of time the battery was discharging.
     */
    public long getDischargeDurationMs() {
        return mDischargeDurationMs;
    }

    /**
     * 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 AggregateBatteryConsumer 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;
    }

    public boolean isProcessStateDataIncluded() {
        return mIncludesProcessStateData;
    }

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

    @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();
        mDischargeDurationMs = source.readLong();
        mBatteryTimeRemainingMs = source.readLong();
        mChargeTimeRemainingMs = source.readLong();
        mCustomPowerComponentNames = source.readStringArray();
        mIncludesPowerModels = source.readBoolean();
        mIncludesProcessStateData = source.readBoolean();

        mBatteryConsumersCursorWindow = CursorWindow.newFromParcel(source);
        BatteryConsumer.BatteryConsumerDataLayout dataLayout =
                BatteryConsumer.createBatteryConsumerDataLayout(mCustomPowerComponentNames,
                        mIncludesPowerModels, mIncludesProcessStateData);

        final int numRows = mBatteryConsumersCursorWindow.getNumRows();

        mAggregateBatteryConsumers =
                new AggregateBatteryConsumer[AGGREGATE_BATTERY_CONSUMER_SCOPE_COUNT];
        mUidBatteryConsumers = new ArrayList<>(numRows);
        mUserBatteryConsumers = new ArrayList<>();

        for (int i = 0; i < numRows; i++) {
            final BatteryConsumer.BatteryConsumerData data =
                    new BatteryConsumer.BatteryConsumerData(mBatteryConsumersCursorWindow, i,
                            dataLayout);

            int consumerType = mBatteryConsumersCursorWindow.getInt(i,
                            BatteryConsumer.COLUMN_INDEX_BATTERY_CONSUMER_TYPE);
            switch (consumerType) {
                case AggregateBatteryConsumer.CONSUMER_TYPE_AGGREGATE: {
                    final AggregateBatteryConsumer consumer = new AggregateBatteryConsumer(data);
                    mAggregateBatteryConsumers[consumer.getScope()] = consumer;
                    break;
                }
                case UidBatteryConsumer.CONSUMER_TYPE_UID: {
                    mUidBatteryConsumers.add(new UidBatteryConsumer(data));
                    break;
                }
                case UserBatteryConsumer.CONSUMER_TYPE_USER:
                    mUserBatteryConsumers.add(new UserBatteryConsumer(data));
                    break;
            }
        }

        if (source.readBoolean()) {
            mBatteryStatsHistory = BatteryStatsHistory.createFromBatteryUsageStatsParcel(source);
        } else {
            mBatteryStatsHistory = 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(mDischargeDurationMs);
        dest.writeLong(mBatteryTimeRemainingMs);
        dest.writeLong(mChargeTimeRemainingMs);
        dest.writeStringArray(mCustomPowerComponentNames);
        dest.writeBoolean(mIncludesPowerModels);
        dest.writeBoolean(mIncludesProcessStateData);

        mBatteryConsumersCursorWindow.writeToParcel(dest, flags);

        if (mBatteryStatsHistory != null) {
            dest.writeBoolean(true);
            mBatteryStatsHistory.writeToBatteryUsageStatsParcel(dest);
        } 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();
    }

    /**
     * Writes contents in a binary protobuffer format, using
     * the android.os.BatteryUsageStatsAtomsProto proto.
     */
    public void dumpToProto(FileDescriptor fd) {
        final ProtoOutputStream proto = new ProtoOutputStream(fd);
        writeStatsProto(proto, /* max size */ Integer.MAX_VALUE);
        proto.flush();
    }

    @NonNull
    private void writeStatsProto(ProtoOutputStream proto, int maxRawSize) {
        final AggregateBatteryConsumer 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());
        proto.write(BatteryUsageStatsAtomsProto.DISCHARGE_DURATION_MILLIS,
                getDischargeDurationMs());
        deviceBatteryConsumer.writeStatsProto(proto,
                BatteryUsageStatsAtomsProto.DEVICE_BATTERY_CONSUMER);
        if (mIncludesPowerModels) {
            deviceBatteryConsumer.writePowerComponentModelProto(proto);
        }
        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: ");
        pw.print(BatteryStats.formatCharge(getBatteryCapacity()));
        pw.print(", Computed drain: ");
        pw.print(BatteryStats.formatCharge(getConsumedPower()));
        final Range<Double> dischargedPowerRange = getDischargedPowerRange();
        pw.print(", actual drain: ");
        pw.print(BatteryStats.formatCharge(dischargedPowerRange.getLower()));
        if (!dischargedPowerRange.getLower().equals(dischargedPowerRange.getUpper())) {
            pw.print("-");
            pw.print(BatteryStats.formatCharge(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++) {
            for (BatteryConsumer.Key key : deviceConsumer.getKeys(componentId)) {
                final double devicePowerMah = deviceConsumer.getConsumedPower(key);
                final double appsPowerMah = appsConsumer.getConsumedPower(key);
                if (devicePowerMah == 0 && appsPowerMah == 0) {
                    continue;
                }

                String label = BatteryConsumer.powerComponentIdToString(componentId);
                if (key.processState != BatteryConsumer.PROCESS_STATE_UNSPECIFIED) {
                    label = label
                            + "(" + BatteryConsumer.processStateToString(key.processState) + ")";
                }
                printPowerComponent(pw, prefix, label, devicePowerMah, appsPowerMah,
                        deviceConsumer.getPowerModel(key),
                        deviceConsumer.getUsageDurationMillis(key));
            }
        }

        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());
        pw.println();
    }

    private void printPowerComponent(PrintWriter pw, String prefix, String label,
            double devicePowerMah, double appsPowerMah, int powerModel, long durationMs) {
        StringBuilder sb = new StringBuilder();
        sb.append(prefix).append("      ").append(label).append(": ")
                .append(BatteryStats.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(BatteryStats.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.attributeBoolean(null, XML_ATTR_PREFIX_INCLUDES_PROC_STATE_DATA,
                mIncludesProcessStateData);
        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_DISCHARGE_DURATION, mDischargeDurationMs);
        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++;
                }

                final boolean includesProcStateData = parser.getAttributeBoolean(null,
                        XML_ATTR_PREFIX_INCLUDES_PROC_STATE_DATA, false);

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

                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.setDischargeDurationMs(
                        parser.getAttributeLong(null, XML_ATTR_DISCHARGE_DURATION));
                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();
    }

    @Override
    public void close() throws IOException {
        mBatteryConsumersCursorWindow.close();
        mBatteryConsumersCursorWindow = null;
    }

    @Override
    protected void finalize() throws Throwable {
        if (mBatteryConsumersCursorWindow != null) {
            mBatteryConsumersCursorWindow.close();
        }
        super.finalize();
    }

    /**
     * Builder for BatteryUsageStats.
     */
    public static final class Builder {
        private final CursorWindow mBatteryConsumersCursorWindow;
        @NonNull
        private final String[] mCustomPowerComponentNames;
        private final boolean mIncludePowerModels;
        private final boolean mIncludesProcessStateData;
        private final BatteryConsumer.BatteryConsumerDataLayout mBatteryConsumerDataLayout;
        private long mStatsStartTimestampMs;
        private long mStatsEndTimestampMs;
        private long mStatsDurationMs = -1;
        private double mBatteryCapacityMah;
        private int mDischargePercentage;
        private double mDischargedPowerLowerBoundMah;
        private double mDischargedPowerUpperBoundMah;
        private long mDischargeDurationMs;
        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 BatteryStatsHistory mBatteryStatsHistory;

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

        public Builder(@NonNull String[] customPowerComponentNames, boolean includePowerModels,
                boolean includeProcessStateData) {
            mBatteryConsumersCursorWindow =
                    new CursorWindow(null, BATTERY_CONSUMER_CURSOR_WINDOW_SIZE);
            mBatteryConsumerDataLayout =
                    BatteryConsumer.createBatteryConsumerDataLayout(customPowerComponentNames,
                            includePowerModels, includeProcessStateData);
            mBatteryConsumersCursorWindow.setNumColumns(mBatteryConsumerDataLayout.columnCount);

            mCustomPowerComponentNames = customPowerComponentNames;
            mIncludePowerModels = includePowerModels;
            mIncludesProcessStateData = includeProcessStateData;
            for (int scope = 0; scope < AGGREGATE_BATTERY_CONSUMER_SCOPE_COUNT; scope++) {
                final BatteryConsumer.BatteryConsumerData data =
                        BatteryConsumer.BatteryConsumerData.create(mBatteryConsumersCursorWindow,
                                mBatteryConsumerDataLayout);
                mAggregateBatteryConsumersBuilders[scope] =
                        new AggregateBatteryConsumer.Builder(data, scope);
            }
        }

        public boolean isProcessStateDataNeeded() {
            return mIncludesProcessStateData;
        }

        /**
         * 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 the total battery discharge time, in milliseconds.
         */
        @NonNull
        public Builder setDischargeDurationMs(long durationMs) {
            mDischargeDurationMs = durationMs;
            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(BatteryStatsHistory batteryStatsHistory) {
            mBatteryStatsHistory = batteryStatsHistory;
            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) {
                final BatteryConsumer.BatteryConsumerData data =
                        BatteryConsumer.BatteryConsumerData.create(mBatteryConsumersCursorWindow,
                                mBatteryConsumerDataLayout);
                builder = new UidBatteryConsumer.Builder(data, 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) {
                final BatteryConsumer.BatteryConsumerData data =
                        BatteryConsumer.BatteryConsumerData.create(mBatteryConsumersCursorWindow,
                                mBatteryConsumerDataLayout);
                builder = new UidBatteryConsumer.Builder(data, 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) {
                final BatteryConsumer.BatteryConsumerData data =
                        BatteryConsumer.BatteryConsumerData.create(mBatteryConsumersCursorWindow,
                                mBatteryConsumerDataLayout);
                builder = new UserBatteryConsumer.Builder(data, 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 (mIncludesProcessStateData && !stats.mIncludesProcessStateData) {
                throw new IllegalArgumentException(
                        "Added BatteryUsageStats does not include process state data");
            }

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

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

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

        /**
         * Dumps raw contents of the cursor window for debugging.
         */
        void dump(PrintWriter writer) {
            final int numRows = mBatteryConsumersCursorWindow.getNumRows();
            int numColumns = mBatteryConsumerDataLayout.columnCount;
            for (int i = 0; i < numRows; i++) {
                StringBuilder sb = new StringBuilder();
                for (int j = 0; j < numColumns; j++) {
                    final int type = mBatteryConsumersCursorWindow.getType(i, j);
                    switch (type) {
                        case Cursor.FIELD_TYPE_NULL:
                            sb.append("null, ");
                            break;
                        case Cursor.FIELD_TYPE_INTEGER:
                            sb.append(mBatteryConsumersCursorWindow.getInt(i, j)).append(", ");
                            break;
                        case Cursor.FIELD_TYPE_FLOAT:
                            sb.append(mBatteryConsumersCursorWindow.getFloat(i, j)).append(", ");
                            break;
                        case Cursor.FIELD_TYPE_STRING:
                            sb.append(mBatteryConsumersCursorWindow.getString(i, j)).append(", ");
                            break;
                        case Cursor.FIELD_TYPE_BLOB:
                            sb.append("BLOB, ");
                            break;
                    }
                }
                sb.setLength(sb.length() - 2);
                writer.println(sb);
            }
        }
    }
}
