• 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.R;
37 import com.android.settings.Utils;
38 import com.android.settings.overlay.FeatureFactory;
39 import com.android.settings.widget.UsageView;
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 isBatteryDefender;
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.isBatteryDefender = batteryBroadcast.getIntExtra(
261                 BatteryManager.EXTRA_CHARGING_STATUS, BatteryManager.CHARGING_POLICY_DEFAULT)
262                 == BatteryManager.CHARGING_POLICY_ADAPTIVE_LONGLIFE;
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.isBatteryDefender && 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 = chargeTimeMs <= 0 ? null : context.getString(
303                     R.string.power_remaining_charging_duration_only, timeString);
304             info.chargeLabel = chargeTimeMs <= 0 ? info.batteryPercentString
305                     : context.getString(resId, info.batteryPercentString, timeString);
306         } else if (dockDefenderMode == BatteryUtils.DockDefenderMode.FUTURE_BYPASS) {
307             // Dock defender will be triggered in the future, charging will be optimized.
308             info.chargeLabel = context.getString(R.string.power_charging_future_paused,
309                     info.batteryPercentString);
310         } else {
311             final String chargeStatusLabel = Utils.getBatteryStatus(context, batteryBroadcast,
312                     compactStatus);
313             info.remainingLabel = null;
314             info.chargeLabel = info.batteryLevel == 100 ? info.batteryPercentString :
315                     resources.getString(R.string.power_charging, info.batteryPercentString,
316                             chargeStatusLabel);
317         }
318     }
319 
updateBatteryInfoDischarging(Context context, boolean shortString, Estimate estimate, BatteryInfo info)320     private static void updateBatteryInfoDischarging(Context context, boolean shortString,
321             Estimate estimate, BatteryInfo info) {
322         final long drainTimeUs = PowerUtil.convertMsToUs(estimate.getEstimateMillis());
323         if (drainTimeUs > 0) {
324             info.remainingTimeUs = drainTimeUs;
325             info.remainingLabel = PowerUtil.getBatteryRemainingShortStringFormatted(
326                     context,
327                     PowerUtil.convertUsToMs(drainTimeUs)
328             );
329             info.chargeLabel = info.remainingLabel;
330             info.suggestionLabel = PowerUtil.getBatteryTipStringFormatted(
331                     context, PowerUtil.convertUsToMs(drainTimeUs));
332         } else {
333             info.remainingLabel = null;
334             info.suggestionLabel = null;
335             info.chargeLabel = info.batteryPercentString;
336         }
337     }
338 
339     public interface BatteryDataParser {
onParsingStarted(long startTime, long endTime)340         void onParsingStarted(long startTime, long endTime);
341 
onDataPoint(long time, HistoryItem record)342         void onDataPoint(long time, HistoryItem record);
343 
onDataGap()344         void onDataGap();
345 
onParsingDone()346         void onParsingDone();
347     }
348 
349     /**
350      * Iterates over battery history included in the BatteryUsageStats that this object
351      * was initialized with.
352      */
parseBatteryHistory(BatteryDataParser... parsers)353     public void parseBatteryHistory(BatteryDataParser... parsers) {
354         long startWalltime = 0;
355         long endWalltime = 0;
356         long historyStart = 0;
357         long historyEnd = 0;
358         long curWalltime = startWalltime;
359         long lastWallTime = 0;
360         long lastRealtime = 0;
361         int lastInteresting = 0;
362         int pos = 0;
363         boolean first = true;
364         final BatteryStatsHistoryIterator iterator1 =
365                 mBatteryUsageStats.iterateBatteryStatsHistory();
366         HistoryItem rec;
367         while ((rec = iterator1.next()) != null) {
368             pos++;
369             if (first) {
370                 first = false;
371                 historyStart = rec.time;
372             }
373             if (rec.cmd == HistoryItem.CMD_CURRENT_TIME
374                     || rec.cmd == HistoryItem.CMD_RESET) {
375                 // If there is a ridiculously large jump in time, then we won't be
376                 // able to create a good chart with that data, so just ignore the
377                 // times we got before and pretend like our data extends back from
378                 // the time we have now.
379                 // Also, if we are getting a time change and we are less than 5 minutes
380                 // since the start of the history real time, then also use this new
381                 // time to compute the base time, since whatever time we had before is
382                 // pretty much just noise.
383                 if (rec.currentTime > (lastWallTime + (180 * 24 * 60 * 60 * 1000L))
384                         || rec.time < (historyStart + (5 * 60 * 1000L))) {
385                     startWalltime = 0;
386                 }
387                 lastWallTime = rec.currentTime;
388                 lastRealtime = rec.time;
389                 if (startWalltime == 0) {
390                     startWalltime = lastWallTime - (lastRealtime - historyStart);
391                 }
392             }
393             if (rec.isDeltaData()) {
394                 lastInteresting = pos;
395                 historyEnd = rec.time;
396             }
397         }
398 
399         endWalltime = lastWallTime + historyEnd - lastRealtime;
400 
401         int i = 0;
402         final int N = lastInteresting;
403 
404         for (int j = 0; j < parsers.length; j++) {
405             parsers[j].onParsingStarted(startWalltime, endWalltime);
406         }
407 
408         if (endWalltime > startWalltime) {
409             final BatteryStatsHistoryIterator iterator2 =
410                     mBatteryUsageStats.iterateBatteryStatsHistory();
411             while ((rec = iterator2.next()) != null && i < N) {
412                 if (rec.isDeltaData()) {
413                     curWalltime += rec.time - lastRealtime;
414                     lastRealtime = rec.time;
415                     long x = (curWalltime - startWalltime);
416                     if (x < 0) {
417                         x = 0;
418                     }
419                     for (int j = 0; j < parsers.length; j++) {
420                         parsers[j].onDataPoint(x, rec);
421                     }
422                 } else {
423                     long lastWalltime = curWalltime;
424                     if (rec.cmd == HistoryItem.CMD_CURRENT_TIME
425                             || rec.cmd == HistoryItem.CMD_RESET) {
426                         if (rec.currentTime >= startWalltime) {
427                             curWalltime = rec.currentTime;
428                         } else {
429                             curWalltime = startWalltime + (rec.time - historyStart);
430                         }
431                         lastRealtime = rec.time;
432                     }
433 
434                     if (rec.cmd != HistoryItem.CMD_OVERFLOW
435                             && (rec.cmd != HistoryItem.CMD_CURRENT_TIME
436                             || Math.abs(lastWalltime - curWalltime) > (60 * 60 * 1000))) {
437                         for (int j = 0; j < parsers.length; j++) {
438                             parsers[j].onDataGap();
439                         }
440                     }
441                 }
442                 i++;
443             }
444         }
445 
446         for (int j = 0; j < parsers.length; j++) {
447             parsers[j].onParsingDone();
448         }
449     }
450 }
451