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