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