Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit c63c0d43 authored by TreeHugger Robot's avatar TreeHugger Robot Committed by Android (Google) Code Review
Browse files

Merge "[B&R] backup the battery optimization mode for installed apps" into sc-v2-dev

parents b5074981 37ddf4bc
Loading
Loading
Loading
Loading
+116 −18
Original line number Diff line number Diff line
@@ -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 @@ public final class BatteryBackupHelper implements BackupHelper {
    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 final class BatteryBackupHelper implements BackupHelper {
    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 final class BatteryBackupHelper implements BackupHelper {
    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 @@ public final class BatteryBackupHelper implements BackupHelper {
        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;
    }
+11 −6
Original line number Diff line number Diff line
@@ -59,20 +59,25 @@ public class BatteryOptimizeUtils {
        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:
+92 −0
Original line number Diff line number Diff line
@@ -27,11 +27,21 @@ import static org.mockito.Mockito.spy;
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 @@ public final class BatteryBackupHelperTest {
    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 @@ public final class BatteryBackupHelperTest {
        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.