blob: 4db1f1b32407f5efd3e42c5291a6cdd08204c2c5 [file] [log] [blame] [edit]
/*
* 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);
}
}
}