From 37ddf4bc3eb83149d0b6f9431ae7fdc1f439eeb7 Mon Sep 17 00:00:00 2001 From: ykhung Date: Fri, 20 Aug 2021 17:32:47 +0800 Subject: [PATCH] [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 --- .../fuelgauge/BatteryBackupHelper.java | 134 +++++++++++++++--- .../fuelgauge/BatteryOptimizeUtils.java | 17 ++- .../fuelgauge/BatteryBackupHelperTest.java | 92 ++++++++++++ 3 files changed, 219 insertions(+), 24 deletions(-) diff --git a/src/com/android/settings/fuelgauge/BatteryBackupHelper.java b/src/com/android/settings/fuelgauge/BatteryBackupHelper.java index aeb5e9f2fcc..44990ffa3ef 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 @@ 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 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 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 allowlistedApps) { + final long timestamp = System.currentTimeMillis(); + final List 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 getInstalledApplications() { + final List applications = new ArrayList<>(); + final UserManager um = mContext.getSystemService(UserManager.class); + for (UserInfo userInfo : um.getProfiles(UserHandle.myUserId())) { + try { + @SuppressWarnings("unchecked") + final ParceledListSlice 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 4a560402667..0be90600ba7 100644 --- a/src/com/android/settings/fuelgauge/BatteryOptimizeUtils.java +++ b/src/com/android/settings/fuelgauge/BatteryOptimizeUtils.java @@ -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: diff --git a/tests/robotests/src/com/android/settings/fuelgauge/BatteryBackupHelperTest.java b/tests/robotests/src/com/android/settings/fuelgauge/BatteryBackupHelperTest.java index 87aa8122caf..47fa59e6271 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.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 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( + 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.