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