【Android】应用电池使用情况(App battery usage)逻辑简要分析
前言
简介
在 "Settings -> App info -> App battery usage" 界面,可以设置应用电池使用情况,通常有以下三种设置选项:
- 不限制应用电池使用
- 电池优化
- 限制应用电池使用
正文
Qcom
下面分析高通机型(Android 13)如何默认开启电池优化(不限制电池使用):
定位到packages/app/Settings/src/com/android/settings/fuelgauge/BatteryOptimizeUtils.java
public class BatteryOptimizeUtils {
private static final String TAG = "BatteryOptimizeUtils";
private static final String UNKNOWN_PACKAGE = "unknown";
@VisibleForTesting AppOpsManager mAppOpsManager;
@VisibleForTesting BatteryUtils mBatteryUtils;
@VisibleForTesting PowerAllowlistBackend mPowerAllowListBackend;
@VisibleForTesting int mMode;
@VisibleForTesting boolean mAllowListed;
private final String mPackageName;
private final int mUid;
// If current user is admin, match apps from all users. Otherwise, only match the currect user.
private static final int RETRIEVE_FLAG_ADMIN =
PackageManager.MATCH_ANY_USER
| PackageManager.MATCH_DISABLED_COMPONENTS
| PackageManager.MATCH_DISABLED_UNTIL_USED_COMPONENTS;
private static final int RETRIEVE_FLAG =
PackageManager.MATCH_DISABLED_COMPONENTS
| PackageManager.MATCH_DISABLED_UNTIL_USED_COMPONENTS;
// Optimization modes.
static final int MODE_UNKNOWN = 0;
static final int MODE_RESTRICTED = 1;
static final int MODE_UNRESTRICTED = 2;
static final int MODE_OPTIMIZED = 3;
@IntDef(prefix = {"MODE_"}, value = {
MODE_UNKNOWN,
MODE_RESTRICTED,
MODE_UNRESTRICTED,
MODE_OPTIMIZED,
})
@Retention(RetentionPolicy.SOURCE)
static @interface OptimizationMode {}
public BatteryOptimizeUtils(Context context, int uid, String packageName) {
mUid = uid;
mPackageName = packageName;
mAppOpsManager = context.getSystemService(AppOpsManager.class);
mBatteryUtils = BatteryUtils.getInstance(context);
mPowerAllowListBackend = PowerAllowlistBackend.getInstance(context);
mMode = mAppOpsManager
.checkOpNoThrow(AppOpsManager.OP_RUN_ANY_IN_BACKGROUND, mUid, mPackageName);
mAllowListed = mPowerAllowListBackend.isAllowlisted(mPackageName);
}
/** Gets the {@link OptimizationMode} based on mode and allowed list. */
@OptimizationMode
public static int getAppOptimizationMode(int mode, boolean isAllowListed) {
if (!isAllowListed && mode == AppOpsManager.MODE_IGNORED) {
return MODE_RESTRICTED;
} else if (isAllowListed && mode == AppOpsManager.MODE_ALLOWED) {
return MODE_UNRESTRICTED;
} else if (!isAllowListed && mode == AppOpsManager.MODE_ALLOWED) {
return MODE_OPTIMIZED;
} else {
return MODE_UNKNOWN;
}
}
/** Gets the {@link OptimizationMode} for associated app. */
@OptimizationMode
public int getAppOptimizationMode() {
refreshState();
return getAppOptimizationMode(mMode, mAllowListed);
}
/** Resets optimization mode for all applications. */
public static void resetAppOptimizationMode(
Context context, IPackageManager ipm, AppOpsManager aom) {
resetAppOptimizationMode(context, ipm, aom,
PowerAllowlistBackend.getInstance(context), BatteryUtils.getInstance(context));
}
/** Sets the {@link OptimizationMode} for associated app. */
public void setAppUsageState(@OptimizationMode int mode) {
if (getAppOptimizationMode(mMode, mAllowListed) == mode) {
Log.w(TAG, "set the same optimization mode for: " + mPackageName);
return;
}
setAppUsageStateInternal(mode, mUid, mPackageName, mBatteryUtils, mPowerAllowListBackend);
}
/**
* Return {@code true} if package name is valid (can get an uid).
*/
public boolean isValidPackageName() {
return mBatteryUtils.getPackageUid(mPackageName) != BatteryUtils.UID_NULL;
}
/**
* Return {@code true} if this package is system or default active app.
*/
public boolean isSystemOrDefaultApp() {
mPowerAllowListBackend.refreshList();
return isSystemOrDefaultApp(mPowerAllowListBackend, mPackageName);
}
/**
* Gets the list of installed applications.
*/
public static ArraySet<ApplicationInfo> getInstalledApplications(
Context context, IPackageManager ipm) {
final ArraySet<ApplicationInfo> applications = new ArraySet<>();
final UserManager um = context.getSystemService(UserManager.class);
for (UserInfo userInfo : um.getProfiles(UserHandle.myUserId())) {
try {
@SuppressWarnings("unchecked")
final ParceledListSlice<ApplicationInfo> infoList = ipm.getInstalledApplications(
userInfo.isAdmin() ? RETRIEVE_FLAG_ADMIN : RETRIEVE_FLAG,
userInfo.id);
if (infoList != null) {
applications.addAll(infoList.getList());
}
} catch (Exception e) {
Log.e(TAG, "getInstalledApplications() is failed", e);
return null;
}
}
// Removes the application which is disabled by the system.
applications.removeIf(
info -> info.enabledSetting != PackageManager.COMPONENT_ENABLED_STATE_DISABLED_USER
&& !info.enabled);
return applications;
}
@VisibleForTesting
static void resetAppOptimizationMode(
Context context, IPackageManager ipm, AppOpsManager aom,
PowerAllowlistBackend allowlistBackend, BatteryUtils batteryUtils) {
final ArraySet<ApplicationInfo> applications = getInstalledApplications(context, ipm);
if (applications == null || applications.isEmpty()) {
Log.w(TAG, "no data found in the getInstalledApplications()");
return;
}
allowlistBackend.refreshList();
// Resets optimization mode for each application.
for (ApplicationInfo info : applications) {
final int mode = aom.checkOpNoThrow(
AppOpsManager.OP_RUN_ANY_IN_BACKGROUND, info.uid, info.packageName);
@OptimizationMode
final int optimizationMode = getAppOptimizationMode(
mode, allowlistBackend.isAllowlisted(info.packageName));
// Ignores default optimized/unknown state or system/default apps.
if (optimizationMode == MODE_OPTIMIZED
|| optimizationMode == MODE_UNKNOWN
|| isSystemOrDefaultApp(allowlistBackend, info.packageName)) {
continue;
}
// Resets to the default mode: MODE_OPTIMIZED.
setAppUsageStateInternal(MODE_OPTIMIZED, info.uid, info.packageName, batteryUtils,
allowlistBackend);
}
}
String getPackageName() {
return mPackageName == null ? UNKNOWN_PACKAGE : mPackageName;
}
private static boolean isSystemOrDefaultApp(
PowerAllowlistBackend powerAllowlistBackend, String packageName) {
return powerAllowlistBackend.isSysAllowlisted(packageName)
|| powerAllowlistBackend.isDefaultActiveApp(packageName);
}
private static void setAppUsageStateInternal(
@OptimizationMode int mode, int uid, String packageName, BatteryUtils batteryUtils,
PowerAllowlistBackend powerAllowlistBackend) {
if (mode == MODE_UNKNOWN) {
Log.d(TAG, "set unknown app optimization mode.");
return;
}
// MODE_RESTRICTED = AppOpsManager.MODE_IGNORED + !allowListed
// MODE_UNRESTRICTED = AppOpsManager.MODE_ALLOWED + allowListed
// MODE_OPTIMIZED = AppOpsManager.MODE_ALLOWED + !allowListed
final int appOpsManagerMode =
mode == MODE_RESTRICTED ? AppOpsManager.MODE_IGNORED : AppOpsManager.MODE_ALLOWED;
final boolean allowListed = mode == MODE_UNRESTRICTED;
setAppOptimizationModeInternal(appOpsManagerMode, allowListed, uid, packageName,
batteryUtils, powerAllowlistBackend);
}
private static void setAppOptimizationModeInternal(
int appStandbyMode, boolean allowListed, int uid, String packageName,
BatteryUtils batteryUtils, PowerAllowlistBackend powerAllowlistBackend) {
try {
batteryUtils.setForceAppStandby(uid, packageName, appStandbyMode);
if (allowListed) {
powerAllowlistBackend.addApp(packageName);
} else {
powerAllowlistBackend.removeApp(packageName);
}
} catch (Exception e) {
Log.e(TAG, "set OPTIMIZATION MODE failed for " + packageName, e);
}
}
private void refreshState() {
mPowerAllowListBackend.refreshList();
mAllowListed = mPowerAllowListBackend.isAllowlisted(mPackageName);
mMode = mAppOpsManager
.checkOpNoThrow(AppOpsManager.OP_RUN_ANY_IN_BACKGROUND, mUid, mPackageName);
Log.d(TAG, String.format("refresh %s state, allowlisted = %s, mode = %d",
mPackageName, mAllowListed, mMode));
}
}
通过分析 BatteryOptimizeUtils.java 的 refreshState() 方法,可以知道电池使用情况主要是由以下两种方式判断:
- 使用 mPowerAllowListBackend.isAllowlisted(mPackageName) 判断
- 判断应用是否拥有 AppOpsManager.OP_RUN_ANY_IN_BACKGROUND 权限
对于方式 1,定位到路径frameworks/base/packages/SettingsLib/src/com/android/settingslib/fuelgauge/PowerAllowlistBackend.java:
public static PowerAllowlistBackend getInstance(Context context) {
if (sInstance == null) {
sInstance = new PowerAllowlistBackend(context);
}
return sInstance;
}
/**
* Check if target package is in System allow list
*/
public boolean isSysAllowlisted(String pkg) {
return mSysAllowlistedApps.contains(pkg);// 若是系统应用,默认允许不限制
}
/**
* Check if target package is in allow list
*/
public boolean isAllowlisted(String pkg) {
if (mAllowlistedApps.contains(pkg)) {// 判断普通应用是否在白名单中
return true;
}
if (isDefaultActiveApp(pkg)) {// 判断是否是设备所有者应用
return true;
}
return false;
}
/**
* Check if it is default active app in multiple area(i.e. SMS, Dialer, Device admin..)
*/
public boolean isDefaultActiveApp(String pkg) {
// Additionally, check if pkg is default dialer/sms. They are considered essential apps and
// should be automatically allowlisted (otherwise user may be able to set restriction on
// them, leading to bad device behavior.)
if (mDefaultActiveApps.contains(pkg)) {
return true;
}
final DevicePolicyManager devicePolicyManager = mAppContext.getSystemService(DevicePolicyManager.class);
if (devicePolicyManager.packageHasActiveAdmins(pkg)) {
return true;
}
return false;
}
/**
* Add app into power save allow list.
* @param pkg packageName
*/
public void addApp(String pkg) {
try {
mDeviceIdleService.addPowerSaveWhitelistApp(pkg);
mAllowlistedApps.add(pkg);
} catch (RemoteException e) {
Log.w(TAG, "Unable to reach IDeviceIdleController", e);
}
}
/**
* Remove package from power save allow list.
* @param pkg
*/
public void removeApp(String pkg) {
try {
mDeviceIdleService.removePowerSaveWhitelistApp(pkg);
mAllowlistedApps.remove(pkg);
} catch (RemoteException e) {
Log.w(TAG, "Unable to reach IDeviceIdleController", e);
}
}
针对上述代码的分析,易知只需要调用 PowerAllowlistBackend.java 的 addApp(String pkg) 方法,即可将应用添加到不限制电池使用白名单中。可以使用以下代码进行调用:
private static final String BATTERY_OPTIMIZE_UTILS_CLASS_NAME =
"com.android.settingslib.fuelgauge.BatteryOptimizeUtils";
private static final String ADD_APP_METHOD_NAME = "addApp";
private enum PowerAllowPkgName {
PKG1("包名1"),
PKG2("包名2");
private final String pkgName;
PowerAllowPkgName(String pkgName) {
this.pkgName = pkgName;
}
@NonNull
@Override
public String toString() {
return pkgName;
}
}
public static boolean judgeApplicationInstalled(Context context, String packageName) {
boolean isInstalled = false;
try {
ApplicationInfo ai = context.getPackageManager().getApplicationInfo(packageName, 0);
isInstalled = (ai != null);
} catch (PackageManager.NameNotFoundException e) {
isInstalled = false;
}
return isInstalled;
}
private void addVendorAppToPowerAllowList() {
for (PowerAllowPkgName pkgName: PowerAllowPkgName.values()) {
if (!judgeApplicationInstalled(mContext, pkgName.toString())) {
continue;
}
LogUtil.d("Add " + pkgName.toString() + " to power allow list.");
addAppToPowerAllowList(pkgName.toString());
}
}
private void addAppToPowerAllowList(String packageName) {
try {
ClassLoader classLoader = mContext.getClassLoader();
@SuppressLint("PrivateApi") Class<?> pwerAllowlistBackendClass = Class.forName(
BATTERY_OPTIMIZE_UTILS_CLASS_NAME,
true,
classLoader);
LogUtil.d("Load PwerAllowlistBackend successfully: "
+ pwerAllowlistBackendClass.getName());
Method getInstanceMethod = pwerAllowlistBackendClass.getMethod(
"getInstance", Context.class);
LogUtil.d("Find getInstance method: " + getInstanceMethod);
Object instance = getInstanceMethod.invoke(null, getAppContext());
LogUtil.d("instance = " + instance);
Method addAppMethod = pwerAllowlistBackendClass.getDeclaredMethod(
ADD_APP_METHOD_NAME, String.class);
addAppMethod.setAccessible(true);
addAppMethod.invoke(instance, packageName);
LogUtil.d("Add app " + packageName + " to power allow list successfully.");
} catch (ClassNotFoundException | NoSuchMethodException | InvocationTargetException |
IllegalAccessException e) {
e.printStackTrace();
}
}
对于方式 2,可以使用以下方法授予权限:
private void grantRunAnyInBackgroundPermission() {
for (PowerAllowPkgName pkgName: PowerAllowPkgName.values()) {
setMode(pkgName.toString(), AppOpsManager.OP_RUN_ANY_IN_BACKGROUND, true);
}
}
public boolean setMode(String packageName, int code, boolean newState) {
try {
PackageManager pm = getPackageManager();
ApplicationInfo ai = pm.getApplicationInfo(packageName, PackageManager.GET_ACTIVITIES);
AppOpsManager mAppOpsManager = (AppOpsManager) getSystemService(Context.APP_OPS_SERVICE);
mAppOpsManager.setMode(code, ai.uid, packageName, newState
? AppOpsManager.MODE_ALLOWED : AppOpsManager.MODE_ERRORED);
return true;
} catch (PackageManager.NameNotFoundException e) {
e.printStackTrace();
return false;
}
}
但是在高通 Android 13 的设备上,会弹出询问 "Let app always run in background?" 的对话框,定位到src/com/android/settings/fuelgauge/RequestIgnoreBatteryOptimizations.java
// 这是一个继承自 AlertActivity 的界面,所以在检测到新安装的应用有授予电池优化权限时
// 核心逻辑是询问是否允许总是允许在后台(Let app always run in background),即不限制电池使用
@Override
public void onClick(DialogInterface dialog, int which) {
switch (which) {
case BUTTON_POSITIVE:
mPowerWhitelistManager.addToWhitelist(mPackageName);
break;
case BUTTON_NEGATIVE:
break;
}
}
对于点击确定按钮的逻辑,定位到路径 frameworks/base/apex/jobscheduler/framework/java/android/os/PowerWhitelistManager.java:
/**
* Add the specified package to the permanent power save whitelist.
*
* @deprecated Use {@link PowerExemptionManager#addToPermanentAllowList(String)} instead
*/
@Deprecated
@RequiresPermission(android.Manifest.permission.DEVICE_POWER)
public void addToWhitelist(@NonNull String packageName) {
mPowerExemptionManager.addToPermanentAllowList(packageName);
}
/**
* Add the specified packages to the permanent power save whitelist.
*
* @deprecated Use {@link PowerExemptionManager#addToPermanentAllowList(List)} instead
*/
@Deprecated
@RequiresPermission(android.Manifest.permission.DEVICE_POWER)
public void addToWhitelist(@NonNull List<String> packageNames) {
mPowerExemptionManager.addToPermanentAllowList(packageNames);
}
/**
* Get a list of app IDs of app that are whitelisted. This does not include temporarily
* whitelisted apps.
*
* @param includingIdle Set to true if the app should be whitelisted from device idle as well
* as other power save restrictions
* @deprecated Use {@link PowerExemptionManager#getAllowListedAppIds(boolean)} instead
* @hide
*/
@Deprecated
@NonNull
public int[] getWhitelistedAppIds(boolean includingIdle) {
return mPowerExemptionManager.getAllowListedAppIds(includingIdle);
}
/**
* Returns true if the app is whitelisted from power save restrictions. This does not include
* temporarily whitelisted apps.
*
* @param includingIdle Set to true if the app should be whitelisted from device
* idle as well as other power save restrictions
* @deprecated Use {@link PowerExemptionManager#isAllowListed(String, boolean)} instead
* @hide
*/
@Deprecated
public boolean isWhitelisted(@NonNull String packageName, boolean includingIdle) {
return mPowerExemptionManager.isAllowListed(packageName, includingIdle);
}
/**
* Remove an app from the permanent power save whitelist. Only apps that were added via
* {@link #addToWhitelist(String)} or {@link #addToWhitelist(List)} will be removed. Apps
* whitelisted by default by the system cannot be removed.
*
* @param packageName The app to remove from the whitelist
* @deprecated Use {@link PowerExemptionManager#removeFromPermanentAllowList(String)} instead
*/
@Deprecated
@RequiresPermission(android.Manifest.permission.DEVICE_POWER)
public void removeFromWhitelist(@NonNull String packageName) {
mPowerExemptionManager.removeFromPermanentAllowList(packageName);
}
由注释信息,可知这个类的方法已经过时了,本文仍以 PowerWhitelistManager.java 进行讲解,可以使用 PowerExemptionManager.java 进行替代,读者可自行翻阅,此不赘述。
如果想不弹窗默认授予,可以修改以下代码解决:
private void addVendorAppToPowerAllowList() {
for (PowerAllowPkgName pkgName: PowerAllowPkgName.values()) {
if (!judgeApplicationInstalled(mContext, pkgName.toString())) {
continue;
}
LogUtil.d("Add " + pkgName.toString() + " to power allow list.");
if (Build.VERSION.SDK_INT <= 33) {
PowerWhitelistManager powerWhitelistManager = mContext.getSystemService(PowerWhitelistManager.class);
powerWhitelistManager.addToWhitelist(pkgName.toString());
} else {
addAppToPowerAllowList(pkgName.toString());
}
}
}
MTK
定位到\vendor\mediatek\proprietary\packages\apps\MtkSettings\src\com\android\settings\fuelgauge\BatteryOptimizeUtils.java:
// 判断是否为不受限制的应用
static boolean isSystemOrDefaultApp(
Context context,
PowerAllowlistBackend powerAllowlistBackend,
String packageName,
int uid) {
return powerAllowlistBackend.isSysAllowlisted(packageName)
// Always forced unrestricted apps are one type of system important apps.
|| getForceBatteryUnrestrictModeList(context).contains(packageName)// 通过属性设置默认配置名单
|| powerAllowlistBackend.isDefaultActiveApp(packageName, uid);
}
// 获取强制开启“电池优化”的应用名单
static List<String> getForceBatteryOptimizeModeList(Context context) {
if (sBatteryOptimizeModeList == null) {
sBatteryOptimizeModeList =
Arrays.asList(
context.getResources()
.getStringArray(
R.array.config_force_battery_optimize_mode_apps));// 通过该配置项进行静态修改
// Begin: 通过以下代码,可以修改系统属性 persist.sys.customize.battery.optimize_apps 的值动态定制“电池优化”的应用名单
String extraAppsProp = SystemProperties.get("persist.sys.customize.battery.optimize_apps", "");
sBatteryOptimizeModeList = Stream.concat(
sBatteryOptimizeModeList.stream(),
Arrays.stream(extraAppsProp.split(","))
.filter(app -> !TextUtils.isEmpty(app.trim()))
.map(String::trim)
).collect(Collectors.toList());
// End
}
return sBatteryOptimizeModeList;
}
// 获取强制开启“电池不受限”的应用名单
static List<String> getForceBatteryUnrestrictModeList(Context context) {
if (sBatteryUnrestrictModeList == null) {
sBatteryUnrestrictModeList =
Arrays.asList(
context.getResources()
.getStringArray(
R.array.config_force_battery_unrestrict_mode_apps));//通过该配置项可以进行静态修改
// Begin: 通过以下代码,可以通过修改系统属性 persist.sys.customize.battery.unrestricted_apps 的值动态定制“电池不受限”的应用名单
String extraAppsProp = SystemProperties.get("persist.sys.customize.battery.unrestricted_apps", "");
sBatteryUnrestrictModeList = Stream.concat(
sBatteryUnrestrictModeList.stream(),
Arrays.stream(extraAppsProp.split(","))
.filter(app -> !TextUtils.isEmpty(app.trim()))
.map(String::trim)
).collect(Collectors.toList());
// End
}
return sBatteryUnrestrictModeList;
}
// 设置应用属于哪种模式
private static void setAppUsageStateInternal(
Context context,
@OptimizationMode int mode,
int uid,
String packageName,
BatteryUtils batteryUtils,
PowerAllowlistBackend powerAllowlistBackend,
Action action) {
if (mode == MODE_UNKNOWN) {
Log.d(TAG, "set unknown app optimization mode.");
return;
}
// MODE_RESTRICTED = AppOpsManager.MODE_IGNORED + !allowListed
// MODE_UNRESTRICTED = AppOpsManager.MODE_ALLOWED + allowListed
// MODE_OPTIMIZED = AppOpsManager.MODE_ALLOWED + !allowListed
final int appOpsManagerMode =
mode == MODE_RESTRICTED ? AppOpsManager.MODE_IGNORED : AppOpsManager.MODE_ALLOWED;
final boolean allowListed = mode == MODE_UNRESTRICTED;
setAppOptimizationModeInternal(
context,
appOpsManagerMode,
allowListed,
uid,
packageName,
batteryUtils,
powerAllowlistBackend,
action);
}
private void refreshState() {
mPowerAllowListBackend.refreshList();
mAllowListed = mPowerAllowListBackend.isAllowlisted(mPackageName);
mMode = mAppOpsManager
.checkOpNoThrow(AppOpsManager.OP_RUN_ANY_IN_BACKGROUND, mUid, mPackageName);
Log.d(TAG, String.format("refresh %s state, allowlisted = %s, mode = %d",
mPackageName, mAllowListed, mMode));
}
定位到路径/packages/apps/MtkSettings/res/values/config.xml,可以对配置项进行静态修改:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- 覆盖电池无限制模式应用列表 -->
<string-array name="config_force_battery_unrestrict_mode_apps" translatable="false">
<item>packageName1</item>
<item>packageName2</item>
</string-array>
</resources>
浙公网安备 33010602011771号