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
diff --git a/protos/fuelgauge_log.proto b/protos/fuelgauge_log.proto
index 8512cb8..150c2e2 100644
--- a/protos/fuelgauge_log.proto
+++ b/protos/fuelgauge_log.proto
@@ -20,6 +20,7 @@
     RESET = 3;
     RESTORE = 4;
     BACKUP = 5;
+    FORCE_RESET = 6;
   }
 
   optional string package_name = 1;
diff --git a/res/values/config.xml b/res/values/config.xml
index 52d7183..334d4e5 100755
--- a/res/values/config.xml
+++ b/res/values/config.xml
@@ -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>
 
diff --git a/src/com/android/settings/fuelgauge/BatteryBackupHelper.java b/src/com/android/settings/fuelgauge/BatteryBackupHelper.java
index 79df57a..1bb3b4d 100644
--- a/src/com/android/settings/fuelgauge/BatteryBackupHelper.java
+++ b/src/com/android/settings/fuelgauge/BatteryBackupHelper.java
@@ -91,11 +91,12 @@
 
     @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 @@
                 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 @@
     }
 
     @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 @@
         }
         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 @@
                 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 @@
 
     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 @@
             Log.e(TAG, "writeBackupData() is failed for " + dataKey, e);
         }
     }
-
-    private static boolean isOwner() {
-        return UserHandle.myUserId() == UserHandle.USER_SYSTEM;
-    }
 }
diff --git a/src/com/android/settings/fuelgauge/BatteryOptimizeUtils.java b/src/com/android/settings/fuelgauge/BatteryOptimizeUtils.java
index b9ac64d..00611de 100644
--- a/src/com/android/settings/fuelgauge/BatteryOptimizeUtils.java
+++ b/src/com/android/settings/fuelgauge/BatteryOptimizeUtils.java
@@ -31,11 +31,14 @@
 
 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 @@
                 || 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,
diff --git a/src/com/android/settings/fuelgauge/BatterySettingsMigrateChecker.java b/src/com/android/settings/fuelgauge/BatterySettingsMigrateChecker.java
index c54e6d8..4b9e6ef 100644
--- a/src/com/android/settings/fuelgauge/BatterySettingsMigrateChecker.java
+++ b/src/com/android/settings/fuelgauge/BatterySettingsMigrateChecker.java
@@ -23,16 +23,27 @@
 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 @@
     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);
diff --git a/tests/robotests/src/com/android/settings/fuelgauge/BatterySettingsMigrateCheckerTest.java b/tests/robotests/src/com/android/settings/fuelgauge/BatterySettingsMigrateCheckerTest.java
index dfee3e7..c34dcec 100644
--- a/tests/robotests/src/com/android/settings/fuelgauge/BatterySettingsMigrateCheckerTest.java
+++ b/tests/robotests/src/com/android/settings/fuelgauge/BatterySettingsMigrateCheckerTest.java
@@ -18,35 +18,76 @@
 
 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 @@
         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 @@
     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;
+        }
+    }
 }