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