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