1 /*
2  * Copyright (C) 2020 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.internal.power;
18 
19 
20 import static android.os.BatteryStats.POWER_DATA_UNAVAILABLE;
21 
22 import android.annotation.IntDef;
23 import android.annotation.NonNull;
24 import android.annotation.Nullable;
25 import android.os.Parcel;
26 import android.text.TextUtils;
27 import android.util.DebugUtils;
28 import android.util.Slog;
29 import android.view.Display;
30 
31 import com.android.internal.annotations.VisibleForTesting;
32 
33 import java.io.PrintWriter;
34 import java.lang.annotation.Retention;
35 import java.lang.annotation.RetentionPolicy;
36 import java.util.Arrays;
37 
38 /**
39  * Tracks the measured charge consumption of various subsystems according to their
40  * {@link StandardPowerBucket} or custom power bucket (which is tied to
41  * {@link android.hardware.power.stats.EnergyConsumer.ordinal}).
42  *
43  * This class doesn't use a TimeBase, and instead requires manually decisions about when to
44  * accumulate since it is trivial. However, in the future, a TimeBase could be used instead.
45  */
46 @VisibleForTesting
47 public class MeasuredEnergyStats {
48     private static final String TAG = "MeasuredEnergyStats";
49 
50     // Note: {@link com.android.internal.os.BatteryStatsImpl#VERSION} MUST be updated if standard
51     // power bucket integers are modified/added/removed.
52     public static final int POWER_BUCKET_UNKNOWN = -1;
53     public static final int POWER_BUCKET_SCREEN_ON = 0;
54     public static final int POWER_BUCKET_SCREEN_DOZE = 1;
55     public static final int POWER_BUCKET_SCREEN_OTHER = 2;
56     public static final int POWER_BUCKET_CPU = 3;
57     public static final int POWER_BUCKET_WIFI = 4;
58     public static final int POWER_BUCKET_BLUETOOTH = 5;
59     public static final int POWER_BUCKET_GNSS = 6;
60     public static final int POWER_BUCKET_MOBILE_RADIO = 7;
61     public static final int NUMBER_STANDARD_POWER_BUCKETS = 8; // Buckets above this are custom.
62 
63     @IntDef(prefix = {"POWER_BUCKET_"}, value = {
64             POWER_BUCKET_UNKNOWN,
65             POWER_BUCKET_SCREEN_ON,
66             POWER_BUCKET_SCREEN_DOZE,
67             POWER_BUCKET_SCREEN_OTHER,
68             POWER_BUCKET_CPU,
69             POWER_BUCKET_WIFI,
70             POWER_BUCKET_BLUETOOTH,
71             POWER_BUCKET_GNSS,
72             POWER_BUCKET_MOBILE_RADIO,
73     })
74     @Retention(RetentionPolicy.SOURCE)
75     public @interface StandardPowerBucket {
76     }
77 
78     /**
79      * Total charge (in microcoulombs) that a power bucket (including both
80      * {@link StandardPowerBucket} and custom buckets) has accumulated since the last reset.
81      * Values MUST be non-zero or POWER_DATA_UNAVAILABLE. Accumulation only occurs
82      * while the necessary conditions are satisfied (e.g. on battery).
83      *
84      * Charge for both {@link StandardPowerBucket}s and custom power buckets are stored in this
85      * array, and may internally both referred to as 'buckets'. This is an implementation detail;
86      * externally, we differentiate between these two data sources.
87      *
88      * Warning: Long array is used for access speed. If the number of supported subsystems
89      * becomes large, consider using an alternate data structure such as a SparseLongArray.
90      */
91     private final long[] mAccumulatedChargeMicroCoulomb;
92 
93     private final String[] mCustomBucketNames;
94 
95     /**
96      * Creates a MeasuredEnergyStats set to support the provided power buckets.
97      * supportedStandardBuckets must be of size {@link #NUMBER_STANDARD_POWER_BUCKETS}.
98      * numCustomBuckets >= 0 is the number of (non-standard) custom power buckets on the device.
99      */
MeasuredEnergyStats(@onNull boolean[] supportedStandardBuckets, @Nullable String[] customBucketNames)100     public MeasuredEnergyStats(@NonNull boolean[] supportedStandardBuckets,
101             @Nullable String[] customBucketNames) {
102         mCustomBucketNames = customBucketNames == null ? new String[0] : customBucketNames;
103         final int numTotalBuckets = NUMBER_STANDARD_POWER_BUCKETS + mCustomBucketNames.length;
104         mAccumulatedChargeMicroCoulomb = new long[numTotalBuckets];
105         // Initialize to all zeros where supported, otherwise POWER_DATA_UNAVAILABLE.
106         // All custom buckets are, by definition, supported, so their values stay at 0.
107         for (int stdBucket = 0; stdBucket < NUMBER_STANDARD_POWER_BUCKETS; stdBucket++) {
108             if (!supportedStandardBuckets[stdBucket]) {
109                 mAccumulatedChargeMicroCoulomb[stdBucket] = POWER_DATA_UNAVAILABLE;
110             }
111         }
112     }
113 
114     /**
115      * Creates a new zero'd MeasuredEnergyStats, using the template to determine which buckets are
116      * supported. This certainly does NOT produce an exact clone of the template.
117      */
MeasuredEnergyStats(MeasuredEnergyStats template)118     private MeasuredEnergyStats(MeasuredEnergyStats template) {
119         final int numIndices = template.getNumberOfIndices();
120         mAccumulatedChargeMicroCoulomb = new long[numIndices];
121         // Initialize to all zeros where supported, otherwise POWER_DATA_UNAVAILABLE.
122         // All custom buckets are, by definition, supported, so their values stay at 0.
123         for (int stdBucket = 0; stdBucket < NUMBER_STANDARD_POWER_BUCKETS; stdBucket++) {
124             if (!template.isIndexSupported(stdBucket)) {
125                 mAccumulatedChargeMicroCoulomb[stdBucket] = POWER_DATA_UNAVAILABLE;
126             }
127         }
128         mCustomBucketNames = template.getCustomBucketNames();
129     }
130 
131     /**
132      * Creates a new zero'd MeasuredEnergyStats, using the template to determine which buckets are
133      * supported.
134      */
createFromTemplate(MeasuredEnergyStats template)135     public static MeasuredEnergyStats createFromTemplate(MeasuredEnergyStats template) {
136         return new MeasuredEnergyStats(template);
137     }
138 
139     /**
140      * Constructor for creating a temp MeasuredEnergyStats.
141      * See {@link #createAndReadSummaryFromParcel(Parcel, MeasuredEnergyStats)}.
142      */
MeasuredEnergyStats(int numIndices)143     private MeasuredEnergyStats(int numIndices) {
144         mAccumulatedChargeMicroCoulomb = new long[numIndices];
145         mCustomBucketNames = new String[numIndices - NUMBER_STANDARD_POWER_BUCKETS];
146     }
147 
148     /** Construct from parcel. */
MeasuredEnergyStats(Parcel in)149     public MeasuredEnergyStats(Parcel in) {
150         final int size = in.readInt();
151         mAccumulatedChargeMicroCoulomb = new long[size];
152         in.readLongArray(mAccumulatedChargeMicroCoulomb);
153         mCustomBucketNames = in.readStringArray();
154     }
155 
156     /** Write to parcel */
writeToParcel(Parcel out)157     public void writeToParcel(Parcel out) {
158         out.writeInt(mAccumulatedChargeMicroCoulomb.length);
159         out.writeLongArray(mAccumulatedChargeMicroCoulomb);
160         out.writeStringArray(mCustomBucketNames);
161     }
162 
163     /**
164      * Read from summary parcel.
165      * Note: Measured subsystem (and therefore bucket) availability may be different from when the
166      * summary parcel was written. Availability has already been correctly set in the constructor.
167      * Note: {@link com.android.internal.os.BatteryStatsImpl#VERSION} must be updated if summary
168      *       parceling changes.
169      *
170      * Corresponding write performed by {@link #writeSummaryToParcel(Parcel, boolean)}.
171      */
readSummaryFromParcel(Parcel in, boolean overwriteAvailability)172     private void readSummaryFromParcel(Parcel in, boolean overwriteAvailability) {
173         final int numWrittenEntries = in.readInt();
174         for (int entry = 0; entry < numWrittenEntries; entry++) {
175             final int index = in.readInt();
176             final long chargeUC = in.readLong();
177             if (overwriteAvailability) {
178                 mAccumulatedChargeMicroCoulomb[index] = chargeUC;
179             } else {
180                 setValueIfSupported(index, chargeUC);
181             }
182         }
183     }
184 
185     /**
186      * Write to summary parcel.
187      * Note: Measured subsystem availability may be different when the summary parcel is read.
188      *
189      * Corresponding read performed by {@link #readSummaryFromParcel(Parcel, boolean)}.
190      */
writeSummaryToParcel(Parcel out, boolean skipZero)191     private void writeSummaryToParcel(Parcel out, boolean skipZero) {
192         final int posOfNumWrittenEntries = out.dataPosition();
193         out.writeInt(0);
194         int numWrittenEntries = 0;
195         // Write only the supported buckets (with non-zero charge, if applicable).
196         for (int index = 0; index < mAccumulatedChargeMicroCoulomb.length; index++) {
197             final long charge = mAccumulatedChargeMicroCoulomb[index];
198             if (charge < 0) continue;
199             if (charge == 0 && skipZero) continue;
200 
201             out.writeInt(index);
202             out.writeLong(charge);
203             numWrittenEntries++;
204         }
205         final int currPos = out.dataPosition();
206         out.setDataPosition(posOfNumWrittenEntries);
207         out.writeInt(numWrittenEntries);
208         out.setDataPosition(currPos);
209     }
210 
211     /** Get number of possible buckets, including both standard and custom ones. */
getNumberOfIndices()212     private int getNumberOfIndices() {
213         return mAccumulatedChargeMicroCoulomb.length;
214     }
215 
216 
217     /** Updates the given standard power bucket with the given charge if accumulate is true. */
updateStandardBucket(@tandardPowerBucket int bucket, long chargeDeltaUC)218     public void updateStandardBucket(@StandardPowerBucket int bucket, long chargeDeltaUC) {
219         checkValidStandardBucket(bucket);
220         updateEntry(bucket, chargeDeltaUC);
221     }
222 
223     /** Updates the given custom power bucket with the given charge if accumulate is true. */
updateCustomBucket(int customBucket, long chargeDeltaUC)224     public void updateCustomBucket(int customBucket, long chargeDeltaUC) {
225         if (!isValidCustomBucket(customBucket)) {
226             Slog.e(TAG, "Attempted to update invalid custom bucket " + customBucket);
227             return;
228         }
229         final int index = customBucketToIndex(customBucket);
230         updateEntry(index, chargeDeltaUC);
231     }
232 
233     /** Updates the given index with the given charge if accumulate is true. */
updateEntry(int index, long chargeDeltaUC)234     private void updateEntry(int index, long chargeDeltaUC) {
235         if (mAccumulatedChargeMicroCoulomb[index] >= 0L) {
236             mAccumulatedChargeMicroCoulomb[index] += chargeDeltaUC;
237         } else {
238             Slog.wtf(TAG, "Attempting to add " + chargeDeltaUC + " to unavailable bucket "
239                     + getBucketName(index) + " whose value was "
240                     + mAccumulatedChargeMicroCoulomb[index]);
241         }
242     }
243 
244     /**
245      * Return accumulated charge (in microcouloumb) for a standard power bucket since last reset.
246      * Returns {@link android.os.BatteryStats#POWER_DATA_UNAVAILABLE} if this data is unavailable.
247      * @throws IllegalArgumentException if no such {@link StandardPowerBucket}.
248      */
getAccumulatedStandardBucketCharge(@tandardPowerBucket int bucket)249     public long getAccumulatedStandardBucketCharge(@StandardPowerBucket int bucket) {
250         checkValidStandardBucket(bucket);
251         return mAccumulatedChargeMicroCoulomb[bucket];
252     }
253 
254     /**
255      * Return accumulated charge (in microcoulomb) for the a custom power bucket since last
256      * reset.
257      * Returns {@link android.os.BatteryStats#POWER_DATA_UNAVAILABLE} if this data is unavailable.
258      */
259     @VisibleForTesting
getAccumulatedCustomBucketCharge(int customBucket)260     public long getAccumulatedCustomBucketCharge(int customBucket) {
261         if (!isValidCustomBucket(customBucket)) {
262             return POWER_DATA_UNAVAILABLE;
263         }
264         return mAccumulatedChargeMicroCoulomb[customBucketToIndex(customBucket)];
265     }
266 
267     /**
268      * Return accumulated charge (in microcoulomb) for all custom power buckets since last reset.
269      */
getAccumulatedCustomBucketCharges()270     public @NonNull long[] getAccumulatedCustomBucketCharges() {
271         final long[] charges = new long[getNumberCustomPowerBuckets()];
272         for (int bucket = 0; bucket < charges.length; bucket++) {
273             charges[bucket] = mAccumulatedChargeMicroCoulomb[customBucketToIndex(bucket)];
274         }
275         return charges;
276     }
277 
278     /**
279      * Map {@link android.view.Display} STATE_ to corresponding {@link StandardPowerBucket}.
280      */
getDisplayPowerBucket(int screenState)281     public static @StandardPowerBucket int getDisplayPowerBucket(int screenState) {
282         if (Display.isOnState(screenState)) {
283             return POWER_BUCKET_SCREEN_ON;
284         }
285         if (Display.isDozeState(screenState)) {
286             return POWER_BUCKET_SCREEN_DOZE;
287         }
288         return POWER_BUCKET_SCREEN_OTHER;
289     }
290 
291     /**
292      * Create a MeasuredEnergyStats object from a summary parcel.
293      *
294      * Corresponding write performed by
295      * {@link #writeSummaryToParcel(MeasuredEnergyStats, Parcel, boolean, boolean)}.
296      *
297      * @return a new MeasuredEnergyStats object as described.
298      *         Returns null if the parcel indicates there is no data to populate.
299      */
createAndReadSummaryFromParcel(Parcel in)300     public static @Nullable MeasuredEnergyStats createAndReadSummaryFromParcel(Parcel in) {
301         final int arraySize = in.readInt();
302         // Check if any MeasuredEnergyStats exists on the parcel
303         if (arraySize == 0) return null;
304 
305         final String[] customBucketNames;
306         if (in.readBoolean()) {
307             customBucketNames = in.readStringArray();
308         } else {
309             customBucketNames = new String[0];
310         }
311         final MeasuredEnergyStats stats = new MeasuredEnergyStats(
312                 new boolean[NUMBER_STANDARD_POWER_BUCKETS], customBucketNames);
313         stats.readSummaryFromParcel(in, true);
314         return stats;
315     }
316 
317     /**
318      * Create a MeasuredEnergyStats using the template to determine which buckets are supported,
319      * and populate this new object from the given parcel.
320      *
321      * The parcel must be consistent with the template in terms of the number of
322      * possible (not necessarily supported) standard and custom buckets.
323      *
324      * Corresponding write performed by
325      * {@link #writeSummaryToParcel(MeasuredEnergyStats, Parcel, boolean, boolean)}.
326      *
327      * @return a new MeasuredEnergyStats object as described.
328      *         Returns null if the stats contain no non-0 information (such as if template is null
329      *         or if the parcel indicates there is no data to populate).
330      *
331      * @see #createFromTemplate
332      */
createAndReadSummaryFromParcel(Parcel in, @Nullable MeasuredEnergyStats template)333     public static @Nullable MeasuredEnergyStats createAndReadSummaryFromParcel(Parcel in,
334             @Nullable MeasuredEnergyStats template) {
335         final int arraySize = in.readInt();
336         // Check if any MeasuredEnergyStats exists on the parcel
337         if (arraySize == 0) return null;
338 
339         boolean includesCustomBucketNames = in.readBoolean();
340         if (includesCustomBucketNames) {
341             // Consume the array of custom bucket names. They are already included in the
342             // template.
343             in.readStringArray();
344         }
345         if (template == null) {
346             // Nothing supported anymore. Create placeholder object just to consume the parcel data.
347             final MeasuredEnergyStats mes = new MeasuredEnergyStats(arraySize);
348             mes.readSummaryFromParcel(in, false);
349             return null;
350         }
351 
352         if (arraySize != template.getNumberOfIndices()) {
353             Slog.wtf(TAG, "Size of MeasuredEnergyStats parcel (" + arraySize
354                     + ") does not match template (" + template.getNumberOfIndices() + ").");
355             // Something is horribly wrong. Just consume the parcel and return null.
356             final MeasuredEnergyStats mes = new MeasuredEnergyStats(arraySize);
357             mes.readSummaryFromParcel(in, false);
358             return null;
359         }
360 
361         final MeasuredEnergyStats stats = createFromTemplate(template);
362         stats.readSummaryFromParcel(in, false);
363         if (stats.containsInterestingData()) {
364             return stats;
365         } else {
366             // Don't waste RAM on it (and make sure not to persist it in the next writeSummary)
367             return null;
368         }
369     }
370 
371     /** Returns true iff any of the buckets are supported and non-zero. */
containsInterestingData()372     private boolean containsInterestingData() {
373         for (int index = 0; index < mAccumulatedChargeMicroCoulomb.length; index++) {
374             if (mAccumulatedChargeMicroCoulomb[index] > 0) return true;
375         }
376         return false;
377     }
378 
379     /**
380      * Write a MeasuredEnergyStats to a parcel. If the stats is null, just write a 0.
381      *
382      * Corresponding read performed by {@link #createAndReadSummaryFromParcel(Parcel)}
383      * and {@link #createAndReadSummaryFromParcel(Parcel, MeasuredEnergyStats)}.
384      */
writeSummaryToParcel(@ullable MeasuredEnergyStats stats, Parcel dest, boolean skipZero, boolean skipCustomBucketNames)385     public static void writeSummaryToParcel(@Nullable MeasuredEnergyStats stats,
386             Parcel dest, boolean skipZero, boolean skipCustomBucketNames) {
387         if (stats == null) {
388             dest.writeInt(0);
389             return;
390         }
391         dest.writeInt(stats.getNumberOfIndices());
392         if (!skipCustomBucketNames) {
393             dest.writeBoolean(true);
394             dest.writeStringArray(stats.getCustomBucketNames());
395         } else {
396             dest.writeBoolean(false);
397         }
398         stats.writeSummaryToParcel(dest, skipZero);
399     }
400 
401     /** Reset accumulated charges. */
reset()402     private void reset() {
403         final int numIndices = getNumberOfIndices();
404         for (int index = 0; index < numIndices; index++) {
405             setValueIfSupported(index, 0L);
406         }
407     }
408 
409     /** Reset accumulated charges of the given stats. */
resetIfNotNull(@ullable MeasuredEnergyStats stats)410     public static void resetIfNotNull(@Nullable MeasuredEnergyStats stats) {
411         if (stats != null) stats.reset();
412     }
413 
414     /** If the index is AVAILABLE, overwrite its value; otherwise leave it as UNAVAILABLE. */
setValueIfSupported(int index, long value)415     private void setValueIfSupported(int index, long value) {
416         if (mAccumulatedChargeMicroCoulomb[index] != POWER_DATA_UNAVAILABLE) {
417             mAccumulatedChargeMicroCoulomb[index] = value;
418         }
419     }
420 
421     /**
422      * Check if measuring the charge consumption of the given bucket is supported by this device.
423      * @throws IllegalArgumentException if not a valid {@link StandardPowerBucket}.
424      */
isStandardBucketSupported(@tandardPowerBucket int bucket)425     public boolean isStandardBucketSupported(@StandardPowerBucket int bucket) {
426         checkValidStandardBucket(bucket);
427         return isIndexSupported(bucket);
428     }
429 
isIndexSupported(int index)430     private boolean isIndexSupported(int index) {
431         return mAccumulatedChargeMicroCoulomb[index] != POWER_DATA_UNAVAILABLE;
432     }
433 
434     /** Check if the supported power buckets are precisely those given. */
isSupportEqualTo( @onNull boolean[] queriedStandardBuckets, @Nullable String[] customBucketNames)435     public boolean isSupportEqualTo(
436             @NonNull boolean[] queriedStandardBuckets, @Nullable String[] customBucketNames) {
437         if (customBucketNames == null) {
438             //In practice customBucketNames should never be null, but sanitize it just to be sure.
439             customBucketNames = new String[0];
440         }
441 
442         final int numBuckets = getNumberOfIndices();
443         final int numCustomBuckets = customBucketNames == null ? 0 : customBucketNames.length;
444         if (numBuckets != NUMBER_STANDARD_POWER_BUCKETS + numCustomBuckets) {
445             return false;
446         }
447 
448         if (!Arrays.equals(mCustomBucketNames, customBucketNames)) {
449             return false;
450         }
451 
452         for (int stdBucket = 0; stdBucket < NUMBER_STANDARD_POWER_BUCKETS; stdBucket++) {
453             if (isStandardBucketSupported(stdBucket) != queriedStandardBuckets[stdBucket]) {
454                 return false;
455             }
456         }
457         return true;
458     }
459 
getCustomBucketNames()460     public String[] getCustomBucketNames() {
461         return mCustomBucketNames;
462     }
463 
464     /** Dump debug data. */
dump(PrintWriter pw)465     public void dump(PrintWriter pw) {
466         pw.print("   ");
467         for (int index = 0; index < mAccumulatedChargeMicroCoulomb.length; index++) {
468             pw.print(getBucketName(index));
469             pw.print(" : ");
470             pw.print(mAccumulatedChargeMicroCoulomb[index]);
471             if (!isIndexSupported(index)) {
472                 pw.print(" (unsupported)");
473             }
474             if (index != mAccumulatedChargeMicroCoulomb.length - 1) {
475                 pw.print(", ");
476             }
477         }
478         pw.println();
479     }
480 
481     /**
482      * If the index is a standard bucket, returns its name; otherwise returns its prefixed custom
483      * bucket number.
484      */
getBucketName(int index)485     private String getBucketName(int index) {
486         if (isValidStandardBucket(index)) {
487             return DebugUtils.valueToString(MeasuredEnergyStats.class, "POWER_BUCKET_", index);
488         }
489         final int customBucket = indexToCustomBucket(index);
490         StringBuilder name = new StringBuilder().append("CUSTOM_").append(customBucket);
491         if (mCustomBucketNames != null && !TextUtils.isEmpty(mCustomBucketNames[customBucket])) {
492             name.append('(').append(mCustomBucketNames[customBucket]).append(')');
493         }
494         return name.toString();
495     }
496 
497     /** Get the number of custom power buckets on this device. */
getNumberCustomPowerBuckets()498     public int getNumberCustomPowerBuckets() {
499         return mAccumulatedChargeMicroCoulomb.length - NUMBER_STANDARD_POWER_BUCKETS;
500     }
501 
customBucketToIndex(int customBucket)502     private static int customBucketToIndex(int customBucket) {
503         return customBucket + NUMBER_STANDARD_POWER_BUCKETS;
504     }
505 
indexToCustomBucket(int index)506     private static int indexToCustomBucket(int index) {
507         return index - NUMBER_STANDARD_POWER_BUCKETS;
508     }
509 
checkValidStandardBucket(@tandardPowerBucket int bucket)510     private static void checkValidStandardBucket(@StandardPowerBucket int bucket) {
511         if (!isValidStandardBucket(bucket)) {
512             throw new IllegalArgumentException("Illegal StandardPowerBucket " + bucket);
513         }
514     }
515 
isValidStandardBucket(@tandardPowerBucket int bucket)516     private static boolean isValidStandardBucket(@StandardPowerBucket int bucket) {
517         return bucket >= 0 && bucket < NUMBER_STANDARD_POWER_BUCKETS;
518     }
519 
520     /** Returns whether the given custom bucket is valid (exists) on this device. */
521     @VisibleForTesting
isValidCustomBucket(int customBucket)522     public boolean isValidCustomBucket(int customBucket) {
523         return customBucket >= 0
524                 && customBucketToIndex(customBucket) < mAccumulatedChargeMicroCoulomb.length;
525     }
526 }
527