[B&R] backup the battery optimization mode for installed apps

BYPASS_INCLUSIVE_LANGUAGE_REASON=legacy method name

Test command for backup manager:
adb shell bmgr backupnow com.android.settings
adb shell dumpsys backup | grep Current

Bug: 192523697
Test: make SettingsRoboTests
Change-Id: I4671e48a9d3b315362b451384db637d5cae36a4d
diff --git a/src/com/android/settings/fuelgauge/BatteryBackupHelper.java b/src/com/android/settings/fuelgauge/BatteryBackupHelper.java
index aeb5e9f..44990ff 100644
--- a/src/com/android/settings/fuelgauge/BatteryBackupHelper.java
+++ b/src/com/android/settings/fuelgauge/BatteryBackupHelper.java
@@ -16,21 +16,33 @@
 
 package com.android.settings.fuelgauge;
 
+import android.app.AppGlobals;
+import android.app.AppOpsManager;
 import android.app.backup.BackupDataInputStream;
 import android.app.backup.BackupDataOutput;
 import android.app.backup.BackupHelper;
 import android.content.Context;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.IPackageManager;
+import android.content.pm.PackageManager;
+import android.content.pm.ParceledListSlice;
+import android.content.pm.UserInfo;
 import android.os.IDeviceIdleController;
 import android.os.RemoteException;
 import android.os.ParcelFileDescriptor;
 import android.os.ServiceManager;
 import android.os.UserHandle;
+import android.os.UserManager;
 import android.util.Log;
 
 import androidx.annotation.VisibleForTesting;
 
+import com.android.settings.fuelgauge.BatteryOptimizeUtils.AppUsageState;
+
 import java.io.IOException;
+import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.List;
 
 /** An implementation to backup and restore battery configurations. */
 public final class BatteryBackupHelper implements BackupHelper {
@@ -39,13 +51,24 @@
     private static final String DEVICE_IDLE_SERVICE = "deviceidle";
     private static final boolean DEBUG = false;
 
-    @VisibleForTesting
-    static final CharSequence DELIMITER = ":";
-    @VisibleForTesting
+    // Only the owner can see all apps.
+    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;
+
+    static final CharSequence DELIMITER = ",";
+    static final CharSequence DELIMITER_MODE = "|";
     static final String KEY_FULL_POWER_LIST = "full_power_list";
+    static final String KEY_OPTIMIZATION_LIST = "optimization_mode_list";
 
     @VisibleForTesting
     IDeviceIdleController mIDeviceIdleController;
+    @VisibleForTesting
+    IPackageManager mIPackageManager;
 
     private final Context mContext;
 
@@ -57,10 +80,13 @@
     public void performBackup(ParcelFileDescriptor oldState, BackupDataOutput data,
             ParcelFileDescriptor newState) {
         if (!isOwner()) {
-            Log.w(TAG, "ignore the backup process for non-owner");
+            Log.w(TAG, "ignore performBackup() for non-owner");
             return;
         }
-        backupFullPowerList(getIDeviceIdleController(), data);
+        final List<String> allowlistedApps = backupFullPowerList(data);
+        if (allowlistedApps != null) {
+            backupOptimizationMode(data, allowlistedApps);
+        }
     }
 
     @Override
@@ -72,33 +98,57 @@
     public void writeNewStateDescription(ParcelFileDescriptor newState) {
     }
 
-    private void backupFullPowerList(
-            IDeviceIdleController deviceIdleService, BackupDataOutput data) {
+    private List<String> backupFullPowerList(BackupDataOutput data) {
         final long timestamp = System.currentTimeMillis();
         String[] allowlistedApps;
         try {
-            allowlistedApps = deviceIdleService.getFullPowerWhitelist();
+            allowlistedApps = getIDeviceIdleController().getFullPowerWhitelist();
         } catch (RemoteException e) {
             Log.e(TAG, "backupFullPowerList() failed", e);
-            return;
+            return null;
         }
         // Ignores unexpected emptty result case.
         if (allowlistedApps == null || allowlistedApps.length == 0) {
             Log.w(TAG, "no data found in the getFullPowerList()");
-            return;
+            return new ArrayList<>();
         }
+
         debugLog("allowlistedApps:" + Arrays.toString(allowlistedApps));
         final String allowedApps = String.join(DELIMITER, allowlistedApps);
-        final byte[] allowedAppsBytes = allowedApps.getBytes();
-        try {
-            data.writeEntityHeader(KEY_FULL_POWER_LIST, allowedAppsBytes.length);
-            data.writeEntityData(allowedAppsBytes, allowedAppsBytes.length);
-        } catch (IOException e) {
-            Log.e(TAG, "backup getFullPowerList() failed", e);
-            return;
-        }
+        writeBackupData(data, KEY_FULL_POWER_LIST, allowedApps);
         Log.d(TAG, String.format("backup getFullPowerList() size=%d in %d/ms",
                 allowlistedApps.length, (System.currentTimeMillis() - timestamp)));
+        return Arrays.asList(allowlistedApps);
+    }
+
+    @VisibleForTesting
+    void backupOptimizationMode(BackupDataOutput data, List<String> allowlistedApps) {
+        final long timestamp = System.currentTimeMillis();
+        final List<ApplicationInfo> applications = getInstalledApplications();
+        if (applications == null || applications.isEmpty()) {
+            Log.w(TAG, "no data found in the getInstalledApplications()");
+            return;
+        }
+        final StringBuilder builder = new StringBuilder();
+        final AppOpsManager appOps = mContext.getSystemService(AppOpsManager.class);
+        // Converts application into the AppUsageState.
+        for (ApplicationInfo info : applications) {
+            final int mode = appOps.checkOpNoThrow(
+                    AppOpsManager.OP_RUN_ANY_IN_BACKGROUND, info.uid, info.packageName);
+            final AppUsageState state = BatteryOptimizeUtils.getAppUsageState(
+                    mode, allowlistedApps.contains(info.packageName));
+            // Ignores default optimized or unknown state.
+            if (state == AppUsageState.OPTIMIZED || state == AppUsageState.UNKNOWN) {
+                continue;
+            }
+            final String packageOptimizeMode = info.packageName + DELIMITER_MODE + state;
+            builder.append(packageOptimizeMode + DELIMITER);
+            debugLog(packageOptimizeMode);
+        }
+
+        writeBackupData(data, KEY_OPTIMIZATION_LIST, builder.toString());
+        Log.d(TAG, String.format("backup getInstalledApplications() size=%d in %d/ms",
+                applications.size(), (System.currentTimeMillis() - timestamp)));
     }
 
     // Provides an opportunity to inject mock IDeviceIdleController for testing.
@@ -111,10 +161,58 @@
         return mIDeviceIdleController;
     }
 
+    private IPackageManager getIPackageManager() {
+        if (mIPackageManager != null) {
+            return mIPackageManager;
+        }
+        mIPackageManager = AppGlobals.getPackageManager();
+        return mIPackageManager;
+    }
+
+    private List<ApplicationInfo> getInstalledApplications() {
+        final List<ApplicationInfo> applications = new ArrayList<>();
+        final UserManager um = mContext.getSystemService(UserManager.class);
+        for (UserInfo userInfo : um.getProfiles(UserHandle.myUserId())) {
+            try {
+                @SuppressWarnings("unchecked")
+                final ParceledListSlice<ApplicationInfo> infoList =
+                        getIPackageManager().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.
+        for (int index = applications.size() - 1; index >= 0; index--) {
+            final ApplicationInfo info = applications.get(index);
+            if (info.enabledSetting != PackageManager.COMPONENT_ENABLED_STATE_DISABLED_USER
+                    && !info.enabled) {
+                applications.remove(index);
+            }
+        }
+        return applications;
+    }
+
     private void debugLog(String debugContent) {
         if (DEBUG) Log.d(TAG, debugContent);
     }
 
+    private static void writeBackupData(
+            BackupDataOutput data, String dataKey, String dataContent) {
+        final byte[] dataContentBytes = dataContent.getBytes();
+        try {
+            data.writeEntityHeader(dataKey, dataContentBytes.length);
+            data.writeEntityData(dataContentBytes, dataContentBytes.length);
+        } catch (IOException e) {
+            Log.e(TAG, "writeBackupData() is failed for " + dataKey, e);
+        }
+    }
+
     private static boolean isOwner() {
         return UserHandle.myUserId() == UserHandle.USER_OWNER;
     }
diff --git a/src/com/android/settings/fuelgauge/BatteryOptimizeUtils.java b/src/com/android/settings/fuelgauge/BatteryOptimizeUtils.java
index 4a56040..0be9060 100644
--- a/src/com/android/settings/fuelgauge/BatteryOptimizeUtils.java
+++ b/src/com/android/settings/fuelgauge/BatteryOptimizeUtils.java
@@ -59,20 +59,25 @@
         mAllowListed = mPowerAllowListBackend.isAllowlisted(mPackageName);
     }
 
-    public AppUsageState getAppUsageState() {
-        refreshState();
-        if (!mAllowListed && mMode == AppOpsManager.MODE_IGNORED) {
+    /** Gets the {@link AppUsageState} based on mode and allowed list. */
+    public static AppUsageState getAppUsageState(int mode, boolean isAllowListed) {
+        if (!isAllowListed && mode == AppOpsManager.MODE_IGNORED) {
             return AppUsageState.RESTRICTED;
-        } else if (mAllowListed && mMode == AppOpsManager.MODE_ALLOWED) {
+        } else if (isAllowListed && mode == AppOpsManager.MODE_ALLOWED) {
             return AppUsageState.UNRESTRICTED;
-        } else if (!mAllowListed && mMode == AppOpsManager.MODE_ALLOWED) {
+        } else if (!isAllowListed && mode == AppOpsManager.MODE_ALLOWED) {
             return AppUsageState.OPTIMIZED;
         } else {
-            Log.d(TAG, "get unknown app usage state.");
             return AppUsageState.UNKNOWN;
         }
     }
 
+    /** Gets the current {@link AppUsageState}. */
+    public AppUsageState getAppUsageState() {
+        refreshState();
+        return getAppUsageState(mMode, mAllowListed);
+    }
+
     public void setAppUsageState(AppUsageState state) {
         switch (state) {
             case RESTRICTED:
diff --git a/tests/robotests/src/com/android/settings/fuelgauge/BatteryBackupHelperTest.java b/tests/robotests/src/com/android/settings/fuelgauge/BatteryBackupHelperTest.java
index 87aa812..47fa59e 100644
--- a/tests/robotests/src/com/android/settings/fuelgauge/BatteryBackupHelperTest.java
+++ b/tests/robotests/src/com/android/settings/fuelgauge/BatteryBackupHelperTest.java
@@ -27,11 +27,21 @@
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
+import android.app.AppOpsManager;
 import android.app.backup.BackupDataOutput;
 import android.content.Context;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.IPackageManager;
+import android.content.pm.PackageManager;
+import android.content.pm.ParceledListSlice;
+import android.content.pm.UserInfo;
 import android.os.IDeviceIdleController;
 import android.os.RemoteException;
 import android.os.UserHandle;
+import android.os.UserManager;
+
+import java.util.Arrays;
+import java.util.List;
 
 import org.junit.After;
 import org.junit.Before;
@@ -57,13 +67,23 @@
     private BackupDataOutput mBackupDataOutput;
     @Mock
     private IDeviceIdleController mDeviceController;
+    @Mock
+    private IPackageManager mIPackageManager;
+    @Mock
+    private AppOpsManager mAppOpsManager;
+    @Mock
+    private UserManager mUserManager;
 
     @Before
     public void setUp() {
         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);
         mBatteryBackupHelper = new BatteryBackupHelper(mContext);
         mBatteryBackupHelper.mIDeviceIdleController = mDeviceController;
+        mBatteryBackupHelper.mIPackageManager = mIPackageManager;
     }
 
     @After
@@ -135,6 +155,78 @@
         verify(mBackupDataOutput, never()).writeEntityHeader(anyString(), anyInt());
     }
 
+    @Test
+    public void backupOptimizationMode_nullInstalledApps_ignoreBackupOptimization()
+            throws Exception {
+        final UserInfo userInfo =
+                new UserInfo(/*userId=*/ 0, /*userName=*/ "google", /*flag=*/ 0);
+        doReturn(Arrays.asList(userInfo)).when(mUserManager).getProfiles(anyInt());
+        doThrow(new RuntimeException())
+                .when(mIPackageManager)
+                .getInstalledApplications(anyInt(), anyInt());
+
+        mBatteryBackupHelper.backupOptimizationMode(mBackupDataOutput, null);
+
+        verify(mBackupDataOutput, never()).writeEntityHeader(anyString(), anyInt());
+    }
+
+    @Test
+    public void backupOptimizationMode_backupOptimizationMode() throws Exception {
+        final String packageName1 = "com.android.testing.1";
+        final String packageName2 = "com.android.testing.2";
+        final String packageName3 = "com.android.testing.3";
+        final List<String> allowlistedApps = Arrays.asList(packageName1);
+        createTestingData(packageName1, packageName2, packageName3);
+
+        mBatteryBackupHelper.backupOptimizationMode(mBackupDataOutput, allowlistedApps);
+
+        final String expectedResult =
+                packageName1 + "|UNRESTRICTED," + packageName2 + "|RESTRICTED,";
+        final byte[] expectedBytes = expectedResult.getBytes();
+        verify(mBackupDataOutput).writeEntityHeader(
+                BatteryBackupHelper.KEY_OPTIMIZATION_LIST, expectedBytes.length);
+        verify(mBackupDataOutput).writeEntityData(expectedBytes, expectedBytes.length);
+    }
+
+    private void createTestingData(
+            String packageName1, String packageName2, String packageName3) throws Exception {
+        // Sets the getInstalledApplications() method for testing.
+        final UserInfo userInfo =
+                new UserInfo(/*userId=*/ 0, /*userName=*/ "google", /*flag=*/ 0);
+        doReturn(Arrays.asList(userInfo)).when(mUserManager).getProfiles(anyInt());
+        final ApplicationInfo applicationInfo1 = new ApplicationInfo();
+        applicationInfo1.enabled = true;
+        applicationInfo1.uid = 1;
+        applicationInfo1.packageName = packageName1;
+        final ApplicationInfo applicationInfo2 = new ApplicationInfo();
+        applicationInfo2.enabled = false;
+        applicationInfo2.uid = 2;
+        applicationInfo2.packageName = packageName2;
+        applicationInfo2.enabledSetting = PackageManager.COMPONENT_ENABLED_STATE_DISABLED_USER;
+        final ApplicationInfo applicationInfo3 = new ApplicationInfo();
+        applicationInfo3.enabled = false;
+        applicationInfo3.uid = 3;
+        applicationInfo3.packageName = packageName3;
+        applicationInfo3.enabledSetting = PackageManager.COMPONENT_ENABLED_STATE_DISABLED;
+        doReturn(new ParceledListSlice<ApplicationInfo>(
+                Arrays.asList(applicationInfo1, applicationInfo2, applicationInfo3)))
+            .when(mIPackageManager)
+            .getInstalledApplications(anyInt(), anyInt());
+        // Sets the AppOpsManager for checkOpNoThrow() method.
+        doReturn(AppOpsManager.MODE_ALLOWED)
+                .when(mAppOpsManager)
+                .checkOpNoThrow(
+                        AppOpsManager.OP_RUN_ANY_IN_BACKGROUND,
+                        applicationInfo1.uid,
+                        applicationInfo1.packageName);
+        doReturn(AppOpsManager.MODE_IGNORED)
+                .when(mAppOpsManager)
+                .checkOpNoThrow(
+                        AppOpsManager.OP_RUN_ANY_IN_BACKGROUND,
+                        applicationInfo2.uid,
+                        applicationInfo2.packageName);
+    }
+
     @Implements(UserHandle.class)
     public static class ShadowUserHandle {
         // Sets the default as thte OWNER role.