Merge "Add TARE state persistence."
This commit is contained in:
@@ -537,10 +537,11 @@ public class InternalResourceService extends SystemService {
|
||||
private void setupHeavyWork() {
|
||||
synchronized (mLock) {
|
||||
loadInstalledPackageListLocked();
|
||||
// TODO: base on if we have anything persisted
|
||||
final boolean isFirstSetup = true;
|
||||
final boolean isFirstSetup = !mScribe.recordExists();
|
||||
if (isFirstSetup) {
|
||||
mAgent.grantBirthrightsLocked();
|
||||
} else {
|
||||
mScribe.loadFromDiskLocked();
|
||||
}
|
||||
scheduleUnusedWealthReclamationLocked();
|
||||
}
|
||||
|
||||
@@ -61,6 +61,11 @@ class Ledger {
|
||||
Ledger() {
|
||||
}
|
||||
|
||||
Ledger(long currentBalance, @NonNull List<Transaction> transactions) {
|
||||
mCurrentBalance = currentBalance;
|
||||
mTransactions.addAll(transactions);
|
||||
}
|
||||
|
||||
long getCurrentBalance() {
|
||||
return mCurrentBalance;
|
||||
}
|
||||
@@ -73,6 +78,11 @@ class Ledger {
|
||||
return null;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
List<Transaction> getTransactions() {
|
||||
return mTransactions;
|
||||
}
|
||||
|
||||
void recordTransaction(@NonNull Transaction transaction) {
|
||||
mTransactions.add(transaction);
|
||||
mCurrentBalance += transaction.delta;
|
||||
|
||||
@@ -21,11 +21,34 @@ import static android.text.format.DateUtils.HOUR_IN_MILLIS;
|
||||
import static com.android.server.tare.TareUtils.appToString;
|
||||
|
||||
import android.annotation.NonNull;
|
||||
import android.annotation.Nullable;
|
||||
import android.hardware.biometrics.face.V1_0.UserHandle;
|
||||
import android.os.Environment;
|
||||
import android.util.AtomicFile;
|
||||
import android.util.IndentingPrintWriter;
|
||||
import android.util.Log;
|
||||
import android.util.Pair;
|
||||
import android.util.Slog;
|
||||
import android.util.SparseArrayMap;
|
||||
import android.util.TypedXmlPullParser;
|
||||
import android.util.TypedXmlSerializer;
|
||||
import android.util.Xml;
|
||||
|
||||
import com.android.internal.annotations.GuardedBy;
|
||||
import com.android.internal.annotations.VisibleForTesting;
|
||||
import com.android.server.LocalServices;
|
||||
import com.android.server.pm.UserManagerInternal;
|
||||
|
||||
import org.xmlpull.v1.XmlPullParser;
|
||||
import org.xmlpull.v1.XmlPullParserException;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Maintains the current TARE state and handles writing it to disk and reading it back from disk.
|
||||
@@ -44,40 +67,76 @@ public class Scribe {
|
||||
*/
|
||||
private static final long MAX_TRANSACTION_AGE_MS = 24 * HOUR_IN_MILLIS;
|
||||
|
||||
private static final String XML_TAG_HIGH_LEVEL_STATE = "irs-state";
|
||||
private static final String XML_TAG_LEDGER = "ledger";
|
||||
private static final String XML_TAG_TARE = "tare";
|
||||
private static final String XML_TAG_TRANSACTION = "transaction";
|
||||
private static final String XML_TAG_USER = "user";
|
||||
|
||||
private static final String XML_ATTR_DELTA = "delta";
|
||||
private static final String XML_ATTR_EVENT_ID = "eventId";
|
||||
private static final String XML_ATTR_TAG = "tag";
|
||||
private static final String XML_ATTR_START_TIME = "startTime";
|
||||
private static final String XML_ATTR_END_TIME = "endTime";
|
||||
private static final String XML_ATTR_PACKAGE_NAME = "pkgName";
|
||||
private static final String XML_ATTR_CURRENT_BALANCE = "currentBalance";
|
||||
private static final String XML_ATTR_USER_ID = "userId";
|
||||
private static final String XML_ATTR_VERSION = "version";
|
||||
private static final String XML_ATTR_LAST_RECLAMATION_TIME = "lastReclamationTime";
|
||||
|
||||
/** Version of the file schema. */
|
||||
private static final int STATE_FILE_VERSION = 0;
|
||||
/** Minimum amount of time between consecutive writes. */
|
||||
private static final long WRITE_DELAY = 30_000L;
|
||||
|
||||
private final AtomicFile mStateFile;
|
||||
private final InternalResourceService mIrs;
|
||||
|
||||
@GuardedBy("mIrs.mLock")
|
||||
@GuardedBy("mIrs.getLock()")
|
||||
private long mLastReclamationTime;
|
||||
@GuardedBy("mIrs.mLock")
|
||||
@GuardedBy("mIrs.getLock()")
|
||||
private long mNarcsInCirculation;
|
||||
@GuardedBy("mIrs.mLock")
|
||||
@GuardedBy("mIrs.getLock()")
|
||||
private final SparseArrayMap<String, Ledger> mLedgers = new SparseArrayMap<>();
|
||||
|
||||
private final Runnable mCleanRunnable = this::cleanupLedgers;
|
||||
private final Runnable mWriteRunnable = this::writeState;
|
||||
|
||||
Scribe(InternalResourceService irs) {
|
||||
mIrs = irs;
|
||||
this(irs, Environment.getDataSystemDirectory());
|
||||
}
|
||||
|
||||
@GuardedBy("mIrs.mLock")
|
||||
@VisibleForTesting
|
||||
Scribe(InternalResourceService irs, File dataDir) {
|
||||
mIrs = irs;
|
||||
|
||||
final File tareDir = new File(dataDir, "tare");
|
||||
//noinspection ResultOfMethodCallIgnored
|
||||
tareDir.mkdirs();
|
||||
mStateFile = new AtomicFile(new File(tareDir, "state.xml"), "tare");
|
||||
}
|
||||
|
||||
@GuardedBy("mIrs.getLock()")
|
||||
void adjustNarcsInCirculationLocked(long delta) {
|
||||
if (delta != 0) {
|
||||
// No point doing any work if the change is 0.
|
||||
mNarcsInCirculation += delta;
|
||||
postWrite();
|
||||
}
|
||||
}
|
||||
|
||||
@GuardedBy("mIrs.mLock")
|
||||
@GuardedBy("mIrs.getLock()")
|
||||
void discardLedgerLocked(final int userId, @NonNull final String pkgName) {
|
||||
mLedgers.delete(userId, pkgName);
|
||||
postWrite();
|
||||
}
|
||||
|
||||
@GuardedBy("mIrs.mLock")
|
||||
@GuardedBy("mIrs.getLock()")
|
||||
long getLastReclamationTimeLocked() {
|
||||
return mLastReclamationTime;
|
||||
}
|
||||
|
||||
@GuardedBy("mIrs.mLock")
|
||||
@GuardedBy("mIrs.getLock()")
|
||||
@NonNull
|
||||
Ledger getLedgerLocked(final int userId, @NonNull final String pkgName) {
|
||||
Ledger ledger = mLedgers.get(userId, pkgName);
|
||||
@@ -89,33 +148,107 @@ public class Scribe {
|
||||
}
|
||||
|
||||
/** Returns the total amount of narcs currently allocated to apps. */
|
||||
@GuardedBy("mIrs.mLock")
|
||||
@GuardedBy("mIrs.getLock()")
|
||||
long getNarcsInCirculationLocked() {
|
||||
return mNarcsInCirculation;
|
||||
}
|
||||
|
||||
@GuardedBy("mIrs.mLock")
|
||||
void setLastReclamationTimeLocked(long time) {
|
||||
mLastReclamationTime = time;
|
||||
@GuardedBy("mIrs.getLock()")
|
||||
void loadFromDiskLocked() {
|
||||
mLedgers.clear();
|
||||
mNarcsInCirculation = 0;
|
||||
if (!recordExists()) {
|
||||
return;
|
||||
}
|
||||
|
||||
UserManagerInternal userManagerInternal =
|
||||
LocalServices.getService(UserManagerInternal.class);
|
||||
final int[] userIds = userManagerInternal.getUserIds();
|
||||
Arrays.sort(userIds);
|
||||
|
||||
try (FileInputStream fis = mStateFile.openRead()) {
|
||||
TypedXmlPullParser parser = Xml.resolvePullParser(fis);
|
||||
|
||||
int eventType = parser.getEventType();
|
||||
while (eventType != XmlPullParser.START_TAG
|
||||
&& eventType != XmlPullParser.END_DOCUMENT) {
|
||||
eventType = parser.next();
|
||||
}
|
||||
if (eventType == XmlPullParser.END_DOCUMENT) {
|
||||
if (DEBUG) {
|
||||
Slog.w(TAG, "No persisted state.");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
String tagName = parser.getName();
|
||||
if (XML_TAG_TARE.equals(tagName)) {
|
||||
final int version = parser.getAttributeInt(null, XML_ATTR_VERSION);
|
||||
if (version < 0 || version > STATE_FILE_VERSION) {
|
||||
Slog.e(TAG, "Invalid version number (" + version + "), aborting file read");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
final long endTimeCutoff = System.currentTimeMillis() - MAX_TRANSACTION_AGE_MS;
|
||||
long earliestEndTime = Long.MAX_VALUE;
|
||||
for (eventType = parser.next(); eventType != XmlPullParser.END_DOCUMENT;
|
||||
eventType = parser.next()) {
|
||||
if (eventType != XmlPullParser.START_TAG) {
|
||||
continue;
|
||||
}
|
||||
tagName = parser.getName();
|
||||
if (tagName == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
switch (tagName) {
|
||||
case XML_TAG_HIGH_LEVEL_STATE:
|
||||
mLastReclamationTime =
|
||||
parser.getAttributeLong(null, XML_ATTR_LAST_RECLAMATION_TIME);
|
||||
break;
|
||||
case XML_TAG_USER:
|
||||
earliestEndTime = Math.min(earliestEndTime,
|
||||
readUserFromXmlLocked(parser, userIds, endTimeCutoff));
|
||||
break;
|
||||
default:
|
||||
Slog.e(TAG, "Unexpected tag: " + tagName);
|
||||
break;
|
||||
}
|
||||
}
|
||||
scheduleCleanup(earliestEndTime);
|
||||
} catch (IOException | XmlPullParserException e) {
|
||||
Slog.wtf(TAG, "Error reading state from disk", e);
|
||||
}
|
||||
}
|
||||
|
||||
@GuardedBy("mIrs.mLock")
|
||||
@VisibleForTesting
|
||||
void postWrite() {
|
||||
TareHandlerThread.getHandler().postDelayed(mWriteRunnable, WRITE_DELAY);
|
||||
}
|
||||
|
||||
boolean recordExists() {
|
||||
return mStateFile.exists();
|
||||
}
|
||||
|
||||
@GuardedBy("mIrs.getLock()")
|
||||
void setLastReclamationTimeLocked(long time) {
|
||||
mLastReclamationTime = time;
|
||||
postWrite();
|
||||
}
|
||||
|
||||
@GuardedBy("mIrs.getLock()")
|
||||
void tearDownLocked() {
|
||||
TareHandlerThread.getHandler().removeCallbacks(mCleanRunnable);
|
||||
TareHandlerThread.getHandler().removeCallbacks(mWriteRunnable);
|
||||
mLedgers.clear();
|
||||
mNarcsInCirculation = 0;
|
||||
mLastReclamationTime = 0;
|
||||
}
|
||||
|
||||
private void scheduleCleanup(long earliestEndTime) {
|
||||
if (earliestEndTime == Long.MAX_VALUE) {
|
||||
return;
|
||||
}
|
||||
// This is just cleanup to manage memory. We don't need to do it too often or at the exact
|
||||
// intended real time, so the delay that comes from using the Handler (and is limited
|
||||
// to uptime) should be fine.
|
||||
final long delayMs = Math.max(HOUR_IN_MILLIS,
|
||||
earliestEndTime + MAX_TRANSACTION_AGE_MS - System.currentTimeMillis());
|
||||
TareHandlerThread.getHandler().postDelayed(mCleanRunnable, delayMs);
|
||||
@VisibleForTesting
|
||||
void writeImmediatelyForTesting() {
|
||||
mWriteRunnable.run();
|
||||
}
|
||||
|
||||
private void cleanupLedgers() {
|
||||
@@ -139,7 +272,206 @@ public class Scribe {
|
||||
}
|
||||
}
|
||||
|
||||
@GuardedBy("mIrs.mLock")
|
||||
/**
|
||||
* @param parser Xml parser at the beginning of a "<ledger/>" tag. The next "parser.next()" call
|
||||
* will take the parser into the body of the ledger tag.
|
||||
* @return Newly instantiated ledger holding all the information we just read out of the xml
|
||||
* tag, and the package name associated with the ledger.
|
||||
*/
|
||||
@Nullable
|
||||
private static Pair<String, Ledger> readLedgerFromXml(TypedXmlPullParser parser,
|
||||
long endTimeCutoff) throws XmlPullParserException, IOException {
|
||||
final String pkgName;
|
||||
final long curBalance;
|
||||
final List<Ledger.Transaction> transactions = new ArrayList<>();
|
||||
|
||||
pkgName = parser.getAttributeValue(null, XML_ATTR_PACKAGE_NAME);
|
||||
curBalance = parser.getAttributeLong(null, XML_ATTR_CURRENT_BALANCE);
|
||||
|
||||
for (int eventType = parser.next(); eventType != XmlPullParser.END_DOCUMENT;
|
||||
eventType = parser.next()) {
|
||||
final String tagName = parser.getName();
|
||||
if (eventType == XmlPullParser.END_TAG) {
|
||||
if (XML_TAG_LEDGER.equals(tagName)) {
|
||||
// We've reached the end of the ledger tag.
|
||||
break;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (eventType != XmlPullParser.START_TAG || !"transaction".equals(tagName)) {
|
||||
// Expecting only "transaction" tags.
|
||||
Slog.e(TAG, "Unexpected event: (" + eventType + ") " + tagName);
|
||||
return null;
|
||||
}
|
||||
if (DEBUG) {
|
||||
Slog.d(TAG, "Starting ledger tag: " + tagName);
|
||||
}
|
||||
final String tag = parser.getAttributeValue(null, XML_ATTR_TAG);
|
||||
final long startTime = parser.getAttributeLong(null, XML_ATTR_START_TIME);
|
||||
final long endTime = parser.getAttributeLong(null, XML_ATTR_END_TIME);
|
||||
final int eventId = parser.getAttributeInt(null, XML_ATTR_EVENT_ID);
|
||||
final long delta = parser.getAttributeLong(null, XML_ATTR_DELTA);
|
||||
if (endTime <= endTimeCutoff) {
|
||||
if (DEBUG) {
|
||||
Slog.d(TAG, "Skipping event because it's too old.");
|
||||
}
|
||||
continue;
|
||||
}
|
||||
transactions.add(new Ledger.Transaction(startTime, endTime, eventId, tag, delta));
|
||||
}
|
||||
|
||||
return Pair.create(pkgName, new Ledger(curBalance, transactions));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param parser Xml parser at the beginning of a "<user>" tag. The next "parser.next()" call
|
||||
* will take the parser into the body of the user tag.
|
||||
* @return The earliest valid transaction end time found for the user.
|
||||
*/
|
||||
@GuardedBy("mIrs.getLock()")
|
||||
private long readUserFromXmlLocked(TypedXmlPullParser parser, int[] validUserIds,
|
||||
long endTimeCutoff) throws XmlPullParserException, IOException {
|
||||
int curUser = parser.getAttributeInt(null, XML_ATTR_USER_ID);
|
||||
if (Arrays.binarySearch(validUserIds, curUser) < 0) {
|
||||
Slog.w(TAG, "Invalid user " + curUser + " is saved to disk");
|
||||
curUser = UserHandle.NONE;
|
||||
// Don't return early since we need to go through all the ledger tags and get to the end
|
||||
// of the user tag.
|
||||
}
|
||||
long earliestEndTime = Long.MAX_VALUE;
|
||||
|
||||
for (int eventType = parser.next(); eventType != XmlPullParser.END_DOCUMENT;
|
||||
eventType = parser.next()) {
|
||||
final String tagName = parser.getName();
|
||||
if (eventType == XmlPullParser.END_TAG) {
|
||||
if (XML_TAG_USER.equals(tagName)) {
|
||||
// We've reached the end of the user tag.
|
||||
break;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (XML_TAG_LEDGER.equals(tagName)) {
|
||||
if (curUser == UserHandle.NONE) {
|
||||
continue;
|
||||
}
|
||||
final Pair<String, Ledger> ledgerData = readLedgerFromXml(parser, endTimeCutoff);
|
||||
final Ledger ledger = ledgerData.second;
|
||||
if (ledger != null) {
|
||||
mLedgers.add(curUser, ledgerData.first, ledger);
|
||||
mNarcsInCirculation += Math.max(0, ledger.getCurrentBalance());
|
||||
final Ledger.Transaction transaction = ledger.getEarliestTransaction();
|
||||
if (transaction != null) {
|
||||
earliestEndTime = Math.min(earliestEndTime, transaction.endTimeMs);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Slog.e(TAG, "Unknown tag: " + tagName);
|
||||
}
|
||||
}
|
||||
|
||||
return earliestEndTime;
|
||||
}
|
||||
|
||||
private void scheduleCleanup(long earliestEndTime) {
|
||||
if (earliestEndTime == Long.MAX_VALUE) {
|
||||
return;
|
||||
}
|
||||
// This is just cleanup to manage memory. We don't need to do it too often or at the exact
|
||||
// intended real time, so the delay that comes from using the Handler (and is limited
|
||||
// to uptime) should be fine.
|
||||
final long delayMs = Math.max(HOUR_IN_MILLIS,
|
||||
earliestEndTime + MAX_TRANSACTION_AGE_MS - System.currentTimeMillis());
|
||||
TareHandlerThread.getHandler().postDelayed(mCleanRunnable, delayMs);
|
||||
}
|
||||
|
||||
private void writeState() {
|
||||
synchronized (mIrs.getLock()) {
|
||||
TareHandlerThread.getHandler().removeCallbacks(mWriteRunnable);
|
||||
// Remove mCleanRunnable callbacks since we're going to clean up the ledgers before
|
||||
// writing anyway.
|
||||
TareHandlerThread.getHandler().removeCallbacks(mCleanRunnable);
|
||||
if (!mIrs.isEnabled()) {
|
||||
// If it's no longer enabled, we would have cleared all the data in memory and would
|
||||
// accidentally write an empty file, thus deleting all the history.
|
||||
return;
|
||||
}
|
||||
long earliestStoredEndTime = Long.MAX_VALUE;
|
||||
try (FileOutputStream fos = mStateFile.startWrite()) {
|
||||
TypedXmlSerializer out = Xml.resolveSerializer(fos);
|
||||
out.startDocument(null, true);
|
||||
|
||||
out.startTag(null, XML_TAG_TARE);
|
||||
out.attributeInt(null, XML_ATTR_VERSION, STATE_FILE_VERSION);
|
||||
|
||||
out.startTag(null, XML_TAG_HIGH_LEVEL_STATE);
|
||||
out.attributeLong(null, XML_ATTR_LAST_RECLAMATION_TIME, mLastReclamationTime);
|
||||
out.endTag(null, XML_TAG_HIGH_LEVEL_STATE);
|
||||
|
||||
for (int uIdx = mLedgers.numMaps() - 1; uIdx >= 0; --uIdx) {
|
||||
final int userId = mLedgers.keyAt(uIdx);
|
||||
earliestStoredEndTime = Math.min(earliestStoredEndTime,
|
||||
writeUserLocked(out, userId));
|
||||
}
|
||||
|
||||
out.endTag(null, XML_TAG_TARE);
|
||||
|
||||
out.endDocument();
|
||||
mStateFile.finishWrite(fos);
|
||||
} catch (IOException e) {
|
||||
Slog.e(TAG, "Error writing state to disk", e);
|
||||
}
|
||||
scheduleCleanup(earliestStoredEndTime);
|
||||
}
|
||||
}
|
||||
|
||||
@GuardedBy("mIrs.getLock()")
|
||||
private long writeUserLocked(@NonNull TypedXmlSerializer out, final int userId)
|
||||
throws IOException {
|
||||
final int uIdx = mLedgers.indexOfKey(userId);
|
||||
long earliestStoredEndTime = Long.MAX_VALUE;
|
||||
|
||||
out.startTag(null, XML_TAG_USER);
|
||||
out.attributeInt(null, XML_ATTR_USER_ID, userId);
|
||||
for (int pIdx = mLedgers.numElementsForKey(userId) - 1; pIdx >= 0; --pIdx) {
|
||||
final String pkgName = mLedgers.keyAt(uIdx, pIdx);
|
||||
final Ledger ledger = mLedgers.get(userId, pkgName);
|
||||
// Remove old transactions so we don't waste space storing them.
|
||||
ledger.removeOldTransactions(MAX_TRANSACTION_AGE_MS);
|
||||
|
||||
out.startTag(null, XML_TAG_LEDGER);
|
||||
out.attribute(null, XML_ATTR_PACKAGE_NAME, pkgName);
|
||||
out.attributeLong(null,
|
||||
XML_ATTR_CURRENT_BALANCE, ledger.getCurrentBalance());
|
||||
|
||||
final List<Ledger.Transaction> transactions = ledger.getTransactions();
|
||||
for (int t = 0; t < transactions.size(); ++t) {
|
||||
Ledger.Transaction transaction = transactions.get(t);
|
||||
if (t == 0) {
|
||||
earliestStoredEndTime = Math.min(earliestStoredEndTime, transaction.endTimeMs);
|
||||
}
|
||||
writeTransaction(out, transaction);
|
||||
}
|
||||
out.endTag(null, XML_TAG_LEDGER);
|
||||
}
|
||||
out.endTag(null, XML_TAG_USER);
|
||||
|
||||
return earliestStoredEndTime;
|
||||
}
|
||||
|
||||
private static void writeTransaction(@NonNull TypedXmlSerializer out,
|
||||
@NonNull Ledger.Transaction transaction) throws IOException {
|
||||
out.startTag(null, XML_TAG_TRANSACTION);
|
||||
out.attributeLong(null, XML_ATTR_START_TIME, transaction.startTimeMs);
|
||||
out.attributeLong(null, XML_ATTR_END_TIME, transaction.endTimeMs);
|
||||
out.attributeInt(null, XML_ATTR_EVENT_ID, transaction.eventId);
|
||||
if (transaction.tag != null) {
|
||||
out.attribute(null, XML_ATTR_TAG, transaction.tag);
|
||||
}
|
||||
out.attributeLong(null, XML_ATTR_DELTA, transaction.delta);
|
||||
out.endTag(null, XML_TAG_TRANSACTION);
|
||||
}
|
||||
|
||||
@GuardedBy("mIrs.getLock()")
|
||||
void dumpLocked(IndentingPrintWriter pw) {
|
||||
pw.println("Ledgers:");
|
||||
pw.increaseIndent();
|
||||
|
||||
@@ -57,6 +57,11 @@ public class AgentTest {
|
||||
MockScribe(InternalResourceService irs) {
|
||||
super(irs);
|
||||
}
|
||||
|
||||
@Override
|
||||
void postWrite() {
|
||||
// Do nothing
|
||||
}
|
||||
}
|
||||
|
||||
@Before
|
||||
|
||||
@@ -0,0 +1,222 @@
|
||||
/*
|
||||
* Copyright (C) 2021 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.server.tare;
|
||||
|
||||
|
||||
import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
|
||||
import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.Log;
|
||||
import android.util.SparseArrayMap;
|
||||
|
||||
import androidx.test.InstrumentationRegistry;
|
||||
import androidx.test.filters.SmallTest;
|
||||
import androidx.test.runner.AndroidJUnit4;
|
||||
|
||||
import com.android.server.LocalServices;
|
||||
import com.android.server.pm.UserManagerInternal;
|
||||
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.MockitoSession;
|
||||
import org.mockito.quality.Strictness;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Tests for various Scribe behavior, including reading and writing correctly from file.
|
||||
*
|
||||
* atest FrameworksServicesTests:ScribeTest
|
||||
*/
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
@SmallTest
|
||||
public class ScribeTest {
|
||||
private static final String TAG = "ScribeTest";
|
||||
|
||||
private static final int TEST_USER_ID = 27;
|
||||
private static final String TEST_PACKAGE = "com.android.test";
|
||||
|
||||
private MockitoSession mMockingSession;
|
||||
private Scribe mScribeUnderTest;
|
||||
private File mTestFileDir;
|
||||
|
||||
@Mock
|
||||
private InternalResourceService mIrs;
|
||||
@Mock
|
||||
private UserManagerInternal mUserManagerInternal;
|
||||
|
||||
private Context getContext() {
|
||||
return InstrumentationRegistry.getContext();
|
||||
}
|
||||
|
||||
@Before
|
||||
public void setUp() throws Exception {
|
||||
mMockingSession = mockitoSession()
|
||||
.initMocks(this)
|
||||
.strictness(Strictness.LENIENT)
|
||||
.mockStatic(LocalServices.class)
|
||||
.startMocking();
|
||||
doReturn(mUserManagerInternal)
|
||||
.when(() -> LocalServices.getService(UserManagerInternal.class));
|
||||
when(mIrs.getLock()).thenReturn(new Object());
|
||||
when(mIrs.isEnabled()).thenReturn(true);
|
||||
when(mUserManagerInternal.getUserIds()).thenReturn(new int[]{TEST_USER_ID});
|
||||
mTestFileDir = new File(getContext().getFilesDir(), "scribe_test");
|
||||
//noinspection ResultOfMethodCallIgnored
|
||||
mTestFileDir.mkdirs();
|
||||
Log.d(TAG, "Saving data to '" + mTestFileDir + "'");
|
||||
mScribeUnderTest = new Scribe(mIrs, mTestFileDir);
|
||||
}
|
||||
|
||||
@After
|
||||
public void tearDown() throws Exception {
|
||||
mScribeUnderTest.tearDownLocked();
|
||||
if (mTestFileDir.exists() && !mTestFileDir.delete()) {
|
||||
Log.w(TAG, "Failed to delete test file directory");
|
||||
}
|
||||
if (mMockingSession != null) {
|
||||
mMockingSession.finishMocking();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testWriteHighLevelStateToDisk() {
|
||||
long lastReclamationTime = System.currentTimeMillis();
|
||||
long narcsInCirculation = 2000L;
|
||||
|
||||
Ledger ledger = mScribeUnderTest.getLedgerLocked(TEST_USER_ID, TEST_PACKAGE);
|
||||
ledger.recordTransaction(new Ledger.Transaction(0, 1000L, 1, null, 2000));
|
||||
// Negative ledger balance shouldn't affect the total circulation value.
|
||||
ledger = mScribeUnderTest.getLedgerLocked(TEST_USER_ID + 1, TEST_PACKAGE);
|
||||
ledger.recordTransaction(new Ledger.Transaction(0, 1000L, 1, null, -5000));
|
||||
mScribeUnderTest.setLastReclamationTimeLocked(lastReclamationTime);
|
||||
mScribeUnderTest.writeImmediatelyForTesting();
|
||||
|
||||
mScribeUnderTest.loadFromDiskLocked();
|
||||
|
||||
assertEquals(lastReclamationTime, mScribeUnderTest.getLastReclamationTimeLocked());
|
||||
assertEquals(narcsInCirculation, mScribeUnderTest.getNarcsInCirculationLocked());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testWritingEmptyLedgerToDisk() {
|
||||
final Ledger ogLedger = mScribeUnderTest.getLedgerLocked(TEST_USER_ID, TEST_PACKAGE);
|
||||
mScribeUnderTest.writeImmediatelyForTesting();
|
||||
|
||||
mScribeUnderTest.loadFromDiskLocked();
|
||||
assertLedgersEqual(ogLedger, mScribeUnderTest.getLedgerLocked(TEST_USER_ID, TEST_PACKAGE));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testWritingPopulatedLedgerToDisk() {
|
||||
final Ledger ogLedger = mScribeUnderTest.getLedgerLocked(TEST_USER_ID, TEST_PACKAGE);
|
||||
ogLedger.recordTransaction(new Ledger.Transaction(0, 1000, 1, null, 51));
|
||||
ogLedger.recordTransaction(new Ledger.Transaction(1500, 2000, 2, "green", 52));
|
||||
ogLedger.recordTransaction(new Ledger.Transaction(2500, 3000, 3, "blue", 3));
|
||||
mScribeUnderTest.writeImmediatelyForTesting();
|
||||
|
||||
mScribeUnderTest.loadFromDiskLocked();
|
||||
assertLedgersEqual(ogLedger, mScribeUnderTest.getLedgerLocked(TEST_USER_ID, TEST_PACKAGE));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testWritingMultipleLedgersToDisk() {
|
||||
final SparseArrayMap<String, Ledger> ledgers = new SparseArrayMap<>();
|
||||
final int numUsers = 3;
|
||||
final int numLedgers = 5;
|
||||
final int[] userIds = new int[numUsers];
|
||||
when(mUserManagerInternal.getUserIds()).thenReturn(userIds);
|
||||
for (int u = 0; u < numUsers; ++u) {
|
||||
final int userId = TEST_USER_ID + u;
|
||||
userIds[u] = userId;
|
||||
for (int l = 0; l < numLedgers; ++l) {
|
||||
final String pkgName = TEST_PACKAGE + l;
|
||||
final Ledger ledger = mScribeUnderTest.getLedgerLocked(userId, pkgName);
|
||||
ledger.recordTransaction(new Ledger.Transaction(
|
||||
0, 1000L * u + l, 1, null, 51L * u + l));
|
||||
ledger.recordTransaction(new Ledger.Transaction(
|
||||
1500L * u + l, 2000L * u + l, 2 * u + l, "green" + u + l, 52L * u + l));
|
||||
ledger.recordTransaction(new Ledger.Transaction(
|
||||
2500L * u + l, 3000L * u + l, 3 * u + l, "blue" + u + l, 3L * u + l));
|
||||
ledgers.add(userId, pkgName, ledger);
|
||||
}
|
||||
}
|
||||
mScribeUnderTest.writeImmediatelyForTesting();
|
||||
|
||||
mScribeUnderTest.loadFromDiskLocked();
|
||||
ledgers.forEach((userId, pkgName, ledger)
|
||||
-> assertLedgersEqual(ledger, mScribeUnderTest.getLedgerLocked(userId, pkgName)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDiscardLedgerFromDisk() {
|
||||
final Ledger ogLedger = mScribeUnderTest.getLedgerLocked(TEST_USER_ID, TEST_PACKAGE);
|
||||
ogLedger.recordTransaction(new Ledger.Transaction(0, 1000, 1, null, 51));
|
||||
ogLedger.recordTransaction(new Ledger.Transaction(1500, 2000, 2, "green", 52));
|
||||
ogLedger.recordTransaction(new Ledger.Transaction(2500, 3000, 3, "blue", 3));
|
||||
mScribeUnderTest.writeImmediatelyForTesting();
|
||||
|
||||
mScribeUnderTest.loadFromDiskLocked();
|
||||
assertLedgersEqual(ogLedger, mScribeUnderTest.getLedgerLocked(TEST_USER_ID, TEST_PACKAGE));
|
||||
|
||||
mScribeUnderTest.discardLedgerLocked(TEST_USER_ID, TEST_PACKAGE);
|
||||
mScribeUnderTest.writeImmediatelyForTesting();
|
||||
|
||||
// Make sure there's no more saved ledger.
|
||||
mScribeUnderTest.loadFromDiskLocked();
|
||||
assertLedgersEqual(new Ledger(),
|
||||
mScribeUnderTest.getLedgerLocked(TEST_USER_ID, TEST_PACKAGE));
|
||||
}
|
||||
|
||||
private void assertLedgersEqual(Ledger expected, Ledger actual) {
|
||||
if (expected == null) {
|
||||
assertNull(actual);
|
||||
return;
|
||||
}
|
||||
assertNotNull(actual);
|
||||
assertEquals(expected.getCurrentBalance(), actual.getCurrentBalance());
|
||||
List<Ledger.Transaction> expectedTransactions = expected.getTransactions();
|
||||
List<Ledger.Transaction> actualTransactions = actual.getTransactions();
|
||||
assertEquals(expectedTransactions.size(), actualTransactions.size());
|
||||
for (int i = 0; i < expectedTransactions.size(); ++i) {
|
||||
assertTransactionsEqual(expectedTransactions.get(i), actualTransactions.get(i));
|
||||
}
|
||||
}
|
||||
|
||||
private void assertTransactionsEqual(Ledger.Transaction expected, Ledger.Transaction actual) {
|
||||
if (expected == null) {
|
||||
assertNull(actual);
|
||||
return;
|
||||
}
|
||||
assertNotNull(actual);
|
||||
assertEquals(expected.startTimeMs, actual.startTimeMs);
|
||||
assertEquals(expected.endTimeMs, actual.endTimeMs);
|
||||
assertEquals(expected.eventId, actual.eventId);
|
||||
assertEquals(expected.tag, actual.tag);
|
||||
assertEquals(expected.delta, actual.delta);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user