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
15package com.android.settings.fuelgauge;
16
17import android.content.Context;
18import android.content.Intent;
19import android.content.IntentFilter;
20import android.content.res.Resources;
21import android.os.AsyncTask;
22import android.os.BatteryManager;
23import android.os.BatteryStats;
24import android.os.BatteryStats.HistoryItem;
25import android.os.Bundle;
26import android.os.SystemClock;
27import android.support.annotation.WorkerThread;
28import android.text.TextUtils;
29import android.text.format.Formatter;
30import android.util.SparseIntArray;
31
32import com.android.internal.os.BatteryStatsHelper;
33import com.android.settings.Utils;
34import com.android.settings.graph.UsageView;
35import com.android.settings.overlay.FeatureFactory;
36import com.android.settingslib.R;
37
38public 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 {
53        void onBatteryInfoLoaded(BatteryInfo info);
54    }
55
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
133    public static void getBatteryInfo(final Context context, final Callback callback) {
134        BatteryInfo.getBatteryInfo(context, callback, false /* shortString */);
135    }
136
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
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
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
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
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 {
265        void onParsingStarted(long startTime, long endTime);
266
267        void onDataPoint(long time, HistoryItem record);
268
269        void onDataGap();
270
271        void onParsingDone();
272    }
273
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