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 static com.android.settings.fuelgauge.BatteryBackupHelper.DELIMITER;
20 import static com.android.settings.fuelgauge.BatteryBackupHelper.DELIMITER_MODE;
21 import static com.android.settings.fuelgauge.BatteryOptimizeUtils.MODE_RESTRICTED;
22 import static com.android.settings.fuelgauge.BatteryOptimizeUtils.MODE_UNRESTRICTED;
23 
24 import static com.google.common.truth.Truth.assertThat;
25 
26 import static org.mockito.ArgumentMatchers.any;
27 import static org.mockito.ArgumentMatchers.anyInt;
28 import static org.mockito.ArgumentMatchers.anyLong;
29 import static org.mockito.ArgumentMatchers.anyString;
30 import static org.mockito.ArgumentMatchers.eq;
31 import static org.mockito.Mockito.atLeastOnce;
32 import static org.mockito.Mockito.doReturn;
33 import static org.mockito.Mockito.doThrow;
34 import static org.mockito.Mockito.inOrder;
35 import static org.mockito.Mockito.never;
36 import static org.mockito.Mockito.spy;
37 import static org.mockito.Mockito.verify;
38 import static org.mockito.Mockito.verifyNoInteractions;
39 
40 import android.app.AppOpsManager;
41 import android.app.backup.BackupDataInputStream;
42 import android.app.backup.BackupDataOutput;
43 import android.content.Context;
44 import android.content.pm.ApplicationInfo;
45 import android.content.pm.IPackageManager;
46 import android.content.pm.PackageManager;
47 import android.content.pm.ParceledListSlice;
48 import android.content.pm.UserInfo;
49 import android.os.Build;
50 import android.os.IDeviceIdleController;
51 import android.os.RemoteException;
52 import android.os.UserHandle;
53 import android.os.UserManager;
54 import android.util.ArraySet;
55 
56 import com.android.settings.TestUtils;
57 import com.android.settings.fuelgauge.BatteryOptimizeHistoricalLogEntry.Action;
58 import com.android.settings.testutils.FakeFeatureFactory;
59 import com.android.settingslib.fuelgauge.PowerAllowlistBackend;
60 
61 import org.junit.After;
62 import org.junit.Before;
63 import org.junit.Test;
64 import org.junit.runner.RunWith;
65 import org.mockito.ArgumentCaptor;
66 import org.mockito.InOrder;
67 import org.mockito.Mock;
68 import org.mockito.MockitoAnnotations;
69 import org.robolectric.RobolectricTestRunner;
70 import org.robolectric.RuntimeEnvironment;
71 import org.robolectric.annotation.Config;
72 import org.robolectric.annotation.Implementation;
73 import org.robolectric.annotation.Implements;
74 import org.robolectric.annotation.Resetter;
75 
76 import java.io.PrintWriter;
77 import java.io.StringWriter;
78 import java.util.Arrays;
79 import java.util.List;
80 import java.util.Set;
81 import java.util.concurrent.TimeUnit;
82 
83 @RunWith(RobolectricTestRunner.class)
84 @Config(shadows = {BatteryBackupHelperTest.ShadowUserHandle.class})
85 public final class BatteryBackupHelperTest {
86     private static final String PACKAGE_NAME1 = "com.android.testing.1";
87     private static final String PACKAGE_NAME2 = "com.android.testing.2";
88     private static final String PACKAGE_NAME3 = "com.android.testing.3";
89     private static final int UID1 = 1;
90 
91     private Context mContext;
92     private PrintWriter mPrintWriter;
93     private StringWriter mStringWriter;
94     private BatteryBackupHelper mBatteryBackupHelper;
95     private PowerUsageFeatureProvider mPowerUsageFeatureProvider;
96 
97     @Mock private PackageManager mPackageManager;
98     @Mock private BackupDataOutput mBackupDataOutput;
99     @Mock private BackupDataInputStream mBackupDataInputStream;
100     @Mock private IDeviceIdleController mDeviceController;
101     @Mock private IPackageManager mIPackageManager;
102     @Mock private AppOpsManager mAppOpsManager;
103     @Mock private UserManager mUserManager;
104     @Mock private PowerAllowlistBackend mPowerAllowlistBackend;
105     @Mock private BatteryOptimizeUtils mBatteryOptimizeUtils;
106 
107     @Before
setUp()108     public void setUp() throws Exception {
109         MockitoAnnotations.initMocks(this);
110         mPowerUsageFeatureProvider = FakeFeatureFactory.setupForTest().powerUsageFeatureProvider;
111         mContext = spy(RuntimeEnvironment.application);
112         mStringWriter = new StringWriter();
113         mPrintWriter = new PrintWriter(mStringWriter);
114         BatteryUtils.getInstance(mContext).reset();
115         doReturn(mContext).when(mContext).getApplicationContext();
116         doReturn(mAppOpsManager).when(mContext).getSystemService(AppOpsManager.class);
117         doReturn(mUserManager).when(mContext).getSystemService(UserManager.class);
118         doReturn(mPackageManager).when(mContext).getPackageManager();
119         mBatteryBackupHelper = new BatteryBackupHelper(mContext);
120         mBatteryBackupHelper.mIDeviceIdleController = mDeviceController;
121         mBatteryBackupHelper.mIPackageManager = mIPackageManager;
122         mBatteryBackupHelper.mPowerAllowlistBackend = mPowerAllowlistBackend;
123         mBatteryBackupHelper.mBatteryOptimizeUtils = mBatteryOptimizeUtils;
124         mockUid(1001 /*fake uid*/, PACKAGE_NAME1);
125         mockUid(1002 /*fake uid*/, PACKAGE_NAME2);
126         mockUid(BatteryUtils.UID_NULL, PACKAGE_NAME3);
127     }
128 
129     @After
resetShadows()130     public void resetShadows() {
131         ShadowUserHandle.reset();
132         BatteryBackupHelper.getSharedPreferences(mContext).edit().clear().apply();
133     }
134 
135     @Test
performBackup_emptyPowerList_backupPowerList()136     public void performBackup_emptyPowerList_backupPowerList() throws Exception {
137         doReturn(new String[0]).when(mDeviceController).getFullPowerWhitelist();
138         mBatteryBackupHelper.performBackup(null, mBackupDataOutput, null);
139 
140         verify(mBackupDataOutput, atLeastOnce()).writeEntityHeader(anyString(), anyInt());
141     }
142 
143     @Test
performBackup_remoteException_notBackupPowerList()144     public void performBackup_remoteException_notBackupPowerList() throws Exception {
145         doThrow(new RemoteException()).when(mDeviceController).getFullPowerWhitelist();
146         mBatteryBackupHelper.performBackup(null, mBackupDataOutput, null);
147 
148         verify(mBackupDataOutput, never()).writeEntityHeader(anyString(), anyInt());
149     }
150 
151     @Test
performBackup_nonOwner_ignoreAllBackupAction()152     public void performBackup_nonOwner_ignoreAllBackupAction() throws Exception {
153         ShadowUserHandle.setUid(1);
154         final String[] fullPowerList = {"com.android.package"};
155         doReturn(fullPowerList).when(mDeviceController).getFullPowerWhitelist();
156 
157         mBatteryBackupHelper.performBackup(null, mBackupDataOutput, null);
158 
159         verify(mBackupDataOutput, never()).writeEntityHeader(anyString(), anyInt());
160     }
161 
162     @Test
backupOptimizationMode_nullInstalledApps_ignoreBackupOptimization()163     public void backupOptimizationMode_nullInstalledApps_ignoreBackupOptimization()
164             throws Exception {
165         final UserInfo userInfo =
166                 new UserInfo(/* userId= */ 0, /* userName= */ "google", /* flag= */ 0);
167         doReturn(Arrays.asList(userInfo)).when(mUserManager).getProfiles(anyInt());
168         doThrow(new RuntimeException())
169                 .when(mIPackageManager)
170                 .getInstalledApplications(anyLong(), anyInt());
171 
172         mBatteryBackupHelper.backupOptimizationMode(mBackupDataOutput, null);
173 
174         verify(mBackupDataOutput, never()).writeEntityHeader(anyString(), anyInt());
175     }
176 
177     @Test
backupOptimizationMode_backupOptimizationMode()178     public void backupOptimizationMode_backupOptimizationMode() throws Exception {
179         final List<String> allowlistedApps = Arrays.asList(PACKAGE_NAME1);
180         createTestingData(PACKAGE_NAME1, UID1, PACKAGE_NAME2, PACKAGE_NAME3);
181 
182         mBatteryBackupHelper.backupOptimizationMode(mBackupDataOutput, allowlistedApps);
183 
184         // 2 for UNRESTRICTED mode and 1 for RESTRICTED mode.
185         final String expectedResult = PACKAGE_NAME1 + ":2," + PACKAGE_NAME2 + ":1,";
186         verifyBackupData(expectedResult);
187         verifyDumpHistoryData("com.android.testing.1\taction:BACKUP\tevent:mode: 2");
188         verifyDumpHistoryData("com.android.testing.2\taction:BACKUP\tevent:mode: 1");
189     }
190 
191     @Test
backupOptimizationMode_backupOptimizationModeAndIgnoreSystemApp()192     public void backupOptimizationMode_backupOptimizationModeAndIgnoreSystemApp() throws Exception {
193         final List<String> allowlistedApps = Arrays.asList(PACKAGE_NAME1);
194         createTestingData(PACKAGE_NAME1, UID1, PACKAGE_NAME2, PACKAGE_NAME3);
195         // Sets "com.android.testing.1" as system app.
196         doReturn(true).when(mPowerAllowlistBackend).isSysAllowlisted(PACKAGE_NAME1);
197         doReturn(false).when(mPowerAllowlistBackend).isDefaultActiveApp(anyString(), anyInt());
198 
199         mBatteryBackupHelper.backupOptimizationMode(mBackupDataOutput, allowlistedApps);
200 
201         // "com.android.testing.2" for RESTRICTED mode.
202         final String expectedResult = PACKAGE_NAME2 + ":1,";
203         verifyBackupData(expectedResult);
204         verifyDumpHistoryData("com.android.testing.2\taction:BACKUP\tevent:mode: 1");
205     }
206 
207     @Test
backupOptimizationMode_backupOptimizationModeAndIgnoreDefaultApp()208     public void backupOptimizationMode_backupOptimizationModeAndIgnoreDefaultApp()
209             throws Exception {
210         final List<String> allowlistedApps = Arrays.asList(PACKAGE_NAME1);
211         createTestingData(PACKAGE_NAME1, UID1, PACKAGE_NAME2, PACKAGE_NAME3);
212         // Sets "com.android.testing.1" as device default app.
213         doReturn(true).when(mPowerAllowlistBackend).isDefaultActiveApp(PACKAGE_NAME1, UID1);
214         doReturn(false).when(mPowerAllowlistBackend).isSysAllowlisted(anyString());
215 
216         mBatteryBackupHelper.backupOptimizationMode(mBackupDataOutput, allowlistedApps);
217 
218         // "com.android.testing.2" for RESTRICTED mode.
219         final String expectedResult = PACKAGE_NAME2 + ":1,";
220         verifyBackupData(expectedResult);
221         verifyDumpHistoryData("com.android.testing.2\taction:BACKUP\tevent:mode: 1");
222     }
223 
224     @Test
restoreEntity_nonOwner_notReadBackupData()225     public void restoreEntity_nonOwner_notReadBackupData() throws Exception {
226         ShadowUserHandle.setUid(1);
227         mockBackupData(30 /*dataSize*/, BatteryBackupHelper.KEY_OPTIMIZATION_LIST);
228 
229         mBatteryBackupHelper.restoreEntity(mBackupDataInputStream);
230 
231         verifyNoInteractions(mBackupDataInputStream);
232     }
233 
234     @Test
restoreEntity_zeroDataSize_notReadBackupData()235     public void restoreEntity_zeroDataSize_notReadBackupData() throws Exception {
236         final int zeroDataSize = 0;
237         mockBackupData(zeroDataSize, BatteryBackupHelper.KEY_OPTIMIZATION_LIST);
238 
239         mBatteryBackupHelper.restoreEntity(mBackupDataInputStream);
240 
241         verify(mBackupDataInputStream, never()).read(any(), anyInt(), anyInt());
242     }
243 
244     @Test
restoreEntity_incorrectDataKey_notReadBackupData()245     public void restoreEntity_incorrectDataKey_notReadBackupData() throws Exception {
246         final String incorrectDataKey = "incorrect_data_key";
247         mockBackupData(30 /*dataSize*/, incorrectDataKey);
248 
249         mBatteryBackupHelper.restoreEntity(mBackupDataInputStream);
250 
251         verify(mBackupDataInputStream, never()).read(any(), anyInt(), anyInt());
252     }
253 
254     @Test
restoreEntity_readExpectedDataFromBackupData()255     public void restoreEntity_readExpectedDataFromBackupData() throws Exception {
256         final int dataSize = 30;
257         mockBackupData(dataSize, BatteryBackupHelper.KEY_OPTIMIZATION_LIST);
258 
259         mBatteryBackupHelper.restoreEntity(mBackupDataInputStream);
260 
261         final ArgumentCaptor<byte[]> captor = ArgumentCaptor.forClass(byte[].class);
262         verify(mBackupDataInputStream).read(captor.capture(), eq(0), eq(dataSize));
263         assertThat(captor.getValue().length).isEqualTo(dataSize);
264     }
265 
266     @Test
restoreEntity_verifyConfiguration()267     public void restoreEntity_verifyConfiguration() {
268         final int invalidScheduledLevel = 5;
269         TestUtils.setScheduledLevel(mContext, invalidScheduledLevel);
270 
271         mBatteryBackupHelper.restoreEntity(mBackupDataInputStream);
272 
273         assertThat(TestUtils.getScheduledLevel(mContext)).isNotEqualTo(invalidScheduledLevel);
274     }
275 
276     @Test
restoreEntity_verifyConfigurationOneTimeOnly()277     public void restoreEntity_verifyConfigurationOneTimeOnly() {
278         final int invalidScheduledLevel = 5;
279         TestUtils.setScheduledLevel(mContext, invalidScheduledLevel);
280         mBatteryBackupHelper.restoreEntity(mBackupDataInputStream);
281         TestUtils.setScheduledLevel(mContext, invalidScheduledLevel);
282 
283         // Invoke the restoreEntity() method 2nd time.
284         mBatteryBackupHelper.restoreEntity(mBackupDataInputStream);
285 
286         assertThat(TestUtils.getScheduledLevel(mContext)).isEqualTo(invalidScheduledLevel);
287     }
288 
289     @Test
restoreOptimizationMode_nullBytesData_skipRestore()290     public void restoreOptimizationMode_nullBytesData_skipRestore() throws Exception {
291         mBatteryBackupHelper.restoreOptimizationMode(new byte[0]);
292         verifyNoInteractions(mBatteryOptimizeUtils);
293 
294         mBatteryBackupHelper.restoreOptimizationMode("invalid data format".getBytes());
295         verifyNoInteractions(mBatteryOptimizeUtils);
296 
297         mBatteryBackupHelper.restoreOptimizationMode(DELIMITER.getBytes());
298         verifyNoInteractions(mBatteryOptimizeUtils);
299     }
300 
301     @Test
restoreOptimizationMode_invalidModeFormat_skipRestore()302     public void restoreOptimizationMode_invalidModeFormat_skipRestore() throws Exception {
303         final String invalidNumberFormat = "google";
304         final String package1Mode = PACKAGE_NAME1
305                 + DELIMITER_MODE
306                 + MODE_RESTRICTED
307                 + DELIMITER;
308         final String package2Mode = PACKAGE_NAME2
309                 + DELIMITER_MODE
310                 + invalidNumberFormat;
311         final String packageModes = package1Mode + package2Mode;
312 
313         mBatteryBackupHelper.restoreOptimizationMode(packageModes.getBytes());
314         TimeUnit.SECONDS.sleep(1);
315 
316         final InOrder inOrder = inOrder(mBatteryOptimizeUtils);
317         inOrder.verify(mBatteryOptimizeUtils).setAppUsageState(MODE_RESTRICTED, Action.RESTORE);
318         inOrder.verify(mBatteryOptimizeUtils, never())
319                 .setAppUsageState(anyInt(), eq(Action.RESTORE));
320     }
321 
322     @Test
restoreOptimizationMode_restoreExpectedModes()323     public void restoreOptimizationMode_restoreExpectedModes() throws Exception {
324         final String package1Mode = PACKAGE_NAME1
325                 + DELIMITER_MODE
326                 + MODE_RESTRICTED
327                 + DELIMITER;
328         final String package2Mode = PACKAGE_NAME2
329                 + DELIMITER_MODE
330                 + MODE_UNRESTRICTED
331                 + DELIMITER;
332         final String package3Mode = PACKAGE_NAME3
333                 + DELIMITER_MODE
334                 + MODE_RESTRICTED
335                 + DELIMITER;
336         final String packageModes = package1Mode + package2Mode + package3Mode;
337 
338         mBatteryBackupHelper.restoreOptimizationMode(packageModes.getBytes());
339         TimeUnit.SECONDS.sleep(1);
340 
341         final InOrder inOrder = inOrder(mBatteryOptimizeUtils);
342         inOrder.verify(mBatteryOptimizeUtils).setAppUsageState(MODE_RESTRICTED, Action.RESTORE);
343         inOrder.verify(mBatteryOptimizeUtils).setAppUsageState(MODE_UNRESTRICTED, Action.RESTORE);
344         inOrder.verify(mBatteryOptimizeUtils, never())
345                 .setAppUsageState(MODE_RESTRICTED, Action.RESTORE);
346     }
347 
348     @Test
performBackup_backupDeviceBuildInformation()349     public void performBackup_backupDeviceBuildInformation() throws Exception {
350         final String[] fullPowerList = {"com.android.package"};
351         doReturn(fullPowerList).when(mDeviceController).getFullPowerWhitelist();
352         doReturn(null).when(mPowerUsageFeatureProvider).getBuildMetadata1(mContext);
353         final String deviceMetadata = "device.metadata.test_device";
354         doReturn(deviceMetadata).when(mPowerUsageFeatureProvider).getBuildMetadata2(mContext);
355 
356         mBatteryBackupHelper.performBackup(null, mBackupDataOutput, null);
357 
358         final InOrder inOrder = inOrder(mBackupDataOutput);
359         verifyBackupData(inOrder, BatteryBackupHelper.KEY_BUILD_BRAND, Build.BRAND);
360         verifyBackupData(inOrder, BatteryBackupHelper.KEY_BUILD_PRODUCT, Build.PRODUCT);
361         verifyBackupData(inOrder, BatteryBackupHelper.KEY_BUILD_MANUFACTURER, Build.MANUFACTURER);
362         verifyBackupData(inOrder, BatteryBackupHelper.KEY_BUILD_FINGERPRINT, Build.FINGERPRINT);
363         inOrder.verify(mBackupDataOutput, never())
364                 .writeEntityHeader(eq(BatteryBackupHelper.KEY_BUILD_METADATA_1), anyInt());
365         verifyBackupData(inOrder, BatteryBackupHelper.KEY_BUILD_METADATA_2, deviceMetadata);
366     }
367 
mockUid(int uid, String packageName)368     private void mockUid(int uid, String packageName) throws Exception {
369         doReturn(uid)
370                 .when(mPackageManager)
371                 .getPackageUid(packageName, PackageManager.GET_META_DATA);
372     }
373 
mockBackupData(int dataSize, String dataKey)374     private void mockBackupData(int dataSize, String dataKey) {
375         doReturn(dataSize).when(mBackupDataInputStream).size();
376         doReturn(dataKey).when(mBackupDataInputStream).getKey();
377     }
378 
verifyDumpHistoryData(String expectedResult)379     private void verifyDumpHistoryData(String expectedResult) {
380         BatteryBackupHelper.dumpHistoricalData(mContext, mPrintWriter);
381         assertThat(mStringWriter.toString().contains(expectedResult)).isTrue();
382     }
383 
verifyBackupData(String expectedResult)384     private void verifyBackupData(String expectedResult) throws Exception {
385         final byte[] expectedBytes = expectedResult.getBytes();
386         final ArgumentCaptor<byte[]> captor = ArgumentCaptor.forClass(byte[].class);
387         final Set<String> expectedResultSet =
388                 Set.of(expectedResult.split(BatteryBackupHelper.DELIMITER));
389 
390         verify(mBackupDataOutput)
391                 .writeEntityHeader(BatteryBackupHelper.KEY_OPTIMIZATION_LIST, expectedBytes.length);
392         verify(mBackupDataOutput).writeEntityData(captor.capture(), eq(expectedBytes.length));
393         final String actualResult = new String(captor.getValue());
394         final Set<String> actualResultSet =
395                 Set.of(actualResult.split(BatteryBackupHelper.DELIMITER));
396         assertThat(actualResultSet).isEqualTo(expectedResultSet);
397     }
398 
createTestingData( String packageName1, int uid1, String packageName2, String packageName3)399     private void createTestingData(
400             String packageName1, int uid1, String packageName2, String packageName3)
401             throws Exception {
402         // Sets the getInstalledApplications() method for testing.
403         final UserInfo userInfo =
404                 new UserInfo(/* userId= */ 0, /* userName= */ "google", /* flag= */ 0);
405         doReturn(Arrays.asList(userInfo)).when(mUserManager).getProfiles(anyInt());
406         final ApplicationInfo applicationInfo1 = new ApplicationInfo();
407         applicationInfo1.enabled = true;
408         applicationInfo1.uid = uid1;
409         applicationInfo1.packageName = packageName1;
410         final ApplicationInfo applicationInfo2 = new ApplicationInfo();
411         applicationInfo2.enabled = false;
412         applicationInfo2.uid = 2;
413         applicationInfo2.packageName = packageName2;
414         applicationInfo2.enabledSetting = PackageManager.COMPONENT_ENABLED_STATE_DISABLED_USER;
415         final ApplicationInfo applicationInfo3 = new ApplicationInfo();
416         applicationInfo3.enabled = false;
417         applicationInfo3.uid = 3;
418         applicationInfo3.packageName = packageName3;
419         applicationInfo3.enabledSetting = PackageManager.COMPONENT_ENABLED_STATE_DISABLED;
420         doReturn(
421                         new ParceledListSlice<ApplicationInfo>(
422                                 Arrays.asList(
423                                         applicationInfo1, applicationInfo2, applicationInfo3)))
424                 .when(mIPackageManager)
425                 .getInstalledApplications(anyLong(), anyInt());
426         // Sets the AppOpsManager for checkOpNoThrow() method.
427         doReturn(AppOpsManager.MODE_ALLOWED)
428                 .when(mAppOpsManager)
429                 .checkOpNoThrow(
430                         AppOpsManager.OP_RUN_ANY_IN_BACKGROUND,
431                         applicationInfo1.uid,
432                         applicationInfo1.packageName);
433         doReturn(AppOpsManager.MODE_IGNORED)
434                 .when(mAppOpsManager)
435                 .checkOpNoThrow(
436                         AppOpsManager.OP_RUN_ANY_IN_BACKGROUND,
437                         applicationInfo2.uid,
438                         applicationInfo2.packageName);
439         mBatteryBackupHelper.mTestApplicationInfoList =
440                 new ArraySet<>(Arrays.asList(applicationInfo1, applicationInfo2, applicationInfo3));
441     }
442 
verifyBackupData(InOrder inOrder, String dataKey, String dataContent)443     private void verifyBackupData(InOrder inOrder, String dataKey, String dataContent)
444             throws Exception {
445         final byte[] expectedBytes = dataContent.getBytes();
446         inOrder.verify(mBackupDataOutput).writeEntityHeader(dataKey, expectedBytes.length);
447         inOrder.verify(mBackupDataOutput).writeEntityData(expectedBytes, expectedBytes.length);
448     }
449 
450     @Implements(UserHandle.class)
451     public static class ShadowUserHandle {
452         // Sets the default as thte OWNER role.
453         private static int sUid = 0;
454 
setUid(int uid)455         public static void setUid(int uid) {
456             sUid = uid;
457         }
458 
459         @Implementation
myUserId()460         public static int myUserId() {
461             return sUid;
462         }
463 
464         @Resetter
reset()465         public static void reset() {
466             sUid = 0;
467         }
468     }
469 }
470