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