Initial commit of PowerStatsService
Bug: 164465661
Bug: 164466995
Bug: 167280723
Test: Verified PowerStatsService is functional on targets that
have ODPM data.
Change-Id: Ic555b380c566ea26bc2214374f142c5448ea2ee7
diff --git a/core/java/android/content/Context.java b/core/java/android/content/Context.java
index 98f7887..bd11bc8 100644
--- a/core/java/android/content/Context.java
+++ b/core/java/android/content/Context.java
@@ -3384,6 +3384,7 @@
/** @hide */
@StringDef(suffix = { "_SERVICE" }, value = {
POWER_SERVICE,
+ //@hide: POWER_STATS_SERVICE,
WINDOW_SERVICE,
LAYOUT_INFLATER_SERVICE,
ACCOUNT_SERVICE,
@@ -3726,6 +3727,16 @@
/**
* Use with {@link #getSystemService(String)} to retrieve a
+ * {@link android.os.PowerStatsService} for accessing power stats
+ * service.
+ *
+ * @see #getSystemService(String)
+ * @hide
+ */
+ public static final String POWER_STATS_SERVICE = "power_stats";
+
+ /**
+ * Use with {@link #getSystemService(String)} to retrieve a
* {@link android.os.RecoverySystem} for accessing the recovery system
* service.
*
diff --git a/core/proto/android/os/incident.proto b/core/proto/android/os/incident.proto
index 5609b36..9fed1b9 100644
--- a/core/proto/android/os/incident.proto
+++ b/core/proto/android/os/incident.proto
@@ -40,6 +40,7 @@
import "frameworks/base/core/proto/android/server/jobscheduler.proto";
import "frameworks/base/core/proto/android/server/location/context_hub.proto";
import "frameworks/base/core/proto/android/server/powermanagerservice.proto";
+import "frameworks/base/core/proto/android/server/powerstatsservice.proto";
import "frameworks/base/core/proto/android/server/rolemanagerservice.proto";
import "frameworks/base/core/proto/android/server/windowmanagerservice.proto";
import "frameworks/base/core/proto/android/service/appwidget.proto";
@@ -510,6 +511,11 @@
(section).args = "sensorservice --proto"
];
+ optional com.android.server.powerstats.PowerStatsServiceProto powerstats = 3054 [
+ (section).type = SECTION_DUMPSYS,
+ (section).args = "power_stats --proto"
+ ];
+
// Dumps in text format (on userdebug and eng builds only): 4000 ~ 4999
optional android.util.TextDumpProto textdump_wifi = 4000 [
(section).type = SECTION_TEXT_DUMPSYS,
diff --git a/core/proto/android/server/powerstatsservice.proto b/core/proto/android/server/powerstatsservice.proto
new file mode 100644
index 0000000..c805244
--- /dev/null
+++ b/core/proto/android/server/powerstatsservice.proto
@@ -0,0 +1,67 @@
+/*
+ * 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.
+ */
+
+syntax = "proto2";
+
+package com.android.server.powerstats;
+
+option java_multiple_files = true;
+
+message IncidentReportProto {
+ /** Section number matches that in incident.proto */
+ optional PowerStatsServiceProto incident_report = 3054;
+}
+
+message PowerStatsServiceProto {
+ repeated RailInfoProto rail_info = 1;
+ repeated EnergyDataProto energy_data = 2;
+}
+
+/**
+ * Rail information:
+ * Reports information related to the rails being monitored.
+ */
+message RailInfoProto {
+ /** Index corresponding to the rail */
+ optional int32 index = 1;
+
+ /** Name of the rail (opaque to the framework) */
+ optional string rail_name = 2;
+
+ /** Name of the subsystem to which this rail belongs (opaque to the framework) */
+ optional string subsys_name = 3;
+
+ /** Hardware sampling rate */
+ optional int32 sampling_rate = 4;
+}
+
+/**
+ * Rail level energy measurements:
+ * Reports accumulated energy since boot on each rail.
+ */
+message EnergyDataProto {
+ /**
+ * Index corresponding to the rail. This index matches
+ * the index returned in RailInfo
+ */
+ optional int32 index = 1;
+
+ /** Time since device boot(CLOCK_BOOTTIME) in milli-seconds */
+ optional int64 timestamp_ms = 2;
+
+ /** Accumulated energy since device boot in microwatt-seconds (uWs) */
+ optional int64 energy_uws = 3;
+}
diff --git a/services/core/java/com/android/server/powerstats/BatteryTrigger.java b/services/core/java/com/android/server/powerstats/BatteryTrigger.java
new file mode 100644
index 0000000..c1f97f2
--- /dev/null
+++ b/services/core/java/com/android/server/powerstats/BatteryTrigger.java
@@ -0,0 +1,65 @@
+/*
+ * 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 com.android.server.powerstats;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.os.BatteryManager;
+import android.util.Log;
+
+/**
+ * BatteryTrigger instantiates a BroadcastReceiver that listens for changes
+ * to the battery. When the battery level drops by 1% a message is sent to
+ * the PowerStatsLogger to log the rail energy data to on-device storage.
+ */
+public final class BatteryTrigger extends PowerStatsLogTrigger {
+ private static final String TAG = BatteryTrigger.class.getSimpleName();
+ private static final boolean DEBUG = false;
+
+ private int mBatteryLevel = 0;
+
+ private final BroadcastReceiver mBatteryLevelReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ switch (intent.getAction()) {
+ case Intent.ACTION_BATTERY_CHANGED:
+ int newBatteryLevel = intent.getIntExtra(BatteryManager.EXTRA_LEVEL, 0);
+
+ if (newBatteryLevel < mBatteryLevel) {
+ if (DEBUG) Log.d(TAG, "Battery level dropped. Log rail data");
+ logPowerStatsData();
+ }
+
+ mBatteryLevel = newBatteryLevel;
+ break;
+ }
+ }
+ };
+
+ public BatteryTrigger(Context context, PowerStatsLogger powerStatsLogger,
+ boolean triggerEnabled) {
+ super(context, powerStatsLogger);
+
+ if (triggerEnabled) {
+ IntentFilter filter = new IntentFilter(Intent.ACTION_BATTERY_CHANGED);
+ Intent batteryStatus = mContext.registerReceiver(mBatteryLevelReceiver, filter);
+ mBatteryLevel = batteryStatus.getIntExtra(BatteryManager.EXTRA_LEVEL, 0);
+ }
+ }
+}
diff --git a/services/core/java/com/android/server/powerstats/PowerStatsData.java b/services/core/java/com/android/server/powerstats/PowerStatsData.java
new file mode 100644
index 0000000..755bd5f
--- /dev/null
+++ b/services/core/java/com/android/server/powerstats/PowerStatsData.java
@@ -0,0 +1,286 @@
+/*
+ * 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 com.android.server.powerstats;
+
+import android.util.Log;
+import android.util.proto.ProtoInputStream;
+import android.util.proto.ProtoOutputStream;
+import android.util.proto.ProtoUtils;
+import android.util.proto.WireTypeMismatchException;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * PowerStatsData is a class that performs two operations:
+ * 1) Unpacks serialized protobuf byte arrays, as defined in powerstatsservice.proto,
+ * into RailInfo or EnergyData object arrays.
+ *
+ * 2) Packs RailInfo or EnergyData object arrays in protobuf byte arrays as
+ * defined in powerstatsservice.proto.
+ *
+ * Inside frameworks, proto source is generated with the genstream option
+ * and therefore the getter/setter helper functions are not available.
+ * The protos need to be packed/unpacked in a more manual way using
+ * ProtoOutputStream/ProtoInputStream.
+ */
+public class PowerStatsData {
+ private static final String TAG = PowerStatsData.class.getSimpleName();
+
+ private List<Data> mDataList;
+
+ public PowerStatsData(ProtoInputStream pis) throws IOException {
+ mDataList = new ArrayList<Data>();
+ unpackProto(pis);
+ }
+
+ public PowerStatsData(Data[] data) {
+ mDataList = new ArrayList<Data>(Arrays.asList(data));
+ }
+
+ private void unpackProto(ProtoInputStream pis) throws IOException {
+ long token;
+
+ while (true) {
+ try {
+ switch (pis.nextField()) {
+ case (int) PowerStatsServiceProto.RAIL_INFO:
+ token = pis.start(PowerStatsServiceProto.RAIL_INFO);
+ mDataList.add(new RailInfo(pis));
+ pis.end(token);
+ break;
+
+ case (int) PowerStatsServiceProto.ENERGY_DATA:
+ token = pis.start(PowerStatsServiceProto.ENERGY_DATA);
+ mDataList.add(new EnergyData(pis));
+ pis.end(token);
+ break;
+
+ case ProtoInputStream.NO_MORE_FIELDS:
+ return;
+
+ default:
+ Log.e(TAG, "Unhandled field in proto: "
+ + ProtoUtils.currentFieldToString(pis));
+ break;
+ }
+ } catch (WireTypeMismatchException wtme) {
+ Log.e(TAG, "Wire Type mismatch in proto: " + ProtoUtils.currentFieldToString(pis));
+ }
+ }
+ }
+
+ /**
+ * Write this object to an output stream in protobuf format.
+ *
+ * @param pos ProtoOutputStream of file where data is to be written. Data is
+ * written in protobuf format as defined by powerstatsservice.proto.
+ */
+ public void toProto(ProtoOutputStream pos) {
+ long token;
+
+ for (Data data : mDataList) {
+ if (data instanceof RailInfo) {
+ token = pos.start(PowerStatsServiceProto.RAIL_INFO);
+ } else {
+ token = pos.start(PowerStatsServiceProto.ENERGY_DATA);
+ }
+ data.toProto(pos);
+ pos.end(token);
+ }
+ }
+
+ /**
+ * Convert mDataList to proto format and return the serialized byte array.
+ *
+ * @return byte array containing a serialized protobuf of mDataList.
+ */
+ public byte[] getProtoBytes() {
+ ProtoOutputStream pos = new ProtoOutputStream();
+ long token;
+
+ for (Data data : mDataList) {
+ if (data instanceof RailInfo) {
+ token = pos.start(PowerStatsServiceProto.RAIL_INFO);
+ } else {
+ token = pos.start(PowerStatsServiceProto.ENERGY_DATA);
+ }
+ data.toProto(pos);
+ pos.end(token);
+ }
+ return pos.getBytes();
+ }
+
+ /**
+ * Print this object to logcat.
+ */
+ public void print() {
+ for (Data data : mDataList) {
+ Log.d(TAG, data.toString());
+ }
+ }
+
+ /**
+ * RailInfo is a class that stores a description for an individual ODPM
+ * rail. It provides functionality to unpack a RailInfo object from a
+ * serialized protobuf byte array, and to pack a RailInfo object into
+ * a ProtoOutputStream.
+ */
+ public static class RailInfo extends Data {
+ public String mRailName;
+ public String mSubSysName;
+ public long mSamplingRate;
+
+ public RailInfo(ProtoInputStream pis) throws IOException {
+ unpackProto(pis);
+ }
+
+ public RailInfo(long index, String railName, String subSysName, long samplingRate) {
+ mIndex = index;
+ mRailName = railName;
+ mSubSysName = subSysName;
+ mSamplingRate = samplingRate;
+ }
+
+ @Override
+ protected void unpackProto(ProtoInputStream pis) throws IOException {
+ while (true) {
+ try {
+ switch (pis.nextField()) {
+ case (int) RailInfoProto.INDEX:
+ mIndex = pis.readInt(RailInfoProto.INDEX);
+ break;
+
+ case (int) RailInfoProto.RAIL_NAME:
+ mRailName = pis.readString(RailInfoProto.RAIL_NAME);
+ break;
+
+ case (int) RailInfoProto.SUBSYS_NAME:
+ mSubSysName = pis.readString(RailInfoProto.SUBSYS_NAME);
+ break;
+
+ case (int) RailInfoProto.SAMPLING_RATE:
+ mSamplingRate = pis.readInt(RailInfoProto.SAMPLING_RATE);
+ break;
+
+ case ProtoInputStream.NO_MORE_FIELDS:
+ return;
+
+ default:
+ Log.e(TAG, "Unhandled field in RailInfoProto: "
+ + ProtoUtils.currentFieldToString(pis));
+ break;
+ }
+ } catch (WireTypeMismatchException wtme) {
+ Log.e(TAG, "Wire Type mismatch in RailInfoProto: "
+ + ProtoUtils.currentFieldToString(pis));
+ }
+ }
+ }
+
+ @Override
+ public void toProto(ProtoOutputStream pos) {
+ pos.write(RailInfoProto.INDEX, mIndex);
+ pos.write(RailInfoProto.RAIL_NAME, mRailName);
+ pos.write(RailInfoProto.SUBSYS_NAME, mSubSysName);
+ pos.write(RailInfoProto.SAMPLING_RATE, mSamplingRate);
+ }
+
+ @Override
+ public String toString() {
+ return String.format("Index = " + mIndex
+ + ", RailName = " + mRailName
+ + ", SubSysName = " + mSubSysName
+ + ", SamplingRate = " + mSamplingRate);
+ }
+ }
+
+ /**
+ * EnergyData is a class that stores an energy (uWs) data reading for an
+ * individual ODPM rail. It provides functionality to unpack an EnergyData
+ * object from a serialized protobuf byte array, and to pack an EnergyData
+ * object into a ProtoOutputStream.
+ */
+ public static class EnergyData extends Data {
+ public long mTimestampMs;
+ public long mEnergyUWs;
+
+ public EnergyData(ProtoInputStream pis) throws IOException {
+ unpackProto(pis);
+ }
+
+ public EnergyData(long index, long timestampMs, long energyUWs) {
+ mIndex = index;
+ mTimestampMs = timestampMs;
+ mEnergyUWs = energyUWs;
+ }
+
+ @Override
+ protected void unpackProto(ProtoInputStream pis) throws IOException {
+ while (true) {
+ try {
+ switch (pis.nextField()) {
+ case (int) EnergyDataProto.INDEX:
+ mIndex = pis.readInt(EnergyDataProto.INDEX);
+ break;
+
+ case (int) EnergyDataProto.TIMESTAMP_MS:
+ mTimestampMs = pis.readLong(EnergyDataProto.TIMESTAMP_MS);
+ break;
+
+ case (int) EnergyDataProto.ENERGY_UWS:
+ mEnergyUWs = pis.readLong(EnergyDataProto.ENERGY_UWS);
+ break;
+
+ case ProtoInputStream.NO_MORE_FIELDS:
+ return;
+
+ default:
+ Log.e(TAG, "Unhandled field in EnergyDataProto: "
+ + ProtoUtils.currentFieldToString(pis));
+ break;
+ }
+ } catch (WireTypeMismatchException wtme) {
+ Log.e(TAG, "Wire Type mismatch in EnergyDataProto: "
+ + ProtoUtils.currentFieldToString(pis));
+ }
+ }
+ }
+
+ @Override
+ protected void toProto(ProtoOutputStream pos) {
+ pos.write(EnergyDataProto.INDEX, mIndex);
+ pos.write(EnergyDataProto.TIMESTAMP_MS, mTimestampMs);
+ pos.write(EnergyDataProto.ENERGY_UWS, mEnergyUWs);
+ }
+
+ @Override
+ public String toString() {
+ return String.format("Index = " + mIndex
+ + ", Timestamp (ms) = " + mTimestampMs
+ + ", Energy (uWs) = " + mEnergyUWs);
+ }
+ }
+
+ private abstract static class Data {
+ public long mIndex;
+ protected abstract void unpackProto(ProtoInputStream pis) throws IOException;
+ protected abstract void toProto(ProtoOutputStream pos);
+ }
+}
diff --git a/services/core/java/com/android/server/powerstats/PowerStatsDataStorage.java b/services/core/java/com/android/server/powerstats/PowerStatsDataStorage.java
new file mode 100644
index 0000000..84a6fc9
--- /dev/null
+++ b/services/core/java/com/android/server/powerstats/PowerStatsDataStorage.java
@@ -0,0 +1,223 @@
+/*
+ * 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 com.android.server.powerstats;
+
+import android.content.Context;
+import android.util.Log;
+
+import com.android.internal.util.FileRotator;
+
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.nio.ByteBuffer;
+import java.util.concurrent.locks.ReentrantLock;
+
+/**
+ * PowerStatsDataStorage implements the on-device storage cache for energy
+ * data. This data must be persisted across boot cycles so we store it
+ * on-device. Versioning of this data is handled by deleting any data that
+ * does not match the current version. The cache is implemented as a circular
+ * buffer using the FileRotator class in android.util. We maintain 48 hours
+ * worth of logs in 12 files (4 hours each).
+ */
+public class PowerStatsDataStorage {
+ private static final String TAG = PowerStatsDataStorage.class.getSimpleName();
+
+ private static final long MILLISECONDS_PER_HOUR = 1000 * 60 * 60;
+ // Rotate files every 4 hours.
+ private static final long ROTATE_AGE_MILLIS = 4 * MILLISECONDS_PER_HOUR;
+ // Store 48 hours worth of data.
+ private static final long DELETE_AGE_MILLIS = 48 * MILLISECONDS_PER_HOUR;
+
+ private final ReentrantLock mLock = new ReentrantLock();
+ private File mDataStorageDir;
+ private final FileRotator mFileRotator;
+
+ private static class DataElement {
+ private static final int LENGTH_FIELD_WIDTH = 4;
+ private static final int MAX_DATA_ELEMENT_SIZE = 1000;
+
+ private byte[] mData;
+
+ private byte[] toByteArray() throws IOException {
+ ByteArrayOutputStream data = new ByteArrayOutputStream();
+ data.write(ByteBuffer.allocate(LENGTH_FIELD_WIDTH).putInt(mData.length).array());
+ data.write(mData);
+ return data.toByteArray();
+ }
+
+ protected byte[] getData() {
+ return mData;
+ }
+
+ private DataElement(byte[] data) {
+ mData = data;
+ }
+
+ private DataElement(InputStream in) throws IOException {
+ byte[] lengthBytes = new byte[LENGTH_FIELD_WIDTH];
+ int bytesRead = in.read(lengthBytes);
+ mData = new byte[0];
+
+ if (bytesRead == LENGTH_FIELD_WIDTH) {
+ int length = ByteBuffer.wrap(lengthBytes).getInt();
+
+ if (0 < length && length < MAX_DATA_ELEMENT_SIZE) {
+ mData = new byte[length];
+ bytesRead = in.read(mData);
+
+ if (bytesRead != length) {
+ throw new IOException("Invalid bytes read, expected: " + length
+ + ", actual: " + bytesRead);
+ }
+ } else {
+ throw new IOException("DataElement size is invalid: " + length);
+ }
+ } else {
+ throw new IOException("Did not read " + LENGTH_FIELD_WIDTH + " bytes (" + bytesRead
+ + ")");
+ }
+ }
+ }
+
+ /**
+ * Used by external classes to read DataElements from on-device storage.
+ * This callback is passed in to the read() function and is called for
+ * each DataElement read from on-device storage.
+ */
+ public interface DataElementReadCallback {
+ /**
+ * When performing a read of the on-device storage this callback
+ * must be passed in to the read function. The function will be
+ * called for each DataElement read from on-device storage.
+ *
+ * @param data Byte array containing a DataElement payload.
+ */
+ void onReadDataElement(byte[] data);
+ }
+
+ private static class DataReader implements FileRotator.Reader {
+ private DataElementReadCallback mCallback;
+
+ DataReader(DataElementReadCallback callback) {
+ mCallback = callback;
+ }
+
+ @Override
+ public void read(InputStream in) throws IOException {
+ while (in.available() > 0) {
+ try {
+ DataElement dataElement = new DataElement(in);
+ mCallback.onReadDataElement(dataElement.getData());
+ } catch (IOException e) {
+ Log.e(TAG, "Failed to read from storage. " + e.getMessage());
+ }
+ }
+ }
+ }
+
+ private static class DataRewriter implements FileRotator.Rewriter {
+ byte[] mActiveFileData;
+ byte[] mNewData;
+
+ DataRewriter(byte[] data) {
+ mActiveFileData = new byte[0];
+ mNewData = data;
+ }
+
+ @Override
+ public void reset() {
+ // ignored
+ }
+
+ @Override
+ public void read(InputStream in) throws IOException {
+ mActiveFileData = new byte[in.available()];
+ in.read(mActiveFileData);
+ }
+
+ @Override
+ public boolean shouldWrite() {
+ return true;
+ }
+
+ @Override
+ public void write(OutputStream out) throws IOException {
+ out.write(mActiveFileData);
+ out.write(mNewData);
+ }
+ }
+
+ public PowerStatsDataStorage(Context context, File dataStoragePath,
+ String dataStorageFilename) {
+ mDataStorageDir = dataStoragePath;
+
+ if (!mDataStorageDir.exists() && !mDataStorageDir.mkdirs()) {
+ Log.wtf(TAG, "mDataStorageDir does not exist: " + mDataStorageDir.getPath());
+ mFileRotator = null;
+ } else {
+ // Delete files written with an old version number. The version is included in the
+ // filename, so any files that don't match the current version number can be deleted.
+ File[] files = mDataStorageDir.listFiles();
+ for (int i = 0; i < files.length; i++) {
+ if (!files[i].getName().matches(dataStorageFilename + "(.*)")) {
+ files[i].delete();
+ }
+ }
+
+ mFileRotator = new FileRotator(mDataStorageDir,
+ dataStorageFilename,
+ ROTATE_AGE_MILLIS,
+ DELETE_AGE_MILLIS);
+ }
+ }
+
+ /**
+ * Writes data stored in PowerStatsDataStorage to a file descriptor.
+ *
+ * @param data Byte array to write to on-device storage. Byte array is
+ * converted to a DataElement which prefixes the payload with
+ * the data length. The DataElement is then converted to a byte
+ * array and written to on-device storage.
+ */
+ public void write(byte[] data) {
+ mLock.lock();
+
+ long currentTimeMillis = System.currentTimeMillis();
+ try {
+ DataElement dataElement = new DataElement(data);
+ mFileRotator.rewriteActive(new DataRewriter(dataElement.toByteArray()),
+ currentTimeMillis);
+ mFileRotator.maybeRotate(currentTimeMillis);
+ } catch (IOException e) {
+ Log.e(TAG, "Failed to write to on-device storage: " + e);
+ }
+
+ mLock.unlock();
+ }
+
+ /**
+ * Reads all DataElements stored in on-device storage. For each
+ * DataElement retrieved from on-device storage, callback is called.
+ */
+ public void read(DataElementReadCallback callback) throws IOException {
+ mFileRotator.readMatching(new DataReader(callback), Long.MIN_VALUE, Long.MAX_VALUE);
+ }
+}
diff --git a/services/core/java/com/android/server/powerstats/PowerStatsHALWrapper.java b/services/core/java/com/android/server/powerstats/PowerStatsHALWrapper.java
new file mode 100644
index 0000000..dc996a3
--- /dev/null
+++ b/services/core/java/com/android/server/powerstats/PowerStatsHALWrapper.java
@@ -0,0 +1,105 @@
+/*
+ * 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 com.android.server.powerstats;
+
+/**
+ * PowerStatsHALWrapper is a wrapper class for the PowerStats HAL API calls.
+ */
+public final class PowerStatsHALWrapper {
+ private static final String TAG = PowerStatsHALWrapper.class.getSimpleName();
+
+ /**
+ * IPowerStatsHALWrapper defines the interface to the PowerStatsHAL.
+ */
+ public interface IPowerStatsHALWrapper {
+ /**
+ * Returns rail info for all available ODPM rails.
+ *
+ * @return array of RailInfo objects containing rail info for all
+ * available rails.
+ */
+ PowerStatsData.RailInfo[] readRailInfo();
+
+ /**
+ * Returns energy data for all available ODPM rails. Available rails can
+ * be retrieved by calling nativeGetRailInfo. Energy data and
+ * rail info can be linked through the index field.
+ *
+ * @return array of EnergyData objects containing energy data for all
+ * available rails.
+ */
+ PowerStatsData.EnergyData[] readEnergyData();
+
+ /**
+ * Returns boolean indicating if connection to power stats HAL was
+ * established.
+ *
+ * @return true if connection to power stats HAL was correctly established
+ * and that energy data and rail info can be read from the interface.
+ * false otherwise
+ */
+ boolean initialize();
+ }
+
+ /**
+ * PowerStatsHALWrapperImpl is the implementation of the IPowerStatsHALWrapper
+ * used by the PowerStatsService. Other implementations will be used by the testing
+ * framework and will be passed into the PowerStatsService through an injector.
+ */
+ public static final class PowerStatsHALWrapperImpl implements IPowerStatsHALWrapper {
+ private static native boolean nativeInit();
+ private static native PowerStatsData.RailInfo[] nativeGetRailInfo();
+ private static native PowerStatsData.EnergyData[] nativeGetEnergyData();
+
+ /**
+ * Returns rail info for all available ODPM rails.
+ *
+ * @return array of RailInfo objects containing rail info for all
+ * available rails.
+ */
+ @Override
+ public PowerStatsData.RailInfo[] readRailInfo() {
+ return nativeGetRailInfo();
+ }
+
+ /**
+ * Returns energy data for all available ODPM rails. Available rails can
+ * be retrieved by calling nativeGetRailInfo. Energy data and
+ * rail info can be linked through the index field.
+ *
+ * @return array of EnergyData objects containing energy data for all
+ * available rails.
+ */
+ @Override
+ public PowerStatsData.EnergyData[] readEnergyData() {
+ return nativeGetEnergyData();
+ }
+
+ /**
+ * Returns boolean indicating if connection to power stats HAL was
+ * established.
+ *
+ * @return true if connection to power stats HAL was correctly established
+ * and that energy data and rail info can be read from the interface.
+ * false otherwise
+ */
+ @Override
+ public boolean initialize() {
+ return nativeInit();
+ }
+ }
+}
diff --git a/services/core/java/com/android/server/powerstats/PowerStatsLogTrigger.java b/services/core/java/com/android/server/powerstats/PowerStatsLogTrigger.java
new file mode 100644
index 0000000..1754185
--- /dev/null
+++ b/services/core/java/com/android/server/powerstats/PowerStatsLogTrigger.java
@@ -0,0 +1,44 @@
+/*
+ * 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 com.android.server.powerstats;
+
+import android.content.Context;
+import android.os.Message;
+
+/**
+ * PowerStatsLogTrigger is the base class for other trigger classes.
+ * It provides the logPowerStatsData() function which sends a message
+ * to the PowerStatsLogger to read the rail energy data and log it to
+ * on-device storage. This class is abstract and cannot be instantiated.
+ */
+public abstract class PowerStatsLogTrigger {
+ private static final String TAG = PowerStatsLogTrigger.class.getSimpleName();
+
+ protected Context mContext;
+ private PowerStatsLogger mPowerStatsLogger;
+
+ protected void logPowerStatsData() {
+ Message.obtain(
+ mPowerStatsLogger,
+ PowerStatsLogger.MSG_LOG_TO_DATA_STORAGE).sendToTarget();
+ }
+
+ public PowerStatsLogTrigger(Context context, PowerStatsLogger powerStatsLogger) {
+ mContext = context;
+ mPowerStatsLogger = powerStatsLogger;
+ }
+}
diff --git a/services/core/java/com/android/server/powerstats/PowerStatsLogger.java b/services/core/java/com/android/server/powerstats/PowerStatsLogger.java
new file mode 100644
index 0000000..71a34a4
--- /dev/null
+++ b/services/core/java/com/android/server/powerstats/PowerStatsLogger.java
@@ -0,0 +1,109 @@
+/*
+ * 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 com.android.server.powerstats;
+
+import android.content.Context;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.util.Log;
+import android.util.proto.ProtoInputStream;
+import android.util.proto.ProtoOutputStream;
+
+import com.android.server.powerstats.PowerStatsHALWrapper.IPowerStatsHALWrapper;
+
+import java.io.ByteArrayInputStream;
+import java.io.File;
+import java.io.FileDescriptor;
+import java.io.IOException;
+
+/**
+ * PowerStatsLogger is responsible for logging energy data to on-device
+ * storage. Messages are sent to its message handler to request that energy
+ * data be logged, at which time it queries the PowerStats HAL and logs the
+ * data to on-device storage. The on-device storage is dumped to file by
+ * calling writeToFile with a file descriptor that points to the output file.
+ */
+public final class PowerStatsLogger extends Handler {
+ private static final String TAG = PowerStatsLogger.class.getSimpleName();
+ private static final boolean DEBUG = false;
+ protected static final int MSG_LOG_TO_DATA_STORAGE = 0;
+
+ private final PowerStatsDataStorage mPowerStatsDataStorage;
+ private final IPowerStatsHALWrapper mPowerStatsHALWrapper;
+
+ @Override
+ public void handleMessage(Message msg) {
+ switch (msg.what) {
+ case MSG_LOG_TO_DATA_STORAGE:
+ if (DEBUG) Log.d(TAG, "Logging to data storage");
+ PowerStatsData energyData =
+ new PowerStatsData(mPowerStatsHALWrapper.readEnergyData());
+ mPowerStatsDataStorage.write(energyData.getProtoBytes());
+ break;
+ }
+ }
+
+ /**
+ * Writes data stored in PowerStatsDataStorage to a file descriptor.
+ *
+ * @param fd FileDescriptor where data stored in PowerStatsDataStorage is
+ * written. Data is written in protobuf format as defined by
+ * powerstatsservice.proto.
+ */
+ public void writeToFile(FileDescriptor fd) {
+ if (DEBUG) Log.d(TAG, "Writing to file");
+
+ final ProtoOutputStream pos = new ProtoOutputStream(fd);
+
+ try {
+ PowerStatsData railInfo = new PowerStatsData(mPowerStatsHALWrapper.readRailInfo());
+ railInfo.toProto(pos);
+ if (DEBUG) railInfo.print();
+
+ mPowerStatsDataStorage.read(new PowerStatsDataStorage.DataElementReadCallback() {
+ @Override
+ public void onReadDataElement(byte[] data) {
+ try {
+ final ProtoInputStream pis =
+ new ProtoInputStream(new ByteArrayInputStream(data));
+ // TODO(b/166535853): ProtoOutputStream doesn't provide a method to write
+ // a byte array that already contains a serialized proto, so I have to
+ // deserialize, then re-serialize. This is computationally inefficient.
+ final PowerStatsData energyData = new PowerStatsData(pis);
+ energyData.toProto(pos);
+ if (DEBUG) energyData.print();
+ } catch (IOException e) {
+ Log.e(TAG, "Failed to write energy data to incident report.");
+ }
+ }
+ });
+ } catch (IOException e) {
+ Log.e(TAG, "Failed to write rail info to incident report.");
+ }
+
+ pos.flush();
+ }
+
+ public PowerStatsLogger(Context context, File dataStoragePath, String dataStorageFilename,
+ IPowerStatsHALWrapper powerStatsHALWrapper) {
+ super(Looper.getMainLooper());
+ mPowerStatsHALWrapper = powerStatsHALWrapper;
+ mPowerStatsDataStorage = new PowerStatsDataStorage(context, dataStoragePath,
+ dataStorageFilename);
+ }
+}
diff --git a/services/core/java/com/android/server/powerstats/PowerStatsService.java b/services/core/java/com/android/server/powerstats/PowerStatsService.java
new file mode 100644
index 0000000..fd609c1
--- /dev/null
+++ b/services/core/java/com/android/server/powerstats/PowerStatsService.java
@@ -0,0 +1,135 @@
+/*
+ * 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 com.android.server.powerstats;
+
+import android.content.Context;
+import android.os.Binder;
+import android.os.Environment;
+import android.os.UserHandle;
+import android.util.Log;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.util.DumpUtils;
+import com.android.server.SystemService;
+import com.android.server.powerstats.PowerStatsHALWrapper.IPowerStatsHALWrapper;
+import com.android.server.powerstats.PowerStatsHALWrapper.PowerStatsHALWrapperImpl;
+
+import java.io.File;
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+
+/**
+ * This class provides a system service that estimates system power usage
+ * per subsystem (modem, wifi, gps, display, etc) and provides those power
+ * estimates to subscribers.
+ */
+public class PowerStatsService extends SystemService {
+ private static final String TAG = PowerStatsService.class.getSimpleName();
+ private static final boolean DEBUG = false;
+ private static final String DATA_STORAGE_SUBDIR = "powerstats";
+ private static final int DATA_STORAGE_VERSION = 0;
+ private static final String DATA_STORAGE_FILENAME = "log.powerstats." + DATA_STORAGE_VERSION;
+
+ private final Injector mInjector;
+
+ private Context mContext;
+ private IPowerStatsHALWrapper mPowerStatsHALWrapper;
+ private PowerStatsLogger mPowerStatsLogger;
+ private BatteryTrigger mBatteryTrigger;
+ private TimerTrigger mTimerTrigger;
+
+ @VisibleForTesting
+ static class Injector {
+ File createDataStoragePath() {
+ return new File(Environment.getDataSystemDeDirectory(UserHandle.USER_SYSTEM),
+ DATA_STORAGE_SUBDIR);
+ }
+
+ String createDataStorageFilename() {
+ return DATA_STORAGE_FILENAME;
+ }
+
+ IPowerStatsHALWrapper createPowerStatsHALWrapperImpl() {
+ return new PowerStatsHALWrapperImpl();
+ }
+
+ PowerStatsLogger createPowerStatsLogger(Context context, File dataStoragePath,
+ String dataStorageFilename, IPowerStatsHALWrapper powerStatsHALWrapper) {
+ return new PowerStatsLogger(context, dataStoragePath, dataStorageFilename,
+ powerStatsHALWrapper);
+ }
+
+ BatteryTrigger createBatteryTrigger(Context context, PowerStatsLogger powerStatsLogger) {
+ return new BatteryTrigger(context, powerStatsLogger, true /* trigger enabled */);
+ }
+
+ TimerTrigger createTimerTrigger(Context context, PowerStatsLogger powerStatsLogger) {
+ return new TimerTrigger(context, powerStatsLogger, true /* trigger enabled */);
+ }
+ }
+
+ private final class BinderService extends Binder {
+ @Override
+ protected void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
+ if (!DumpUtils.checkDumpPermission(mContext, TAG, pw)) return;
+
+ if (args.length > 0 && "--proto".equals(args[0])) {
+ mPowerStatsLogger.writeToFile(fd);
+ }
+ }
+ }
+
+ @Override
+ public void onBootPhase(int phase) {
+ if (phase == SystemService.PHASE_BOOT_COMPLETED) {
+ onSystemServiceReady();
+ }
+ }
+
+ @Override
+ public void onStart() {
+ publishBinderService(Context.POWER_STATS_SERVICE, new BinderService());
+ }
+
+ private void onSystemServiceReady() {
+ mPowerStatsHALWrapper = mInjector.createPowerStatsHALWrapperImpl();
+
+ if (mPowerStatsHALWrapper.initialize()) {
+ if (DEBUG) Log.d(TAG, "Starting PowerStatsService");
+
+ // Only start logger and triggers if initialization is successful.
+ mPowerStatsLogger = mInjector.createPowerStatsLogger(mContext,
+ mInjector.createDataStoragePath(), mInjector.createDataStorageFilename(),
+ mPowerStatsHALWrapper);
+ mBatteryTrigger = mInjector.createBatteryTrigger(mContext, mPowerStatsLogger);
+ mTimerTrigger = mInjector.createTimerTrigger(mContext, mPowerStatsLogger);
+ } else {
+ Log.e(TAG, "Initialization of PowerStatsHAL wrapper failed");
+ }
+ }
+
+ public PowerStatsService(Context context) {
+ this(context, new Injector());
+ }
+
+ @VisibleForTesting
+ public PowerStatsService(Context context, Injector injector) {
+ super(context);
+ mContext = context;
+ mInjector = injector;
+ }
+}
diff --git a/services/core/java/com/android/server/powerstats/TEST_MAPPING b/services/core/java/com/android/server/powerstats/TEST_MAPPING
new file mode 100644
index 0000000..79224a5
--- /dev/null
+++ b/services/core/java/com/android/server/powerstats/TEST_MAPPING
@@ -0,0 +1,12 @@
+{
+ "presubmit": [
+ {
+ "name": "FrameworksServicesTests",
+ "options": [
+ {
+ "include-filter": "com.android.server.powerstats"
+ }
+ ]
+ }
+ ]
+}
diff --git a/services/core/java/com/android/server/powerstats/TimerTrigger.java b/services/core/java/com/android/server/powerstats/TimerTrigger.java
new file mode 100644
index 0000000..a9bee8b
--- /dev/null
+++ b/services/core/java/com/android/server/powerstats/TimerTrigger.java
@@ -0,0 +1,56 @@
+/*
+ * 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 com.android.server.powerstats;
+
+import android.content.Context;
+import android.os.Handler;
+import android.util.Log;
+
+/**
+ * TimerTrigger sets a 60 second opportunistic timer using postDelayed.
+ * When the timer expires a message is sent to the PowerStatsLogger to
+ * read the rail energy data and log it to on-device storage.
+ */
+public final class TimerTrigger extends PowerStatsLogTrigger {
+ private static final String TAG = TimerTrigger.class.getSimpleName();
+ private static final boolean DEBUG = false;
+ // TODO(b/166689029): Make configurable through global settings.
+ private static final long LOG_PERIOD_MS = 60 * 1000;
+
+ private final Handler mHandler;
+
+ private Runnable mLogData = new Runnable() {
+ @Override
+ public void run() {
+ // Do not wake the device for these messages. Opportunistically log rail data every
+ // LOG_PERIOD_MS.
+ mHandler.postDelayed(mLogData, LOG_PERIOD_MS);
+ if (DEBUG) Log.d(TAG, "Received delayed message. Log rail data");
+ logPowerStatsData();
+ }
+ };
+
+ public TimerTrigger(Context context, PowerStatsLogger powerStatsLogger,
+ boolean triggerEnabled) {
+ super(context, powerStatsLogger);
+ mHandler = mContext.getMainThreadHandler();
+
+ if (triggerEnabled) {
+ mLogData.run();
+ }
+ }
+}
diff --git a/services/core/jni/Android.bp b/services/core/jni/Android.bp
index d84f9d1..1b649fd 100644
--- a/services/core/jni/Android.bp
+++ b/services/core/jni/Android.bp
@@ -37,6 +37,7 @@
"com_android_server_locksettings_SyntheticPasswordManager.cpp",
"com_android_server_net_NetworkStatsService.cpp",
"com_android_server_power_PowerManagerService.cpp",
+ "com_android_server_powerstats_PowerStatsService.cpp",
"com_android_server_security_VerityUtils.cpp",
"com_android_server_SerialService.cpp",
"com_android_server_soundtrigger_middleware_AudioSessionProviderImpl.cpp",
diff --git a/services/core/jni/com_android_server_powerstats_PowerStatsService.cpp b/services/core/jni/com_android_server_powerstats_PowerStatsService.cpp
new file mode 100644
index 0000000..5eb6b73
--- /dev/null
+++ b/services/core/jni/com_android_server_powerstats_PowerStatsService.cpp
@@ -0,0 +1,222 @@
+/*
+ * 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.
+ */
+
+#define LOG_TAG "PowerStatsService"
+
+#include <android/hardware/power/stats/1.0/IPowerStats.h>
+#include <jni.h>
+#include <nativehelper/JNIHelp.h>
+
+#include <log/log.h>
+
+using android::hardware::hidl_vec;
+using android::hardware::Return;
+using android::hardware::power::stats::V1_0::EnergyData;
+using android::hardware::power::stats::V1_0::RailInfo;
+using android::hardware::power::stats::V1_0::Status;
+
+static jclass class_railInfo;
+static jmethodID method_railInfoInit;
+static jclass class_energyData;
+static jmethodID method_energyDataInit;
+
+namespace android {
+
+static std::mutex gPowerStatsHalMutex;
+static sp<android::hardware::power::stats::V1_0::IPowerStats> gPowerStatsHalV1_0_ptr = nullptr;
+
+static void deinitPowerStats() {
+ gPowerStatsHalV1_0_ptr = nullptr;
+}
+
+struct PowerStatsHalDeathRecipient : virtual public hardware::hidl_death_recipient {
+ virtual void serviceDied(uint64_t cookie,
+ const wp<android::hidl::base::V1_0::IBase> &who) override {
+ // The HAL just died. Reset all handles to HAL services.
+ std::lock_guard<std::mutex> lock(gPowerStatsHalMutex);
+ deinitPowerStats();
+ }
+};
+
+sp<PowerStatsHalDeathRecipient> gPowerStatsHalDeathRecipient = new PowerStatsHalDeathRecipient();
+
+static bool connectToPowerStatsHal() {
+ if (gPowerStatsHalV1_0_ptr == nullptr) {
+ gPowerStatsHalV1_0_ptr = android::hardware::power::stats::V1_0::IPowerStats::getService();
+
+ if (gPowerStatsHalV1_0_ptr == nullptr) {
+ ALOGE("Unable to get power.stats HAL service.");
+ return false;
+ }
+
+ // Link death recipient to power.stats service handle
+ hardware::Return<bool> linked =
+ gPowerStatsHalV1_0_ptr->linkToDeath(gPowerStatsHalDeathRecipient, 0);
+ if (!linked.isOk()) {
+ ALOGE("Transaction error in linking to power.stats HAL death: %s",
+ linked.description().c_str());
+ deinitPowerStats();
+ return false;
+ } else if (!linked) {
+ ALOGW("Unable to link to power.stats HAL death notifications");
+ return false;
+ }
+ }
+ return true;
+}
+
+static bool checkResult(const Return<void> &ret, const char *function) {
+ if (!ret.isOk()) {
+ ALOGE("%s failed: requested HAL service not available. Description: %s", function,
+ ret.description().c_str());
+ if (ret.isDeadObject()) {
+ deinitPowerStats();
+ }
+ return false;
+ }
+ return true;
+}
+
+static jobjectArray nativeGetRailInfo(JNIEnv *env, jclass clazz) {
+ std::lock_guard<std::mutex> lock(gPowerStatsHalMutex);
+
+ if (!connectToPowerStatsHal()) {
+ ALOGE("nativeGetRailInfo failed to connect to power.stats HAL");
+ return nullptr;
+ }
+
+ hidl_vec<RailInfo> list;
+ Return<void> ret = gPowerStatsHalV1_0_ptr->getRailInfo([&list](auto rails, auto status) {
+ if (status != Status::SUCCESS) {
+ ALOGW("Rail information is not available");
+ } else {
+ list = std::move(rails);
+ }
+ });
+
+ if (!checkResult(ret, __func__)) {
+ ALOGE("getRailInfo failed");
+ return nullptr;
+ } else {
+ jobjectArray railInfoArray = env->NewObjectArray(list.size(), class_railInfo, nullptr);
+ for (int i = 0; i < list.size(); i++) {
+ jstring railName = env->NewStringUTF(list[i].railName.c_str());
+ jstring subsysName = env->NewStringUTF(list[i].subsysName.c_str());
+ jobject railInfo = env->NewObject(class_railInfo, method_railInfoInit, list[i].index,
+ railName, subsysName, list[i].samplingRate);
+ env->SetObjectArrayElement(railInfoArray, i, railInfo);
+ env->DeleteLocalRef(railName);
+ env->DeleteLocalRef(subsysName);
+ env->DeleteLocalRef(railInfo);
+ }
+ return railInfoArray;
+ }
+}
+
+static jobjectArray nativeGetEnergyData(JNIEnv *env, jclass clazz) {
+ std::lock_guard<std::mutex> lock(gPowerStatsHalMutex);
+
+ if (!connectToPowerStatsHal()) {
+ ALOGE("nativeGetEnergy failed to connect to power.stats HAL");
+ }
+
+ hidl_vec<EnergyData> list;
+ Return<void> ret =
+ gPowerStatsHalV1_0_ptr->getEnergyData({}, [&list](auto energyData, auto status) {
+ if (status != Status::SUCCESS) {
+ ALOGW("getEnergyData is not supported");
+ } else {
+ list = std::move(energyData);
+ }
+ });
+
+ if (!checkResult(ret, __func__)) {
+ ALOGE("getEnergyData failed");
+ return nullptr;
+ } else {
+ jobjectArray energyDataArray = env->NewObjectArray(list.size(), class_energyData, nullptr);
+ for (int i = 0; i < list.size(); i++) {
+ jobject energyData = env->NewObject(class_energyData, method_energyDataInit,
+ list[i].index, list[i].timestamp, list[i].energy);
+ env->SetObjectArrayElement(energyDataArray, i, energyData);
+ env->DeleteLocalRef(energyData);
+ }
+ return energyDataArray;
+ }
+}
+
+static jboolean nativeInit(JNIEnv *env, jclass clazz) {
+ std::lock_guard<std::mutex> lock(gPowerStatsHalMutex);
+
+ jclass temp = env->FindClass("com/android/server/powerstats/PowerStatsData$RailInfo");
+ if (temp == nullptr) return false;
+
+ class_railInfo = (jclass)env->NewGlobalRef(temp);
+ if (class_railInfo == nullptr) return false;
+
+ method_railInfoInit =
+ env->GetMethodID(class_railInfo, "<init>", "(JLjava/lang/String;Ljava/lang/String;J)V");
+ if (method_railInfoInit == nullptr) return false;
+
+ temp = env->FindClass("com/android/server/powerstats/PowerStatsData$EnergyData");
+ if (temp == nullptr) return false;
+
+ class_energyData = (jclass)env->NewGlobalRef(temp);
+ if (class_energyData == nullptr) return false;
+
+ method_energyDataInit = env->GetMethodID(class_energyData, "<init>", "(JJJ)V");
+ if (method_energyDataInit == nullptr) return false;
+
+ bool rv = true;
+
+ if (!connectToPowerStatsHal()) {
+ ALOGE("nativeInit failed to connect to power.stats HAL");
+ rv = false;
+ } else {
+ Return<void> ret = gPowerStatsHalV1_0_ptr->getRailInfo([&rv](auto rails, auto status) {
+ if (status != Status::SUCCESS) {
+ ALOGE("nativeInit RailInfo is unavailable");
+ rv = false;
+ }
+ });
+
+ ret = gPowerStatsHalV1_0_ptr->getEnergyData({}, [&rv](auto energyData, auto status) {
+ if (status != Status::SUCCESS) {
+ ALOGE("nativeInit EnergyData is unavailable");
+ rv = false;
+ }
+ });
+ }
+
+ return rv;
+}
+
+static const JNINativeMethod method_table[] = {
+ {"nativeInit", "()Z", (void *)nativeInit},
+ {"nativeGetRailInfo", "()[Lcom/android/server/powerstats/PowerStatsData$RailInfo;",
+ (void *)nativeGetRailInfo},
+ {"nativeGetEnergyData", "()[Lcom/android/server/powerstats/PowerStatsData$EnergyData;",
+ (void *)nativeGetEnergyData},
+};
+
+int register_android_server_PowerStatsService(JNIEnv *env) {
+ return jniRegisterNativeMethods(env,
+ "com/android/server/powerstats/"
+ "PowerStatsHALWrapper$PowerStatsHALWrapperImpl",
+ method_table, NELEM(method_table));
+}
+
+}; // namespace android
diff --git a/services/core/jni/onload.cpp b/services/core/jni/onload.cpp
index 5df1ada..e7f6db9 100644
--- a/services/core/jni/onload.cpp
+++ b/services/core/jni/onload.cpp
@@ -29,6 +29,7 @@
int register_android_server_InputManager(JNIEnv* env);
int register_android_server_LightsService(JNIEnv* env);
int register_android_server_PowerManagerService(JNIEnv* env);
+int register_android_server_PowerStatsService(JNIEnv* env);
int register_android_server_storage_AppFuse(JNIEnv* env);
int register_android_server_SerialService(JNIEnv* env);
int register_android_server_SystemServer(JNIEnv* env);
@@ -82,6 +83,7 @@
register_android_server_broadcastradio_BroadcastRadioService(env);
register_android_server_broadcastradio_Tuner(vm, env);
register_android_server_PowerManagerService(env);
+ register_android_server_PowerStatsService(env);
register_android_server_SerialService(env);
register_android_server_InputManager(env);
register_android_server_LightsService(env);
diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java
index 97ae505..ddd2377 100644
--- a/services/java/com/android/server/SystemServer.java
+++ b/services/java/com/android/server/SystemServer.java
@@ -150,6 +150,7 @@
import com.android.server.power.PowerManagerService;
import com.android.server.power.ShutdownThread;
import com.android.server.power.ThermalManagerService;
+import com.android.server.powerstats.PowerStatsService;
import com.android.server.profcollect.ProfcollectForwardingService;
import com.android.server.recoverysystem.RecoverySystemService;
import com.android.server.restrictions.RestrictionsManagerService;
@@ -761,6 +762,11 @@
mSystemServiceManager.startService(UriGrantsManagerService.Lifecycle.class);
t.traceEnd();
+ t.traceBegin("StartPowerStatsService");
+ // Tracks rail data to be used for power statistics.
+ mSystemServiceManager.startService(PowerStatsService.class);
+ t.traceEnd();
+
// Activity manager runs the show.
t.traceBegin("StartActivityManager");
// TODO: Might need to move after migration to WM.
diff --git a/services/tests/servicestests/src/com/android/server/powerstats/PowerStatsServiceTest.java b/services/tests/servicestests/src/com/android/server/powerstats/PowerStatsServiceTest.java
new file mode 100644
index 0000000..3221a4d
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/powerstats/PowerStatsServiceTest.java
@@ -0,0 +1,271 @@
+/*
+ * 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 com.android.server.powerstats;
+
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import android.content.Context;
+
+import androidx.test.InstrumentationRegistry;
+
+import com.android.server.SystemService;
+import com.android.server.powerstats.PowerStatsHALWrapper.IPowerStatsHALWrapper;
+import com.android.server.powerstats.nano.PowerStatsServiceProto;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.file.Files;
+import java.util.Random;
+
+/**
+ * Tests for {@link com.android.server.powerstats.PowerStatsService}.
+ *
+ * Build/Install/Run:
+ * atest FrameworksServicesTests:PowerStatsServiceTest
+ */
+public class PowerStatsServiceTest {
+ private static final String TAG = PowerStatsServiceTest.class.getSimpleName();
+ private static final String DATA_STORAGE_SUBDIR = "powerstatstest";
+ private static final String DATA_STORAGE_FILENAME = "test";
+ private static final String PROTO_OUTPUT_FILENAME = "powerstats.proto";
+ private static final String RAIL_NAME = "railname";
+ private static final String SUBSYS_NAME = "subsysname";
+ private static final int POWER_RAIL_COUNT = 8;
+
+ private final Context mContext = InstrumentationRegistry.getInstrumentation().getContext();
+ private PowerStatsService mService;
+ private File mDataStorageDir;
+ private TimerTrigger mTimerTrigger;
+ private PowerStatsLogger mPowerStatsLogger;
+
+ private final PowerStatsService.Injector mInjector = new PowerStatsService.Injector() {
+ @Override
+ File createDataStoragePath() {
+ mDataStorageDir = null;
+
+ try {
+ mDataStorageDir = Files.createTempDirectory(DATA_STORAGE_SUBDIR).toFile();
+ } catch (IOException e) {
+ fail("Could not create temp directory.");
+ }
+
+ return mDataStorageDir;
+ }
+
+ @Override
+ String createDataStorageFilename() {
+ return DATA_STORAGE_FILENAME;
+ }
+
+ @Override
+ IPowerStatsHALWrapper createPowerStatsHALWrapperImpl() {
+ return new TestPowerStatsHALWrapper();
+ }
+
+ @Override
+ PowerStatsLogger createPowerStatsLogger(Context context, File dataStoragePath,
+ String dataStorageFilename, IPowerStatsHALWrapper powerStatsHALWrapper) {
+ mPowerStatsLogger = new PowerStatsLogger(context, dataStoragePath, dataStorageFilename,
+ powerStatsHALWrapper);
+ return mPowerStatsLogger;
+ }
+
+ @Override
+ BatteryTrigger createBatteryTrigger(Context context, PowerStatsLogger powerStatsLogger) {
+ return new BatteryTrigger(context, powerStatsLogger, false /* trigger enabled */);
+ }
+
+ @Override
+ TimerTrigger createTimerTrigger(Context context, PowerStatsLogger powerStatsLogger) {
+ mTimerTrigger = new TimerTrigger(context, powerStatsLogger,
+ false /* trigger enabled */);
+ return mTimerTrigger;
+ }
+ };
+
+ public static final class TestPowerStatsHALWrapper implements IPowerStatsHALWrapper {
+ @Override
+ public PowerStatsData.RailInfo[] readRailInfo() {
+ PowerStatsData.RailInfo[] railInfoArray = new PowerStatsData.RailInfo[POWER_RAIL_COUNT];
+ for (int i = 0; i < POWER_RAIL_COUNT; i++) {
+ railInfoArray[i] = new PowerStatsData.RailInfo(i, RAIL_NAME + i, SUBSYS_NAME + i,
+ i);
+ }
+ return railInfoArray;
+ }
+
+ @Override
+ public PowerStatsData.EnergyData[] readEnergyData() {
+ PowerStatsData.EnergyData[] energyDataArray =
+ new PowerStatsData.EnergyData[POWER_RAIL_COUNT];
+ for (int i = 0; i < POWER_RAIL_COUNT; i++) {
+ energyDataArray[i] = new PowerStatsData.EnergyData(i, i, i);
+ }
+ return energyDataArray;
+ }
+
+ @Override
+ public boolean initialize() {
+ return true;
+ }
+ }
+
+ @Before
+ public void setUp() {
+ mService = new PowerStatsService(mContext, mInjector);
+ }
+
+ @Test
+ public void testWrittenPowerStatsHALDataMatchesReadIncidentReportData()
+ throws InterruptedException, IOException {
+ mService.onBootPhase(SystemService.PHASE_BOOT_COMPLETED);
+
+ // Write data to on-device storage.
+ mTimerTrigger.logPowerStatsData();
+
+ // The above call puts a message on a handler. Wait for
+ // it to be processed.
+ Thread.sleep(100);
+
+ // Write on-device storage to an incident report.
+ File incidentReport = new File(mDataStorageDir, PROTO_OUTPUT_FILENAME);
+ FileOutputStream fos = new FileOutputStream(incidentReport);
+ mPowerStatsLogger.writeToFile(fos.getFD());
+
+ // Read the incident report in to a byte array.
+ FileInputStream fis = new FileInputStream(incidentReport);
+ byte[] fileContent = new byte[(int) incidentReport.length()];
+ fis.read(fileContent);
+
+ // Parse the incident data into a PowerStatsServiceProto object.
+ PowerStatsServiceProto pssProto = PowerStatsServiceProto.parseFrom(fileContent);
+
+ // Validate the railInfo array matches what was written to on-device storage.
+ assertTrue(pssProto.railInfo.length == POWER_RAIL_COUNT);
+ for (int i = 0; i < pssProto.railInfo.length; i++) {
+ assertTrue(pssProto.railInfo[i].index == i);
+ assertTrue(pssProto.railInfo[i].railName.equals(RAIL_NAME + i));
+ assertTrue(pssProto.railInfo[i].subsysName.equals(SUBSYS_NAME + i));
+ assertTrue(pssProto.railInfo[i].samplingRate == i);
+ }
+
+ // Validate the energyData array matches what was written to on-device storage.
+ assertTrue(pssProto.energyData.length == POWER_RAIL_COUNT);
+ for (int i = 0; i < pssProto.energyData.length; i++) {
+ assertTrue(pssProto.energyData[i].index == i);
+ assertTrue(pssProto.energyData[i].timestampMs == i);
+ assertTrue(pssProto.energyData[i].energyUws == i);
+ }
+ }
+
+ @Test
+ public void testCorruptOnDeviceStorage() throws IOException {
+ mService.onBootPhase(SystemService.PHASE_BOOT_COMPLETED);
+
+ // Generate random array of bytes to emulate corrupt data.
+ Random rd = new Random();
+ byte[] bytes = new byte[100];
+ rd.nextBytes(bytes);
+
+ // Store corrupt data in on-device storage. Add fake timestamp to filename
+ // to match format expected by FileRotator.
+ File onDeviceStorageFile = new File(mDataStorageDir, DATA_STORAGE_FILENAME + ".1234-2234");
+ FileOutputStream onDeviceStorageFos = new FileOutputStream(onDeviceStorageFile);
+ onDeviceStorageFos.write(bytes);
+ onDeviceStorageFos.close();
+
+ // Write on-device storage to an incident report.
+ File incidentReport = new File(mDataStorageDir, PROTO_OUTPUT_FILENAME);
+ FileOutputStream incidentReportFos = new FileOutputStream(incidentReport);
+ mPowerStatsLogger.writeToFile(incidentReportFos.getFD());
+
+ // Read the incident report in to a byte array.
+ FileInputStream fis = new FileInputStream(incidentReport);
+ byte[] fileContent = new byte[(int) incidentReport.length()];
+ fis.read(fileContent);
+
+ // Parse the incident data into a PowerStatsServiceProto object.
+ PowerStatsServiceProto pssProto = PowerStatsServiceProto.parseFrom(fileContent);
+
+ // Valid railInfo data is written to the incident report in the call to
+ // mPowerStatsLogger.writeToFile().
+ assertTrue(pssProto.railInfo.length == POWER_RAIL_COUNT);
+ for (int i = 0; i < pssProto.railInfo.length; i++) {
+ assertTrue(pssProto.railInfo[i].index == i);
+ assertTrue(pssProto.railInfo[i].railName.equals(RAIL_NAME + i));
+ assertTrue(pssProto.railInfo[i].subsysName.equals(SUBSYS_NAME + i));
+ assertTrue(pssProto.railInfo[i].samplingRate == i);
+ }
+
+ // No energyData should be written to the incident report since it
+ // is all corrupt (random bytes generated above).
+ assertTrue(pssProto.energyData.length == 0);
+ }
+
+ @Test
+ public void testNotEnoughBytesAfterLengthField() throws IOException {
+ mService.onBootPhase(SystemService.PHASE_BOOT_COMPLETED);
+
+ // Create corrupt data.
+ // Length field is correct, but there is no data following the length.
+ ByteArrayOutputStream data = new ByteArrayOutputStream();
+ data.write(ByteBuffer.allocate(4).putInt(50).array());
+ byte[] test = data.toByteArray();
+
+ // Store corrupt data in on-device storage. Add fake timestamp to filename
+ // to match format expected by FileRotator.
+ File onDeviceStorageFile = new File(mDataStorageDir, DATA_STORAGE_FILENAME + ".1234-2234");
+ FileOutputStream onDeviceStorageFos = new FileOutputStream(onDeviceStorageFile);
+ onDeviceStorageFos.write(data.toByteArray());
+ onDeviceStorageFos.close();
+
+ // Write on-device storage to an incident report.
+ File incidentReport = new File(mDataStorageDir, PROTO_OUTPUT_FILENAME);
+ FileOutputStream incidentReportFos = new FileOutputStream(incidentReport);
+ mPowerStatsLogger.writeToFile(incidentReportFos.getFD());
+
+ // Read the incident report in to a byte array.
+ FileInputStream fis = new FileInputStream(incidentReport);
+ byte[] fileContent = new byte[(int) incidentReport.length()];
+ fis.read(fileContent);
+
+ // Parse the incident data into a PowerStatsServiceProto object.
+ PowerStatsServiceProto pssProto = PowerStatsServiceProto.parseFrom(fileContent);
+
+ // Valid railInfo data is written to the incident report in the call to
+ // mPowerStatsLogger.writeToFile().
+ assertTrue(pssProto.railInfo.length == POWER_RAIL_COUNT);
+ for (int i = 0; i < pssProto.railInfo.length; i++) {
+ assertTrue(pssProto.railInfo[i].index == i);
+ assertTrue(pssProto.railInfo[i].railName.equals(RAIL_NAME + i));
+ assertTrue(pssProto.railInfo[i].subsysName.equals(SUBSYS_NAME + i));
+ assertTrue(pssProto.railInfo[i].samplingRate == i);
+ }
+
+ // No energyData should be written to the incident report since the
+ // input buffer had only length and no data.
+ assertTrue(pssProto.energyData.length == 0);
+ }
+}
diff --git a/tools/powerstats/Android.bp b/tools/powerstats/Android.bp
new file mode 100644
index 0000000..af41144
--- /dev/null
+++ b/tools/powerstats/Android.bp
@@ -0,0 +1,10 @@
+java_binary_host {
+ name: "PowerStatsServiceProtoParser",
+ manifest: "PowerStatsServiceProtoParser_manifest.txt",
+ srcs: [
+ "*.java",
+ ],
+ static_libs: [
+ "platformprotos",
+ ],
+}
diff --git a/tools/powerstats/PowerStatsServiceProtoParser.java b/tools/powerstats/PowerStatsServiceProtoParser.java
new file mode 100644
index 0000000..8ab302a
--- /dev/null
+++ b/tools/powerstats/PowerStatsServiceProtoParser.java
@@ -0,0 +1,95 @@
+/*
+ * 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 com.android.server.powerstats;
+
+import java.io.FileInputStream;
+import java.io.IOException;
+
+/**
+ * This class implements a utility to parse ODPM data out
+ * of incident reports contained in bugreports. The data
+ * is output to STDOUT in csv format.
+ */
+public class PowerStatsServiceProtoParser {
+ private static void printRailInfo(PowerStatsServiceProto proto) {
+ String csvHeader = new String();
+ for (int i = 0; i < proto.getRailInfoCount(); i++) {
+ RailInfoProto railInfo = proto.getRailInfo(i);
+ csvHeader += "Index" + ","
+ + "Timestamp" + ","
+ + railInfo.getRailName() + "/" + railInfo.getSubsysName() + ",";
+ }
+ System.out.println(csvHeader);
+ }
+
+ private static void printEnergyData(PowerStatsServiceProto proto) {
+ int railInfoCount = proto.getRailInfoCount();
+
+ if (railInfoCount > 0) {
+ int energyDataCount = proto.getEnergyDataCount();
+ int energyDataSetCount = energyDataCount / railInfoCount;
+
+ for (int i = 0; i < energyDataSetCount; i++) {
+ String csvRow = new String();
+ for (int j = 0; j < railInfoCount; j++) {
+ EnergyDataProto energyData = proto.getEnergyData(i * railInfoCount + j);
+ csvRow += energyData.getIndex() + ","
+ + energyData.getTimestampMs() + ","
+ + energyData.getEnergyUws() + ",";
+ }
+ System.out.println(csvRow);
+ }
+ } else {
+ System.out.println("Error: railInfoCount is zero");
+ }
+ }
+
+ private static void generateCsvFile(String pathToIncidentReport) {
+ try {
+ IncidentReportProto irProto =
+ IncidentReportProto.parseFrom(new FileInputStream(pathToIncidentReport));
+
+ if (irProto.hasIncidentReport()) {
+ PowerStatsServiceProto pssProto = irProto.getIncidentReport();
+ printRailInfo(pssProto);
+ printEnergyData(pssProto);
+ } else {
+ System.out.println("Incident report not found. Exiting.");
+ }
+ } catch (IOException e) {
+ System.out.println("Unable to open incident report file: " + pathToIncidentReport);
+ System.out.println(e);
+ }
+ }
+
+ /**
+ * This is the entry point to parse the ODPM data out of incident reports.
+ * It requires one argument which is the path to the incident_report.proto
+ * file captured in a bugreport.
+ *
+ * @param args Path to incident_report.proto passed in from command line.
+ */
+ public static void main(String[] args) {
+ if (args.length > 0) {
+ generateCsvFile(args[0]);
+ } else {
+ System.err.println("Usage: PowerStatsServiceProtoParser <incident_report.proto>");
+ System.err.println("Missing path to incident_report.proto. Exiting.");
+ System.exit(1);
+ }
+ }
+}
diff --git a/tools/powerstats/PowerStatsServiceProtoParser_manifest.txt b/tools/powerstats/PowerStatsServiceProtoParser_manifest.txt
new file mode 100644
index 0000000..5df1211
--- /dev/null
+++ b/tools/powerstats/PowerStatsServiceProtoParser_manifest.txt
@@ -0,0 +1 @@
+Main-class: com.android.server.powerstats.PowerStatsServiceProtoParser