/* * 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 static android.os.BatteryConsumer.POWER_COMPONENT_ANY; import static android.os.BatteryConsumer.POWER_COMPONENT_BASE; import static android.os.BatteryConsumer.POWER_STATE_ANY; import static android.os.BatteryConsumer.POWER_STATE_UNSPECIFIED; import static android.os.BatteryConsumer.PROCESS_STATE_ANY; import static android.os.BatteryConsumer.PROCESS_STATE_UNSPECIFIED; import static android.os.BatteryConsumer.SCREEN_STATE_ANY; import static android.os.BatteryConsumer.SCREEN_STATE_UNSPECIFIED; import static android.os.BatteryConsumer.convertMahToDeciCoulombs; import android.annotation.NonNull; import android.annotation.Nullable; import android.util.proto.ProtoOutputStream; 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.IOException; import java.io.PrintWriter; /** * Contains details of battery attribution data broken down to individual power drain types * such as CPU, RAM, GPU etc. * * @hide */ @android.ravenwood.annotation.RavenwoodKeepWholeClass class PowerComponents { private final BatteryConsumer.BatteryConsumerData mData; PowerComponents(@NonNull Builder builder) { mData = builder.mData; } PowerComponents(BatteryConsumer.BatteryConsumerData data) { mData = data; } /** * Total power consumed by this consumer, aggregated over the specified dimensions, in mAh. */ public double getConsumedPower(@NonNull BatteryConsumer.Dimensions dimensions) { return getConsumedPower(dimensions.powerComponentId, dimensions.processState, dimensions.screenState, dimensions.powerState); } /** * Total power consumed by this consumer, aggregated over the specified dimensions, in mAh. */ public double getConsumedPower(@BatteryConsumer.PowerComponentId int powerComponent, @BatteryConsumer.ProcessState int processState, @BatteryConsumer.ScreenState int screenState, @BatteryConsumer.PowerState int powerState) { if (powerComponent == POWER_COMPONENT_ANY && processState == PROCESS_STATE_ANY && screenState == SCREEN_STATE_ANY && powerState == POWER_STATE_ANY) { return mData.getDouble(mData.layout.totalConsumedPowerColumnIndex); } if (!mData.layout.processStateDataIncluded && !(processState == PROCESS_STATE_UNSPECIFIED || processState == PROCESS_STATE_ANY)) { return 0; } BatteryConsumer.Key key = mData.layout.getKey(powerComponent, mData.layout.processStateDataIncluded && processState != PROCESS_STATE_ANY ? processState : PROCESS_STATE_UNSPECIFIED, mData.layout.screenStateDataIncluded && screenState != SCREEN_STATE_ANY ? screenState : SCREEN_STATE_UNSPECIFIED, mData.layout.powerStateDataIncluded && powerState != POWER_STATE_ANY ? powerState : POWER_STATE_UNSPECIFIED); if (key != null && mData.hasValue(key.mPowerColumnIndex)) { return mData.getDouble(key.mPowerColumnIndex); } double total = 0; for (BatteryConsumer.Key k : mData.layout.keys) { if (k.matches(powerComponent, processState, screenState, powerState)) { total += mData.getDouble(k.mPowerColumnIndex); } } return total; } /** * Total usage duration by this consumer, aggregated over the specified dimensions, in ms. */ public long getUsageDurationMillis(@NonNull BatteryConsumer.Dimensions dimensions) { return getUsageDurationMillis(dimensions.powerComponentId, dimensions.processState, dimensions.screenState, dimensions.powerState); } /** * Total usage duration by this consumer, aggregated over the specified dimensions, in ms. */ public long getUsageDurationMillis(@BatteryConsumer.PowerComponentId int powerComponent, @BatteryConsumer.ProcessState int processState, @BatteryConsumer.ScreenState int screenState, @BatteryConsumer.PowerState int powerState) { BatteryConsumer.Key key = mData.layout.getKey(powerComponent, mData.layout.processStateDataIncluded && processState != PROCESS_STATE_ANY ? processState : PROCESS_STATE_UNSPECIFIED, mData.layout.screenStateDataIncluded && screenState != SCREEN_STATE_ANY ? screenState : SCREEN_STATE_UNSPECIFIED, mData.layout.powerStateDataIncluded && powerState != POWER_STATE_ANY ? powerState : POWER_STATE_UNSPECIFIED); if (key != null && mData.hasValue(key.mDurationColumnIndex)) { return mData.getLong(key.mDurationColumnIndex); } long total = 0; for (BatteryConsumer.Key k : mData.layout.keys) { if (k.matches(powerComponent, processState, screenState, powerState)) { total += mData.getLong(k.mDurationColumnIndex); } } return total; } /** * Returns the amount of drain attributed to the specified drain type, e.g. CPU, WiFi etc. * * @param key The key of the power component, obtained by calling {@link BatteryConsumer#getKey} * or {@link BatteryConsumer#getKeys} method. * @return Amount of consumed power in mAh. */ public double getConsumedPower(@NonNull BatteryConsumer.Key key) { if (mData.hasValue(key.mPowerColumnIndex)) { return mData.getDouble(key.mPowerColumnIndex); } return getConsumedPower(key.powerComponentId, key.processState, key.screenState, key.powerState); } public String getCustomPowerComponentName(int componentId) { return mData.layout.getPowerComponentName(componentId); } /** * Returns the amount of time used by the specified component, e.g. CPU, WiFi etc. * * @param key The key of the power component, obtained by calling {@link BatteryConsumer#getKey} * or {@link BatteryConsumer#getKeys} method. * @return Amount of time in milliseconds. */ public long getUsageDurationMillis(BatteryConsumer.Key key) { if (mData.hasValue(key.mDurationColumnIndex)) { return mData.getLong(key.mDurationColumnIndex); } return getUsageDurationMillis(key.powerComponentId, key.processState, key.screenState, key.powerState); } void dump(PrintWriter pw, @BatteryConsumer.ScreenState int screenState, @BatteryConsumer.PowerState int powerState, boolean skipEmptyComponents) { StringBuilder sb = new StringBuilder(); for (@BatteryConsumer.PowerComponentId int id : mData.layout.powerComponentIds) { if (id == POWER_COMPONENT_BASE) { continue; } dump(sb, id, PROCESS_STATE_ANY, screenState, powerState, skipEmptyComponents); if (mData.layout.processStateDataIncluded) { for (int processState = 0; processState < BatteryConsumer.PROCESS_STATE_COUNT; processState++) { if (processState == PROCESS_STATE_UNSPECIFIED) { continue; } dump(sb, id, processState, screenState, powerState, skipEmptyComponents); } } } // Remove trailing spaces while (!sb.isEmpty() && Character.isWhitespace(sb.charAt(sb.length() - 1))) { sb.setLength(sb.length() - 1); } pw.println(sb); } private void dump(StringBuilder sb, @BatteryConsumer.PowerComponentId int powerComponent, @BatteryConsumer.ProcessState int processState, @BatteryConsumer.ScreenState int screenState, @BatteryConsumer.PowerState int powerState, boolean skipEmptyComponents) { final double power = getConsumedPower(powerComponent, processState, screenState, powerState); final long durationMs = getUsageDurationMillis(powerComponent, processState, screenState, powerState); if (skipEmptyComponents && power == 0 && durationMs == 0) { return; } sb.append(mData.layout.getPowerComponentName(powerComponent)); if (processState != PROCESS_STATE_ANY) { sb.append(':'); sb.append(BatteryConsumer.processStateToString(processState)); } sb.append("="); sb.append(BatteryStats.formatCharge(power)); if (durationMs != 0) { sb.append(" ("); BatteryStats.formatTimeMsNoSpace(sb, durationMs); sb.append(")"); } sb.append(' '); } /** 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 (@BatteryConsumer.PowerComponentId int componentId : mData.layout.powerComponentIds) { final BatteryConsumer.Key[] keys = mData.layout.getKeys(componentId); for (BatteryConsumer.Key key : keys) { final long powerDeciCoulombs = convertMahToDeciCoulombs( getConsumedPower(key.powerComponentId, key.processState, key.screenState, key.powerState)); final long durationMs = getUsageDurationMillis(key.powerComponentId, key.processState, key.screenState, key.powerState); 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; } if (key.processState == PROCESS_STATE_UNSPECIFIED) { writePowerComponentUsage(proto, BatteryUsageStatsAtomsProto.BatteryConsumerData.POWER_COMPONENTS, componentId, powerDeciCoulombs, durationMs); } else { writePowerUsageSlice(proto, componentId, powerDeciCoulombs, durationMs, key.processState); } } } return interestingData; } private void writePowerUsageSlice(ProtoOutputStream proto, int componentId, long powerDeciCoulombs, long durationMs, int processState) { final long slicesToken = proto.start(BatteryUsageStatsAtomsProto.BatteryConsumerData.SLICES); writePowerComponentUsage(proto, BatteryUsageStatsAtomsProto.BatteryConsumerData.PowerComponentUsageSlice .POWER_COMPONENT, componentId, powerDeciCoulombs, durationMs); final int procState; switch (processState) { case BatteryConsumer.PROCESS_STATE_FOREGROUND: procState = BatteryUsageStatsAtomsProto.BatteryConsumerData.PowerComponentUsageSlice .FOREGROUND; break; case BatteryConsumer.PROCESS_STATE_BACKGROUND: procState = BatteryUsageStatsAtomsProto.BatteryConsumerData.PowerComponentUsageSlice .BACKGROUND; break; case BatteryConsumer.PROCESS_STATE_FOREGROUND_SERVICE: procState = BatteryUsageStatsAtomsProto.BatteryConsumerData.PowerComponentUsageSlice .FOREGROUND_SERVICE; break; case BatteryConsumer.PROCESS_STATE_CACHED: procState = BatteryUsageStatsAtomsProto.BatteryConsumerData.PowerComponentUsageSlice .CACHED; break; default: throw new IllegalArgumentException("Unknown process state: " + processState); } proto.write(BatteryUsageStatsAtomsProto.BatteryConsumerData.PowerComponentUsageSlice .PROCESS_STATE, procState); proto.end(slicesToken); } private void writePowerComponentUsage(ProtoOutputStream proto, long tag, @BatteryConsumer.PowerComponentId int componentId, long powerDeciCoulombs, long durationMs) { final long token = proto.start(tag); 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); } void writeToXml(TypedXmlSerializer serializer) throws IOException { serializer.startTag(null, BatteryUsageStats.XML_TAG_POWER_COMPONENTS); for (BatteryConsumer.Key key : mData.layout.keys) { if (!mData.hasValue(key.mPowerColumnIndex) && !mData.hasValue(key.mDurationColumnIndex)) { continue; } final double powerMah = getConsumedPower(key); final long durationMs = getUsageDurationMillis(key); if (powerMah == 0 && durationMs == 0) { continue; } serializer.startTag(null, BatteryUsageStats.XML_TAG_COMPONENT); serializer.attributeInt(null, BatteryUsageStats.XML_ATTR_ID, key.powerComponentId); if (key.processState != PROCESS_STATE_UNSPECIFIED) { serializer.attributeInt(null, BatteryUsageStats.XML_ATTR_PROCESS_STATE, key.processState); } if (key.screenState != SCREEN_STATE_UNSPECIFIED) { serializer.attributeInt(null, BatteryUsageStats.XML_ATTR_SCREEN_STATE, key.screenState); } if (key.powerState != POWER_STATE_UNSPECIFIED) { serializer.attributeInt(null, BatteryUsageStats.XML_ATTR_POWER_STATE, key.powerState); } if (powerMah != 0) { serializer.attributeDouble(null, BatteryUsageStats.XML_ATTR_POWER, powerMah); } if (durationMs != 0) { serializer.attributeLong(null, BatteryUsageStats.XML_ATTR_DURATION, durationMs); } serializer.endTag(null, BatteryUsageStats.XML_TAG_COMPONENT); } serializer.endTag(null, BatteryUsageStats.XML_TAG_POWER_COMPONENTS); } // No longer part of the BatteryUsageStats XML format. Preserved for compatibility private static final String XML_TAG_CUSTOM_COMPONENT_COMPAT = "custom_component"; static void parseXml(TypedXmlPullParser parser, PowerComponents.Builder builder) throws XmlPullParserException, IOException { int eventType = parser.getEventType(); if (eventType != XmlPullParser.START_TAG || !parser.getName().equals( BatteryUsageStats.XML_TAG_POWER_COMPONENTS)) { throw new XmlPullParserException("Invalid XML parser state"); } while (!(eventType == XmlPullParser.END_TAG && parser.getName().equals( BatteryUsageStats.XML_TAG_POWER_COMPONENTS)) && eventType != XmlPullParser.END_DOCUMENT) { if (eventType == XmlPullParser.START_TAG) { switch (parser.getName()) { case BatteryUsageStats.XML_TAG_COMPONENT: case XML_TAG_CUSTOM_COMPONENT_COMPAT: { int componentId = -1; int processState = PROCESS_STATE_UNSPECIFIED; int screenState = SCREEN_STATE_UNSPECIFIED; int powerState = POWER_STATE_UNSPECIFIED; double powerMah = 0; long durationMs = 0; for (int i = 0; i < parser.getAttributeCount(); i++) { switch (parser.getAttributeName(i)) { case BatteryUsageStats.XML_ATTR_ID: componentId = parser.getAttributeInt(i); break; case BatteryUsageStats.XML_ATTR_PROCESS_STATE: processState = parser.getAttributeInt(i); break; case BatteryUsageStats.XML_ATTR_SCREEN_STATE: screenState = parser.getAttributeInt(i); break; case BatteryUsageStats.XML_ATTR_POWER_STATE: powerState = parser.getAttributeInt(i); break; case BatteryUsageStats.XML_ATTR_POWER: powerMah = parser.getAttributeDouble(i); break; case BatteryUsageStats.XML_ATTR_DURATION: durationMs = parser.getAttributeLong(i); break; } } final BatteryConsumer.Key key = builder.mData.layout.getKey(componentId, processState, screenState, powerState); builder.addConsumedPower(key, powerMah); builder.addUsageDurationMillis(key, durationMs); break; } } } eventType = parser.next(); } } /** * Builder for PowerComponents. */ static final class Builder { private final BatteryConsumer.BatteryConsumerData mData; private final double mMinConsumedPowerThreshold; Builder(BatteryConsumer.BatteryConsumerData data, double minConsumedPowerThreshold) { mData = data; mMinConsumedPowerThreshold = minConsumedPowerThreshold; } /** * @deprecated use {@link #addConsumedPower(BatteryConsumer.Key, double)} */ @Deprecated @NonNull public Builder setConsumedPower(BatteryConsumer.Key key, double componentPower) { mData.putDouble(key.mPowerColumnIndex, componentPower); return this; } @NonNull public Builder addConsumedPower(BatteryConsumer.Key key, double componentPower) { mData.putDouble(key.mPowerColumnIndex, mData.getDouble(key.mPowerColumnIndex) + componentPower); return this; } /** * @deprecated use {@link #addUsageDurationMillis(BatteryConsumer.Key, long)} */ @Deprecated @NonNull public Builder setUsageDurationMillis(BatteryConsumer.Key key, long componentUsageDurationMillis) { mData.putLong(key.mDurationColumnIndex, componentUsageDurationMillis); return this; } @NonNull public Builder addUsageDurationMillis(BatteryConsumer.Key key, long componentUsageDurationMillis) { mData.putLong(key.mDurationColumnIndex, mData.getLong(key.mDurationColumnIndex) + componentUsageDurationMillis); return this; } public void addPowerAndDuration(PowerComponents.Builder other) { addPowerAndDuration(other.mData); } public void addPowerAndDuration(PowerComponents other) { addPowerAndDuration(other.mData); } private void addPowerAndDuration(BatteryConsumer.BatteryConsumerData otherData) { if (mData.layout.customPowerComponentCount != otherData.layout.customPowerComponentCount) { throw new IllegalArgumentException( "Number of custom power components does not match: " + otherData.layout.customPowerComponentCount + ", expected: " + mData.layout.customPowerComponentCount); } for (BatteryConsumer.Key key : mData.layout.keys) { BatteryConsumer.Key otherKey = otherData.layout.getKey(key.powerComponentId, key.processState, key.screenState, key.powerState); if (otherKey == null) { continue; } if (mData.hasValue(key.mPowerColumnIndex) || otherData.hasValue(otherKey.mPowerColumnIndex)) { mData.putDouble(key.mPowerColumnIndex, mData.getDouble(key.mPowerColumnIndex) + otherData.getDouble(otherKey.mPowerColumnIndex)); } if (mData.hasValue(key.mDurationColumnIndex) || otherData.hasValue(otherKey.mDurationColumnIndex)) { mData.putLong(key.mDurationColumnIndex, mData.getLong(key.mDurationColumnIndex) + otherData.getLong(otherKey.mDurationColumnIndex)); } } } /** * Returns the total power accumulated by this builder so far. It may change * by the time the {@code build()} method is called. */ public double getTotalPower() { double totalPowerMah = 0; for (BatteryConsumer.Key key : mData.layout.keys) { if (key.processState == PROCESS_STATE_UNSPECIFIED && key.screenState == SCREEN_STATE_UNSPECIFIED && key.powerState == POWER_STATE_UNSPECIFIED) { totalPowerMah += mData.getDouble(key.mPowerColumnIndex); } } return totalPowerMah; } /** * Creates a read-only object out of the Builder values. */ @NonNull public PowerComponents build() { for (BatteryConsumer.Key key : mData.layout.keys) { if (mMinConsumedPowerThreshold != 0) { if (mData.getDouble(key.mPowerColumnIndex) < mMinConsumedPowerThreshold) { mData.putDouble(key.mPowerColumnIndex, 0); } } } mData.putDouble(mData.layout.totalConsumedPowerColumnIndex, getTotalPower()); return new PowerComponents(this); } } }