Files
app_Settings/src/com/android/settings/fuelgauge/BatteryInfo.java
Yiling Chuang 318636d51b Update the conditions of power plugged determination.
Under charging optimization mode, use ADAPTIVE_LONGLIFE as the additional condition to check whether a device is plugged.

Bug: 349949603
Flag: EXEMPT bugfix
Test: atest SettingsRoboTests
Change-Id: Idbdfaaa1d5c54be325b6182bcda81d4282c21ba0
2024-07-02 10:54:36 +00:00

687 lines
29 KiB
Java

/*
* Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
* except in compliance with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the
* License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the specific language governing
* permissions and limitations under the License.
*/
package com.android.settings.fuelgauge;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.res.Resources;
import android.os.AsyncTask;
import android.os.BatteryManager;
import android.os.BatteryStats.HistoryItem;
import android.os.BatteryStatsManager;
import android.os.BatteryUsageStats;
import android.os.SystemClock;
import android.provider.Settings;
import android.text.format.Formatter;
import android.util.Log;
import android.util.SparseIntArray;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.WorkerThread;
import com.android.internal.os.BatteryStatsHistoryIterator;
import com.android.settings.Utils;
import com.android.settings.overlay.FeatureFactory;
import com.android.settings.widget.UsageView;
import com.android.settingslib.R;
import com.android.settingslib.fuelgauge.BatteryStatus;
import com.android.settingslib.fuelgauge.Estimate;
import com.android.settingslib.fuelgauge.EstimateKt;
import com.android.settingslib.utils.PowerUtil;
import com.android.settingslib.utils.StringUtil;
public class BatteryInfo {
private static final String TAG = "BatteryInfo";
public CharSequence chargeLabel;
public CharSequence remainingLabel;
public int batteryLevel;
public int batteryStatus;
public int pluggedStatus;
public boolean discharging = true;
public boolean isBatteryDefender = false;
public boolean isLongLife = false;
public boolean isFastCharging;
public long remainingTimeUs = 0;
public long averageTimeToDischarge = EstimateKt.AVERAGE_TIME_TO_DISCHARGE_UNKNOWN;
public String batteryPercentString;
public String statusLabel;
public String suggestionLabel;
private boolean mCharging;
private BatteryUsageStats mBatteryUsageStats;
private static final String LOG_TAG = "BatteryInfo";
private long timePeriod;
public interface Callback {
void onBatteryInfoLoaded(BatteryInfo info);
}
public void bindHistory(final UsageView view, BatteryDataParser... parsers) {
final Context context = view.getContext();
BatteryDataParser parser =
new BatteryDataParser() {
SparseIntArray mPoints = new SparseIntArray();
long mStartTime;
int mLastTime = -1;
byte mLastLevel;
@Override
public void onParsingStarted(long startTime, long endTime) {
this.mStartTime = startTime;
timePeriod = endTime - startTime;
view.clearPaths();
// Initially configure the graph for history only.
view.configureGraph((int) timePeriod, 100);
}
@Override
public void onDataPoint(long time, HistoryItem record) {
mLastTime = (int) time;
mLastLevel = record.batteryLevel;
mPoints.put(mLastTime, mLastLevel);
}
@Override
public void onDataGap() {
if (mPoints.size() > 1) {
view.addPath(mPoints);
}
mPoints.clear();
}
@Override
public void onParsingDone() {
onDataGap();
// Add projection if we have an estimate.
if (remainingTimeUs != 0) {
PowerUsageFeatureProvider provider =
FeatureFactory.getFeatureFactory()
.getPowerUsageFeatureProvider();
if (!mCharging
&& provider.isEnhancedBatteryPredictionEnabled(context)) {
mPoints =
provider.getEnhancedBatteryPredictionCurve(
context, mStartTime);
} else {
// Linear extrapolation.
if (mLastTime >= 0) {
mPoints.put(mLastTime, mLastLevel);
mPoints.put(
(int)
(timePeriod
+ PowerUtil.convertUsToMs(
remainingTimeUs)),
mCharging ? 100 : 0);
}
}
}
// If we have a projection, reconfigure the graph to show it.
if (mPoints != null && mPoints.size() > 0) {
int maxTime = mPoints.keyAt(mPoints.size() - 1);
view.configureGraph(maxTime, 100);
view.addProjectedPath(mPoints);
}
}
};
BatteryDataParser[] parserList = new BatteryDataParser[parsers.length + 1];
for (int i = 0; i < parsers.length; i++) {
parserList[i] = parsers[i];
}
parserList[parsers.length] = parser;
parseBatteryHistory(parserList);
String timeString =
context.getString(
R.string.charge_length_format,
Formatter.formatShortElapsedTime(context, timePeriod));
String remaining = "";
if (remainingTimeUs != 0) {
remaining =
context.getString(
R.string.remaining_length_format,
Formatter.formatShortElapsedTime(context, remainingTimeUs / 1000));
}
view.setBottomLabels(new CharSequence[] {timeString, remaining});
}
/** Gets battery info */
public static void getBatteryInfo(
final Context context, final Callback callback, boolean shortString) {
BatteryInfo.getBatteryInfo(context, callback, /* batteryUsageStats */ null, shortString);
}
static long getSettingsChargeTimeRemaining(final Context context) {
return Settings.Global.getLong(
context.getContentResolver(),
com.android.settingslib.fuelgauge.BatteryUtils.GLOBAL_TIME_TO_FULL_MILLIS,
-1);
}
/** Gets battery info */
public static void getBatteryInfo(
final Context context,
final Callback callback,
@Nullable final BatteryUsageStats batteryUsageStats,
boolean shortString) {
new AsyncTask<Void, Void, BatteryInfo>() {
@Override
protected BatteryInfo doInBackground(Void... params) {
boolean shouldCloseBatteryUsageStats = false;
BatteryUsageStats stats;
if (batteryUsageStats != null) {
stats = batteryUsageStats;
} else {
try {
stats =
context.getSystemService(BatteryStatsManager.class)
.getBatteryUsageStats();
shouldCloseBatteryUsageStats = true;
} catch (RuntimeException e) {
Log.e(TAG, "getBatteryInfo() from getBatteryUsageStats()", e);
// Use default BatteryUsageStats.
stats = new BatteryUsageStats.Builder(new String[0]).build();
}
}
final BatteryInfo batteryInfo = getBatteryInfo(context, stats, shortString);
if (shouldCloseBatteryUsageStats) {
try {
stats.close();
} catch (Exception e) {
Log.e(TAG, "BatteryUsageStats.close() failed", e);
}
}
return batteryInfo;
}
@Override
protected void onPostExecute(BatteryInfo batteryInfo) {
final long startTime = System.currentTimeMillis();
callback.onBatteryInfoLoaded(batteryInfo);
BatteryUtils.logRuntime(LOG_TAG, "time for callback", startTime);
}
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}
/** Creates a BatteryInfo based on BatteryUsageStats */
@WorkerThread
public static BatteryInfo getBatteryInfo(
final Context context,
@NonNull final BatteryUsageStats batteryUsageStats,
boolean shortString) {
final long batteryStatsTime = System.currentTimeMillis();
BatteryUtils.logRuntime(LOG_TAG, "time for getStats", batteryStatsTime);
final long startTime = System.currentTimeMillis();
PowerUsageFeatureProvider provider =
FeatureFactory.getFeatureFactory().getPowerUsageFeatureProvider();
final long elapsedRealtimeUs = PowerUtil.convertMsToUs(SystemClock.elapsedRealtime());
final Intent batteryBroadcast =
context.registerReceiver(null, new IntentFilter(Intent.ACTION_BATTERY_CHANGED));
// 0 means we are discharging, anything else means charging
final boolean discharging =
batteryBroadcast.getIntExtra(BatteryManager.EXTRA_PLUGGED, -1) == 0;
if (discharging && provider.isEnhancedBatteryPredictionEnabled(context)) {
Estimate estimate = provider.getEnhancedBatteryPrediction(context);
if (estimate != null) {
Estimate.storeCachedEstimate(context, estimate);
BatteryUtils.logRuntime(LOG_TAG, "time for enhanced BatteryInfo", startTime);
return BatteryInfo.getBatteryInfo(
context,
batteryBroadcast,
batteryUsageStats,
estimate,
elapsedRealtimeUs,
shortString);
}
}
final long prediction = discharging ? batteryUsageStats.getBatteryTimeRemainingMs() : 0;
final Estimate estimate =
new Estimate(
prediction,
false, /* isBasedOnUsage */
EstimateKt.AVERAGE_TIME_TO_DISCHARGE_UNKNOWN);
BatteryUtils.logRuntime(LOG_TAG, "time for regular BatteryInfo", startTime);
return BatteryInfo.getBatteryInfo(
context,
batteryBroadcast,
batteryUsageStats,
estimate,
elapsedRealtimeUs,
shortString);
}
@WorkerThread
public static BatteryInfo getBatteryInfoOld(
Context context,
Intent batteryBroadcast,
BatteryUsageStats batteryUsageStats,
long elapsedRealtimeUs,
boolean shortString) {
Estimate estimate =
new Estimate(
batteryUsageStats.getBatteryTimeRemainingMs(),
false,
EstimateKt.AVERAGE_TIME_TO_DISCHARGE_UNKNOWN);
return getBatteryInfo(
context,
batteryBroadcast,
batteryUsageStats,
estimate,
elapsedRealtimeUs,
shortString);
}
@WorkerThread
public static BatteryInfo getBatteryInfo(
Context context,
Intent batteryBroadcast,
@NonNull BatteryUsageStats batteryUsageStats,
Estimate estimate,
long elapsedRealtimeUs,
boolean shortString,
long currentTimeMs) {
final boolean isCompactStatus =
context.getResources()
.getBoolean(com.android.settings.R.bool.config_use_compact_battery_status);
BatteryInfo info = new BatteryInfo();
info.mBatteryUsageStats = batteryUsageStats;
info.batteryLevel = Utils.getBatteryLevel(batteryBroadcast);
info.batteryPercentString = Utils.formatPercentage(info.batteryLevel);
info.pluggedStatus = batteryBroadcast.getIntExtra(BatteryManager.EXTRA_PLUGGED, 0);
info.mCharging = info.pluggedStatus != 0;
info.averageTimeToDischarge = estimate.getAverageDischargeTime();
final int chargingPolicy =
batteryBroadcast.getIntExtra(
BatteryManager.EXTRA_CHARGING_STATUS,
BatteryManager.CHARGING_POLICY_DEFAULT);
info.isLongLife = chargingPolicy == BatteryManager.CHARGING_POLICY_ADAPTIVE_LONGLIFE;
info.statusLabel = Utils.getBatteryStatus(context, batteryBroadcast, isCompactStatus);
info.batteryStatus =
batteryBroadcast.getIntExtra(
BatteryManager.EXTRA_STATUS, BatteryManager.BATTERY_STATUS_UNKNOWN);
info.isFastCharging =
BatteryStatus.getChargingSpeed(context, batteryBroadcast)
== BatteryStatus.CHARGING_FAST;
if (info.isLongLife) {
info.isBatteryDefender =
FeatureFactory.getFeatureFactory()
.getPowerUsageFeatureProvider()
.isBatteryDefend(info);
}
Log.d(
TAG,
"chargingPolicy = "
+ chargingPolicy
+ ", pluggedStatus = "
+ info.pluggedStatus
+ ", batteryStatus = "
+ info.batteryStatus);
if (!isPluggedIn(context, info.mCharging, chargingPolicy)) {
updateBatteryInfoDischarging(context, shortString, estimate, info);
} else {
updateBatteryInfoCharging(
context,
batteryBroadcast,
batteryUsageStats,
info,
isCompactStatus,
currentTimeMs);
}
BatteryUtils.logRuntime(LOG_TAG, "time for getBatteryInfo", currentTimeMs);
return info;
}
/** Returns a {@code BatteryInfo} with battery and charging relative information. */
@WorkerThread
public static BatteryInfo getBatteryInfo(
Context context,
Intent batteryBroadcast,
BatteryUsageStats batteryUsageStats,
Estimate estimate,
long elapsedRealtimeUs,
boolean shortString) {
long currentTimeMs = System.currentTimeMillis();
return getBatteryInfo(
context,
batteryBroadcast,
batteryUsageStats,
estimate,
elapsedRealtimeUs,
shortString,
currentTimeMs);
}
private static void updateBatteryInfoCharging(
Context context,
Intent batteryBroadcast,
BatteryUsageStats stats,
BatteryInfo info,
boolean compactStatus,
long currentTimeMs) {
final Resources resources = context.getResources();
final long chargeTimeMs = stats.getChargeTimeRemainingMs();
if (getSettingsChargeTimeRemaining(context) != chargeTimeMs) {
Settings.Global.putLong(
context.getContentResolver(),
com.android.settingslib.fuelgauge.BatteryUtils.GLOBAL_TIME_TO_FULL_MILLIS,
chargeTimeMs);
}
final int status =
batteryBroadcast.getIntExtra(
BatteryManager.EXTRA_STATUS, BatteryManager.BATTERY_STATUS_UNKNOWN);
info.discharging = false;
info.suggestionLabel = null;
int dockDefenderMode = BatteryUtils.getCurrentDockDefenderMode(context, info);
if ((info.isBatteryDefender
&& status != BatteryManager.BATTERY_STATUS_FULL
&& dockDefenderMode == BatteryUtils.DockDefenderMode.DISABLED)
|| dockDefenderMode == BatteryUtils.DockDefenderMode.ACTIVE) {
// Battery defender active, battery charging paused
info.remainingLabel = null;
int chargingLimitedResId = R.string.power_charging_limited;
info.chargeLabel = context.getString(chargingLimitedResId, info.batteryPercentString);
return;
}
final BatterySettingsFeatureProvider featureProvider =
FeatureFactory.getFeatureFactory().getBatterySettingsFeatureProvider();
if (featureProvider.isChargingOptimizationMode(context)) {
final CharSequence chargeLabel =
featureProvider.getChargingOptimizationChargeLabel(
context,
info.batteryLevel,
info.batteryPercentString,
chargeTimeMs,
currentTimeMs);
if (chargeLabel != null) {
final CharSequence remainingLabel =
featureProvider.getChargingOptimizationRemainingLabel(
context,
info.batteryLevel,
info.pluggedStatus,
chargeTimeMs,
currentTimeMs);
if (remainingLabel != null) {
info.chargeLabel = chargeLabel;
info.remainingLabel = remainingLabel;
return;
}
}
}
if ((chargeTimeMs > 0
&& status != BatteryManager.BATTERY_STATUS_FULL
&& dockDefenderMode == BatteryUtils.DockDefenderMode.DISABLED)
|| dockDefenderMode == BatteryUtils.DockDefenderMode.TEMPORARILY_BYPASSED) {
// Battery is charging to full
info.remainingTimeUs = PowerUtil.convertMsToUs(chargeTimeMs);
int resId = getChargingDurationResId(info.isFastCharging);
info.remainingLabel =
chargeTimeMs <= 0
? null
: getPowerRemainingChargingLabel(
context,
chargeTimeMs,
info.isFastCharging,
info.pluggedStatus,
currentTimeMs,
featureProvider);
info.chargeLabel =
chargeTimeMs <= 0
? info.batteryPercentString
: getChargeLabelWithTimeToFull(
context,
resId,
info.batteryPercentString,
chargeTimeMs,
info.isFastCharging,
currentTimeMs);
} else if (dockDefenderMode == BatteryUtils.DockDefenderMode.FUTURE_BYPASS) {
// Dock defender will be triggered in the future, charging will be optimized.
info.chargeLabel =
context.getString(
R.string.power_charging_future_paused, info.batteryPercentString);
} else {
final String chargeStatusLabel =
Utils.getBatteryStatus(context, batteryBroadcast, compactStatus);
info.remainingLabel = null;
info.chargeLabel =
info.batteryLevel == 100
? info.batteryPercentString
: resources.getString(
R.string.power_charging,
info.batteryPercentString,
chargeStatusLabel);
}
}
private static CharSequence getPowerRemainingChargingLabel(
Context context,
long chargeRemainingTimeMs,
boolean isFastCharging,
int pluggedStatus,
long currentTimeMs,
BatterySettingsFeatureProvider featureProvider) {
if (pluggedStatus == BatteryManager.BATTERY_PLUGGED_WIRELESS) {
final CharSequence wirelessChargingRemainingLabel =
featureProvider.getWirelessChargingRemainingLabel(
context, chargeRemainingTimeMs, currentTimeMs);
if (wirelessChargingRemainingLabel != null) {
return wirelessChargingRemainingLabel;
}
}
if (com.android.settingslib.fuelgauge.BatteryUtils.isChargingStringV2Enabled()) {
int chargeLabelResId =
isFastCharging
? R.string.power_remaining_fast_charging_duration_only_v2
: R.string.power_remaining_charging_duration_only_v2;
String timeString =
PowerUtil.getTargetTimeShortString(
context, chargeRemainingTimeMs, currentTimeMs);
return context.getString(chargeLabelResId, timeString);
}
final CharSequence timeString =
StringUtil.formatElapsedTime(
context,
chargeRemainingTimeMs,
/* withSeconds= */ false,
/* collapseTimeUnit= */ true);
return context.getString(R.string.power_remaining_charging_duration_only, timeString);
}
private static CharSequence getChargeLabelWithTimeToFull(
Context context,
int chargeLabelResId,
String batteryPercentString,
long chargeTimeMs,
boolean isFastCharging,
long currentTimeMs) {
if (com.android.settingslib.fuelgauge.BatteryUtils.isChargingStringV2Enabled()) {
var timeString =
PowerUtil.getTargetTimeShortString(context, chargeTimeMs, currentTimeMs);
return isFastCharging
? context.getString(
chargeLabelResId,
batteryPercentString,
context.getString(R.string.battery_info_status_charging_fast_v2),
timeString)
: context.getString(chargeLabelResId, batteryPercentString, timeString);
} else {
var timeString =
StringUtil.formatElapsedTime(
context,
(double) chargeTimeMs,
/* withSeconds= */ false,
/* collapseTimeUnit= */ true);
return context.getString(chargeLabelResId, batteryPercentString, timeString);
}
}
private static int getChargingDurationResId(boolean isFastCharging) {
if (com.android.settingslib.fuelgauge.BatteryUtils.isChargingStringV2Enabled()) {
return isFastCharging
? R.string.power_fast_charging_duration_v2
: R.string.power_charging_duration_v2;
}
return R.string.power_charging_duration;
}
private static void updateBatteryInfoDischarging(
Context context, boolean shortString, Estimate estimate, BatteryInfo info) {
final long drainTimeUs = PowerUtil.convertMsToUs(estimate.getEstimateMillis());
if (drainTimeUs > 0) {
info.remainingTimeUs = drainTimeUs;
info.remainingLabel =
PowerUtil.getBatteryRemainingShortStringFormatted(
context, PowerUtil.convertUsToMs(drainTimeUs));
info.chargeLabel = info.remainingLabel;
info.suggestionLabel =
PowerUtil.getBatteryTipStringFormatted(
context, PowerUtil.convertUsToMs(drainTimeUs));
} else {
info.remainingLabel = null;
info.suggestionLabel = null;
info.chargeLabel = info.batteryPercentString;
}
}
private static boolean isPluggedIn(Context context, boolean isCharging, int chargingPolicy) {
return isCharging
|| (chargingPolicy == BatteryManager.CHARGING_POLICY_ADAPTIVE_LONGLIFE
&& FeatureFactory.getFeatureFactory()
.getBatterySettingsFeatureProvider()
.isChargingOptimizationMode(context));
}
public interface BatteryDataParser {
void onParsingStarted(long startTime, long endTime);
void onDataPoint(long time, HistoryItem record);
void onDataGap();
void onParsingDone();
}
/**
* Iterates over battery history included in the BatteryUsageStats that this object was
* initialized with.
*/
public void parseBatteryHistory(BatteryDataParser... parsers) {
long startWalltime = 0;
long endWalltime = 0;
long historyStart = 0;
long historyEnd = 0;
long curWalltime = startWalltime;
long lastWallTime = 0;
long lastRealtime = 0;
int lastInteresting = 0;
int pos = 0;
boolean first = true;
final BatteryStatsHistoryIterator iterator1 =
mBatteryUsageStats.iterateBatteryStatsHistory();
HistoryItem rec;
while ((rec = iterator1.next()) != null) {
pos++;
if (first) {
first = false;
historyStart = rec.time;
}
if (rec.cmd == HistoryItem.CMD_CURRENT_TIME || rec.cmd == HistoryItem.CMD_RESET) {
// If there is a ridiculously large jump in time, then we won't be
// able to create a good chart with that data, so just ignore the
// times we got before and pretend like our data extends back from
// the time we have now.
// Also, if we are getting a time change and we are less than 5 minutes
// since the start of the history real time, then also use this new
// time to compute the base time, since whatever time we had before is
// pretty much just noise.
if (rec.currentTime > (lastWallTime + (180 * 24 * 60 * 60 * 1000L))
|| rec.time < (historyStart + (5 * 60 * 1000L))) {
startWalltime = 0;
}
lastWallTime = rec.currentTime;
lastRealtime = rec.time;
if (startWalltime == 0) {
startWalltime = lastWallTime - (lastRealtime - historyStart);
}
}
if (rec.isDeltaData()) {
lastInteresting = pos;
historyEnd = rec.time;
}
}
endWalltime = lastWallTime + historyEnd - lastRealtime;
int i = 0;
final int N = lastInteresting;
for (int j = 0; j < parsers.length; j++) {
parsers[j].onParsingStarted(startWalltime, endWalltime);
}
if (endWalltime > startWalltime) {
final BatteryStatsHistoryIterator iterator2 =
mBatteryUsageStats.iterateBatteryStatsHistory();
while ((rec = iterator2.next()) != null && i < N) {
if (rec.isDeltaData()) {
curWalltime += rec.time - lastRealtime;
lastRealtime = rec.time;
long x = (curWalltime - startWalltime);
if (x < 0) {
x = 0;
}
for (int j = 0; j < parsers.length; j++) {
parsers[j].onDataPoint(x, rec);
}
} else {
long lastWalltime = curWalltime;
if (rec.cmd == HistoryItem.CMD_CURRENT_TIME
|| rec.cmd == HistoryItem.CMD_RESET) {
if (rec.currentTime >= startWalltime) {
curWalltime = rec.currentTime;
} else {
curWalltime = startWalltime + (rec.time - historyStart);
}
lastRealtime = rec.time;
}
if (rec.cmd != HistoryItem.CMD_OVERFLOW
&& (rec.cmd != HistoryItem.CMD_CURRENT_TIME
|| Math.abs(lastWalltime - curWalltime) > (60 * 60 * 1000))) {
for (int j = 0; j < parsers.length; j++) {
parsers[j].onDataGap();
}
}
}
i++;
}
}
for (int j = 0; j < parsers.length; j++) {
parsers[j].onParsingDone();
}
}
}