Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 8a747d7e authored by Kweku Adams's avatar Kweku Adams
Browse files

Add TARE state persistence.

Bug: 158300259
Test: atest FrameworksMockingServicesTests:ScribeTest
Test: Verify contents of file on disk
Test: Verify persisted state is loaded back via dumpsys
Change-Id: I0ad0bd2646814d630b7aaecedcee1a006aa92340
parent 638e4594
Loading
Loading
Loading
Loading
+3 −2
Original line number Diff line number Diff line
@@ -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();
        }
+10 −0
Original line number Diff line number Diff line
@@ -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;
+353 −21
Original line number Diff line number Diff line
@@ -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) {
        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);
@@ -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;
@@ -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();
+5 −0
Original line number Diff line number Diff line
@@ -57,6 +57,11 @@ public class AgentTest {
        MockScribe(InternalResourceService irs) {
            super(irs);
        }

        @Override
        void postWrite() {
            // Do nothing
        }
    }

    @Before
+222 −0

File added.

Preview size limit exceeded, changes collapsed.