diff --git a/src/com/android/settings/fuelgauge/BatteryBackupHelper.java b/src/com/android/settings/fuelgauge/BatteryBackupHelper.java index d35fc6483bf..af16edbe018 100644 --- a/src/com/android/settings/fuelgauge/BatteryBackupHelper.java +++ b/src/com/android/settings/fuelgauge/BatteryBackupHelper.java @@ -27,6 +27,7 @@ import android.content.pm.IPackageManager; import android.content.pm.PackageManager; import android.content.pm.ParceledListSlice; import android.content.pm.UserInfo; +import android.os.Build; import android.os.IDeviceIdleController; import android.os.RemoteException; import android.os.ParcelFileDescriptor; @@ -40,6 +41,7 @@ import androidx.annotation.VisibleForTesting; import com.android.settingslib.fuelgauge.PowerAllowlistBackend; import java.io.IOException; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -49,7 +51,7 @@ public final class BatteryBackupHelper implements BackupHelper { /** An inditifier for {@link BackupHelper}. */ public static final String TAG = "BatteryBackupHelper"; private static final String DEVICE_IDLE_SERVICE = "deviceidle"; - private static final boolean DEBUG = false; + private static final boolean DEBUG = Build.TYPE.equals("userdebug"); // Only the owner can see all apps. private static final int RETRIEVE_FLAG_ADMIN = @@ -60,8 +62,8 @@ public final class BatteryBackupHelper implements BackupHelper { PackageManager.MATCH_DISABLED_COMPONENTS | PackageManager.MATCH_DISABLED_UNTIL_USED_COMPONENTS; - static final CharSequence DELIMITER = ","; - static final CharSequence DELIMITER_MODE = "|"; + static final String DELIMITER = ","; + static final String DELIMITER_MODE = ":"; static final String KEY_FULL_POWER_LIST = "full_power_list"; static final String KEY_OPTIMIZATION_LIST = "optimization_mode_list"; @@ -71,6 +73,8 @@ public final class BatteryBackupHelper implements BackupHelper { IDeviceIdleController mIDeviceIdleController; @VisibleForTesting IPackageManager mIPackageManager; + @VisibleForTesting + BatteryOptimizeUtils mBatteryOptimizeUtils; private final Context mContext; @@ -81,8 +85,8 @@ public final class BatteryBackupHelper implements BackupHelper { @Override public void performBackup(ParcelFileDescriptor oldState, BackupDataOutput data, ParcelFileDescriptor newState) { - if (!isOwner()) { - Log.w(TAG, "ignore performBackup() for non-owner"); + if (!isOwner() || data == null) { + Log.w(TAG, "ignore performBackup() for non-owner or empty data"); return; } final List allowlistedApps = backupFullPowerList(data); @@ -93,7 +97,21 @@ public final class BatteryBackupHelper implements BackupHelper { @Override public void restoreEntity(BackupDataInputStream data) { - Log.d(TAG, "restoreEntity()"); + 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]; + try { + data.read(dataBytes, 0 /*offset*/, dataSize); + } catch (IOException e) { + Log.e(TAG, "failed to load BackupDataInputStream", e); + return; + } + restoreOptimizationMode(dataBytes); + } } @Override @@ -115,7 +133,6 @@ public final class BatteryBackupHelper implements BackupHelper { return new ArrayList<>(); } - debugLog("allowlistedApps:" + Arrays.toString(allowlistedApps)); final String allowedApps = String.join(DELIMITER, allowlistedApps); writeBackupData(data, KEY_FULL_POWER_LIST, allowedApps); Log.d(TAG, String.format("backup getFullPowerList() size=%d in %d/ms", @@ -159,6 +176,64 @@ public final class BatteryBackupHelper implements BackupHelper { applications.size(), backupCount, (System.currentTimeMillis() - timestamp))); } + @VisibleForTesting + void 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; + } + final String[] appConfigurations = dataContent.split(BatteryBackupHelper.DELIMITER); + if (appConfigurations == null || appConfigurations.length == 0) { + Log.w(TAG, "no data found from the split() processing"); + return; + } + int restoreCount = 0; + for (int index = 0; index < appConfigurations.length; index++) { + final String[] results = appConfigurations[index] + .split(BatteryBackupHelper.DELIMITER_MODE); + // Example format: com.android.systemui:2 we should have length=2 + if (results == null || results.length != 2) { + Log.w(TAG, "invalid raw data found:" + appConfigurations[index]); + continue; + } + final String packageName = results[0]; + // Ignores system/default apps. + if (isSystemOrDefaultApp(packageName)) { + Log.w(TAG, "ignore from isSystemOrDefaultApp():" + packageName); + continue; + } + @BatteryOptimizeUtils.OptimizationMode + int optimizationMode = BatteryOptimizeUtils.MODE_UNKNOWN; + try { + optimizationMode = Integer.parseInt(results[1]); + } catch (NumberFormatException e) { + Log.e(TAG, "failed to parse the optimization mode: " + + appConfigurations[index], e); + continue; + } + restoreOptimizationMode(packageName, optimizationMode); + restoreCount++; + } + Log.d(TAG, String.format("restoreOptimizationMode() count=%d in %d/ms", + restoreCount, (System.currentTimeMillis() - timestamp))); + } + + private void restoreOptimizationMode( + String packageName, @BatteryOptimizeUtils.OptimizationMode int mode) { + final int uid = BatteryUtils.getInstance(mContext).getPackageUid(packageName); + if (uid == BatteryUtils.UID_NULL) { + return; + } + final BatteryOptimizeUtils batteryOptimizeUtils = + mBatteryOptimizeUtils != null + ? mBatteryOptimizeUtils /*testing only*/ + : new BatteryOptimizeUtils(mContext, uid, packageName); + batteryOptimizeUtils.setAppOptimizationMode(mode); + Log.d(TAG, String.format("restore:%s mode=%d", packageName, mode)); + } + // Provides an opportunity to inject mock IDeviceIdleController for testing. private IDeviceIdleController getIDeviceIdleController() { if (mIDeviceIdleController != null) { diff --git a/tests/robotests/src/com/android/settings/fuelgauge/BatteryBackupHelperTest.java b/tests/robotests/src/com/android/settings/fuelgauge/BatteryBackupHelperTest.java index f86d7a14e0e..5ac8553b91c 100644 --- a/tests/robotests/src/com/android/settings/fuelgauge/BatteryBackupHelperTest.java +++ b/tests/robotests/src/com/android/settings/fuelgauge/BatteryBackupHelperTest.java @@ -16,18 +16,27 @@ package com.android.settings.fuelgauge; +import static com.android.settings.fuelgauge.BatteryBackupHelper.DELIMITER; +import static com.android.settings.fuelgauge.BatteryBackupHelper.DELIMITER_MODE; +import static com.android.settings.fuelgauge.BatteryOptimizeUtils.MODE_RESTRICTED; +import static com.android.settings.fuelgauge.BatteryOptimizeUtils.MODE_UNRESTRICTED; import static com.google.common.truth.Truth.assertThat; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.doThrow; +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.verifyZeroInteractions; import static org.mockito.Mockito.when; import android.app.AppOpsManager; +import android.app.backup.BackupDataInputStream; import android.app.backup.BackupDataOutput; import android.content.Context; import android.content.pm.ApplicationInfo; @@ -49,6 +58,8 @@ import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.InOrder; import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.robolectric.RobolectricTestRunner; @@ -68,9 +79,13 @@ public final class BatteryBackupHelperTest { private Context mContext; private BatteryBackupHelper mBatteryBackupHelper; + @Mock + private PackageManager mPackageManager; @Mock private BackupDataOutput mBackupDataOutput; @Mock + private BackupDataInputStream mBackupDataInputStream; + @Mock private IDeviceIdleController mDeviceController; @Mock private IPackageManager mIPackageManager; @@ -80,18 +95,25 @@ public final class BatteryBackupHelperTest { private UserManager mUserManager; @Mock private PowerAllowlistBackend mPowerAllowlistBackend; + @Mock + private BatteryOptimizeUtils mBatteryOptimizeUtils; @Before - public void setUp() { + public void setUp() throws Exception { MockitoAnnotations.initMocks(this); mContext = spy(RuntimeEnvironment.application); doReturn(mContext).when(mContext).getApplicationContext(); doReturn(mAppOpsManager).when(mContext).getSystemService(AppOpsManager.class); doReturn(mUserManager).when(mContext).getSystemService(UserManager.class); + doReturn(mPackageManager).when(mContext).getPackageManager(); mBatteryBackupHelper = new BatteryBackupHelper(mContext); mBatteryBackupHelper.mIDeviceIdleController = mDeviceController; mBatteryBackupHelper.mIPackageManager = mIPackageManager; mBatteryBackupHelper.mPowerAllowlistBackend = mPowerAllowlistBackend; + mBatteryBackupHelper.mBatteryOptimizeUtils = mBatteryOptimizeUtils; + mockUid(1001 /*fake uid*/, PACKAGE_NAME1); + mockUid(1002 /*fake uid*/, PACKAGE_NAME2); + mockUid(BatteryUtils.UID_NULL, PACKAGE_NAME3); } @After @@ -144,8 +166,7 @@ public final class BatteryBackupHelperTest { mBatteryBackupHelper.performBackup(null, mBackupDataOutput, null); - final String expectedResult = fullPowerList[0] - + BatteryBackupHelper.DELIMITER + fullPowerList[1]; + final String expectedResult = fullPowerList[0] + DELIMITER + fullPowerList[1]; final byte[] expectedBytes = expectedResult.getBytes(); verify(mBackupDataOutput).writeEntityHeader( BatteryBackupHelper.KEY_FULL_POWER_LIST, expectedBytes.length); @@ -186,7 +207,7 @@ public final class BatteryBackupHelperTest { mBatteryBackupHelper.backupOptimizationMode(mBackupDataOutput, allowlistedApps); // 2 for UNRESTRICTED mode and 1 for RESTRICTED mode. - final String expectedResult = PACKAGE_NAME1 + "|2," + PACKAGE_NAME2 + "|1,"; + final String expectedResult = PACKAGE_NAME1 + ":2," + PACKAGE_NAME2 + ":1,"; verifyBackupData(expectedResult); } @@ -202,7 +223,7 @@ public final class BatteryBackupHelperTest { mBatteryBackupHelper.backupOptimizationMode(mBackupDataOutput, allowlistedApps); // "com.android.testing.2" for RESTRICTED mode. - final String expectedResult = PACKAGE_NAME2 + "|1,"; + final String expectedResult = PACKAGE_NAME2 + ":1,"; verifyBackupData(expectedResult); } @@ -218,10 +239,103 @@ public final class BatteryBackupHelperTest { mBatteryBackupHelper.backupOptimizationMode(mBackupDataOutput, allowlistedApps); // "com.android.testing.2" for RESTRICTED mode. - final String expectedResult = PACKAGE_NAME2 + "|1,"; + final String expectedResult = PACKAGE_NAME2 + ":1,"; verifyBackupData(expectedResult); } + @Test + public void restoreEntity_nonOwner_notReadBackupData() throws Exception { + ShadowUserHandle.setUid(1); + mockBackupData(30 /*dataSize*/, BatteryBackupHelper.KEY_OPTIMIZATION_LIST); + + mBatteryBackupHelper.restoreEntity(mBackupDataInputStream); + + verifyZeroInteractions(mBackupDataInputStream); + } + + @Test + public void restoreEntity_zeroDataSize_notReadBackupData() throws Exception { + final int zeroDataSize = 0; + mockBackupData(zeroDataSize, BatteryBackupHelper.KEY_OPTIMIZATION_LIST); + + mBatteryBackupHelper.restoreEntity(mBackupDataInputStream); + + verify(mBackupDataInputStream, never()).read(any(), anyInt(), anyInt()); + } + + @Test + public void restoreEntity_incorrectDataKey_notReadBackupData() throws Exception { + final String incorrectDataKey = BatteryBackupHelper.KEY_FULL_POWER_LIST; + mockBackupData(30 /*dataSize*/, incorrectDataKey); + + mBatteryBackupHelper.restoreEntity(mBackupDataInputStream); + + verify(mBackupDataInputStream, never()).read(any(), anyInt(), anyInt()); + } + + @Test + public void restoreEntity_readExpectedDataFromBackupData() throws Exception { + final int dataSize = 30; + mockBackupData(dataSize, BatteryBackupHelper.KEY_OPTIMIZATION_LIST); + + mBatteryBackupHelper.restoreEntity(mBackupDataInputStream); + + final ArgumentCaptor captor = ArgumentCaptor.forClass(byte[].class); + verify(mBackupDataInputStream).read(captor.capture(), eq(0), eq(dataSize)); + assertThat(captor.getValue().length).isEqualTo(dataSize); + } + + @Test + public void restoreOptimizationMode_nullBytesData_skipRestore() throws Exception { + mBatteryBackupHelper.restoreOptimizationMode(new byte[0]); + verifyZeroInteractions(mBatteryOptimizeUtils); + + mBatteryBackupHelper.restoreOptimizationMode("invalid data format".getBytes()); + verifyZeroInteractions(mBatteryOptimizeUtils); + + mBatteryBackupHelper.restoreOptimizationMode(DELIMITER.getBytes()); + verifyZeroInteractions(mBatteryOptimizeUtils); + } + + @Test + public void restoreOptimizationMode_invalidModeFormat_skipRestore() throws Exception { + final String invalidNumberFormat = "google"; + final String packageModes = + PACKAGE_NAME1 + DELIMITER_MODE + MODE_RESTRICTED + DELIMITER + + PACKAGE_NAME2 + DELIMITER_MODE + invalidNumberFormat; + + mBatteryBackupHelper.restoreOptimizationMode(packageModes.getBytes()); + + final InOrder inOrder = inOrder(mBatteryOptimizeUtils); + inOrder.verify(mBatteryOptimizeUtils).setAppOptimizationMode(MODE_RESTRICTED); + inOrder.verify(mBatteryOptimizeUtils, never()).setAppOptimizationMode(anyInt()); + } + + @Test + public void restoreOptimizationMode_restoreExpectedModes() throws Exception { + final String packageModes = + PACKAGE_NAME1 + DELIMITER_MODE + MODE_RESTRICTED + DELIMITER + + PACKAGE_NAME2 + DELIMITER_MODE + MODE_UNRESTRICTED + DELIMITER + + PACKAGE_NAME3 + DELIMITER_MODE + MODE_RESTRICTED + DELIMITER; + + mBatteryBackupHelper.restoreOptimizationMode(packageModes.getBytes()); + + final InOrder inOrder = inOrder(mBatteryOptimizeUtils); + inOrder.verify(mBatteryOptimizeUtils).setAppOptimizationMode(MODE_RESTRICTED); + inOrder.verify(mBatteryOptimizeUtils).setAppOptimizationMode(MODE_UNRESTRICTED); + inOrder.verify(mBatteryOptimizeUtils, never()).setAppOptimizationMode(MODE_RESTRICTED); + } + + private void mockUid(int uid, String packageName) throws Exception { + doReturn(uid).when(mPackageManager) + .getPackageUid(packageName, PackageManager.GET_META_DATA); + } + + private void mockBackupData(int dataSize, String dataKey) { + doReturn(dataSize).when(mBackupDataInputStream).size(); + doReturn(dataKey).when(mBackupDataInputStream).getKey(); + } + private void verifyBackupData(String expectedResult) throws Exception { final byte[] expectedBytes = expectedResult.getBytes(); verify(mBackupDataOutput).writeEntityHeader(