Add allowlist mechanism for battery optimization mode

Add a mechanism to add package name into the allowlist to avoid users
change the battery optimization modes for specific apps in the list

https://screenshot.googleplex.com/8hrHCcTh5bNYXqp

Bug: 281566984
Test: make test RunSettingsRoboTests ROBOTEST_FILTER=com.android.settings.fuelgauge.*
Change-Id: I8efa6a55646d761f5bee3667a59b38ab68c74bc1
This commit is contained in:
ykhung
2023-05-15 11:01:16 +08:00
parent b71dd78a86
commit 0cd24adec5
6 changed files with 193 additions and 19 deletions

View File

@@ -20,6 +20,7 @@ message BatteryOptimizeHistoricalLogEntry {
RESET = 3;
RESTORE = 4;
BACKUP = 5;
FORCE_RESET = 6;
}
optional string package_name = 1;

View File

@@ -526,6 +526,10 @@
<item>content://com.android.settings.slices/intent/media_output_indicator</item>
</string-array>
<!-- List containing the apps cannot be changed the battery optimize modes -->
<string-array name="config_disable_optimization_mode_apps" translatable="false">
</string-array>
<!-- Uri to query non-public Slice Uris. -->
<string name="config_non_public_slice_query_uri" translatable="false"></string>

View File

@@ -91,11 +91,12 @@ public final class BatteryBackupHelper implements BackupHelper {
@Override
public void restoreEntity(BackupDataInputStream data) {
BatterySettingsMigrateChecker.verifyConfiguration(mContext);
BatterySettingsMigrateChecker.verifySaverConfiguration(mContext);
if (!isOwner() || data == null || data.size() == 0) {
Log.w(TAG, "ignore restoreEntity() for non-owner or empty data");
return;
}
if (KEY_OPTIMIZATION_LIST.equals(data.getKey())) {
final int dataSize = data.size();
final byte[] dataBytes = new byte[dataSize];
@@ -105,7 +106,10 @@ public final class BatteryBackupHelper implements BackupHelper {
Log.e(TAG, "failed to load BackupDataInputStream", e);
return;
}
restoreOptimizationMode(dataBytes);
final int restoreCount = restoreOptimizationMode(dataBytes);
if (restoreCount > 0) {
BatterySettingsMigrateChecker.verifyOptimizationModes(mContext);
}
}
}
@@ -175,17 +179,17 @@ public final class BatteryBackupHelper implements BackupHelper {
}
@VisibleForTesting
void restoreOptimizationMode(byte[] dataBytes) {
int restoreOptimizationMode(byte[] dataBytes) {
final long timestamp = System.currentTimeMillis();
final String dataContent = new String(dataBytes, StandardCharsets.UTF_8);
if (dataContent == null || dataContent.isEmpty()) {
Log.w(TAG, "no data found in the restoreOptimizationMode()");
return;
return 0;
}
final String[] appConfigurations = dataContent.split(BatteryBackupHelper.DELIMITER);
if (appConfigurations == null || appConfigurations.length == 0) {
Log.w(TAG, "no data found from the split() processing");
return;
return 0;
}
int restoreCount = 0;
for (int index = 0; index < appConfigurations.length; index++) {
@@ -217,6 +221,7 @@ public final class BatteryBackupHelper implements BackupHelper {
}
Log.d(TAG, String.format("restoreOptimizationMode() count=%d in %d/ms",
restoreCount, (System.currentTimeMillis() - timestamp)));
return restoreCount;
}
/** Dump the app optimization mode backup history data. */
@@ -225,6 +230,23 @@ public final class BatteryBackupHelper implements BackupHelper {
getSharedPreferences(context), writer);
}
static boolean isOwner() {
return UserHandle.myUserId() == UserHandle.USER_SYSTEM;
}
static BatteryOptimizeUtils newBatteryOptimizeUtils(
Context context, String packageName, BatteryOptimizeUtils testOptimizeUtils) {
final int uid = BatteryUtils.getInstance(context).getPackageUid(packageName);
if (uid == BatteryUtils.UID_NULL) {
return null;
}
final BatteryOptimizeUtils batteryOptimizeUtils =
testOptimizeUtils != null
? testOptimizeUtils /*testing only*/
: new BatteryOptimizeUtils(context, uid, packageName);
return batteryOptimizeUtils;
}
@VisibleForTesting
static SharedPreferences getSharedPreferences(Context context) {
return context.getSharedPreferences(
@@ -233,14 +255,11 @@ public final class BatteryBackupHelper implements BackupHelper {
private void restoreOptimizationMode(
String packageName, @BatteryOptimizeUtils.OptimizationMode int mode) {
final int uid = BatteryUtils.getInstance(mContext).getPackageUid(packageName);
if (uid == BatteryUtils.UID_NULL) {
final BatteryOptimizeUtils batteryOptimizeUtils =
newBatteryOptimizeUtils(mContext, packageName, mBatteryOptimizeUtils);
if (batteryOptimizeUtils == null) {
return;
}
final BatteryOptimizeUtils batteryOptimizeUtils =
mBatteryOptimizeUtils != null
? mBatteryOptimizeUtils /*testing only*/
: new BatteryOptimizeUtils(mContext, uid, packageName);
batteryOptimizeUtils.setAppUsageState(
mode, BatteryOptimizeHistoricalLogEntry.Action.RESTORE);
Log.d(TAG, String.format("restore:%s mode=%d", packageName, mode));
@@ -294,8 +313,4 @@ public final class BatteryBackupHelper implements BackupHelper {
Log.e(TAG, "writeBackupData() is failed for " + dataKey, e);
}
}
private static boolean isOwner() {
return UserHandle.myUserId() == UserHandle.USER_SYSTEM;
}
}

View File

@@ -31,11 +31,14 @@ import android.util.Log;
import androidx.annotation.VisibleForTesting;
import com.android.settings.R;
import com.android.settings.fuelgauge.BatteryOptimizeHistoricalLogEntry.Action;
import com.android.settingslib.fuelgauge.PowerAllowlistBackend;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.Arrays;
import java.util.List;
/** A utility class for application usage operation. */
public class BatteryOptimizeUtils {
@@ -214,6 +217,11 @@ public class BatteryOptimizeUtils {
|| powerAllowlistBackend.isDefaultActiveApp(packageName, uid);
}
static List<String> getAllowList(Context context) {
return Arrays.asList(context.getResources().getStringArray(
R.array.config_disable_optimization_mode_apps));
}
private static void setAppUsageStateInternal(
Context context, @OptimizationMode int mode, int uid, String packageName,
BatteryUtils batteryUtils, PowerAllowlistBackend powerAllowlistBackend,

View File

@@ -23,16 +23,27 @@ import android.content.Intent;
import android.provider.Settings;
import android.util.Log;
import androidx.annotation.VisibleForTesting;
import com.android.settings.R;
import com.android.settings.fuelgauge.BatteryOptimizeHistoricalLogEntry;
import com.android.settings.fuelgauge.batterysaver.BatterySaverScheduleRadioButtonsController;
import com.android.settingslib.fuelgauge.BatterySaverUtils;
import java.util.List;
/** Execute battery settings migration tasks in the device booting stage. */
public final class BatterySettingsMigrateChecker extends BroadcastReceiver {
private static final String TAG = "BatterySettingsMigrateChecker";
@VisibleForTesting
static BatteryOptimizeUtils sBatteryOptimizeUtils = null;
@Override
public void onReceive(Context context, Intent intent) {
if (intent != null && Intent.ACTION_BOOT_COMPLETED.equals(intent.getAction())) {
if (intent != null
&& Intent.ACTION_BOOT_COMPLETED.equals(intent.getAction())
&& BatteryBackupHelper.isOwner()) {
verifyConfiguration(context);
}
}
@@ -40,9 +51,35 @@ public final class BatterySettingsMigrateChecker extends BroadcastReceiver {
static void verifyConfiguration(Context context) {
context = context.getApplicationContext();
verifySaverConfiguration(context);
verifyOptimizationModes(context);
}
private static void verifySaverConfiguration(Context context) {
/** Avoid users set important apps into the unexpected battery optimize modes */
static void verifyOptimizationModes(Context context) {
Log.d(TAG, "invoke verifyOptimizationModes()");
verifyOptimizationModes(context, BatteryOptimizeUtils.getAllowList(context));
}
@VisibleForTesting
static void verifyOptimizationModes(Context context, List<String> allowList) {
allowList.forEach(packageName -> {
final BatteryOptimizeUtils batteryOptimizeUtils =
BatteryBackupHelper.newBatteryOptimizeUtils(context, packageName,
/* testOptimizeUtils */ sBatteryOptimizeUtils);
if (batteryOptimizeUtils == null) {
return;
}
if (batteryOptimizeUtils.getAppOptimizationMode() !=
BatteryOptimizeUtils.MODE_OPTIMIZED) {
Log.w(TAG, "Reset optimization mode for: " + packageName);
batteryOptimizeUtils.setAppUsageState(BatteryOptimizeUtils.MODE_OPTIMIZED,
BatteryOptimizeHistoricalLogEntry.Action.FORCE_RESET);
}
});
}
static void verifySaverConfiguration(Context context) {
Log.d(TAG, "invoke verifySaverConfiguration()");
final ContentResolver resolver = context.getContentResolver();
final int threshold = Settings.Global.getInt(resolver,
Settings.Global.LOW_POWER_MODE_TRIGGER_LEVEL, 0);

View File

@@ -18,35 +18,76 @@ package com.android.settings.fuelgauge;
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.inOrder;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.os.UserHandle;
import android.os.UserManager;
import com.android.settings.TestUtils;
import com.android.settings.fuelgauge.BatteryOptimizeHistoricalLogEntry;
import com.android.settings.fuelgauge.batterysaver.BatterySaverScheduleRadioButtonsController;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InOrder;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.RuntimeEnvironment;
import org.robolectric.annotation.Config;
import org.robolectric.annotation.Implementation;
import org.robolectric.annotation.Implements;
import org.robolectric.annotation.Resetter;
import java.util.ArrayList;
import java.util.Arrays;
@RunWith(RobolectricTestRunner.class)
@Config(shadows = {BatterySettingsMigrateCheckerTest.ShadowUserHandle.class})
public final class BatterySettingsMigrateCheckerTest {
private static final Intent BOOT_COMPLETED_INTENT =
new Intent(Intent.ACTION_BOOT_COMPLETED);
private static final int UID = 2003;
private static final String PACKAGE_NAME = "com.android.test.app";
private Context mContext;
private BatterySettingsMigrateChecker mBatterySettingsMigrateChecker;
@Mock
private PackageManager mPackageManager;
@Mock
private BatteryOptimizeUtils mBatteryOptimizeUtils;
@Before
public void setUp() {
public void setUp() throws Exception {
MockitoAnnotations.initMocks(this);
mContext = RuntimeEnvironment.application;
mContext = spy(RuntimeEnvironment.application);
doReturn(mContext).when(mContext).getApplicationContext();
doReturn(mPackageManager).when(mContext).getPackageManager();
doReturn(UID).when(mPackageManager)
.getPackageUid(PACKAGE_NAME, PackageManager.GET_META_DATA);
BatterySettingsMigrateChecker.sBatteryOptimizeUtils = mBatteryOptimizeUtils;
mBatterySettingsMigrateChecker = new BatterySettingsMigrateChecker();
}
@After
public void resetShadows() {
ShadowUserHandle.reset();
}
@Test
public void onReceive_invalidScheduledLevel_resetScheduledValue() {
final int invalidScheduledLevel = 5;
@@ -98,6 +139,54 @@ public final class BatterySettingsMigrateCheckerTest {
assertThat(getScheduledLevel()).isEqualTo(invalidScheduledLevel);
}
@Test
public void onReceive_nonOwner_noAction() {
ShadowUserHandle.setUid(1);
final int invalidScheduledLevel = 5;
setScheduledLevel(invalidScheduledLevel);
mBatterySettingsMigrateChecker.onReceive(mContext, BOOT_COMPLETED_INTENT);
assertThat(getScheduledLevel()).isEqualTo(invalidScheduledLevel);
}
@Test
public void verifyOptimizationModes_inAllowList_resetOptimizationMode() throws Exception {
doReturn(BatteryOptimizeUtils.MODE_RESTRICTED).when(mBatteryOptimizeUtils)
.getAppOptimizationMode();
mBatterySettingsMigrateChecker.verifyOptimizationModes(
mContext, Arrays.asList(PACKAGE_NAME));
final InOrder inOrder = inOrder(mBatteryOptimizeUtils);
inOrder.verify(mBatteryOptimizeUtils).getAppOptimizationMode();
inOrder.verify(mBatteryOptimizeUtils).setAppUsageState(
BatteryOptimizeUtils.MODE_OPTIMIZED,
BatteryOptimizeHistoricalLogEntry.Action.FORCE_RESET);
}
@Test
public void verifyOptimizationModes_optimizedMode_noAction() throws Exception {
doReturn(BatteryOptimizeUtils.MODE_OPTIMIZED).when(mBatteryOptimizeUtils)
.getAppOptimizationMode();
mBatterySettingsMigrateChecker.verifyOptimizationModes(
mContext, Arrays.asList(PACKAGE_NAME));
verify(mBatteryOptimizeUtils, never()).setAppUsageState(anyInt(), any());
}
@Test
public void verifyOptimizationModes_notInAllowList_noAction() throws Exception {
doReturn(BatteryOptimizeUtils.MODE_RESTRICTED).when(mBatteryOptimizeUtils)
.getAppOptimizationMode();
mBatterySettingsMigrateChecker.verifyOptimizationModes(
mContext, new ArrayList<String>());
verifyNoInteractions(mBatteryOptimizeUtils);
}
private void setScheduledLevel(int scheduledLevel) {
TestUtils.setScheduledLevel(mContext, scheduledLevel);
}
@@ -105,4 +194,24 @@ public final class BatterySettingsMigrateCheckerTest {
private int getScheduledLevel() {
return TestUtils.getScheduledLevel(mContext);
}
@Implements(UserHandle.class)
public static class ShadowUserHandle {
// Sets the default as thte OWNER role.
private static int sUid = 0;
public static void setUid(int uid) {
sUid = uid;
}
@Implementation
public static int myUserId() {
return sUid;
}
@Resetter
public static void reset() {
sUid = 0;
}
}
}