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.anyString;
29 import static org.mockito.ArgumentMatchers.eq;
30 import static org.mockito.Mockito.doReturn;
31 import static org.mockito.Mockito.doThrow;
32 import static org.mockito.Mockito.inOrder;
33 import static org.mockito.Mockito.never;
34 import static org.mockito.Mockito.spy;
35 import static org.mockito.Mockito.verify;
36 import static org.mockito.Mockito.verifyZeroInteractions;
37 
38 import android.app.AppOpsManager;
39 import android.app.backup.BackupDataInputStream;
40 import android.app.backup.BackupDataOutput;
41 import android.content.Context;
42 import android.content.pm.ApplicationInfo;
43 import android.content.pm.IPackageManager;
44 import android.content.pm.PackageManager;
45 import android.content.pm.ParceledListSlice;
46 import android.content.pm.UserInfo;
47 import android.os.IDeviceIdleController;
48 import android.os.RemoteException;
49 import android.os.UserHandle;
50 import android.os.UserManager;
51 
52 import com.android.settingslib.fuelgauge.PowerAllowlistBackend;
53 
54 import org.junit.After;
55 import org.junit.Before;
56 import org.junit.Test;
57 import org.junit.runner.RunWith;
58 import org.mockito.ArgumentCaptor;
59 import org.mockito.InOrder;
60 import org.mockito.Mock;
61 import org.mockito.MockitoAnnotations;
62 import org.robolectric.RobolectricTestRunner;
63 import org.robolectric.RuntimeEnvironment;
64 import org.robolectric.annotation.Config;
65 import org.robolectric.annotation.Implementation;
66 import org.robolectric.annotation.Implements;
67 import org.robolectric.annotation.Resetter;
68 
69 import java.util.Arrays;
70 import java.util.List;
71 import java.util.concurrent.TimeUnit;
72 
73 @RunWith(RobolectricTestRunner.class)
74 @Config(shadows = {BatteryBackupHelperTest.ShadowUserHandle.class})
75 public final class BatteryBackupHelperTest {
76     private static final String PACKAGE_NAME1 = "com.android.testing.1";
77     private static final String PACKAGE_NAME2 = "com.android.testing.2";
78     private static final String PACKAGE_NAME3 = "com.android.testing.3";
79 
80     private Context mContext;
81     private BatteryBackupHelper mBatteryBackupHelper;
82 
83     @Mock
84     private PackageManager mPackageManager;
85     @Mock
86     private BackupDataOutput mBackupDataOutput;
87     @Mock
88     private BackupDataInputStream mBackupDataInputStream;
89     @Mock
90     private IDeviceIdleController mDeviceController;
91     @Mock
92     private IPackageManager mIPackageManager;
93     @Mock
94     private AppOpsManager mAppOpsManager;
95     @Mock
96     private UserManager mUserManager;
97     @Mock
98     private PowerAllowlistBackend mPowerAllowlistBackend;
99     @Mock
100     private BatteryOptimizeUtils mBatteryOptimizeUtils;
101 
102     @Before
setUp()103     public void setUp() throws Exception {
104         MockitoAnnotations.initMocks(this);
105         mContext = spy(RuntimeEnvironment.application);
106         doReturn(mContext).when(mContext).getApplicationContext();
107         doReturn(mAppOpsManager).when(mContext).getSystemService(AppOpsManager.class);
108         doReturn(mUserManager).when(mContext).getSystemService(UserManager.class);
109         doReturn(mPackageManager).when(mContext).getPackageManager();
110         mBatteryBackupHelper = new BatteryBackupHelper(mContext);
111         mBatteryBackupHelper.mIDeviceIdleController = mDeviceController;
112         mBatteryBackupHelper.mIPackageManager = mIPackageManager;
113         mBatteryBackupHelper.mPowerAllowlistBackend = mPowerAllowlistBackend;
114         mBatteryBackupHelper.mBatteryOptimizeUtils = mBatteryOptimizeUtils;
115         mockUid(1001 /*fake uid*/, PACKAGE_NAME1);
116         mockUid(1002 /*fake uid*/, PACKAGE_NAME2);
117         mockUid(BatteryUtils.UID_NULL, PACKAGE_NAME3);
118     }
119 
120     @After
resetShadows()121     public void resetShadows() {
122         ShadowUserHandle.reset();
123     }
124 
125     @Test
performBackup_nullPowerList_notBackupPowerList()126     public void performBackup_nullPowerList_notBackupPowerList() throws Exception {
127         doReturn(null).when(mDeviceController).getFullPowerWhitelist();
128         mBatteryBackupHelper.performBackup(null, mBackupDataOutput, null);
129 
130         verify(mBackupDataOutput, never()).writeEntityHeader(anyString(), anyInt());
131     }
132 
133     @Test
performBackup_emptyPowerList_notBackupPowerList()134     public void performBackup_emptyPowerList_notBackupPowerList() throws Exception {
135         doReturn(new String[0]).when(mDeviceController).getFullPowerWhitelist();
136         mBatteryBackupHelper.performBackup(null, mBackupDataOutput, null);
137 
138         verify(mBackupDataOutput, never()).writeEntityHeader(anyString(), anyInt());
139     }
140 
141     @Test
performBackup_remoteException_notBackupPowerList()142     public void performBackup_remoteException_notBackupPowerList() throws Exception {
143         doThrow(new RemoteException()).when(mDeviceController).getFullPowerWhitelist();
144         mBatteryBackupHelper.performBackup(null, mBackupDataOutput, null);
145 
146         verify(mBackupDataOutput, never()).writeEntityHeader(anyString(), anyInt());
147     }
148 
149     @Test
performBackup_oneFullPowerListElement_backupFullPowerListData()150     public void performBackup_oneFullPowerListElement_backupFullPowerListData()
151             throws Exception {
152         final String[] fullPowerList = {"com.android.package"};
153         doReturn(fullPowerList).when(mDeviceController).getFullPowerWhitelist();
154 
155         mBatteryBackupHelper.performBackup(null, mBackupDataOutput, null);
156 
157         final byte[] expectedBytes = fullPowerList[0].getBytes();
158         verify(mBackupDataOutput).writeEntityHeader(
159                 BatteryBackupHelper.KEY_FULL_POWER_LIST, expectedBytes.length);
160         verify(mBackupDataOutput).writeEntityData(expectedBytes, expectedBytes.length);
161     }
162 
163     @Test
performBackup_backupFullPowerListData()164     public void performBackup_backupFullPowerListData() throws Exception {
165         final String[] fullPowerList = {"com.android.package1", "com.android.package2"};
166         doReturn(fullPowerList).when(mDeviceController).getFullPowerWhitelist();
167 
168         mBatteryBackupHelper.performBackup(null, mBackupDataOutput, null);
169 
170         final String expectedResult = fullPowerList[0] + DELIMITER + fullPowerList[1];
171         final byte[] expectedBytes = expectedResult.getBytes();
172         verify(mBackupDataOutput).writeEntityHeader(
173                 BatteryBackupHelper.KEY_FULL_POWER_LIST, expectedBytes.length);
174         verify(mBackupDataOutput).writeEntityData(expectedBytes, expectedBytes.length);
175     }
176 
177     @Test
performBackup_nonOwner_ignoreAllBackupAction()178     public void performBackup_nonOwner_ignoreAllBackupAction() throws Exception {
179         ShadowUserHandle.setUid(1);
180         final String[] fullPowerList = {"com.android.package"};
181         doReturn(fullPowerList).when(mDeviceController).getFullPowerWhitelist();
182 
183         mBatteryBackupHelper.performBackup(null, mBackupDataOutput, null);
184 
185         verify(mBackupDataOutput, never()).writeEntityHeader(anyString(), anyInt());
186     }
187 
188     @Test
backupOptimizationMode_nullInstalledApps_ignoreBackupOptimization()189     public void backupOptimizationMode_nullInstalledApps_ignoreBackupOptimization()
190             throws Exception {
191         final UserInfo userInfo =
192                 new UserInfo(/*userId=*/ 0, /*userName=*/ "google", /*flag=*/ 0);
193         doReturn(Arrays.asList(userInfo)).when(mUserManager).getProfiles(anyInt());
194         doThrow(new RuntimeException())
195                 .when(mIPackageManager)
196                 .getInstalledApplications(anyInt(), anyInt());
197 
198         mBatteryBackupHelper.backupOptimizationMode(mBackupDataOutput, null);
199 
200         verify(mBackupDataOutput, never()).writeEntityHeader(anyString(), anyInt());
201     }
202 
203     @Test
backupOptimizationMode_backupOptimizationMode()204     public void backupOptimizationMode_backupOptimizationMode() throws Exception {
205         final List<String> allowlistedApps = Arrays.asList(PACKAGE_NAME1);
206         createTestingData(PACKAGE_NAME1, PACKAGE_NAME2, PACKAGE_NAME3);
207 
208         mBatteryBackupHelper.backupOptimizationMode(mBackupDataOutput, allowlistedApps);
209 
210         // 2 for UNRESTRICTED mode and 1 for RESTRICTED mode.
211         final String expectedResult = PACKAGE_NAME1 + ":2," + PACKAGE_NAME2 + ":1,";
212         verifyBackupData(expectedResult);
213     }
214 
215     @Test
backupOptimizationMode_backupOptimizationModeAndIgnoreSystemApp()216     public void backupOptimizationMode_backupOptimizationModeAndIgnoreSystemApp()
217             throws Exception {
218         final List<String> allowlistedApps = Arrays.asList(PACKAGE_NAME1);
219         createTestingData(PACKAGE_NAME1, PACKAGE_NAME2, PACKAGE_NAME3);
220         // Sets "com.android.testing.1" as system app.
221         doReturn(true).when(mPowerAllowlistBackend).isSysAllowlisted(PACKAGE_NAME1);
222         doReturn(false).when(mPowerAllowlistBackend).isDefaultActiveApp(anyString());
223 
224         mBatteryBackupHelper.backupOptimizationMode(mBackupDataOutput, allowlistedApps);
225 
226         // "com.android.testing.2" for RESTRICTED mode.
227         final String expectedResult = PACKAGE_NAME2 + ":1,";
228         verifyBackupData(expectedResult);
229     }
230 
231     @Test
backupOptimizationMode_backupOptimizationModeAndIgnoreDefaultApp()232     public void backupOptimizationMode_backupOptimizationModeAndIgnoreDefaultApp()
233             throws Exception {
234         final List<String> allowlistedApps = Arrays.asList(PACKAGE_NAME1);
235         createTestingData(PACKAGE_NAME1, PACKAGE_NAME2, PACKAGE_NAME3);
236         // Sets "com.android.testing.1" as device default app.
237         doReturn(true).when(mPowerAllowlistBackend).isDefaultActiveApp(PACKAGE_NAME1);
238         doReturn(false).when(mPowerAllowlistBackend).isSysAllowlisted(anyString());
239 
240         mBatteryBackupHelper.backupOptimizationMode(mBackupDataOutput, allowlistedApps);
241 
242         // "com.android.testing.2" for RESTRICTED mode.
243         final String expectedResult = PACKAGE_NAME2 + ":1,";
244         verifyBackupData(expectedResult);
245     }
246 
247     @Test
restoreEntity_nonOwner_notReadBackupData()248     public void restoreEntity_nonOwner_notReadBackupData() throws Exception {
249         ShadowUserHandle.setUid(1);
250         mockBackupData(30 /*dataSize*/, BatteryBackupHelper.KEY_OPTIMIZATION_LIST);
251 
252         mBatteryBackupHelper.restoreEntity(mBackupDataInputStream);
253 
254         verifyZeroInteractions(mBackupDataInputStream);
255     }
256 
257     @Test
restoreEntity_zeroDataSize_notReadBackupData()258     public void restoreEntity_zeroDataSize_notReadBackupData() throws Exception {
259         final int zeroDataSize = 0;
260         mockBackupData(zeroDataSize, BatteryBackupHelper.KEY_OPTIMIZATION_LIST);
261 
262         mBatteryBackupHelper.restoreEntity(mBackupDataInputStream);
263 
264         verify(mBackupDataInputStream, never()).read(any(), anyInt(), anyInt());
265     }
266 
267     @Test
restoreEntity_incorrectDataKey_notReadBackupData()268     public void restoreEntity_incorrectDataKey_notReadBackupData() throws Exception {
269         final String incorrectDataKey = BatteryBackupHelper.KEY_FULL_POWER_LIST;
270         mockBackupData(30 /*dataSize*/, incorrectDataKey);
271 
272         mBatteryBackupHelper.restoreEntity(mBackupDataInputStream);
273 
274         verify(mBackupDataInputStream, never()).read(any(), anyInt(), anyInt());
275     }
276 
277     @Test
restoreEntity_readExpectedDataFromBackupData()278     public void restoreEntity_readExpectedDataFromBackupData() throws Exception {
279         final int dataSize = 30;
280         mockBackupData(dataSize, BatteryBackupHelper.KEY_OPTIMIZATION_LIST);
281 
282         mBatteryBackupHelper.restoreEntity(mBackupDataInputStream);
283 
284         final ArgumentCaptor<byte[]> captor = ArgumentCaptor.forClass(byte[].class);
285         verify(mBackupDataInputStream).read(captor.capture(), eq(0), eq(dataSize));
286         assertThat(captor.getValue().length).isEqualTo(dataSize);
287     }
288 
289     @Test
restoreOptimizationMode_nullBytesData_skipRestore()290     public void restoreOptimizationMode_nullBytesData_skipRestore() throws Exception {
291         mBatteryBackupHelper.restoreOptimizationMode(new byte[0]);
292         verifyZeroInteractions(mBatteryOptimizeUtils);
293 
294         mBatteryBackupHelper.restoreOptimizationMode("invalid data format".getBytes());
295         verifyZeroInteractions(mBatteryOptimizeUtils);
296 
297         mBatteryBackupHelper.restoreOptimizationMode(DELIMITER.getBytes());
298         verifyZeroInteractions(mBatteryOptimizeUtils);
299     }
300 
301     @Test
restoreOptimizationMode_invalidModeFormat_skipRestore()302     public void restoreOptimizationMode_invalidModeFormat_skipRestore() throws Exception {
303         final String invalidNumberFormat = "google";
304         final String packageModes =
305                 PACKAGE_NAME1 + DELIMITER_MODE + MODE_RESTRICTED + DELIMITER +
306                 PACKAGE_NAME2 + DELIMITER_MODE + invalidNumberFormat;
307 
308         mBatteryBackupHelper.restoreOptimizationMode(packageModes.getBytes());
309         TimeUnit.SECONDS.sleep(1);
310 
311         final InOrder inOrder = inOrder(mBatteryOptimizeUtils);
312         inOrder.verify(mBatteryOptimizeUtils).setAppUsageState(MODE_RESTRICTED);
313         inOrder.verify(mBatteryOptimizeUtils, never()).setAppUsageState(anyInt());
314     }
315 
316     @Test
restoreOptimizationMode_restoreExpectedModes()317     public void restoreOptimizationMode_restoreExpectedModes() throws Exception {
318         final String packageModes =
319                 PACKAGE_NAME1 + DELIMITER_MODE + MODE_RESTRICTED + DELIMITER +
320                 PACKAGE_NAME2 + DELIMITER_MODE + MODE_UNRESTRICTED + DELIMITER +
321                 PACKAGE_NAME3 + DELIMITER_MODE + MODE_RESTRICTED + DELIMITER;
322 
323         mBatteryBackupHelper.restoreOptimizationMode(packageModes.getBytes());
324         TimeUnit.SECONDS.sleep(1);
325 
326         final InOrder inOrder = inOrder(mBatteryOptimizeUtils);
327         inOrder.verify(mBatteryOptimizeUtils).setAppUsageState(MODE_RESTRICTED);
328         inOrder.verify(mBatteryOptimizeUtils).setAppUsageState(MODE_UNRESTRICTED);
329         inOrder.verify(mBatteryOptimizeUtils, never()).setAppUsageState(MODE_RESTRICTED);
330     }
331 
mockUid(int uid, String packageName)332     private void mockUid(int uid, String packageName) throws Exception {
333         doReturn(uid).when(mPackageManager)
334                 .getPackageUid(packageName, PackageManager.GET_META_DATA);
335     }
336 
mockBackupData(int dataSize, String dataKey)337     private void mockBackupData(int dataSize, String dataKey) {
338         doReturn(dataSize).when(mBackupDataInputStream).size();
339         doReturn(dataKey).when(mBackupDataInputStream).getKey();
340     }
341 
verifyBackupData(String expectedResult)342     private void verifyBackupData(String expectedResult) throws Exception {
343         final byte[] expectedBytes = expectedResult.getBytes();
344         verify(mBackupDataOutput).writeEntityHeader(
345                 BatteryBackupHelper.KEY_OPTIMIZATION_LIST, expectedBytes.length);
346         verify(mBackupDataOutput).writeEntityData(expectedBytes, expectedBytes.length);
347     }
348 
createTestingData( String packageName1, String packageName2, String packageName3)349     private void createTestingData(
350             String packageName1, String packageName2, String packageName3) throws Exception {
351         // Sets the getInstalledApplications() method for testing.
352         final UserInfo userInfo =
353                 new UserInfo(/*userId=*/ 0, /*userName=*/ "google", /*flag=*/ 0);
354         doReturn(Arrays.asList(userInfo)).when(mUserManager).getProfiles(anyInt());
355         final ApplicationInfo applicationInfo1 = new ApplicationInfo();
356         applicationInfo1.enabled = true;
357         applicationInfo1.uid = 1;
358         applicationInfo1.packageName = packageName1;
359         final ApplicationInfo applicationInfo2 = new ApplicationInfo();
360         applicationInfo2.enabled = false;
361         applicationInfo2.uid = 2;
362         applicationInfo2.packageName = packageName2;
363         applicationInfo2.enabledSetting = PackageManager.COMPONENT_ENABLED_STATE_DISABLED_USER;
364         final ApplicationInfo applicationInfo3 = new ApplicationInfo();
365         applicationInfo3.enabled = false;
366         applicationInfo3.uid = 3;
367         applicationInfo3.packageName = packageName3;
368         applicationInfo3.enabledSetting = PackageManager.COMPONENT_ENABLED_STATE_DISABLED;
369         doReturn(new ParceledListSlice<ApplicationInfo>(
370                 Arrays.asList(applicationInfo1, applicationInfo2, applicationInfo3)))
371             .when(mIPackageManager)
372             .getInstalledApplications(anyInt(), anyInt());
373         // Sets the AppOpsManager for checkOpNoThrow() method.
374         doReturn(AppOpsManager.MODE_ALLOWED)
375                 .when(mAppOpsManager)
376                 .checkOpNoThrow(
377                         AppOpsManager.OP_RUN_ANY_IN_BACKGROUND,
378                         applicationInfo1.uid,
379                         applicationInfo1.packageName);
380         doReturn(AppOpsManager.MODE_IGNORED)
381                 .when(mAppOpsManager)
382                 .checkOpNoThrow(
383                         AppOpsManager.OP_RUN_ANY_IN_BACKGROUND,
384                         applicationInfo2.uid,
385                         applicationInfo2.packageName);
386     }
387 
388     @Implements(UserHandle.class)
389     public static class ShadowUserHandle {
390         // Sets the default as thte OWNER role.
391         private static int sUid = 0;
392 
setUid(int uid)393         public static void setUid(int uid) {
394             sUid = uid;
395         }
396 
397         @Implementation
myUserId()398         public static int myUserId() {
399             return sUid;
400         }
401 
402         @Resetter
reset()403         public static void reset() {
404             sUid = 0;
405         }
406     }
407 }
408