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.pm.ApplicationInfo;
26 import android.content.pm.IPackageManager;
27 import android.content.pm.PackageManager;
28 import android.content.pm.ParceledListSlice;
29 import android.content.pm.UserInfo;
30 import android.os.Build;
31 import android.os.IDeviceIdleController;
32 import android.os.ParcelFileDescriptor;
33 import android.os.RemoteException;
34 import android.os.ServiceManager;
35 import android.os.UserHandle;
36 import android.os.UserManager;
37 import android.util.Log;
38 
39 import androidx.annotation.VisibleForTesting;
40 
41 import com.android.settingslib.fuelgauge.PowerAllowlistBackend;
42 
43 import java.io.IOException;
44 import java.nio.charset.StandardCharsets;
45 import java.util.ArrayList;
46 import java.util.Arrays;
47 import java.util.List;
48 
49 /** An implementation to backup and restore battery configurations. */
50 public final class BatteryBackupHelper implements BackupHelper {
51     /** An inditifier for {@link BackupHelper}. */
52     public static final String TAG = "BatteryBackupHelper";
53     private static final String DEVICE_IDLE_SERVICE = "deviceidle";
54     private static final boolean DEBUG = Build.TYPE.equals("userdebug");
55 
56     // Only the owner can see all apps.
57     private static final int RETRIEVE_FLAG_ADMIN =
58             PackageManager.MATCH_ANY_USER |
59             PackageManager.MATCH_DISABLED_COMPONENTS |
60             PackageManager.MATCH_DISABLED_UNTIL_USED_COMPONENTS;
61     private static final int RETRIEVE_FLAG =
62             PackageManager.MATCH_DISABLED_COMPONENTS |
63             PackageManager.MATCH_DISABLED_UNTIL_USED_COMPONENTS;
64 
65     static final String DELIMITER = ",";
66     static final String DELIMITER_MODE = ":";
67     static final String KEY_FULL_POWER_LIST = "full_power_list";
68     static final String KEY_OPTIMIZATION_LIST = "optimization_mode_list";
69 
70     @VisibleForTesting
71     PowerAllowlistBackend mPowerAllowlistBackend;
72     @VisibleForTesting
73     IDeviceIdleController mIDeviceIdleController;
74     @VisibleForTesting
75     IPackageManager mIPackageManager;
76     @VisibleForTesting
77     BatteryOptimizeUtils mBatteryOptimizeUtils;
78 
79     private final Context mContext;
80 
BatteryBackupHelper(Context context)81     public BatteryBackupHelper(Context context) {
82         mContext = context.getApplicationContext();
83     }
84 
85     @Override
performBackup(ParcelFileDescriptor oldState, BackupDataOutput data, ParcelFileDescriptor newState)86     public void performBackup(ParcelFileDescriptor oldState, BackupDataOutput data,
87             ParcelFileDescriptor newState) {
88         if (!isOwner() || data == null) {
89             Log.w(TAG, "ignore performBackup() for non-owner or empty data");
90             return;
91         }
92         final List<String> allowlistedApps = backupFullPowerList(data);
93         if (allowlistedApps != null) {
94             backupOptimizationMode(data, allowlistedApps);
95         }
96     }
97 
98     @Override
restoreEntity(BackupDataInputStream data)99     public void restoreEntity(BackupDataInputStream data) {
100         if (!isOwner() || data == null || data.size() == 0) {
101             Log.w(TAG, "ignore restoreEntity() for non-owner or empty data");
102             return;
103         }
104         if (KEY_OPTIMIZATION_LIST.equals(data.getKey())) {
105             final int dataSize = data.size();
106             final byte[] dataBytes = new byte[dataSize];
107             try {
108                 data.read(dataBytes, 0 /*offset*/, dataSize);
109             } catch (IOException e) {
110                 Log.e(TAG, "failed to load BackupDataInputStream", e);
111                 return;
112             }
113             restoreOptimizationMode(dataBytes);
114         }
115     }
116 
117     @Override
writeNewStateDescription(ParcelFileDescriptor newState)118     public void writeNewStateDescription(ParcelFileDescriptor newState) {
119     }
120 
backupFullPowerList(BackupDataOutput data)121     private List<String> backupFullPowerList(BackupDataOutput data) {
122         final long timestamp = System.currentTimeMillis();
123         String[] allowlistedApps;
124         try {
125             allowlistedApps = getIDeviceIdleController().getFullPowerWhitelist();
126         } catch (RemoteException e) {
127             Log.e(TAG, "backupFullPowerList() failed", e);
128             return null;
129         }
130         // Ignores unexpected emptty result case.
131         if (allowlistedApps == null || allowlistedApps.length == 0) {
132             Log.w(TAG, "no data found in the getFullPowerList()");
133             return new ArrayList<>();
134         }
135 
136         final String allowedApps = String.join(DELIMITER, allowlistedApps);
137         writeBackupData(data, KEY_FULL_POWER_LIST, allowedApps);
138         Log.d(TAG, String.format("backup getFullPowerList() size=%d in %d/ms",
139                 allowlistedApps.length, (System.currentTimeMillis() - timestamp)));
140         return Arrays.asList(allowlistedApps);
141     }
142 
143     @VisibleForTesting
backupOptimizationMode(BackupDataOutput data, List<String> allowlistedApps)144     void backupOptimizationMode(BackupDataOutput data, List<String> allowlistedApps) {
145         final long timestamp = System.currentTimeMillis();
146         final List<ApplicationInfo> applications = getInstalledApplications();
147         if (applications == null || applications.isEmpty()) {
148             Log.w(TAG, "no data found in the getInstalledApplications()");
149             return;
150         }
151         int backupCount = 0;
152         final StringBuilder builder = new StringBuilder();
153         final AppOpsManager appOps = mContext.getSystemService(AppOpsManager.class);
154         // Converts application into the AppUsageState.
155         for (ApplicationInfo info : applications) {
156             final int mode = appOps.checkOpNoThrow(
157                     AppOpsManager.OP_RUN_ANY_IN_BACKGROUND, info.uid, info.packageName);
158             @BatteryOptimizeUtils.OptimizationMode
159             final int optimizationMode = BatteryOptimizeUtils.getAppOptimizationMode(
160                     mode, allowlistedApps.contains(info.packageName));
161             // Ignores default optimized/unknown state or system/default apps.
162             if (optimizationMode == BatteryOptimizeUtils.MODE_OPTIMIZED
163                     || optimizationMode == BatteryOptimizeUtils.MODE_UNKNOWN
164                     || isSystemOrDefaultApp(info.packageName)) {
165                 continue;
166             }
167             final String packageOptimizeMode =
168                     info.packageName + DELIMITER_MODE + optimizationMode;
169             builder.append(packageOptimizeMode + DELIMITER);
170             debugLog(packageOptimizeMode);
171             backupCount++;
172         }
173 
174         writeBackupData(data, KEY_OPTIMIZATION_LIST, builder.toString());
175         Log.d(TAG, String.format("backup getInstalledApplications():%d count=%d in %d/ms",
176                 applications.size(), backupCount, (System.currentTimeMillis() - timestamp)));
177     }
178 
179     @VisibleForTesting
restoreOptimizationMode(byte[] dataBytes)180     void restoreOptimizationMode(byte[] dataBytes) {
181         final long timestamp = System.currentTimeMillis();
182         final String dataContent = new String(dataBytes, StandardCharsets.UTF_8);
183         if (dataContent == null || dataContent.isEmpty()) {
184             Log.w(TAG, "no data found in the restoreOptimizationMode()");
185             return;
186         }
187         final String[] appConfigurations = dataContent.split(BatteryBackupHelper.DELIMITER);
188         if (appConfigurations == null || appConfigurations.length == 0) {
189             Log.w(TAG, "no data found from the split() processing");
190             return;
191         }
192         int restoreCount = 0;
193         for (int index = 0; index < appConfigurations.length; index++) {
194             final String[] results = appConfigurations[index]
195                     .split(BatteryBackupHelper.DELIMITER_MODE);
196             // Example format: com.android.systemui:2 we should have length=2
197             if (results == null || results.length != 2) {
198                 Log.w(TAG, "invalid raw data found:" + appConfigurations[index]);
199                 continue;
200             }
201             final String packageName = results[0];
202             // Ignores system/default apps.
203             if (isSystemOrDefaultApp(packageName)) {
204                 Log.w(TAG, "ignore from isSystemOrDefaultApp():" + packageName);
205                 continue;
206             }
207             @BatteryOptimizeUtils.OptimizationMode
208             int optimizationMode = BatteryOptimizeUtils.MODE_UNKNOWN;
209             try {
210                 optimizationMode = Integer.parseInt(results[1]);
211             } catch (NumberFormatException e) {
212                 Log.e(TAG, "failed to parse the optimization mode: "
213                         + appConfigurations[index], e);
214                 continue;
215             }
216             restoreOptimizationMode(packageName, optimizationMode);
217             restoreCount++;
218         }
219         Log.d(TAG, String.format("restoreOptimizationMode() count=%d in %d/ms",
220                 restoreCount, (System.currentTimeMillis() - timestamp)));
221     }
222 
restoreOptimizationMode( String packageName, @BatteryOptimizeUtils.OptimizationMode int mode)223     private void restoreOptimizationMode(
224             String packageName, @BatteryOptimizeUtils.OptimizationMode int mode) {
225         final int uid = BatteryUtils.getInstance(mContext).getPackageUid(packageName);
226         if (uid == BatteryUtils.UID_NULL) {
227             return;
228         }
229         final BatteryOptimizeUtils batteryOptimizeUtils =
230                 mBatteryOptimizeUtils != null
231                         ? mBatteryOptimizeUtils /*testing only*/
232                         : new BatteryOptimizeUtils(mContext, uid, packageName);
233         batteryOptimizeUtils.setAppUsageState(mode);
234         Log.d(TAG, String.format("restore:%s mode=%d", packageName, mode));
235     }
236 
237     // Provides an opportunity to inject mock IDeviceIdleController for testing.
getIDeviceIdleController()238     private IDeviceIdleController getIDeviceIdleController() {
239         if (mIDeviceIdleController != null) {
240             return mIDeviceIdleController;
241         }
242         mIDeviceIdleController = IDeviceIdleController.Stub.asInterface(
243                 ServiceManager.getService(DEVICE_IDLE_SERVICE));
244         return mIDeviceIdleController;
245     }
246 
getIPackageManager()247     private IPackageManager getIPackageManager() {
248         if (mIPackageManager != null) {
249             return mIPackageManager;
250         }
251         mIPackageManager = AppGlobals.getPackageManager();
252         return mIPackageManager;
253     }
254 
getPowerAllowlistBackend()255     private PowerAllowlistBackend getPowerAllowlistBackend() {
256         if (mPowerAllowlistBackend != null) {
257             return mPowerAllowlistBackend;
258         }
259         mPowerAllowlistBackend = PowerAllowlistBackend.getInstance(mContext);
260         return mPowerAllowlistBackend;
261     }
262 
isSystemOrDefaultApp(String packageName)263     private boolean isSystemOrDefaultApp(String packageName) {
264         final PowerAllowlistBackend powerAllowlistBackend = getPowerAllowlistBackend();
265         return powerAllowlistBackend.isSysAllowlisted(packageName)
266                 || powerAllowlistBackend.isDefaultActiveApp(packageName);
267     }
268 
getInstalledApplications()269     private List<ApplicationInfo> getInstalledApplications() {
270         final List<ApplicationInfo> applications = new ArrayList<>();
271         final UserManager um = mContext.getSystemService(UserManager.class);
272         for (UserInfo userInfo : um.getProfiles(UserHandle.myUserId())) {
273             try {
274                 @SuppressWarnings("unchecked")
275                 final ParceledListSlice<ApplicationInfo> infoList =
276                         getIPackageManager().getInstalledApplications(
277                                 userInfo.isAdmin() ? RETRIEVE_FLAG_ADMIN : RETRIEVE_FLAG,
278                                 userInfo.id);
279                 if (infoList != null) {
280                     applications.addAll(infoList.getList());
281                 }
282             } catch (Exception e) {
283                 Log.e(TAG, "getInstalledApplications() is failed", e);
284                 return null;
285             }
286         }
287         // Removes the application which is disabled by the system.
288         for (int index = applications.size() - 1; index >= 0; index--) {
289             final ApplicationInfo info = applications.get(index);
290             if (info.enabledSetting != PackageManager.COMPONENT_ENABLED_STATE_DISABLED_USER
291                     && !info.enabled) {
292                 applications.remove(index);
293             }
294         }
295         return applications;
296     }
297 
debugLog(String debugContent)298     private void debugLog(String debugContent) {
299         if (DEBUG) Log.d(TAG, debugContent);
300     }
301 
writeBackupData( BackupDataOutput data, String dataKey, String dataContent)302     private static void writeBackupData(
303             BackupDataOutput data, String dataKey, String dataContent) {
304         final byte[] dataContentBytes = dataContent.getBytes();
305         try {
306             data.writeEntityHeader(dataKey, dataContentBytes.length);
307             data.writeEntityData(dataContentBytes, dataContentBytes.length);
308         } catch (IOException e) {
309             Log.e(TAG, "writeBackupData() is failed for " + dataKey, e);
310         }
311     }
312 
isOwner()313     private static boolean isOwner() {
314         return UserHandle.myUserId() == UserHandle.USER_OWNER;
315     }
316 }
317