Loading apex/jobscheduler/service/java/com/android/server/tare/InternalResourceService.java +3 −2 Original line number Diff line number Diff line Loading @@ -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(); } Loading apex/jobscheduler/service/java/com/android/server/tare/Ledger.java +10 −0 Original line number Diff line number Diff line Loading @@ -61,6 +61,11 @@ class Ledger { Ledger() { } Ledger(long currentBalance, @NonNull List<Transaction> transactions) { mCurrentBalance = currentBalance; mTransactions.addAll(transactions); } long getCurrentBalance() { return mCurrentBalance; } Loading @@ -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; Loading apex/jobscheduler/service/java/com/android/server/tare/Scribe.java +353 −21 Original line number Diff line number Diff line Loading @@ -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. Loading @@ -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) { this(irs, Environment.getDataSystemDirectory()); } @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.mLock") @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); Loading @@ -89,23 +148,230 @@ 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") @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); } } @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.mLock") @GuardedBy("mIrs.getLock()") void tearDownLocked() { TareHandlerThread.getHandler().removeCallbacks(mCleanRunnable); TareHandlerThread.getHandler().removeCallbacks(mWriteRunnable); mLedgers.clear(); mNarcsInCirculation = 0; mLastReclamationTime = 0; } @VisibleForTesting void writeImmediatelyForTesting() { mWriteRunnable.run(); } private void cleanupLedgers() { synchronized (mIrs.getLock()) { TareHandlerThread.getHandler().removeCallbacks(mCleanRunnable); long earliestEndTime = Long.MAX_VALUE; for (int uIdx = mLedgers.numMaps() - 1; uIdx >= 0; --uIdx) { final int userId = mLedgers.keyAt(uIdx); for (int pIdx = mLedgers.numElementsForKey(userId) - 1; pIdx >= 0; --pIdx) { final String pkgName = mLedgers.keyAt(uIdx, pIdx); final Ledger ledger = mLedgers.get(userId, pkgName); ledger.removeOldTransactions(MAX_TRANSACTION_AGE_MS); Ledger.Transaction transaction = ledger.getEarliestTransaction(); if (transaction != null) { earliestEndTime = Math.min(earliestEndTime, transaction.endTimeMs); } } } scheduleCleanup(earliestEndTime); } } /** * @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; Loading @@ -118,28 +384,94 @@ public class Scribe { TareHandlerThread.getHandler().postDelayed(mCleanRunnable, delayMs); } private void cleanupLedgers() { 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); long earliestEndTime = Long.MAX_VALUE; 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); Ledger.Transaction transaction = ledger.getEarliestTransaction(); if (transaction != null) { earliestEndTime = Math.min(earliestEndTime, transaction.endTimeMs); 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); } scheduleCleanup(earliestEndTime); 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.mLock") @GuardedBy("mIrs.getLock()") void dumpLocked(IndentingPrintWriter pw) { pw.println("Ledgers:"); pw.increaseIndent(); Loading services/tests/mockingservicestests/src/com/android/server/tare/AgentTest.java +5 −0 Original line number Diff line number Diff line Loading @@ -57,6 +57,11 @@ public class AgentTest { MockScribe(InternalResourceService irs) { super(irs); } @Override void postWrite() { // Do nothing } } @Before Loading services/tests/mockingservicestests/src/com/android/server/tare/ScribeTest.java 0 → 100644 +222 −0 File added.Preview size limit exceeded, changes collapsed. Show changes Loading
apex/jobscheduler/service/java/com/android/server/tare/InternalResourceService.java +3 −2 Original line number Diff line number Diff line Loading @@ -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(); } Loading
apex/jobscheduler/service/java/com/android/server/tare/Ledger.java +10 −0 Original line number Diff line number Diff line Loading @@ -61,6 +61,11 @@ class Ledger { Ledger() { } Ledger(long currentBalance, @NonNull List<Transaction> transactions) { mCurrentBalance = currentBalance; mTransactions.addAll(transactions); } long getCurrentBalance() { return mCurrentBalance; } Loading @@ -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; Loading
apex/jobscheduler/service/java/com/android/server/tare/Scribe.java +353 −21 Original line number Diff line number Diff line Loading @@ -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. Loading @@ -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) { this(irs, Environment.getDataSystemDirectory()); } @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.mLock") @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); Loading @@ -89,23 +148,230 @@ 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") @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); } } @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.mLock") @GuardedBy("mIrs.getLock()") void tearDownLocked() { TareHandlerThread.getHandler().removeCallbacks(mCleanRunnable); TareHandlerThread.getHandler().removeCallbacks(mWriteRunnable); mLedgers.clear(); mNarcsInCirculation = 0; mLastReclamationTime = 0; } @VisibleForTesting void writeImmediatelyForTesting() { mWriteRunnable.run(); } private void cleanupLedgers() { synchronized (mIrs.getLock()) { TareHandlerThread.getHandler().removeCallbacks(mCleanRunnable); long earliestEndTime = Long.MAX_VALUE; for (int uIdx = mLedgers.numMaps() - 1; uIdx >= 0; --uIdx) { final int userId = mLedgers.keyAt(uIdx); for (int pIdx = mLedgers.numElementsForKey(userId) - 1; pIdx >= 0; --pIdx) { final String pkgName = mLedgers.keyAt(uIdx, pIdx); final Ledger ledger = mLedgers.get(userId, pkgName); ledger.removeOldTransactions(MAX_TRANSACTION_AGE_MS); Ledger.Transaction transaction = ledger.getEarliestTransaction(); if (transaction != null) { earliestEndTime = Math.min(earliestEndTime, transaction.endTimeMs); } } } scheduleCleanup(earliestEndTime); } } /** * @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; Loading @@ -118,28 +384,94 @@ public class Scribe { TareHandlerThread.getHandler().postDelayed(mCleanRunnable, delayMs); } private void cleanupLedgers() { 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); long earliestEndTime = Long.MAX_VALUE; 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); Ledger.Transaction transaction = ledger.getEarliestTransaction(); if (transaction != null) { earliestEndTime = Math.min(earliestEndTime, transaction.endTimeMs); 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); } scheduleCleanup(earliestEndTime); 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.mLock") @GuardedBy("mIrs.getLock()") void dumpLocked(IndentingPrintWriter pw) { pw.println("Ledgers:"); pw.increaseIndent(); Loading
services/tests/mockingservicestests/src/com/android/server/tare/AgentTest.java +5 −0 Original line number Diff line number Diff line Loading @@ -57,6 +57,11 @@ public class AgentTest { MockScribe(InternalResourceService irs) { super(irs); } @Override void postWrite() { // Do nothing } } @Before Loading
services/tests/mockingservicestests/src/com/android/server/tare/ScribeTest.java 0 → 100644 +222 −0 File added.Preview size limit exceeded, changes collapsed. Show changes