1 /*
2  * Copyright (C) 2021 The Android Open Source Project
3  * Licensed under the Apache License, Version 2.0 (the "License");
4  * you may not use this file except in compliance with the License.
5  * You may obtain a copy of the License at
6  *
7  *      http://www.apache.org/licenses/LICENSE-2.0
8  *
9  * Unless required by applicable law or agreed to in writing, software
10  * distributed under the License is distributed on an "AS IS" BASIS,
11  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12  * See the License for the specific language governing permissions and
13  * limitations under the License.
14  *
15  */
16 
17 package com.android.settings.fuelgauge;
18 
19 import android.app.AppGlobals;
20 import android.app.AppOpsManager;
21 import android.app.backup.BackupDataInputStream;
22 import android.app.backup.BackupDataOutput;
23 import android.app.backup.BackupHelper;
24 import android.content.Context;
25 import android.content.SharedPreferences;
26 import android.content.pm.ApplicationInfo;
27 import android.content.pm.IPackageManager;
28 import android.os.Build;
29 import android.os.IDeviceIdleController;
30 import android.os.ParcelFileDescriptor;
31 import android.os.RemoteException;
32 import android.os.ServiceManager;
33 import android.os.UserHandle;
34 import android.util.ArrayMap;
35 import android.util.ArraySet;
36 import android.util.Log;
37 
38 import androidx.annotation.VisibleForTesting;
39 
40 import com.android.settings.fuelgauge.BatteryOptimizeHistoricalLogEntry.Action;
41 import com.android.settings.fuelgauge.batteryusage.AppOptModeSharedPreferencesUtils;
42 import com.android.settings.fuelgauge.batteryusage.AppOptimizationModeEvent;
43 import com.android.settings.overlay.FeatureFactory;
44 import com.android.settingslib.fuelgauge.PowerAllowlistBackend;
45 
46 import java.io.IOException;
47 import java.io.PrintWriter;
48 import java.nio.charset.StandardCharsets;
49 import java.util.ArrayList;
50 import java.util.Arrays;
51 import java.util.List;
52 import java.util.Map;
53 import java.util.stream.Collectors;
54 
55 /** An implementation to backup and restore battery configurations. */
56 public final class BatteryBackupHelper implements BackupHelper {
57     /** An inditifier for {@link BackupHelper}. */
58     public static final String TAG = "BatteryBackupHelper";
59 
60     // Definition for the device build information.
61     public static final String KEY_BUILD_BRAND = "device_build_brand";
62     public static final String KEY_BUILD_PRODUCT = "device_build_product";
63     public static final String KEY_BUILD_MANUFACTURER = "device_build_manufacture";
64     public static final String KEY_BUILD_FINGERPRINT = "device_build_fingerprint";
65     // Customized fields for device extra information.
66     public static final String KEY_BUILD_METADATA_1 = "device_build_metadata_1";
67     public static final String KEY_BUILD_METADATA_2 = "device_build_metadata_2";
68 
69     private static final String DEVICE_IDLE_SERVICE = "deviceidle";
70     private static final String BATTERY_OPTIMIZE_BACKUP_FILE_NAME =
71             "battery_optimize_backup_historical_logs";
72     private static final int DEVICE_BUILD_INFO_SIZE = 6;
73 
74     static final String DELIMITER = ",";
75     static final String DELIMITER_MODE = ":";
76     static final String KEY_OPTIMIZATION_LIST = "optimization_mode_list";
77 
78     @VisibleForTesting ArraySet<ApplicationInfo> mTestApplicationInfoList = null;
79 
80     @VisibleForTesting PowerAllowlistBackend mPowerAllowlistBackend;
81     @VisibleForTesting IDeviceIdleController mIDeviceIdleController;
82     @VisibleForTesting IPackageManager mIPackageManager;
83     @VisibleForTesting BatteryOptimizeUtils mBatteryOptimizeUtils;
84 
85     private byte[] mOptimizationModeBytes;
86     private boolean mVerifyMigrateConfiguration = false;
87 
88     private final Context mContext;
89     // Device information map from the restoreEntity() method.
90     private final ArrayMap<String, String> mDeviceBuildInfoMap =
91             new ArrayMap<>(DEVICE_BUILD_INFO_SIZE);
92 
BatteryBackupHelper(Context context)93     public BatteryBackupHelper(Context context) {
94         mContext = context.getApplicationContext();
95     }
96 
97     @Override
performBackup( ParcelFileDescriptor oldState, BackupDataOutput data, ParcelFileDescriptor newState)98     public void performBackup(
99             ParcelFileDescriptor oldState, BackupDataOutput data, ParcelFileDescriptor newState) {
100         if (!isOwner() || data == null) {
101             Log.w(TAG, "ignore performBackup() for non-owner or empty data");
102             return;
103         }
104         final List<String> allowlistedApps = getFullPowerList();
105         if (allowlistedApps == null) {
106             return;
107         }
108 
109         writeBackupData(data, KEY_BUILD_BRAND, Build.BRAND);
110         writeBackupData(data, KEY_BUILD_PRODUCT, Build.PRODUCT);
111         writeBackupData(data, KEY_BUILD_MANUFACTURER, Build.MANUFACTURER);
112         writeBackupData(data, KEY_BUILD_FINGERPRINT, Build.FINGERPRINT);
113         // Add customized device build metadata fields.
114         final PowerUsageFeatureProvider provider =
115                 FeatureFactory.getFeatureFactory().getPowerUsageFeatureProvider();
116         writeBackupData(data, KEY_BUILD_METADATA_1, provider.getBuildMetadata1(mContext));
117         writeBackupData(data, KEY_BUILD_METADATA_2, provider.getBuildMetadata2(mContext));
118 
119         backupOptimizationMode(data, allowlistedApps);
120     }
121 
122     @Override
restoreEntity(BackupDataInputStream data)123     public void restoreEntity(BackupDataInputStream data) {
124         // Ensure we only verify the migrate configuration one time.
125         if (!mVerifyMigrateConfiguration) {
126             mVerifyMigrateConfiguration = true;
127             BatterySettingsMigrateChecker.verifySaverConfiguration(mContext);
128         }
129         if (!isOwner() || data == null || data.size() == 0) {
130             Log.w(TAG, "ignore restoreEntity() for non-owner or empty data");
131             return;
132         }
133         final String dataKey = data.getKey();
134         switch (dataKey) {
135             case KEY_BUILD_BRAND:
136             case KEY_BUILD_PRODUCT:
137             case KEY_BUILD_MANUFACTURER:
138             case KEY_BUILD_FINGERPRINT:
139             case KEY_BUILD_METADATA_1:
140             case KEY_BUILD_METADATA_2:
141                 restoreBackupData(dataKey, data);
142                 break;
143             case KEY_OPTIMIZATION_LIST:
144                 // Hold the optimization mode data until all conditions are matched.
145                 mOptimizationModeBytes = getBackupData(dataKey, data);
146                 break;
147         }
148         performRestoreIfNeeded();
149     }
150 
151     @Override
writeNewStateDescription(ParcelFileDescriptor newState)152     public void writeNewStateDescription(ParcelFileDescriptor newState) {}
153 
getFullPowerList()154     private List<String> getFullPowerList() {
155         final long timestamp = System.currentTimeMillis();
156         String[] allowlistedApps;
157         try {
158             allowlistedApps = getIDeviceIdleController().getFullPowerWhitelist();
159         } catch (RemoteException e) {
160             Log.e(TAG, "backupFullPowerList() failed", e);
161             return null;
162         }
163         // Ignores unexpected empty result case.
164         if (allowlistedApps == null || allowlistedApps.length == 0) {
165             Log.w(TAG, "no data found in the getFullPowerList()");
166             return new ArrayList<>();
167         }
168         Log.d(
169                 TAG,
170                 String.format(
171                         "getFullPowerList() size=%d in %d/ms",
172                         allowlistedApps.length, (System.currentTimeMillis() - timestamp)));
173         return Arrays.asList(allowlistedApps);
174     }
175 
176     @VisibleForTesting
backupOptimizationMode(BackupDataOutput data, List<String> allowlistedApps)177     void backupOptimizationMode(BackupDataOutput data, List<String> allowlistedApps) {
178         final long timestamp = System.currentTimeMillis();
179         final ArraySet<ApplicationInfo> applications = getInstalledApplications();
180         if (applications == null || applications.isEmpty()) {
181             Log.w(TAG, "no data found in the getInstalledApplications()");
182             return;
183         }
184         int backupCount = 0;
185         final StringBuilder builder = new StringBuilder();
186         final AppOpsManager appOps = mContext.getSystemService(AppOpsManager.class);
187         final SharedPreferences sharedPreferences = getSharedPreferences(mContext);
188         final Map<Integer, AppOptimizationModeEvent> appOptModeMap =
189                 AppOptModeSharedPreferencesUtils.getAllEvents(mContext).stream()
190                         .collect(Collectors.toMap(AppOptimizationModeEvent::getUid, e -> e));
191         // Converts application into the AppUsageState.
192         for (ApplicationInfo info : applications) {
193             final int mode = BatteryOptimizeUtils.getMode(appOps, info.uid, info.packageName);
194             @BatteryOptimizeUtils.OptimizationMode
195             final int optimizationMode =
196                     appOptModeMap.containsKey(info.uid)
197                             ? (int) appOptModeMap.get(info.uid).getResetOptimizationMode()
198                             : BatteryOptimizeUtils.getAppOptimizationMode(
199                                     mode, allowlistedApps.contains(info.packageName));
200             // Ignores default optimized/unknown state or system/default apps.
201             if (optimizationMode == BatteryOptimizeUtils.MODE_OPTIMIZED
202                     || optimizationMode == BatteryOptimizeUtils.MODE_UNKNOWN
203                     || isSystemOrDefaultApp(info.packageName, info.uid)) {
204                 continue;
205             }
206             final String packageOptimizeMode = info.packageName + DELIMITER_MODE + optimizationMode;
207             builder.append(packageOptimizeMode + DELIMITER);
208             Log.d(TAG, "backupOptimizationMode: " + packageOptimizeMode);
209             BatteryOptimizeLogUtils.writeLog(
210                     sharedPreferences,
211                     Action.BACKUP,
212                     info.packageName,
213                     /* actionDescription */ "mode: " + optimizationMode);
214             backupCount++;
215         }
216 
217         writeBackupData(data, KEY_OPTIMIZATION_LIST, builder.toString());
218         Log.d(
219                 TAG,
220                 String.format(
221                         "backup getInstalledApplications():%d count=%d in %d/ms",
222                         applications.size(),
223                         backupCount,
224                         (System.currentTimeMillis() - timestamp)));
225     }
226 
227     @VisibleForTesting
restoreOptimizationMode(byte[] dataBytes)228     int restoreOptimizationMode(byte[] dataBytes) {
229         final long timestamp = System.currentTimeMillis();
230         final String dataContent = new String(dataBytes, StandardCharsets.UTF_8);
231         if (dataContent == null || dataContent.isEmpty()) {
232             Log.w(TAG, "no data found in the restoreOptimizationMode()");
233             return 0;
234         }
235         final String[] appConfigurations = dataContent.split(BatteryBackupHelper.DELIMITER);
236         if (appConfigurations == null || appConfigurations.length == 0) {
237             Log.w(TAG, "no data found from the split() processing");
238             return 0;
239         }
240         int restoreCount = 0;
241         for (int index = 0; index < appConfigurations.length; index++) {
242             final String[] results =
243                     appConfigurations[index].split(BatteryBackupHelper.DELIMITER_MODE);
244             // Example format: com.android.systemui:2 we should have length=2
245             if (results == null || results.length != 2) {
246                 Log.w(TAG, "invalid raw data found:" + appConfigurations[index]);
247                 continue;
248             }
249             final String packageName = results[0];
250             final int uid = BatteryUtils.getInstance(mContext).getPackageUid(packageName);
251             // Ignores system/default apps.
252             if (isSystemOrDefaultApp(packageName, uid)) {
253                 Log.w(TAG, "ignore from isSystemOrDefaultApp():" + packageName);
254                 continue;
255             }
256             @BatteryOptimizeUtils.OptimizationMode
257             int optimizationMode = BatteryOptimizeUtils.MODE_UNKNOWN;
258             try {
259                 optimizationMode = Integer.parseInt(results[1]);
260             } catch (NumberFormatException e) {
261                 Log.e(TAG, "failed to parse the optimization mode: " + appConfigurations[index], e);
262                 continue;
263             }
264             restoreOptimizationMode(packageName, optimizationMode);
265             restoreCount++;
266         }
267         Log.d(
268                 TAG,
269                 String.format(
270                         "restoreOptimizationMode() count=%d in %d/ms",
271                         restoreCount, (System.currentTimeMillis() - timestamp)));
272         return restoreCount;
273     }
274 
performRestoreIfNeeded()275     private void performRestoreIfNeeded() {
276         if (mOptimizationModeBytes == null || mOptimizationModeBytes.length == 0) {
277             return;
278         }
279         final PowerUsageFeatureProvider provider =
280                 FeatureFactory.getFeatureFactory().getPowerUsageFeatureProvider();
281         if (!provider.isValidToRestoreOptimizationMode(mDeviceBuildInfoMap)) {
282             return;
283         }
284         // Start to restore the app optimization mode data.
285         final int restoreCount = restoreOptimizationMode(mOptimizationModeBytes);
286         if (restoreCount > 0) {
287             BatterySettingsMigrateChecker.verifyBatteryOptimizeModes(mContext);
288         }
289         mOptimizationModeBytes = null; // clear data
290     }
291 
292     /** Dump the app optimization mode backup history data. */
dumpHistoricalData(Context context, PrintWriter writer)293     public static void dumpHistoricalData(Context context, PrintWriter writer) {
294         BatteryOptimizeLogUtils.printBatteryOptimizeHistoricalLog(
295                 getSharedPreferences(context), writer);
296     }
297 
isOwner()298     static boolean isOwner() {
299         return UserHandle.myUserId() == UserHandle.USER_SYSTEM;
300     }
301 
newBatteryOptimizeUtils( Context context, String packageName, BatteryOptimizeUtils testOptimizeUtils)302     static BatteryOptimizeUtils newBatteryOptimizeUtils(
303             Context context, String packageName, BatteryOptimizeUtils testOptimizeUtils) {
304         final int uid = BatteryUtils.getInstance(context).getPackageUid(packageName);
305         if (uid == BatteryUtils.UID_NULL) {
306             return null;
307         }
308         final BatteryOptimizeUtils batteryOptimizeUtils =
309                 testOptimizeUtils != null
310                         ? testOptimizeUtils /*testing only*/
311                         : new BatteryOptimizeUtils(context, uid, packageName);
312         return batteryOptimizeUtils;
313     }
314 
315     @VisibleForTesting
getSharedPreferences(Context context)316     static SharedPreferences getSharedPreferences(Context context) {
317         return context.getSharedPreferences(
318                 BATTERY_OPTIMIZE_BACKUP_FILE_NAME, Context.MODE_PRIVATE);
319     }
320 
restoreOptimizationMode( String packageName, @BatteryOptimizeUtils.OptimizationMode int mode)321     private void restoreOptimizationMode(
322             String packageName, @BatteryOptimizeUtils.OptimizationMode int mode) {
323         final BatteryOptimizeUtils batteryOptimizeUtils =
324                 newBatteryOptimizeUtils(mContext, packageName, mBatteryOptimizeUtils);
325         if (batteryOptimizeUtils == null) {
326             return;
327         }
328         batteryOptimizeUtils.setAppUsageState(
329                 mode, BatteryOptimizeHistoricalLogEntry.Action.RESTORE);
330         Log.d(TAG, String.format("restore:%s mode=%d", packageName, mode));
331     }
332 
333     // Provides an opportunity to inject mock IDeviceIdleController for testing.
getIDeviceIdleController()334     private IDeviceIdleController getIDeviceIdleController() {
335         if (mIDeviceIdleController != null) {
336             return mIDeviceIdleController;
337         }
338         mIDeviceIdleController =
339                 IDeviceIdleController.Stub.asInterface(
340                         ServiceManager.getService(DEVICE_IDLE_SERVICE));
341         return mIDeviceIdleController;
342     }
343 
getIPackageManager()344     private IPackageManager getIPackageManager() {
345         if (mIPackageManager != null) {
346             return mIPackageManager;
347         }
348         mIPackageManager = AppGlobals.getPackageManager();
349         return mIPackageManager;
350     }
351 
getPowerAllowlistBackend()352     private PowerAllowlistBackend getPowerAllowlistBackend() {
353         if (mPowerAllowlistBackend != null) {
354             return mPowerAllowlistBackend;
355         }
356         mPowerAllowlistBackend = PowerAllowlistBackend.getInstance(mContext);
357         return mPowerAllowlistBackend;
358     }
359 
isSystemOrDefaultApp(String packageName, int uid)360     private boolean isSystemOrDefaultApp(String packageName, int uid) {
361         return BatteryOptimizeUtils.isSystemOrDefaultApp(
362                 mContext, getPowerAllowlistBackend(), packageName, uid);
363     }
364 
getInstalledApplications()365     private ArraySet<ApplicationInfo> getInstalledApplications() {
366         if (mTestApplicationInfoList != null) {
367             return mTestApplicationInfoList;
368         }
369         return BatteryOptimizeUtils.getInstalledApplications(mContext, getIPackageManager());
370     }
371 
restoreBackupData(String dataKey, BackupDataInputStream data)372     private void restoreBackupData(String dataKey, BackupDataInputStream data) {
373         final byte[] dataBytes = getBackupData(dataKey, data);
374         if (dataBytes == null || dataBytes.length == 0) {
375             return;
376         }
377         final String dataContent = new String(dataBytes, StandardCharsets.UTF_8);
378         mDeviceBuildInfoMap.put(dataKey, dataContent);
379         Log.d(TAG, String.format("restore:%s:%s", dataKey, dataContent));
380     }
381 
getBackupData(String dataKey, BackupDataInputStream data)382     private static byte[] getBackupData(String dataKey, BackupDataInputStream data) {
383         final int dataSize = data.size();
384         final byte[] dataBytes = new byte[dataSize];
385         try {
386             data.read(dataBytes, 0 /*offset*/, dataSize);
387         } catch (IOException e) {
388             Log.e(TAG, "failed to getBackupData() " + dataKey, e);
389             return null;
390         }
391         return dataBytes;
392     }
393 
writeBackupData(BackupDataOutput data, String dataKey, String dataContent)394     private static void writeBackupData(BackupDataOutput data, String dataKey, String dataContent) {
395         if (dataContent == null || dataContent.isEmpty()) {
396             return;
397         }
398         final byte[] dataContentBytes = dataContent.getBytes();
399         try {
400             data.writeEntityHeader(dataKey, dataContentBytes.length);
401             data.writeEntityData(dataContentBytes, dataContentBytes.length);
402         } catch (IOException e) {
403             Log.e(TAG, "writeBackupData() is failed for " + dataKey, e);
404         }
405         Log.d(TAG, String.format("backup:%s:%s", dataKey, dataContent));
406     }
407 }
408