/* * 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.SharedPreferences; import android.content.pm.ApplicationInfo; import android.content.pm.IPackageManager; 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.util.ArrayMap; import android.util.ArraySet; import android.util.Log; import androidx.annotation.VisibleForTesting; import com.android.settings.fuelgauge.BatteryOptimizeHistoricalLogEntry.Action; import com.android.settings.fuelgauge.batteryusage.AppOptModeSharedPreferencesUtils; import com.android.settings.fuelgauge.batteryusage.AppOptimizationModeEvent; import com.android.settings.overlay.FeatureFactory; import com.android.settingslib.fuelgauge.PowerAllowlistBackend; import java.io.IOException; import java.io.PrintWriter; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Map; import java.util.stream.Collectors; /** 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"; // Definition for the device build information. public static final String KEY_BUILD_BRAND = "device_build_brand"; public static final String KEY_BUILD_PRODUCT = "device_build_product"; public static final String KEY_BUILD_MANUFACTURER = "device_build_manufacture"; public static final String KEY_BUILD_FINGERPRINT = "device_build_fingerprint"; // Customized fields for device extra information. public static final String KEY_BUILD_METADATA_1 = "device_build_metadata_1"; public static final String KEY_BUILD_METADATA_2 = "device_build_metadata_2"; private static final String DEVICE_IDLE_SERVICE = "deviceidle"; private static final String BATTERY_OPTIMIZE_BACKUP_FILE_NAME = "battery_optimize_backup_historical_logs"; private static final int DEVICE_BUILD_INFO_SIZE = 6; static final String DELIMITER = ","; static final String DELIMITER_MODE = ":"; static final String KEY_OPTIMIZATION_LIST = "optimization_mode_list"; @VisibleForTesting ArraySet mTestApplicationInfoList = null; @VisibleForTesting PowerAllowlistBackend mPowerAllowlistBackend; @VisibleForTesting IDeviceIdleController mIDeviceIdleController; @VisibleForTesting IPackageManager mIPackageManager; @VisibleForTesting BatteryOptimizeUtils mBatteryOptimizeUtils; private byte[] mOptimizationModeBytes; private boolean mVerifyMigrateConfiguration = false; private final Context mContext; // Device information map from the restoreEntity() method. private final ArrayMap mDeviceBuildInfoMap = new ArrayMap<>(DEVICE_BUILD_INFO_SIZE); 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 = getFullPowerList(); if (allowlistedApps == null) { return; } writeBackupData(data, KEY_BUILD_BRAND, Build.BRAND); writeBackupData(data, KEY_BUILD_PRODUCT, Build.PRODUCT); writeBackupData(data, KEY_BUILD_MANUFACTURER, Build.MANUFACTURER); writeBackupData(data, KEY_BUILD_FINGERPRINT, Build.FINGERPRINT); // Add customized device build metadata fields. final PowerUsageFeatureProvider provider = FeatureFactory.getFeatureFactory().getPowerUsageFeatureProvider(); writeBackupData(data, KEY_BUILD_METADATA_1, provider.getBuildMetadata1(mContext)); writeBackupData(data, KEY_BUILD_METADATA_2, provider.getBuildMetadata2(mContext)); backupOptimizationMode(data, allowlistedApps); } @Override public void restoreEntity(BackupDataInputStream data) { // Ensure we only verify the migrate configuration one time. if (!mVerifyMigrateConfiguration) { mVerifyMigrateConfiguration = true; BatterySettingsMigrateChecker.verifySaverConfiguration(mContext); } if (!isOwner() || data == null || data.size() == 0) { Log.w(TAG, "ignore restoreEntity() for non-owner or empty data"); return; } final String dataKey = data.getKey(); switch (dataKey) { case KEY_BUILD_BRAND: case KEY_BUILD_PRODUCT: case KEY_BUILD_MANUFACTURER: case KEY_BUILD_FINGERPRINT: case KEY_BUILD_METADATA_1: case KEY_BUILD_METADATA_2: restoreBackupData(dataKey, data); break; case KEY_OPTIMIZATION_LIST: // Hold the optimization mode data until all conditions are matched. mOptimizationModeBytes = getBackupData(dataKey, data); break; } performRestoreIfNeeded(); } @Override public void writeNewStateDescription(ParcelFileDescriptor newState) {} private List getFullPowerList() { final long timestamp = System.currentTimeMillis(); String[] allowlistedApps; try { allowlistedApps = getIDeviceIdleController().getFullPowerWhitelist(); } catch (RemoteException e) { Log.e(TAG, "backupFullPowerList() failed", e); return null; } // Ignores unexpected empty result case. if (allowlistedApps == null || allowlistedApps.length == 0) { Log.w(TAG, "no data found in the getFullPowerList()"); return new ArrayList<>(); } Log.d( TAG, String.format( "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 ArraySet 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); final SharedPreferences sharedPreferences = getSharedPreferences(mContext); final Map appOptModeMap = AppOptModeSharedPreferencesUtils.getAllEvents(mContext).stream() .collect(Collectors.toMap(AppOptimizationModeEvent::getUid, e -> e)); // Converts application into the AppUsageState. for (ApplicationInfo info : applications) { final int mode = BatteryOptimizeUtils.getMode(appOps, info.uid, info.packageName); @BatteryOptimizeUtils.OptimizationMode final int optimizationMode = appOptModeMap.containsKey(info.uid) ? (int) appOptModeMap.get(info.uid).getResetOptimizationMode() : 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, info.uid)) { continue; } final String packageOptimizeMode = info.packageName + DELIMITER_MODE + optimizationMode; builder.append(packageOptimizeMode + DELIMITER); Log.d(TAG, "backupOptimizationMode: " + packageOptimizeMode); BatteryOptimizeLogUtils.writeLog( sharedPreferences, Action.BACKUP, info.packageName, /* actionDescription */ "mode: " + optimizationMode); 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 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 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 0; } 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]; final int uid = BatteryUtils.getInstance(mContext).getPackageUid(packageName); // Ignores system/default apps. if (isSystemOrDefaultApp(packageName, uid)) { 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))); return restoreCount; } private void performRestoreIfNeeded() { if (mOptimizationModeBytes == null || mOptimizationModeBytes.length == 0) { return; } final PowerUsageFeatureProvider provider = FeatureFactory.getFeatureFactory().getPowerUsageFeatureProvider(); if (!provider.isValidToRestoreOptimizationMode(mDeviceBuildInfoMap)) { return; } // Start to restore the app optimization mode data. final int restoreCount = restoreOptimizationMode(mOptimizationModeBytes); if (restoreCount > 0) { BatterySettingsMigrateChecker.verifyBatteryOptimizeModes(mContext); } mOptimizationModeBytes = null; // clear data } /** Dump the app optimization mode backup history data. */ public static void dumpHistoricalData(Context context, PrintWriter writer) { BatteryOptimizeLogUtils.printBatteryOptimizeHistoricalLog( 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( BATTERY_OPTIMIZE_BACKUP_FILE_NAME, Context.MODE_PRIVATE); } private void restoreOptimizationMode( String packageName, @BatteryOptimizeUtils.OptimizationMode int mode) { final BatteryOptimizeUtils batteryOptimizeUtils = newBatteryOptimizeUtils(mContext, packageName, mBatteryOptimizeUtils); if (batteryOptimizeUtils == null) { return; } batteryOptimizeUtils.setAppUsageState( mode, BatteryOptimizeHistoricalLogEntry.Action.RESTORE); 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, int uid) { return BatteryOptimizeUtils.isSystemOrDefaultApp( mContext, getPowerAllowlistBackend(), packageName, uid); } private ArraySet getInstalledApplications() { if (mTestApplicationInfoList != null) { return mTestApplicationInfoList; } return BatteryOptimizeUtils.getInstalledApplications(mContext, getIPackageManager()); } private void restoreBackupData(String dataKey, BackupDataInputStream data) { final byte[] dataBytes = getBackupData(dataKey, data); if (dataBytes == null || dataBytes.length == 0) { return; } final String dataContent = new String(dataBytes, StandardCharsets.UTF_8); mDeviceBuildInfoMap.put(dataKey, dataContent); Log.d(TAG, String.format("restore:%s:%s", dataKey, dataContent)); } private static byte[] getBackupData(String dataKey, BackupDataInputStream data) { 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 getBackupData() " + dataKey, e); return null; } return dataBytes; } private static void writeBackupData(BackupDataOutput data, String dataKey, String dataContent) { if (dataContent == null || dataContent.isEmpty()) { return; } 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); } Log.d(TAG, String.format("backup:%s:%s", dataKey, dataContent)); } }