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