1 /*
2  * Copyright (C) 2021 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.server.tare;
18 
19 import static com.android.dx.mockito.inline.extended.ExtendedMockito.inOrder;
20 import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession;
21 import static com.android.server.tare.TareTestUtils.assertLedgersEqual;
22 
23 import static org.junit.Assert.assertEquals;
24 import static org.junit.Assert.assertNotNull;
25 import static org.junit.Assert.assertNull;
26 import static org.mockito.Mockito.when;
27 
28 import android.app.tare.EconomyManager;
29 import android.content.Context;
30 import android.content.pm.ApplicationInfo;
31 import android.content.pm.PackageInfo;
32 import android.os.UserHandle;
33 import android.util.Log;
34 import android.util.SparseArrayMap;
35 
36 import androidx.test.InstrumentationRegistry;
37 import androidx.test.filters.SmallTest;
38 import androidx.test.runner.AndroidJUnit4;
39 
40 import com.android.server.LocalServices;
41 
42 import org.junit.After;
43 import org.junit.Before;
44 import org.junit.Test;
45 import org.junit.runner.RunWith;
46 import org.mockito.ArgumentCaptor;
47 import org.mockito.InOrder;
48 import org.mockito.Mock;
49 import org.mockito.MockitoSession;
50 import org.mockito.quality.Strictness;
51 
52 import java.io.File;
53 import java.util.ArrayList;
54 import java.util.List;
55 
56 /**
57  * Tests for various Scribe behavior, including reading and writing correctly from file.
58  *
59  * atest FrameworksServicesTests:ScribeTest
60  */
61 @RunWith(AndroidJUnit4.class)
62 @SmallTest
63 public class ScribeTest {
64     private static final String TAG = "ScribeTest";
65 
66     private static final int TEST_USER_ID = 27;
67     private static final String TEST_PACKAGE = "com.android.test";
68 
69     private MockitoSession mMockingSession;
70     private Scribe mScribeUnderTest;
71     private File mTestFileDir;
72     private final SparseArrayMap<String, InstalledPackageInfo> mInstalledPackages =
73             new SparseArrayMap<>();
74     private final List<Analyst.Report> mReports = new ArrayList<>();
75 
76     @Mock
77     private Analyst mAnalyst;
78     @Mock
79     private InternalResourceService mIrs;
80 
getContext()81     private Context getContext() {
82         return InstrumentationRegistry.getContext();
83     }
84 
85     @Before
setUp()86     public void setUp() throws Exception {
87         mMockingSession = mockitoSession()
88                 .initMocks(this)
89                 .strictness(Strictness.LENIENT)
90                 .mockStatic(LocalServices.class)
91                 .startMocking();
92         when(mIrs.getLock()).thenReturn(new Object());
93         when(mIrs.getEnabledMode()).thenReturn(EconomyManager.ENABLED_MODE_ON);
94         when(mIrs.getInstalledPackages()).thenReturn(mInstalledPackages);
95         when(mAnalyst.getReports()).thenReturn(mReports);
96         mTestFileDir = new File(getContext().getFilesDir(), "scribe_test");
97         //noinspection ResultOfMethodCallIgnored
98         mTestFileDir.mkdirs();
99         Log.d(TAG, "Saving data to '" + mTestFileDir + "'");
100         mScribeUnderTest = new Scribe(mIrs, mAnalyst, mTestFileDir);
101 
102         addInstalledPackage(TEST_USER_ID, TEST_PACKAGE);
103     }
104 
105     @After
tearDown()106     public void tearDown() throws Exception {
107         mScribeUnderTest.tearDownLocked();
108         if (mTestFileDir.exists() && !mTestFileDir.delete()) {
109             Log.w(TAG, "Failed to delete test file directory");
110         }
111         if (mMockingSession != null) {
112             mMockingSession.finishMocking();
113         }
114     }
115 
116     @Test
testWritingAnalystReportsToDisk()117     public void testWritingAnalystReportsToDisk() {
118         ArgumentCaptor<List<Analyst.Report>> reportCaptor =
119                 ArgumentCaptor.forClass(List.class);
120 
121         InOrder inOrder = inOrder(mAnalyst);
122 
123         // Empty set
124         mReports.clear();
125         mScribeUnderTest.writeImmediatelyForTesting();
126         mScribeUnderTest.loadFromDiskLocked();
127         inOrder.verify(mAnalyst).loadReports(reportCaptor.capture());
128         List<Analyst.Report> result = reportCaptor.getValue();
129         assertReportListsEqual(mReports, result);
130 
131         Analyst.Report report1 = new Analyst.Report();
132         report1.cumulativeBatteryDischarge = 1;
133         report1.currentBatteryLevel = 2;
134         report1.cumulativeProfit = 3;
135         report1.numProfitableActions = 4;
136         report1.cumulativeLoss = 5;
137         report1.numUnprofitableActions = 6;
138         report1.cumulativeRewards = 7;
139         report1.numRewards = 8;
140         report1.cumulativePositiveRegulations = 9;
141         report1.numPositiveRegulations = 10;
142         report1.cumulativeNegativeRegulations = 11;
143         report1.numNegativeRegulations = 12;
144         report1.screenOffDurationMs = 13;
145         report1.screenOffDischargeMah = 14;
146         mReports.add(report1);
147         mScribeUnderTest.writeImmediatelyForTesting();
148         mScribeUnderTest.loadFromDiskLocked();
149         inOrder.verify(mAnalyst).loadReports(reportCaptor.capture());
150         result = reportCaptor.getValue();
151         assertReportListsEqual(mReports, result);
152 
153         Analyst.Report report2 = new Analyst.Report();
154         report2.cumulativeBatteryDischarge = 10;
155         report2.currentBatteryLevel = 20;
156         report2.cumulativeProfit = 30;
157         report2.numProfitableActions = 40;
158         report2.cumulativeLoss = 50;
159         report2.numUnprofitableActions = 60;
160         report2.cumulativeRewards = 70;
161         report2.numRewards = 80;
162         report2.cumulativePositiveRegulations = 90;
163         report2.numPositiveRegulations = 100;
164         report2.cumulativeNegativeRegulations = 110;
165         report2.numNegativeRegulations = 120;
166         report2.screenOffDurationMs = 130;
167         report2.screenOffDischargeMah = 140;
168         mReports.add(report2);
169         mScribeUnderTest.writeImmediatelyForTesting();
170         mScribeUnderTest.loadFromDiskLocked();
171         inOrder.verify(mAnalyst).loadReports(reportCaptor.capture());
172         result = reportCaptor.getValue();
173         assertReportListsEqual(mReports, result);
174     }
175 
176     @Test
testWriteHighLevelStateToDisk()177     public void testWriteHighLevelStateToDisk() {
178         long lastReclamationTime = System.currentTimeMillis();
179         long remainingConsumableCakes = 2000L;
180         long consumptionLimit = 500_000L;
181         when(mIrs.getConsumptionLimitLocked()).thenReturn(consumptionLimit);
182 
183         Ledger ledger = mScribeUnderTest.getLedgerLocked(TEST_USER_ID, TEST_PACKAGE);
184         ledger.recordTransaction(
185                 new Ledger.Transaction(0, 1000L, EconomicPolicy.TYPE_REWARD | 1, null, 2000, 0));
186         // Negative ledger balance shouldn't affect the total circulation value.
187         ledger = mScribeUnderTest.getLedgerLocked(TEST_USER_ID + 1, TEST_PACKAGE);
188         ledger.recordTransaction(
189                 new Ledger.Transaction(0, 1000L,
190                         EconomicPolicy.TYPE_ACTION | 1, null, -5000, 3000));
191         mScribeUnderTest.setLastReclamationTimeLocked(lastReclamationTime);
192         mScribeUnderTest.setConsumptionLimitLocked(consumptionLimit);
193         mScribeUnderTest.adjustRemainingConsumableCakesLocked(
194                 remainingConsumableCakes - consumptionLimit);
195 
196         assertEquals(lastReclamationTime, mScribeUnderTest.getLastReclamationTimeLocked());
197         assertEquals(remainingConsumableCakes,
198                 mScribeUnderTest.getRemainingConsumableCakesLocked());
199         assertEquals(consumptionLimit, mScribeUnderTest.getSatiatedConsumptionLimitLocked());
200 
201         mScribeUnderTest.writeImmediatelyForTesting();
202         mScribeUnderTest.loadFromDiskLocked();
203 
204         assertEquals(lastReclamationTime, mScribeUnderTest.getLastReclamationTimeLocked());
205         assertEquals(remainingConsumableCakes,
206                 mScribeUnderTest.getRemainingConsumableCakesLocked());
207         assertEquals(consumptionLimit, mScribeUnderTest.getSatiatedConsumptionLimitLocked());
208     }
209 
210     @Test
testWritingEmptyLedgerToDisk()211     public void testWritingEmptyLedgerToDisk() {
212         final Ledger ogLedger = mScribeUnderTest.getLedgerLocked(TEST_USER_ID, TEST_PACKAGE);
213         mScribeUnderTest.writeImmediatelyForTesting();
214 
215         mScribeUnderTest.loadFromDiskLocked();
216         assertLedgersEqual(ogLedger, mScribeUnderTest.getLedgerLocked(TEST_USER_ID, TEST_PACKAGE));
217     }
218 
219     @Test
testWritingPopulatedLedgerToDisk()220     public void testWritingPopulatedLedgerToDisk() {
221         final Ledger ogLedger = mScribeUnderTest.getLedgerLocked(TEST_USER_ID, TEST_PACKAGE);
222         ogLedger.recordTransaction(
223                 new Ledger.Transaction(0, 1000, EconomicPolicy.TYPE_REWARD | 1, null, 51, 0));
224         ogLedger.recordTransaction(
225                 new Ledger.Transaction(1500, 2000,
226                         EconomicPolicy.TYPE_REWARD | 2, "green", 52, -1));
227         ogLedger.recordTransaction(
228                 new Ledger.Transaction(2500, 3000, EconomicPolicy.TYPE_REWARD | 3, "blue", 3, 12));
229         mScribeUnderTest.writeImmediatelyForTesting();
230 
231         mScribeUnderTest.loadFromDiskLocked();
232         assertLedgersEqual(ogLedger, mScribeUnderTest.getLedgerLocked(TEST_USER_ID, TEST_PACKAGE));
233     }
234 
235     @Test
testWritingMultipleLedgersToDisk()236     public void testWritingMultipleLedgersToDisk() {
237         final SparseArrayMap<String, Ledger> ledgers = new SparseArrayMap<>();
238         final int numUsers = 3;
239         final int numLedgers = 5;
240         for (int u = 0; u < numUsers; ++u) {
241             final int userId = TEST_USER_ID + u;
242             for (int l = 0; l < numLedgers; ++l) {
243                 final String pkgName = TEST_PACKAGE + l;
244                 addInstalledPackage(userId, pkgName);
245                 final Ledger ledger = mScribeUnderTest.getLedgerLocked(userId, pkgName);
246                 ledger.recordTransaction(new Ledger.Transaction(
247                         0, 1000L * u + l, EconomicPolicy.TYPE_ACTION | 1, null, -51L * u + l, 50));
248                 ledger.recordTransaction(new Ledger.Transaction(
249                         1500L * u + l, 2000L * u + l,
250                         EconomicPolicy.TYPE_REWARD | 2 * u + l, "green" + u + l, 52L * u + l, 0));
251                 ledger.recordTransaction(new Ledger.Transaction(
252                         2500L * u + l, 3000L * u + l,
253                         EconomicPolicy.TYPE_REWARD | 3 * u + l, "blue" + u + l, 3L * u + l, 0));
254                 ledgers.add(userId, pkgName, ledger);
255             }
256         }
257         mScribeUnderTest.writeImmediatelyForTesting();
258 
259         mScribeUnderTest.loadFromDiskLocked();
260         ledgers.forEach((userId, pkgName, ledger)
261                 -> assertLedgersEqual(ledger, mScribeUnderTest.getLedgerLocked(userId, pkgName)));
262     }
263 
264     @Test
testDiscardLedgerFromDisk()265     public void testDiscardLedgerFromDisk() {
266         final Ledger ogLedger = mScribeUnderTest.getLedgerLocked(TEST_USER_ID, TEST_PACKAGE);
267         ogLedger.recordTransaction(
268                 new Ledger.Transaction(0, 1000, EconomicPolicy.TYPE_REWARD | 1, null, 51, 1));
269         ogLedger.recordTransaction(
270                 new Ledger.Transaction(1500, 2000, EconomicPolicy.TYPE_REWARD | 2, "green", 52, 0));
271         ogLedger.recordTransaction(
272                 new Ledger.Transaction(2500, 3000, EconomicPolicy.TYPE_REWARD | 3, "blue", 3, 1));
273         mScribeUnderTest.writeImmediatelyForTesting();
274 
275         mScribeUnderTest.loadFromDiskLocked();
276         assertLedgersEqual(ogLedger, mScribeUnderTest.getLedgerLocked(TEST_USER_ID, TEST_PACKAGE));
277 
278         mScribeUnderTest.discardLedgerLocked(TEST_USER_ID, TEST_PACKAGE);
279         mScribeUnderTest.writeImmediatelyForTesting();
280 
281         // Make sure there's no more saved ledger.
282         mScribeUnderTest.loadFromDiskLocked();
283         assertLedgersEqual(new Ledger(),
284                 mScribeUnderTest.getLedgerLocked(TEST_USER_ID, TEST_PACKAGE));
285     }
286 
287     @Test
testLoadingMissingPackageFromDisk()288     public void testLoadingMissingPackageFromDisk() {
289         final String pkgName = TEST_PACKAGE + ".uninstalled";
290         final Ledger ogLedger = mScribeUnderTest.getLedgerLocked(TEST_USER_ID, pkgName);
291         ogLedger.recordTransaction(
292                 new Ledger.Transaction(0, 1000, EconomicPolicy.TYPE_REGULATION | 1, null, 51, 1));
293         ogLedger.recordTransaction(
294                 new Ledger.Transaction(1500, 2000, EconomicPolicy.TYPE_REWARD | 2, "green", 52, 2));
295         ogLedger.recordTransaction(
296                 new Ledger.Transaction(2500, 3000, EconomicPolicy.TYPE_ACTION | 3, "blue", -3, 3));
297         mScribeUnderTest.writeImmediatelyForTesting();
298 
299         // Package isn't installed, so make sure it's not saved to memory after loading.
300         mScribeUnderTest.loadFromDiskLocked();
301         assertLedgersEqual(new Ledger(), mScribeUnderTest.getLedgerLocked(TEST_USER_ID, pkgName));
302     }
303 
304     @Test
testLoadingMissingUserFromDisk()305     public void testLoadingMissingUserFromDisk() {
306         final int userId = TEST_USER_ID + 1;
307         final Ledger ogLedger = mScribeUnderTest.getLedgerLocked(userId, TEST_PACKAGE);
308         ogLedger.recordTransaction(
309                 new Ledger.Transaction(0, 1000, EconomicPolicy.TYPE_REWARD | 1, null, 51, 0));
310         ogLedger.recordTransaction(
311                 new Ledger.Transaction(1500, 2000, EconomicPolicy.TYPE_REWARD | 2, "green", 52, 1));
312         ogLedger.recordTransaction(
313                 new Ledger.Transaction(2500, 3000,
314                         EconomicPolicy.TYPE_REGULATION | 3, "blue", 3, 3));
315         mScribeUnderTest.writeImmediatelyForTesting();
316 
317         // User doesn't show up with any packages, so make sure nothing is saved after loading.
318         mScribeUnderTest.loadFromDiskLocked();
319         assertLedgersEqual(new Ledger(), mScribeUnderTest.getLedgerLocked(userId, TEST_PACKAGE));
320     }
321 
322     @Test
testChangingConsumable()323     public void testChangingConsumable() {
324         assertEquals(0, mScribeUnderTest.getSatiatedConsumptionLimitLocked());
325         assertEquals(0, mScribeUnderTest.getRemainingConsumableCakesLocked());
326 
327         // Limit increased, so remaining value should be adjusted as well
328         mScribeUnderTest.setConsumptionLimitLocked(1000);
329         assertEquals(1000, mScribeUnderTest.getSatiatedConsumptionLimitLocked());
330         assertEquals(1000, mScribeUnderTest.getRemainingConsumableCakesLocked());
331 
332         // Limit decreased below remaining, so remaining value should be adjusted as well
333         mScribeUnderTest.setConsumptionLimitLocked(500);
334         assertEquals(500, mScribeUnderTest.getSatiatedConsumptionLimitLocked());
335         assertEquals(500, mScribeUnderTest.getRemainingConsumableCakesLocked());
336 
337         mScribeUnderTest.adjustRemainingConsumableCakesLocked(-100);
338         assertEquals(500, mScribeUnderTest.getSatiatedConsumptionLimitLocked());
339         assertEquals(400, mScribeUnderTest.getRemainingConsumableCakesLocked());
340 
341         // Limit increased, so remaining value should be adjusted by the difference as well
342         mScribeUnderTest.setConsumptionLimitLocked(1000);
343         assertEquals(1000, mScribeUnderTest.getSatiatedConsumptionLimitLocked());
344         assertEquals(900, mScribeUnderTest.getRemainingConsumableCakesLocked());
345 
346 
347         // Limit decreased, but above remaining, so remaining value should left alone
348         mScribeUnderTest.setConsumptionLimitLocked(950);
349         assertEquals(950, mScribeUnderTest.getSatiatedConsumptionLimitLocked());
350         assertEquals(900, mScribeUnderTest.getRemainingConsumableCakesLocked());
351     }
352 
assertReportListsEqual(List<Analyst.Report> expected, List<Analyst.Report> actual)353     private void assertReportListsEqual(List<Analyst.Report> expected,
354             List<Analyst.Report> actual) {
355         if (expected == null) {
356             assertNull(actual);
357             return;
358         }
359         assertNotNull(actual);
360         assertEquals(expected.size(), actual.size());
361         for (int i = 0; i < expected.size(); ++i) {
362             Analyst.Report eReport = expected.get(i);
363             Analyst.Report aReport = actual.get(i);
364             if (eReport == null) {
365                 assertNull(aReport);
366                 continue;
367             }
368             assertNotNull(aReport);
369             assertEquals("Reports #" + i + " cumulativeBatteryDischarge are not equal",
370                     eReport.cumulativeBatteryDischarge, aReport.cumulativeBatteryDischarge);
371             assertEquals("Reports #" + i + " currentBatteryLevel are not equal",
372                     eReport.currentBatteryLevel, aReport.currentBatteryLevel);
373             assertEquals("Reports #" + i + " cumulativeProfit are not equal",
374                     eReport.cumulativeProfit, aReport.cumulativeProfit);
375             assertEquals("Reports #" + i + " numProfitableActions are not equal",
376                     eReport.numProfitableActions, aReport.numProfitableActions);
377             assertEquals("Reports #" + i + " cumulativeLoss are not equal",
378                     eReport.cumulativeLoss, aReport.cumulativeLoss);
379             assertEquals("Reports #" + i + " numUnprofitableActions are not equal",
380                     eReport.numUnprofitableActions, aReport.numUnprofitableActions);
381             assertEquals("Reports #" + i + " cumulativeRewards are not equal",
382                     eReport.cumulativeRewards, aReport.cumulativeRewards);
383             assertEquals("Reports #" + i + " numRewards are not equal",
384                     eReport.numRewards, aReport.numRewards);
385             assertEquals("Reports #" + i + " cumulativePositiveRegulations are not equal",
386                     eReport.cumulativePositiveRegulations, aReport.cumulativePositiveRegulations);
387             assertEquals("Reports #" + i + " numPositiveRegulations are not equal",
388                     eReport.numPositiveRegulations, aReport.numPositiveRegulations);
389             assertEquals("Reports #" + i + " cumulativeNegativeRegulations are not equal",
390                     eReport.cumulativeNegativeRegulations, aReport.cumulativeNegativeRegulations);
391             assertEquals("Reports #" + i + " numNegativeRegulations are not equal",
392                     eReport.numNegativeRegulations, aReport.numNegativeRegulations);
393             assertEquals("Reports #" + i + " screenOffDurationMs are not equal",
394                     eReport.screenOffDurationMs, aReport.screenOffDurationMs);
395             assertEquals("Reports #" + i + " screenOffDischargeMah are not equal",
396                     eReport.screenOffDischargeMah, aReport.screenOffDischargeMah);
397         }
398     }
399 
addInstalledPackage(int userId, String pkgName)400     private void addInstalledPackage(int userId, String pkgName) {
401         PackageInfo pkgInfo = new PackageInfo();
402         pkgInfo.packageName = pkgName;
403         ApplicationInfo applicationInfo = new ApplicationInfo();
404         applicationInfo.uid = UserHandle.getUid(userId, Math.abs(pkgName.hashCode()));
405         pkgInfo.applicationInfo = applicationInfo;
406         mInstalledPackages.add(userId, pkgName, new InstalledPackageInfo(getContext(), userId,
407                 pkgInfo));
408     }
409 }
410