/* * Copyright (C) 2021 The Android Open Source Project * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ 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.Build; import android.os.IDeviceIdleController; import android.os.ParcelFileDescriptor; import android.os.RemoteException; import android.os.ServiceManager; import android.os.UserHandle; import android.os.UserManager; import android.util.Log; 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; /** An implementation to backup and restore battery configurations. */ 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 = Build.TYPE.equals("userdebug"); // 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 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"; @VisibleForTesting PowerAllowlistBackend mPowerAllowlistBackend; @VisibleForTesting IDeviceIdleController mIDeviceIdleController; @VisibleForTesting IPackageManager mIPackageManager; @VisibleForTesting BatteryOptimizeUtils mBatteryOptimizeUtils; private final Context mContext; public BatteryBackupHelper(Context context) { mContext = context.getApplicationContext(); } @Override public void performBackup(ParcelFileDescriptor oldState, BackupDataOutput data, ParcelFileDescriptor newState) { if (!isOwner() || data == null) { Log.w(TAG, "ignore performBackup() for non-owner or empty data"); return; } final List allowlistedApps = backupFullPowerList(data); if (allowlistedApps != null) { backupOptimizationMode(data, allowlistedApps); } } @Override public void restoreEntity(BackupDataInputStream data) { 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 public void writeNewStateDescription(ParcelFileDescriptor newState) { } private List backupFullPowerList(BackupDataOutput data) { final long timestamp = System.currentTimeMillis(); String[] allowlistedApps; try { allowlistedApps = getIDeviceIdleController().getFullPowerWhitelist(); } catch (RemoteException e) { Log.e(TAG, "backupFullPowerList() failed", e); return null; } // Ignores unexpected emptty result case. if (allowlistedApps == null || allowlistedApps.length == 0) { Log.w(TAG, "no data found in the getFullPowerList()"); return new ArrayList<>(); } 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", 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; } int backupCount = 0; 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); @BatteryOptimizeUtils.OptimizationMode final int optimizationMode = BatteryOptimizeUtils.getAppOptimizationMode( mode, allowlistedApps.contains(info.packageName)); // Ignores default optimized/unknown state or system/default apps. if (optimizationMode == BatteryOptimizeUtils.MODE_OPTIMIZED || optimizationMode == BatteryOptimizeUtils.MODE_UNKNOWN || isSystemOrDefaultApp(info.packageName)) { continue; } final String packageOptimizeMode = info.packageName + DELIMITER_MODE + optimizationMode; builder.append(packageOptimizeMode + DELIMITER); debugLog(packageOptimizeMode); backupCount++; } writeBackupData(data, KEY_OPTIMIZATION_LIST, builder.toString()); Log.d(TAG, String.format("backup getInstalledApplications():%d count=%d in %d/ms", 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.setAppUsageState(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) { return mIDeviceIdleController; } mIDeviceIdleController = IDeviceIdleController.Stub.asInterface( ServiceManager.getService(DEVICE_IDLE_SERVICE)); return mIDeviceIdleController; } private IPackageManager getIPackageManager() { if (mIPackageManager != null) { return mIPackageManager; } mIPackageManager = AppGlobals.getPackageManager(); return mIPackageManager; } private PowerAllowlistBackend getPowerAllowlistBackend() { if (mPowerAllowlistBackend != null) { return mPowerAllowlistBackend; } mPowerAllowlistBackend = PowerAllowlistBackend.getInstance(mContext); return mPowerAllowlistBackend; } private boolean isSystemOrDefaultApp(String packageName) { final PowerAllowlistBackend powerAllowlistBackend = getPowerAllowlistBackend(); return powerAllowlistBackend.isSysAllowlisted(packageName) || powerAllowlistBackend.isDefaultActiveApp(packageName); } 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; } }