/* * 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.BatteryConsumerDataLayout.POWER_MODEL_NOT_INCLUDED; import static android.os.BatteryConsumer.POWER_COMPONENT_ANY; import static android.os.BatteryConsumer.PROCESS_STATE_ANY; import static android.os.BatteryConsumer.PROCESS_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) { if (dimensions.powerComponent != POWER_COMPONENT_ANY) { return mData.getDouble(mData.getKeyOrThrow(dimensions.powerComponent, dimensions.processState).mPowerColumnIndex); } else if (dimensions.processState != PROCESS_STATE_ANY) { if (!mData.layout.processStateDataIncluded) { throw new IllegalArgumentException( "No data included in BatteryUsageStats for " + dimensions); } final BatteryConsumer.Key[] keys = mData.layout.processStateKeys[dimensions.processState]; double totalPowerMah = 0; for (int i = keys.length - 1; i >= 0; i--) { totalPowerMah += mData.getDouble(keys[i].mPowerColumnIndex); } return totalPowerMah; } else { return mData.getDouble(mData.layout.totalConsumedPowerColumnIndex); } } /** * 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) { return mData.getDouble(key.mPowerColumnIndex); } /** * Returns the amount of drain attributed to the specified custom drain type. * * @param componentId The ID of the custom power component. * @return Amount of consumed power in mAh. */ public double getConsumedPowerForCustomComponent(int componentId) { final int index = componentId - BatteryConsumer.FIRST_CUSTOM_POWER_COMPONENT_ID; if (index >= 0 && index < mData.layout.customPowerComponentCount) { return mData.getDouble(mData.layout.firstCustomConsumedPowerColumn + index); } else { throw new IllegalArgumentException( "Unsupported custom power component ID: " + componentId); } } public String getCustomPowerComponentName(int componentId) { final int index = componentId - BatteryConsumer.FIRST_CUSTOM_POWER_COMPONENT_ID; if (index >= 0 && index < mData.layout.customPowerComponentCount) { try { return mData.layout.customPowerComponentNames[index]; } catch (ArrayIndexOutOfBoundsException e) { throw new IllegalArgumentException( "Unsupported custom power component ID: " + componentId); } } else { throw new IllegalArgumentException( "Unsupported custom power component ID: " + componentId); } } @BatteryConsumer.PowerModel int getPowerModel(BatteryConsumer.Key key) { if (key.mPowerModelColumnIndex == POWER_MODEL_NOT_INCLUDED) { throw new IllegalStateException( "Power model IDs were not requested in the BatteryUsageStatsQuery"); } return mData.getInt(key.mPowerModelColumnIndex); } /** * 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) { return mData.getLong(key.mDurationColumnIndex); } /** * Returns the amount of usage time attributed to the specified custom component. * * @param componentId The ID of the custom power component. * @return Amount of time in milliseconds. */ public long getUsageDurationForCustomComponentMillis(int componentId) { final int index = componentId - BatteryConsumer.FIRST_CUSTOM_POWER_COMPONENT_ID; if (index >= 0 && index < mData.layout.customPowerComponentCount) { return mData.getLong(mData.layout.firstCustomUsageDurationColumn + index); } else { throw new IllegalArgumentException( "Unsupported custom power component ID: " + componentId); } } public void dump(PrintWriter pw, boolean skipEmptyComponents) { String separator = ""; StringBuilder sb = new StringBuilder(); for (int componentId = 0; componentId < BatteryConsumer.POWER_COMPONENT_COUNT; componentId++) { for (BatteryConsumer.Key key: mData.getKeys(componentId)) { final double componentPower = getConsumedPower(key); final long durationMs = getUsageDurationMillis(key); if (skipEmptyComponents && componentPower == 0 && durationMs == 0) { continue; } sb.append(separator); separator = " "; sb.append(key.toShortString()); sb.append("="); sb.append(BatteryStats.formatCharge(componentPower)); if (durationMs != 0) { sb.append(" ("); BatteryStats.formatTimeMsNoSpace(sb, durationMs); sb.append(")"); } } } final int customComponentCount = mData.layout.customPowerComponentCount; for (int customComponentId = BatteryConsumer.FIRST_CUSTOM_POWER_COMPONENT_ID; customComponentId < BatteryConsumer.FIRST_CUSTOM_POWER_COMPONENT_ID + customComponentCount; customComponentId++) { final double customComponentPower = getConsumedPowerForCustomComponent(customComponentId); if (skipEmptyComponents && customComponentPower == 0) { continue; } sb.append(separator); separator = " "; sb.append(getCustomPowerComponentName(customComponentId)); sb.append("="); sb.append(BatteryStats.formatCharge(customComponentPower)); } pw.print(sb); } /** Returns whether there are any atoms.proto POWER_COMPONENTS data to write to a proto. */ boolean hasStatsProtoData() { return writeStatsProtoImpl(null); } /** Writes all atoms.proto POWER_COMPONENTS for this PowerComponents to the given proto. */ void writeStatsProto(@NonNull ProtoOutputStream proto) { writeStatsProtoImpl(proto); } /** * Returns whether there are any atoms.proto POWER_COMPONENTS data to write to a proto, * and writes it to the given proto if it is non-null. */ private boolean writeStatsProtoImpl(@Nullable ProtoOutputStream proto) { boolean interestingData = false; for (int componentId = 0; componentId < BatteryConsumer.POWER_COMPONENT_COUNT; componentId++) { final BatteryConsumer.Key[] keys = mData.getKeys(componentId); for (BatteryConsumer.Key key : keys) { final long powerDeciCoulombs = convertMahToDeciCoulombs(getConsumedPower(key)); final long durationMs = getUsageDurationMillis(key); 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_ANY) { writePowerComponentUsage(proto, BatteryUsageStatsAtomsProto.BatteryConsumerData.POWER_COMPONENTS, componentId, powerDeciCoulombs, durationMs); } else { writePowerUsageSlice(proto, componentId, powerDeciCoulombs, durationMs, key.processState); } } } for (int idx = 0; idx < mData.layout.customPowerComponentCount; idx++) { final int componentId = BatteryConsumer.FIRST_CUSTOM_POWER_COMPONENT_ID + idx; final long powerDeciCoulombs = convertMahToDeciCoulombs(getConsumedPowerForCustomComponent(componentId)); final long durationMs = getUsageDurationForCustomComponentMillis(componentId); 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; } writePowerComponentUsage(proto, BatteryUsageStatsAtomsProto.BatteryConsumerData.POWER_COMPONENTS, componentId, powerDeciCoulombs, durationMs); } 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, 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 (int componentId = 0; componentId < BatteryConsumer.POWER_COMPONENT_COUNT; componentId++) { final BatteryConsumer.Key[] keys = mData.getKeys(componentId); for (BatteryConsumer.Key key : keys) { 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, componentId); if (key.processState != PROCESS_STATE_UNSPECIFIED) { serializer.attributeInt(null, BatteryUsageStats.XML_ATTR_PROCESS_STATE, key.processState); } if (powerMah != 0) { serializer.attributeDouble(null, BatteryUsageStats.XML_ATTR_POWER, powerMah); } if (durationMs != 0) { serializer.attributeLong(null, BatteryUsageStats.XML_ATTR_DURATION, durationMs); } if (mData.layout.powerModelsIncluded) { serializer.attributeInt(null, BatteryUsageStats.XML_ATTR_MODEL, getPowerModel(key)); } serializer.endTag(null, BatteryUsageStats.XML_TAG_COMPONENT); } } final int customComponentEnd = BatteryConsumer.FIRST_CUSTOM_POWER_COMPONENT_ID + mData.layout.customPowerComponentCount; for (int componentId = BatteryConsumer.FIRST_CUSTOM_POWER_COMPONENT_ID; componentId < customComponentEnd; componentId++) { final double powerMah = getConsumedPowerForCustomComponent(componentId); final long durationMs = getUsageDurationForCustomComponentMillis(componentId); if (powerMah == 0 && durationMs == 0) { continue; } serializer.startTag(null, BatteryUsageStats.XML_TAG_CUSTOM_COMPONENT); serializer.attributeInt(null, BatteryUsageStats.XML_ATTR_ID, componentId); 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_CUSTOM_COMPONENT); } serializer.endTag(null, BatteryUsageStats.XML_TAG_POWER_COMPONENTS); } 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: { int componentId = -1; int processState = PROCESS_STATE_UNSPECIFIED; double powerMah = 0; long durationMs = 0; int model = BatteryConsumer.POWER_MODEL_UNDEFINED; 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_POWER: powerMah = parser.getAttributeDouble(i); break; case BatteryUsageStats.XML_ATTR_DURATION: durationMs = parser.getAttributeLong(i); break; case BatteryUsageStats.XML_ATTR_MODEL: model = parser.getAttributeInt(i); break; } } final BatteryConsumer.Key key = builder.mData.getKey(componentId, processState); builder.setConsumedPower(key, powerMah, model); builder.setUsageDurationMillis(key, durationMs); break; } case BatteryUsageStats.XML_TAG_CUSTOM_COMPONENT: { int componentId = -1; 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_POWER: powerMah = parser.getAttributeDouble(i); break; case BatteryUsageStats.XML_ATTR_DURATION: durationMs = parser.getAttributeLong(i); break; } } builder.setConsumedPowerForCustomComponent(componentId, powerMah); builder.setUsageDurationForCustomComponentMillis(componentId, durationMs); break; } } } eventType = parser.next(); } } /** * Builder for PowerComponents. */ static final class Builder { private static final byte POWER_MODEL_UNINITIALIZED = -1; private final BatteryConsumer.BatteryConsumerData mData; private final double mMinConsumedPowerThreshold; Builder(BatteryConsumer.BatteryConsumerData data, double minConsumedPowerThreshold) { mData = data; mMinConsumedPowerThreshold = minConsumedPowerThreshold; for (BatteryConsumer.Key[] keys : mData.layout.keys) { for (BatteryConsumer.Key key : keys) { if (key.mPowerModelColumnIndex != POWER_MODEL_NOT_INCLUDED) { mData.putInt(key.mPowerModelColumnIndex, POWER_MODEL_UNINITIALIZED); } } } } @NonNull public Builder setConsumedPower(BatteryConsumer.Key key, double componentPower, int powerModel) { mData.putDouble(key.mPowerColumnIndex, componentPower); if (key.mPowerModelColumnIndex != POWER_MODEL_NOT_INCLUDED) { mData.putInt(key.mPowerModelColumnIndex, powerModel); } return this; } @NonNull public Builder addConsumedPower(BatteryConsumer.Key key, double componentPower, int powerModel) { mData.putDouble(key.mPowerColumnIndex, mData.getDouble(key.mPowerColumnIndex) + componentPower); if (key.mPowerModelColumnIndex != POWER_MODEL_NOT_INCLUDED) { mData.putInt(key.mPowerModelColumnIndex, powerModel); } return this; } /** * Sets the amount of drain attributed to the specified custom drain type. * * @param componentId The ID of the custom power component. * @param componentPower Amount of consumed power in mAh. */ @NonNull public Builder setConsumedPowerForCustomComponent(int componentId, double componentPower) { final int index = componentId - BatteryConsumer.FIRST_CUSTOM_POWER_COMPONENT_ID; if (index < 0 || index >= mData.layout.customPowerComponentCount) { throw new IllegalArgumentException( "Unsupported custom power component ID: " + componentId); } mData.putDouble(mData.layout.firstCustomConsumedPowerColumn + index, componentPower); return this; } @NonNull public Builder setUsageDurationMillis(BatteryConsumer.Key key, long componentUsageDurationMillis) { mData.putLong(key.mDurationColumnIndex, componentUsageDurationMillis); return this; } /** * Sets the amount of time used by the specified custom component. * * @param componentId The ID of the custom power component. * @param componentUsageDurationMillis Amount of time in milliseconds. */ @NonNull public Builder setUsageDurationForCustomComponentMillis(int componentId, long componentUsageDurationMillis) { final int index = componentId - BatteryConsumer.FIRST_CUSTOM_POWER_COMPONENT_ID; if (index < 0 || index >= mData.layout.customPowerComponentCount) { throw new IllegalArgumentException( "Unsupported custom power component ID: " + componentId); } mData.putLong(mData.layout.firstCustomUsageDurationColumn + index, 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 (int componentId = BatteryConsumer.POWER_COMPONENT_COUNT - 1; componentId >= 0; componentId--) { final BatteryConsumer.Key[] keys = mData.layout.keys[componentId]; for (BatteryConsumer.Key key: keys) { BatteryConsumer.Key otherKey = null; for (BatteryConsumer.Key aKey: otherData.layout.keys[componentId]) { if (aKey.equals(key)) { otherKey = aKey; break; } } if (otherKey == null) { continue; } mData.putDouble(key.mPowerColumnIndex, mData.getDouble(key.mPowerColumnIndex) + otherData.getDouble(otherKey.mPowerColumnIndex)); mData.putLong(key.mDurationColumnIndex, mData.getLong(key.mDurationColumnIndex) + otherData.getLong(otherKey.mDurationColumnIndex)); if (key.mPowerModelColumnIndex == POWER_MODEL_NOT_INCLUDED) { continue; } boolean undefined = false; if (otherKey.mPowerModelColumnIndex == POWER_MODEL_NOT_INCLUDED) { undefined = true; } else { final int powerModel = mData.getInt(key.mPowerModelColumnIndex); int otherPowerModel = otherData.getInt(otherKey.mPowerModelColumnIndex); if (powerModel == POWER_MODEL_UNINITIALIZED) { mData.putInt(key.mPowerModelColumnIndex, otherPowerModel); } else if (powerModel != otherPowerModel && otherPowerModel != POWER_MODEL_UNINITIALIZED) { undefined = true; } } if (undefined) { mData.putInt(key.mPowerModelColumnIndex, BatteryConsumer.POWER_MODEL_UNDEFINED); } } } for (int i = mData.layout.customPowerComponentCount - 1; i >= 0; i--) { final int powerColumnIndex = mData.layout.firstCustomConsumedPowerColumn + i; final int otherPowerColumnIndex = otherData.layout.firstCustomConsumedPowerColumn + i; mData.putDouble(powerColumnIndex, mData.getDouble(powerColumnIndex) + otherData.getDouble( otherPowerColumnIndex)); final int usageColumnIndex = mData.layout.firstCustomUsageDurationColumn + i; final int otherDurationColumnIndex = otherData.layout.firstCustomUsageDurationColumn + i; mData.putLong(usageColumnIndex, mData.getLong(usageColumnIndex) + otherData.getLong( otherDurationColumnIndex) ); } } /** * 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 (int componentId = 0; componentId < BatteryConsumer.POWER_COMPONENT_COUNT; componentId++) { totalPowerMah += mData.getDouble( mData.getKeyOrThrow(componentId, PROCESS_STATE_ANY).mPowerColumnIndex); } for (int i = 0; i < mData.layout.customPowerComponentCount; i++) { totalPowerMah += mData.getDouble( mData.layout.firstCustomConsumedPowerColumn + i); } return totalPowerMah; } /** * Creates a read-only object out of the Builder values. */ @NonNull public PowerComponents build() { for (BatteryConsumer.Key[] keys : mData.layout.keys) { for (BatteryConsumer.Key key : keys) { if (key.mPowerModelColumnIndex != POWER_MODEL_NOT_INCLUDED) { if (mData.getInt(key.mPowerModelColumnIndex) == POWER_MODEL_UNINITIALIZED) { mData.putInt(key.mPowerModelColumnIndex, BatteryConsumer.POWER_MODEL_UNDEFINED); } } if (mMinConsumedPowerThreshold != 0) { if (mData.getDouble(key.mPowerColumnIndex) < mMinConsumedPowerThreshold) { mData.putDouble(key.mPowerColumnIndex, 0); } } } } if (mData.getDouble(mData.layout.totalConsumedPowerColumnIndex) == 0) { mData.putDouble(mData.layout.totalConsumedPowerColumnIndex, getTotalPower()); } return new PowerComponents(this); } } }