• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2016 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
5  * except in compliance with the License. You may obtain a copy of the License at
6  *
7  *      http://www.apache.org/licenses/LICENSE-2.0
8  *
9  * Unless required by applicable law or agreed to in writing, software distributed under the
10  * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
11  * KIND, either express or implied. See the License for the specific language governing
12  * permissions and limitations under the License.
13  */
14 
15 package com.android.settings.fuelgauge;
16 
17 import android.content.Context;
18 import android.content.Intent;
19 import android.content.IntentFilter;
20 import android.content.res.Resources;
21 import android.os.AsyncTask;
22 import android.os.BatteryManager;
23 import android.os.BatteryStats.HistoryItem;
24 import android.os.BatteryStatsManager;
25 import android.os.BatteryUsageStats;
26 import android.os.SystemClock;
27 import android.text.format.Formatter;
28 import android.util.SparseIntArray;
29 
30 import androidx.annotation.NonNull;
31 import androidx.annotation.Nullable;
32 import androidx.annotation.WorkerThread;
33 
34 import com.android.internal.os.BatteryStatsHistoryIterator;
35 import com.android.settings.Utils;
36 import com.android.settings.overlay.FeatureFactory;
37 import com.android.settings.widget.UsageView;
38 import com.android.settingslib.R;
39 import com.android.settingslib.fuelgauge.Estimate;
40 import com.android.settingslib.fuelgauge.EstimateKt;
41 import com.android.settingslib.utils.PowerUtil;
42 import com.android.settingslib.utils.StringUtil;
43 
44 public class BatteryInfo {
45 
46     public CharSequence chargeLabel;
47     public CharSequence remainingLabel;
48     public int batteryLevel;
49     public int batteryStatus;
50     public boolean discharging = true;
51     public boolean isOverheated;
52     public long remainingTimeUs = 0;
53     public long averageTimeToDischarge = EstimateKt.AVERAGE_TIME_TO_DISCHARGE_UNKNOWN;
54     public String batteryPercentString;
55     public String statusLabel;
56     public String suggestionLabel;
57     private boolean mCharging;
58     private BatteryUsageStats mBatteryUsageStats;
59     private static final String LOG_TAG = "BatteryInfo";
60     private long timePeriod;
61 
62     public interface Callback {
onBatteryInfoLoaded(BatteryInfo info)63         void onBatteryInfoLoaded(BatteryInfo info);
64     }
65 
bindHistory(final UsageView view, BatteryDataParser... parsers)66     public void bindHistory(final UsageView view, BatteryDataParser... parsers) {
67         final Context context = view.getContext();
68         BatteryDataParser parser = new BatteryDataParser() {
69             SparseIntArray points = new SparseIntArray();
70             long startTime;
71             int lastTime = -1;
72             byte lastLevel;
73 
74             @Override
75             public void onParsingStarted(long startTime, long endTime) {
76                 this.startTime = startTime;
77                 timePeriod = endTime - startTime;
78                 view.clearPaths();
79                 // Initially configure the graph for history only.
80                 view.configureGraph((int) timePeriod, 100);
81             }
82 
83             @Override
84             public void onDataPoint(long time, HistoryItem record) {
85                 lastTime = (int) time;
86                 lastLevel = record.batteryLevel;
87                 points.put(lastTime, lastLevel);
88             }
89 
90             @Override
91             public void onDataGap() {
92                 if (points.size() > 1) {
93                     view.addPath(points);
94                 }
95                 points.clear();
96             }
97 
98             @Override
99             public void onParsingDone() {
100                 onDataGap();
101 
102                 // Add projection if we have an estimate.
103                 if (remainingTimeUs != 0) {
104                     PowerUsageFeatureProvider provider = FeatureFactory.getFactory(context)
105                             .getPowerUsageFeatureProvider(context);
106                     if (!mCharging && provider.isEnhancedBatteryPredictionEnabled(context)) {
107                         points = provider.getEnhancedBatteryPredictionCurve(context, startTime);
108                     } else {
109                         // Linear extrapolation.
110                         if (lastTime >= 0) {
111                             points.put(lastTime, lastLevel);
112                             points.put((int) (timePeriod +
113                                             PowerUtil.convertUsToMs(remainingTimeUs)),
114                                     mCharging ? 100 : 0);
115                         }
116                     }
117                 }
118 
119                 // If we have a projection, reconfigure the graph to show it.
120                 if (points != null && points.size() > 0) {
121                     int maxTime = points.keyAt(points.size() - 1);
122                     view.configureGraph(maxTime, 100);
123                     view.addProjectedPath(points);
124                 }
125             }
126         };
127         BatteryDataParser[] parserList = new BatteryDataParser[parsers.length + 1];
128         for (int i = 0; i < parsers.length; i++) {
129             parserList[i] = parsers[i];
130         }
131         parserList[parsers.length] = parser;
132         parseBatteryHistory(parserList);
133         String timeString = context.getString(R.string.charge_length_format,
134                 Formatter.formatShortElapsedTime(context, timePeriod));
135         String remaining = "";
136         if (remainingTimeUs != 0) {
137             remaining = context.getString(R.string.remaining_length_format,
138                     Formatter.formatShortElapsedTime(context, remainingTimeUs / 1000));
139         }
140         view.setBottomLabels(new CharSequence[]{timeString, remaining});
141     }
142 
getBatteryInfo(final Context context, final Callback callback, boolean shortString)143     public static void getBatteryInfo(final Context context, final Callback callback,
144             boolean shortString) {
145         BatteryInfo.getBatteryInfo(context, callback,  /* batteryUsageStats */ null, shortString);
146     }
147 
getBatteryInfo(final Context context, final Callback callback, @Nullable final BatteryUsageStats batteryUsageStats, boolean shortString)148     public static void getBatteryInfo(final Context context, final Callback callback,
149             @Nullable final BatteryUsageStats batteryUsageStats,
150             boolean shortString) {
151         new AsyncTask<Void, Void, BatteryInfo>() {
152             @Override
153             protected BatteryInfo doInBackground(Void... params) {
154                 BatteryUsageStats stats;
155                 if (batteryUsageStats != null) {
156                     stats = batteryUsageStats;
157                 } else {
158                     stats = context.getSystemService(BatteryStatsManager.class)
159                             .getBatteryUsageStats();
160                 }
161                 return getBatteryInfo(context, stats, shortString);
162             }
163 
164             @Override
165             protected void onPostExecute(BatteryInfo batteryInfo) {
166                 final long startTime = System.currentTimeMillis();
167                 callback.onBatteryInfoLoaded(batteryInfo);
168                 BatteryUtils.logRuntime(LOG_TAG, "time for callback", startTime);
169             }
170         }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
171     }
172 
173     /**
174      * Creates a BatteryInfo based on BatteryUsageStats
175      */
176     @WorkerThread
getBatteryInfo(final Context context, @NonNull final BatteryUsageStats batteryUsageStats, boolean shortString)177     public static BatteryInfo getBatteryInfo(final Context context,
178             @NonNull final BatteryUsageStats batteryUsageStats, boolean shortString) {
179         final long batteryStatsTime = System.currentTimeMillis();
180         BatteryUtils.logRuntime(LOG_TAG, "time for getStats", batteryStatsTime);
181 
182         final long startTime = System.currentTimeMillis();
183         PowerUsageFeatureProvider provider =
184                 FeatureFactory.getFactory(context).getPowerUsageFeatureProvider(context);
185         final long elapsedRealtimeUs =
186                 PowerUtil.convertMsToUs(SystemClock.elapsedRealtime());
187 
188         final Intent batteryBroadcast = context.registerReceiver(null,
189                 new IntentFilter(Intent.ACTION_BATTERY_CHANGED));
190         // 0 means we are discharging, anything else means charging
191         final boolean discharging =
192                 batteryBroadcast.getIntExtra(BatteryManager.EXTRA_PLUGGED, -1) == 0;
193 
194         if (discharging && provider != null
195                 && provider.isEnhancedBatteryPredictionEnabled(context)) {
196             Estimate estimate = provider.getEnhancedBatteryPrediction(context);
197             if (estimate != null) {
198                 Estimate.storeCachedEstimate(context, estimate);
199                 BatteryUtils
200                         .logRuntime(LOG_TAG, "time for enhanced BatteryInfo", startTime);
201                 return BatteryInfo.getBatteryInfo(context, batteryBroadcast, batteryUsageStats,
202                         estimate, elapsedRealtimeUs, shortString);
203             }
204         }
205         final long prediction = discharging ? batteryUsageStats.getBatteryTimeRemainingMs() : 0;
206         final Estimate estimate = new Estimate(
207                 prediction,
208                 false, /* isBasedOnUsage */
209                 EstimateKt.AVERAGE_TIME_TO_DISCHARGE_UNKNOWN);
210         BatteryUtils.logRuntime(LOG_TAG, "time for regular BatteryInfo", startTime);
211         return BatteryInfo.getBatteryInfo(context, batteryBroadcast, batteryUsageStats,
212                 estimate, elapsedRealtimeUs, shortString);
213     }
214 
215     @WorkerThread
getBatteryInfoOld(Context context, Intent batteryBroadcast, BatteryUsageStats batteryUsageStats, long elapsedRealtimeUs, boolean shortString)216     public static BatteryInfo getBatteryInfoOld(Context context, Intent batteryBroadcast,
217             BatteryUsageStats batteryUsageStats, long elapsedRealtimeUs, boolean shortString) {
218         Estimate estimate = new Estimate(
219                 batteryUsageStats.getBatteryTimeRemainingMs(),
220                 false,
221                 EstimateKt.AVERAGE_TIME_TO_DISCHARGE_UNKNOWN);
222         return getBatteryInfo(context, batteryBroadcast, batteryUsageStats, estimate,
223                 elapsedRealtimeUs, shortString);
224     }
225 
226     @WorkerThread
getBatteryInfo(Context context, Intent batteryBroadcast, @NonNull BatteryUsageStats batteryUsageStats, Estimate estimate, long elapsedRealtimeUs, boolean shortString)227     public static BatteryInfo getBatteryInfo(Context context, Intent batteryBroadcast,
228             @NonNull BatteryUsageStats batteryUsageStats, Estimate estimate,
229             long elapsedRealtimeUs, boolean shortString) {
230         final long startTime = System.currentTimeMillis();
231         BatteryInfo info = new BatteryInfo();
232         info.mBatteryUsageStats = batteryUsageStats;
233         info.batteryLevel = Utils.getBatteryLevel(batteryBroadcast);
234         info.batteryPercentString = Utils.formatPercentage(info.batteryLevel);
235         info.mCharging = batteryBroadcast.getIntExtra(BatteryManager.EXTRA_PLUGGED, 0) != 0;
236         info.averageTimeToDischarge = estimate.getAverageDischargeTime();
237         info.isOverheated = batteryBroadcast.getIntExtra(
238                 BatteryManager.EXTRA_HEALTH, BatteryManager.BATTERY_HEALTH_UNKNOWN)
239                 == BatteryManager.BATTERY_HEALTH_OVERHEAT;
240 
241         info.statusLabel = Utils.getBatteryStatus(context, batteryBroadcast);
242         info.batteryStatus = batteryBroadcast.getIntExtra(
243                 BatteryManager.EXTRA_STATUS, BatteryManager.BATTERY_STATUS_UNKNOWN);
244         if (!info.mCharging) {
245             updateBatteryInfoDischarging(context, shortString, estimate, info);
246         } else {
247             updateBatteryInfoCharging(context, batteryBroadcast, batteryUsageStats,
248                     info);
249         }
250         BatteryUtils.logRuntime(LOG_TAG, "time for getBatteryInfo", startTime);
251         return info;
252     }
253 
updateBatteryInfoCharging(Context context, Intent batteryBroadcast, BatteryUsageStats stats, BatteryInfo info)254     private static void updateBatteryInfoCharging(Context context, Intent batteryBroadcast,
255             BatteryUsageStats stats, BatteryInfo info) {
256         final Resources resources = context.getResources();
257         final long chargeTimeMs = stats.getChargeTimeRemainingMs();
258         final int status = batteryBroadcast.getIntExtra(BatteryManager.EXTRA_STATUS,
259                 BatteryManager.BATTERY_STATUS_UNKNOWN);
260         info.discharging = false;
261         info.suggestionLabel = null;
262         if (info.isOverheated && status != BatteryManager.BATTERY_STATUS_FULL) {
263             info.remainingLabel = null;
264             int chargingLimitedResId = R.string.power_charging_limited;
265             info.chargeLabel =
266                 context.getString(chargingLimitedResId, info.batteryPercentString);
267         } else if (chargeTimeMs > 0 && status != BatteryManager.BATTERY_STATUS_FULL) {
268             info.remainingTimeUs = PowerUtil.convertMsToUs(chargeTimeMs);
269             final CharSequence timeString = StringUtil.formatElapsedTime(
270                     context,
271                     PowerUtil.convertUsToMs(info.remainingTimeUs),
272                     false /* withSeconds */,
273                     true /* collapseTimeUnit */);
274             int resId = R.string.power_charging_duration;
275             info.remainingLabel = context.getString(
276                     R.string.power_remaining_charging_duration_only, timeString);
277             info.chargeLabel = context.getString(resId, info.batteryPercentString, timeString);
278         } else {
279             final String chargeStatusLabel = Utils.getBatteryStatus(context, batteryBroadcast);
280             info.remainingLabel = null;
281             info.chargeLabel = info.batteryLevel == 100 ? info.batteryPercentString :
282                     resources.getString(R.string.power_charging, info.batteryPercentString,
283                             chargeStatusLabel.toLowerCase());
284         }
285     }
286 
updateBatteryInfoDischarging(Context context, boolean shortString, Estimate estimate, BatteryInfo info)287     private static void updateBatteryInfoDischarging(Context context, boolean shortString,
288             Estimate estimate, BatteryInfo info) {
289         final long drainTimeUs = PowerUtil.convertMsToUs(estimate.getEstimateMillis());
290         if (drainTimeUs > 0) {
291             info.remainingTimeUs = drainTimeUs;
292             info.remainingLabel = PowerUtil.getBatteryRemainingStringFormatted(
293                     context,
294                     PowerUtil.convertUsToMs(drainTimeUs),
295                     null /* percentageString */,
296                     false /* basedOnUsage */
297             );
298             info.chargeLabel = PowerUtil.getBatteryRemainingStringFormatted(
299                     context,
300                     PowerUtil.convertUsToMs(drainTimeUs),
301                     info.batteryPercentString,
302                     estimate.isBasedOnUsage() && !shortString
303             );
304             info.suggestionLabel = PowerUtil.getBatteryTipStringFormatted(
305                     context, PowerUtil.convertUsToMs(drainTimeUs));
306         } else {
307             info.remainingLabel = null;
308             info.suggestionLabel = null;
309             info.chargeLabel = info.batteryPercentString;
310         }
311     }
312 
313     public interface BatteryDataParser {
onParsingStarted(long startTime, long endTime)314         void onParsingStarted(long startTime, long endTime);
315 
onDataPoint(long time, HistoryItem record)316         void onDataPoint(long time, HistoryItem record);
317 
onDataGap()318         void onDataGap();
319 
onParsingDone()320         void onParsingDone();
321     }
322 
323     /**
324      * Iterates over battery history included in the BatteryUsageStats that this object
325      * was initialized with.
326      */
parseBatteryHistory(BatteryDataParser... parsers)327     public void parseBatteryHistory(BatteryDataParser... parsers) {
328         long startWalltime = 0;
329         long endWalltime = 0;
330         long historyStart = 0;
331         long historyEnd = 0;
332         long curWalltime = startWalltime;
333         long lastWallTime = 0;
334         long lastRealtime = 0;
335         int lastInteresting = 0;
336         int pos = 0;
337         boolean first = true;
338         final BatteryStatsHistoryIterator iterator1 =
339                 mBatteryUsageStats.iterateBatteryStatsHistory();
340         final HistoryItem rec = new HistoryItem();
341         while (iterator1.next(rec)) {
342             pos++;
343             if (first) {
344                 first = false;
345                 historyStart = rec.time;
346             }
347             if (rec.cmd == HistoryItem.CMD_CURRENT_TIME
348                     || rec.cmd == HistoryItem.CMD_RESET) {
349                 // If there is a ridiculously large jump in time, then we won't be
350                 // able to create a good chart with that data, so just ignore the
351                 // times we got before and pretend like our data extends back from
352                 // the time we have now.
353                 // Also, if we are getting a time change and we are less than 5 minutes
354                 // since the start of the history real time, then also use this new
355                 // time to compute the base time, since whatever time we had before is
356                 // pretty much just noise.
357                 if (rec.currentTime > (lastWallTime + (180 * 24 * 60 * 60 * 1000L))
358                         || rec.time < (historyStart + (5 * 60 * 1000L))) {
359                     startWalltime = 0;
360                 }
361                 lastWallTime = rec.currentTime;
362                 lastRealtime = rec.time;
363                 if (startWalltime == 0) {
364                     startWalltime = lastWallTime - (lastRealtime - historyStart);
365                 }
366             }
367             if (rec.isDeltaData()) {
368                 lastInteresting = pos;
369                 historyEnd = rec.time;
370             }
371         }
372 
373         endWalltime = lastWallTime + historyEnd - lastRealtime;
374 
375         int i = 0;
376         final int N = lastInteresting;
377 
378         for (int j = 0; j < parsers.length; j++) {
379             parsers[j].onParsingStarted(startWalltime, endWalltime);
380         }
381 
382         if (endWalltime > startWalltime) {
383             final BatteryStatsHistoryIterator iterator2 =
384                     mBatteryUsageStats.iterateBatteryStatsHistory();
385             while (iterator2.next(rec) && i < N) {
386                 if (rec.isDeltaData()) {
387                     curWalltime += rec.time - lastRealtime;
388                     lastRealtime = rec.time;
389                     long x = (curWalltime - startWalltime);
390                     if (x < 0) {
391                         x = 0;
392                     }
393                     for (int j = 0; j < parsers.length; j++) {
394                         parsers[j].onDataPoint(x, rec);
395                     }
396                 } else {
397                     long lastWalltime = curWalltime;
398                     if (rec.cmd == HistoryItem.CMD_CURRENT_TIME
399                             || rec.cmd == HistoryItem.CMD_RESET) {
400                         if (rec.currentTime >= startWalltime) {
401                             curWalltime = rec.currentTime;
402                         } else {
403                             curWalltime = startWalltime + (rec.time - historyStart);
404                         }
405                         lastRealtime = rec.time;
406                     }
407 
408                     if (rec.cmd != HistoryItem.CMD_OVERFLOW
409                             && (rec.cmd != HistoryItem.CMD_CURRENT_TIME
410                             || Math.abs(lastWalltime - curWalltime) > (60 * 60 * 1000))) {
411                         for (int j = 0; j < parsers.length; j++) {
412                             parsers[j].onDataGap();
413                         }
414                     }
415                 }
416                 i++;
417             }
418         }
419 
420         for (int j = 0; j < parsers.length; j++) {
421             parsers[j].onParsingDone();
422         }
423     }
424 }
425