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