Merge "Android.mk to Android.bp for frameworks/base/"
diff --git a/apex/jobscheduler/service/java/com/android/server/tare/EconomicPolicy.java b/apex/jobscheduler/service/java/com/android/server/tare/EconomicPolicy.java
index d401373..aeb6abc 100644
--- a/apex/jobscheduler/service/java/com/android/server/tare/EconomicPolicy.java
+++ b/apex/jobscheduler/service/java/com/android/server/tare/EconomicPolicy.java
@@ -306,6 +306,10 @@
         return eventId & MASK_TYPE;
     }
 
+    static boolean isReward(int eventId) {
+        return getEventType(eventId) == TYPE_REWARD;
+    }
+
     @NonNull
     static String eventToString(int eventId) {
         switch (eventId & MASK_TYPE) {
diff --git a/apex/jobscheduler/service/java/com/android/server/tare/Ledger.java b/apex/jobscheduler/service/java/com/android/server/tare/Ledger.java
index 2e2a9b5..3b5e444 100644
--- a/apex/jobscheduler/service/java/com/android/server/tare/Ledger.java
+++ b/apex/jobscheduler/service/java/com/android/server/tare/Ledger.java
@@ -22,10 +22,14 @@
 import static com.android.server.tare.TareUtils.dumpTime;
 import static com.android.server.tare.TareUtils.getCurrentTimeMillis;
 
+import android.annotation.CurrentTimeMillisLong;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.util.IndentingPrintWriter;
 import android.util.SparseLongArray;
+import android.util.TimeUtils;
+
+import com.android.internal.annotations.VisibleForTesting;
 
 import java.util.ArrayList;
 import java.util.List;
@@ -34,6 +38,21 @@
  * Ledger to track the last recorded balance and recent activities of an app.
  */
 class Ledger {
+    /** The window size within which rewards will be counted and used towards reward limiting. */
+    private static final long TOTAL_REWARD_WINDOW_MS = 24 * HOUR_IN_MILLIS;
+    /** The number of buckets to split {@link #TOTAL_REWARD_WINDOW_MS} into. */
+    @VisibleForTesting
+    static final int NUM_REWARD_BUCKET_WINDOWS = 4;
+    /**
+     * The duration size of each bucket resulting from splitting {@link #TOTAL_REWARD_WINDOW_MS}
+     * into smaller buckets.
+     */
+    private static final long REWARD_BUCKET_WINDOW_SIZE_MS =
+            TOTAL_REWARD_WINDOW_MS / NUM_REWARD_BUCKET_WINDOWS;
+    /** The maximum number of transactions to retain in memory at any one time. */
+    @VisibleForTesting
+    static final int MAX_TRANSACTION_COUNT = 50;
+
     static class Transaction {
         public final long startTimeMs;
         public final long endTimeMs;
@@ -54,18 +73,47 @@
         }
     }
 
+    static class RewardBucket {
+        @CurrentTimeMillisLong
+        public long startTimeMs;
+        public final SparseLongArray cumulativeDelta = new SparseLongArray();
+
+        private void reset() {
+            startTimeMs = 0;
+            cumulativeDelta.clear();
+        }
+    }
+
     /** Last saved balance. This doesn't take currently ongoing events into account. */
     private long mCurrentBalance = 0;
-    private final List<Transaction> mTransactions = new ArrayList<>();
-    private final SparseLongArray mCumulativeDeltaPerReason = new SparseLongArray();
-    private long mEarliestSumTime;
+    private final Transaction[] mTransactions = new Transaction[MAX_TRANSACTION_COUNT];
+    /** Index within {@link #mTransactions} where the next transaction should be placed. */
+    private int mTransactionIndex = 0;
+    private final RewardBucket[] mRewardBuckets = new RewardBucket[NUM_REWARD_BUCKET_WINDOWS];
+    /** Index within {@link #mRewardBuckets} of the current active bucket. */
+    private int mRewardBucketIndex = 0;
 
     Ledger() {
     }
 
-    Ledger(long currentBalance, @NonNull List<Transaction> transactions) {
+    Ledger(long currentBalance, @NonNull List<Transaction> transactions,
+            @NonNull List<RewardBucket> rewardBuckets) {
         mCurrentBalance = currentBalance;
-        mTransactions.addAll(transactions);
+
+        final int numTxs = transactions.size();
+        for (int i = Math.max(0, numTxs - MAX_TRANSACTION_COUNT); i < numTxs; ++i) {
+            mTransactions[mTransactionIndex++] = transactions.get(i);
+        }
+        mTransactionIndex %= MAX_TRANSACTION_COUNT;
+
+        final int numBuckets = rewardBuckets.size();
+        if (numBuckets > 0) {
+            // Set the index to -1 so that we put the first bucket in index 0.
+            mRewardBucketIndex = -1;
+            for (int i = Math.max(0, numBuckets - NUM_REWARD_BUCKET_WINDOWS); i < numBuckets; ++i) {
+                mRewardBuckets[++mRewardBucketIndex] = rewardBuckets.get(i);
+            }
+        }
     }
 
     long getCurrentBalance() {
@@ -74,66 +122,142 @@
 
     @Nullable
     Transaction getEarliestTransaction() {
-        if (mTransactions.size() > 0) {
-            return mTransactions.get(0);
+        for (int t = 0; t < mTransactions.length; ++t) {
+            final Transaction transaction =
+                    mTransactions[(mTransactionIndex + t) % mTransactions.length];
+            if (transaction != null) {
+                return transaction;
+            }
         }
         return null;
     }
 
     @NonNull
+    List<RewardBucket> getRewardBuckets() {
+        final long cutoffMs = getCurrentTimeMillis() - TOTAL_REWARD_WINDOW_MS;
+        final List<RewardBucket> list = new ArrayList<>(NUM_REWARD_BUCKET_WINDOWS);
+        for (int i = 1; i <= NUM_REWARD_BUCKET_WINDOWS; ++i) {
+            final int idx = (mRewardBucketIndex + i) % NUM_REWARD_BUCKET_WINDOWS;
+            final RewardBucket rewardBucket = mRewardBuckets[idx];
+            if (rewardBucket != null) {
+                if (cutoffMs <= rewardBucket.startTimeMs) {
+                    list.add(rewardBucket);
+                } else {
+                    rewardBucket.reset();
+                }
+            }
+        }
+        return list;
+    }
+
+    @NonNull
     List<Transaction> getTransactions() {
-        return mTransactions;
+        final List<Transaction> list = new ArrayList<>(MAX_TRANSACTION_COUNT);
+        for (int i = 0; i < MAX_TRANSACTION_COUNT; ++i) {
+            final int idx = (mTransactionIndex + i) % MAX_TRANSACTION_COUNT;
+            final Transaction transaction = mTransactions[idx];
+            if (transaction != null) {
+                list.add(transaction);
+            }
+        }
+        return list;
     }
 
     void recordTransaction(@NonNull Transaction transaction) {
-        mTransactions.add(transaction);
+        mTransactions[mTransactionIndex] = transaction;
         mCurrentBalance += transaction.delta;
+        mTransactionIndex = (mTransactionIndex + 1) % MAX_TRANSACTION_COUNT;
 
-        final long sum = mCumulativeDeltaPerReason.get(transaction.eventId);
-        mCumulativeDeltaPerReason.put(transaction.eventId, sum + transaction.delta);
-        mEarliestSumTime = Math.min(mEarliestSumTime, transaction.startTimeMs);
+        if (EconomicPolicy.isReward(transaction.eventId)) {
+            final RewardBucket bucket = getCurrentRewardBucket();
+            bucket.cumulativeDelta.put(transaction.eventId,
+                    bucket.cumulativeDelta.get(transaction.eventId, 0) + transaction.delta);
+        }
+    }
+
+    @NonNull
+    private RewardBucket getCurrentRewardBucket() {
+        RewardBucket bucket = mRewardBuckets[mRewardBucketIndex];
+        final long now = getCurrentTimeMillis();
+        if (bucket == null) {
+            bucket = new RewardBucket();
+            bucket.startTimeMs = now;
+            mRewardBuckets[mRewardBucketIndex] = bucket;
+            return bucket;
+        }
+
+        if (now - bucket.startTimeMs < REWARD_BUCKET_WINDOW_SIZE_MS) {
+            return bucket;
+        }
+
+        mRewardBucketIndex = (mRewardBucketIndex + 1) % NUM_REWARD_BUCKET_WINDOWS;
+        bucket = mRewardBuckets[mRewardBucketIndex];
+        if (bucket == null) {
+            bucket = new RewardBucket();
+            mRewardBuckets[mRewardBucketIndex] = bucket;
+        }
+        bucket.reset();
+        // Using now as the start time means there will be some gaps between sequential buckets,
+        // but makes processing of large gaps between events easier.
+        bucket.startTimeMs = now;
+        return bucket;
     }
 
     long get24HourSum(int eventId, final long now) {
         final long windowStartTime = now - 24 * HOUR_IN_MILLIS;
-        if (mEarliestSumTime < windowStartTime) {
-            // Need to redo sums
-            mCumulativeDeltaPerReason.clear();
-            for (int i = mTransactions.size() - 1; i >= 0; --i) {
-                final Transaction transaction = mTransactions.get(i);
-                if (transaction.endTimeMs <= windowStartTime) {
-                    break;
-                }
-                long sum = mCumulativeDeltaPerReason.get(transaction.eventId);
-                if (transaction.startTimeMs >= windowStartTime) {
-                    sum += transaction.delta;
-                } else {
-                    // Pro-rate durationed deltas. Intentionally floor the result.
-                    sum += (long) (1.0 * (transaction.endTimeMs - windowStartTime)
-                            * transaction.delta)
-                            / (transaction.endTimeMs - transaction.startTimeMs);
-                }
-                mCumulativeDeltaPerReason.put(transaction.eventId, sum);
+        long sum = 0;
+        for (int i = 0; i < mRewardBuckets.length; ++i) {
+            final RewardBucket bucket = mRewardBuckets[i];
+            if (bucket != null
+                    && bucket.startTimeMs >= windowStartTime && bucket.startTimeMs < now) {
+                sum += bucket.cumulativeDelta.get(eventId, 0);
             }
-            mEarliestSumTime = windowStartTime;
         }
-        return mCumulativeDeltaPerReason.get(eventId);
+        return sum;
     }
 
-    /** Deletes transactions that are older than {@code minAgeMs}. */
-    void removeOldTransactions(long minAgeMs) {
+    /**
+     * Deletes transactions that are older than {@code minAgeMs}.
+     * @return The earliest transaction in the ledger, or {@code null} if there are no more
+     * transactions.
+     */
+    @Nullable
+    Transaction removeOldTransactions(long minAgeMs) {
         final long cutoff = getCurrentTimeMillis() - minAgeMs;
-        while (mTransactions.size() > 0 && mTransactions.get(0).endTimeMs <= cutoff) {
-            mTransactions.remove(0);
+        for (int t = 0; t < mTransactions.length; ++t) {
+            final int idx = (mTransactionIndex + t) % mTransactions.length;
+            final Transaction transaction = mTransactions[idx];
+            if (transaction == null) {
+                continue;
+            }
+            if (transaction.endTimeMs <= cutoff) {
+                mTransactions[idx] = null;
+            } else {
+                // Everything we look at after this transaction will also be within the window,
+                // so no need to go further.
+                return transaction;
+            }
         }
+        return null;
     }
 
     void dump(IndentingPrintWriter pw, int numRecentTransactions) {
         pw.print("Current balance", cakeToString(getCurrentBalance())).println();
+        pw.println();
 
-        final int size = mTransactions.size();
-        for (int i = Math.max(0, size - numRecentTransactions); i < size; ++i) {
-            final Transaction transaction = mTransactions.get(i);
+        boolean printedTransactionTitle = false;
+        for (int t = 0; t < Math.min(MAX_TRANSACTION_COUNT, numRecentTransactions); ++t) {
+            final int idx = (mTransactionIndex - t + MAX_TRANSACTION_COUNT) % MAX_TRANSACTION_COUNT;
+            final Transaction transaction = mTransactions[idx];
+            if (transaction == null) {
+                continue;
+            }
+
+            if (!printedTransactionTitle) {
+                pw.println("Transactions:");
+                pw.increaseIndent();
+                printedTransactionTitle = true;
+            }
 
             dumpTime(pw, transaction.startTimeMs);
             pw.print("--");
@@ -151,5 +275,42 @@
             pw.print(cakeToString(transaction.ctp));
             pw.println(")");
         }
+        if (printedTransactionTitle) {
+            pw.decreaseIndent();
+            pw.println();
+        }
+
+        final long now = getCurrentTimeMillis();
+        boolean printedBucketTitle = false;
+        for (int b = 0; b < NUM_REWARD_BUCKET_WINDOWS; ++b) {
+            final int idx = (mRewardBucketIndex - b + NUM_REWARD_BUCKET_WINDOWS)
+                    % NUM_REWARD_BUCKET_WINDOWS;
+            final RewardBucket rewardBucket = mRewardBuckets[idx];
+            if (rewardBucket == null) {
+                continue;
+            }
+
+            if (!printedBucketTitle) {
+                pw.println("Reward buckets:");
+                pw.increaseIndent();
+                printedBucketTitle = true;
+            }
+
+            dumpTime(pw, rewardBucket.startTimeMs);
+            pw.print(" (");
+            TimeUtils.formatDuration(now - rewardBucket.startTimeMs, pw);
+            pw.println(" ago):");
+            pw.increaseIndent();
+            for (int r = 0; r < rewardBucket.cumulativeDelta.size(); ++r) {
+                pw.print(EconomicPolicy.eventToString(rewardBucket.cumulativeDelta.keyAt(r)));
+                pw.print(": ");
+                pw.println(cakeToString(rewardBucket.cumulativeDelta.valueAt(r)));
+            }
+            pw.decreaseIndent();
+        }
+        if (printedBucketTitle) {
+            pw.decreaseIndent();
+            pw.println();
+        }
     }
 }
diff --git a/apex/jobscheduler/service/java/com/android/server/tare/Scribe.java b/apex/jobscheduler/service/java/com/android/server/tare/Scribe.java
index 941cc39..8f7657e 100644
--- a/apex/jobscheduler/service/java/com/android/server/tare/Scribe.java
+++ b/apex/jobscheduler/service/java/com/android/server/tare/Scribe.java
@@ -62,15 +62,14 @@
     private static final int MAX_NUM_TRANSACTION_DUMP = 25;
     /**
      * The maximum amount of time we'll keep a transaction around for.
-     * For now, only keep transactions we actually have a use for. We can increase it if we want
-     * to use older transactions or provide older transactions to apps.
      */
-    private static final long MAX_TRANSACTION_AGE_MS = 24 * HOUR_IN_MILLIS;
+    private static final long MAX_TRANSACTION_AGE_MS = 8 * 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_REWARD_BUCKET = "rewardBucket";
     private static final String XML_TAG_USER = "user";
     private static final String XML_TAG_PERIOD_REPORT = "report";
 
@@ -346,8 +345,8 @@
                 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();
+                    final Ledger.Transaction transaction =
+                            ledger.removeOldTransactions(MAX_TRANSACTION_AGE_MS);
                     if (transaction != null) {
                         earliestEndTime = Math.min(earliestEndTime, transaction.endTimeMs);
                     }
@@ -370,6 +369,7 @@
         final String pkgName;
         final long curBalance;
         final List<Ledger.Transaction> transactions = new ArrayList<>();
+        final List<Ledger.RewardBucket> rewardBuckets = new ArrayList<>();
 
         pkgName = parser.getAttributeValue(null, XML_ATTR_PACKAGE_NAME);
         curBalance = parser.getAttributeLong(null, XML_ATTR_CURRENT_BALANCE);
@@ -391,8 +391,7 @@
                 }
                 continue;
             }
-            if (eventType != XmlPullParser.START_TAG || !XML_TAG_TRANSACTION.equals(tagName)) {
-                // Expecting only "transaction" tags.
+            if (eventType != XmlPullParser.START_TAG || tagName == null) {
                 Slog.e(TAG, "Unexpected event: (" + eventType + ") " + tagName);
                 return null;
             }
@@ -402,25 +401,37 @@
             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);
-            final long ctp = parser.getAttributeLong(null, XML_ATTR_CTP);
-            if (endTime <= endTimeCutoff) {
-                if (DEBUG) {
-                    Slog.d(TAG, "Skipping event because it's too old.");
-                }
-                continue;
+            switch (tagName) {
+                case XML_TAG_TRANSACTION:
+                    final long endTime = parser.getAttributeLong(null, XML_ATTR_END_TIME);
+                    if (endTime <= endTimeCutoff) {
+                        if (DEBUG) {
+                            Slog.d(TAG, "Skipping event because it's too old.");
+                        }
+                        continue;
+                    }
+                    final String tag = parser.getAttributeValue(null, XML_ATTR_TAG);
+                    final long startTime = parser.getAttributeLong(null, XML_ATTR_START_TIME);
+                    final int eventId = parser.getAttributeInt(null, XML_ATTR_EVENT_ID);
+                    final long delta = parser.getAttributeLong(null, XML_ATTR_DELTA);
+                    final long ctp = parser.getAttributeLong(null, XML_ATTR_CTP);
+                    transactions.add(
+                            new Ledger.Transaction(startTime, endTime, eventId, tag, delta, ctp));
+                    break;
+                case XML_TAG_REWARD_BUCKET:
+                    rewardBuckets.add(readRewardBucketFromXml(parser));
+                    break;
+                default:
+                    // Expecting only "transaction" and "rewardBucket" tags.
+                    Slog.e(TAG, "Unexpected event: (" + eventType + ") " + tagName);
+                    return null;
             }
-            transactions.add(new Ledger.Transaction(startTime, endTime, eventId, tag, delta, ctp));
         }
 
         if (!isInstalled) {
             return null;
         }
-        return Pair.create(pkgName, new Ledger(curBalance, transactions));
+        return Pair.create(pkgName, new Ledger(curBalance, transactions, rewardBuckets));
     }
 
     /**
@@ -508,6 +519,44 @@
         return report;
     }
 
+    /**
+     * @param parser Xml parser at the beginning of a {@value #XML_TAG_REWARD_BUCKET} tag. The next
+     *               "parser.next()" call will take the parser into the body of the tag.
+     * @return Newly instantiated {@link Ledger.RewardBucket} holding all the information we just
+     * read out of the xml tag.
+     */
+    @Nullable
+    private static Ledger.RewardBucket readRewardBucketFromXml(TypedXmlPullParser parser)
+            throws XmlPullParserException, IOException {
+
+        final Ledger.RewardBucket rewardBucket = new Ledger.RewardBucket();
+
+        rewardBucket.startTimeMs = parser.getAttributeLong(null, XML_ATTR_START_TIME);
+
+        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_REWARD_BUCKET.equals(tagName)) {
+                    // We've reached the end of the rewardBucket tag.
+                    break;
+                }
+                continue;
+            }
+            if (eventType != XmlPullParser.START_TAG || !XML_ATTR_DELTA.equals(tagName)) {
+                // Expecting only delta tags.
+                Slog.e(TAG, "Unexpected event: (" + eventType + ") " + tagName);
+                return null;
+            }
+
+            final int eventId = parser.getAttributeInt(null, XML_ATTR_EVENT_ID);
+            final long delta = parser.getAttributeLong(null, XML_ATTR_DELTA);
+            rewardBucket.cumulativeDelta.put(eventId, delta);
+        }
+
+        return rewardBucket;
+    }
+
     private void scheduleCleanup(long earliestEndTime) {
         if (earliestEndTime == Long.MAX_VALUE) {
             return;
@@ -595,6 +644,11 @@
                 }
                 writeTransaction(out, transaction);
             }
+
+            final List<Ledger.RewardBucket> rewardBuckets = ledger.getRewardBuckets();
+            for (int r = 0; r < rewardBuckets.size(); ++r) {
+                writeRewardBucket(out, rewardBuckets.get(r));
+            }
             out.endTag(null, XML_TAG_LEDGER);
         }
         out.endTag(null, XML_TAG_USER);
@@ -616,6 +670,23 @@
         out.endTag(null, XML_TAG_TRANSACTION);
     }
 
+    private static void writeRewardBucket(@NonNull TypedXmlSerializer out,
+            @NonNull Ledger.RewardBucket rewardBucket) throws IOException {
+        final int numEvents = rewardBucket.cumulativeDelta.size();
+        if (numEvents == 0) {
+            return;
+        }
+        out.startTag(null, XML_TAG_REWARD_BUCKET);
+        out.attributeLong(null, XML_ATTR_START_TIME, rewardBucket.startTimeMs);
+        for (int i = 0; i < numEvents; ++i) {
+            out.startTag(null, XML_ATTR_DELTA);
+            out.attributeInt(null, XML_ATTR_EVENT_ID, rewardBucket.cumulativeDelta.keyAt(i));
+            out.attributeLong(null, XML_ATTR_DELTA, rewardBucket.cumulativeDelta.valueAt(i));
+            out.endTag(null, XML_ATTR_DELTA);
+        }
+        out.endTag(null, XML_TAG_REWARD_BUCKET);
+    }
+
     private static void writeReport(@NonNull TypedXmlSerializer out,
             @NonNull Analyst.Report report) throws IOException {
         out.startTag(null, XML_TAG_PERIOD_REPORT);
diff --git a/core/java/android/hardware/camera2/CameraCharacteristics.java b/core/java/android/hardware/camera2/CameraCharacteristics.java
index 8db298f..861a850 100644
--- a/core/java/android/hardware/camera2/CameraCharacteristics.java
+++ b/core/java/android/hardware/camera2/CameraCharacteristics.java
@@ -1325,8 +1325,11 @@
      * flashlight brightness level via
      * {@link android.hardware.camera2.CameraManager#turnOnTorchWithStrengthLevel }.
      * If this value is equal to 1, flashlight brightness control is not supported.
-     * The value for this key will be null for devices with no flash unit.
-     * This level must be set to a safe value to prevent any burn out issues.</p>
+     * The value for this key will be null for devices with no flash unit.</p>
+     * <p>The maximum value is guaranteed to be safe to use for an indefinite duration in
+     * terms of device flashlight lifespan, but may be too bright for comfort for many
+     * use cases. Use the default torch brightness value to avoid problems with an
+     * over-bright flashlight.</p>
      * <p><b>Optional</b> - The value for this key may be {@code null} on some devices.</p>
      */
     @PublicKey
diff --git a/keystore/java/android/security/keystore/KeyProtection.java b/keystore/java/android/security/keystore/KeyProtection.java
index c14c3c5..5ab21bc 100644
--- a/keystore/java/android/security/keystore/KeyProtection.java
+++ b/keystore/java/android/security/keystore/KeyProtection.java
@@ -237,6 +237,7 @@
     private final boolean mUnlockedDeviceRequired;
     private final boolean mIsStrongBoxBacked;
     private final int mMaxUsageCount;
+    private final boolean mRollbackResistant;
 
     private KeyProtection(
             Date keyValidityStart,
@@ -259,7 +260,8 @@
             boolean userConfirmationRequired,
             boolean unlockedDeviceRequired,
             boolean isStrongBoxBacked,
-            int maxUsageCount) {
+            int maxUsageCount,
+            boolean rollbackResistant) {
         mKeyValidityStart = Utils.cloneIfNotNull(keyValidityStart);
         mKeyValidityForOriginationEnd = Utils.cloneIfNotNull(keyValidityForOriginationEnd);
         mKeyValidityForConsumptionEnd = Utils.cloneIfNotNull(keyValidityForConsumptionEnd);
@@ -283,6 +285,7 @@
         mUnlockedDeviceRequired = unlockedDeviceRequired;
         mIsStrongBoxBacked = isStrongBoxBacked;
         mMaxUsageCount = maxUsageCount;
+        mRollbackResistant = rollbackResistant;
     }
 
     /**
@@ -563,6 +566,17 @@
     }
 
     /**
+     * Returns {@code true} if the key is rollback-resistant, meaning that when deleted it is
+     * guaranteed to be permanently deleted and unusable.
+     *
+     * @see Builder#setRollbackResistant(boolean)
+     * @hide
+     */
+    public boolean isRollbackResistant() {
+        return mRollbackResistant;
+    }
+
+    /**
      * Builder of {@link KeyProtection} instances.
      */
     public final static class Builder {
@@ -591,6 +605,7 @@
         private boolean mIsStrongBoxBacked = false;
         private int mMaxUsageCount = KeyProperties.UNRESTRICTED_USAGE_COUNT;
         private String mAttestKeyAlias = null;
+        private boolean mRollbackResistant = false;
 
         /**
          * Creates a new instance of the {@code Builder}.
@@ -1097,6 +1112,21 @@
         }
 
         /**
+         * Sets whether the key should be rollback-resistant, meaning that when deleted it is
+         * guaranteed to be permanently deleted and unusable.  Not all implementations support
+         * rollback-resistant keys.  This method is hidden because implementations only support a
+         * limited number of rollback-resistant keys; currently the available space is reserved for
+         * critical system keys.
+         *
+         * @hide
+         */
+        @NonNull
+        public Builder setRollbackResistant(boolean rollbackResistant) {
+            mRollbackResistant = rollbackResistant;
+            return this;
+        }
+
+        /**
          * Builds an instance of {@link KeyProtection}.
          *
          * @throws IllegalArgumentException if a required field is missing
@@ -1124,7 +1154,8 @@
                     mUserConfirmationRequired,
                     mUnlockedDeviceRequired,
                     mIsStrongBoxBacked,
-                    mMaxUsageCount);
+                    mMaxUsageCount,
+                    mRollbackResistant);
         }
     }
 }
diff --git a/keystore/java/android/security/keystore2/AndroidKeyStoreSpi.java b/keystore/java/android/security/keystore2/AndroidKeyStoreSpi.java
index 33411e1..dfda356 100644
--- a/keystore/java/android/security/keystore2/AndroidKeyStoreSpi.java
+++ b/keystore/java/android/security/keystore2/AndroidKeyStoreSpi.java
@@ -783,6 +783,12 @@
                         params.getMaxUsageCount()
                 ));
             }
+
+            if (params.isRollbackResistant()) {
+                importArgs.add(KeyStore2ParameterUtils.makeBool(
+                        KeymasterDefs.KM_TAG_ROLLBACK_RESISTANT
+                ));
+            }
         } catch (IllegalArgumentException | IllegalStateException e) {
             throw new KeyStoreException(e);
         }
diff --git a/packages/SettingsLib/src/com/android/settingslib/wifi/WifiStatusTracker.java b/packages/SettingsLib/src/com/android/settingslib/wifi/WifiStatusTracker.java
index 4332bd2..45c0d78 100644
--- a/packages/SettingsLib/src/com/android/settingslib/wifi/WifiStatusTracker.java
+++ b/packages/SettingsLib/src/com/android/settingslib/wifi/WifiStatusTracker.java
@@ -57,6 +57,7 @@
     private final Handler mHandler;
     private final Handler mMainThreadHandler;
     private final Set<Integer> mNetworks = new HashSet<>();
+    private int mPrimaryNetworkId;
     // Save the previous HISTORY_SIZE states for logging.
     private final String[] mHistory = new String[HISTORY_SIZE];
     // Where to copy the next state into.
@@ -106,6 +107,7 @@
             if (!mNetworks.contains(network.getNetId())) {
                 mNetworks.add(network.getNetId());
             }
+            mPrimaryNetworkId = network.getNetId();
             updateWifiInfo(wifiInfo);
             updateStatusLabel();
             mMainThreadHandler.post(() -> postResults());
@@ -121,10 +123,13 @@
             recordLastWifiNetwork(log);
             if (mNetworks.contains(network.getNetId())) {
                 mNetworks.remove(network.getNetId());
-                updateWifiInfo(null);
-                updateStatusLabel();
-                mMainThreadHandler.post(() -> postResults());
             }
+            if (network.getNetId() != mPrimaryNetworkId) {
+                return;
+            }
+            updateWifiInfo(null);
+            updateStatusLabel();
+            mMainThreadHandler.post(() -> postResults());
         }
     };
     private final NetworkCallback mDefaultNetworkCallback =
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/wifi/WifiStatusTrackerTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/wifi/WifiStatusTrackerTest.java
new file mode 100644
index 0000000..dc7e313d
--- /dev/null
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/wifi/WifiStatusTrackerTest.java
@@ -0,0 +1,125 @@
+/*
+ * Copyright (C) 2022 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.settingslib.wifi;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.content.Context;
+import android.net.ConnectivityManager;
+import android.net.Network;
+import android.net.NetworkCapabilities;
+import android.net.NetworkScoreManager;
+import android.net.wifi.WifiInfo;
+import android.net.wifi.WifiManager;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.MockitoAnnotations;
+import org.robolectric.RobolectricTestRunner;
+
+@RunWith(RobolectricTestRunner.class)
+public class WifiStatusTrackerTest {
+    @Mock Context mContext;
+    @Mock WifiManager mWifiManager;
+    @Mock NetworkScoreManager mNetworkScoreManager;
+    @Mock ConnectivityManager mConnectivityManager;
+    @Mock Runnable mCallback;
+
+    private final ArgumentCaptor<ConnectivityManager.NetworkCallback>
+            mNetworkCallbackCaptor =
+            ArgumentCaptor.forClass(ConnectivityManager.NetworkCallback.class);
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+    }
+
+    /**
+     * Verify that we only clear the WifiInfo if the primary network was lost.
+     */
+    @Test
+    public void testWifiInfoClearedOnPrimaryNetworkLost() {
+        WifiStatusTracker wifiStatusTracker = new WifiStatusTracker(mContext, mWifiManager,
+                mNetworkScoreManager, mConnectivityManager, mCallback);
+        wifiStatusTracker.setListening(true);
+
+        verify(mConnectivityManager)
+                .registerNetworkCallback(any(), mNetworkCallbackCaptor.capture(), any());
+
+        // Trigger a validation callback for the primary Wifi network.
+        WifiInfo primaryWifiInfo = Mockito.mock(WifiInfo.class);
+        when(primaryWifiInfo.makeCopy(anyLong())).thenReturn(primaryWifiInfo);
+        when(primaryWifiInfo.isPrimary()).thenReturn(true);
+        int primaryRssi = -55;
+        when(primaryWifiInfo.getRssi()).thenReturn(primaryRssi);
+        NetworkCapabilities primaryCap = new NetworkCapabilities.Builder()
+                .addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
+                .setTransportInfo(primaryWifiInfo)
+                .addCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)
+                .build();
+        Network primaryNetwork = Mockito.mock(Network.class);
+        int primaryNetworkId = 1;
+        when(primaryNetwork.getNetId()).thenReturn(primaryNetworkId);
+        mNetworkCallbackCaptor.getValue().onCapabilitiesChanged(primaryNetwork, primaryCap);
+
+        // Verify primary wifi info is the one being used.
+        assertThat(wifiStatusTracker.connected).isTrue();
+        assertThat(wifiStatusTracker.rssi).isEqualTo(primaryRssi);
+
+        // Trigger a validation callback for the non-primary Wifi network.
+        WifiInfo nonPrimaryWifiInfo = Mockito.mock(WifiInfo.class);
+        when(nonPrimaryWifiInfo.makeCopy(anyLong())).thenReturn(nonPrimaryWifiInfo);
+        when(nonPrimaryWifiInfo.isPrimary()).thenReturn(false);
+        int nonPrimaryRssi = -75;
+        when(primaryWifiInfo.getRssi()).thenReturn(nonPrimaryRssi);
+        NetworkCapabilities nonPrimaryCap = new NetworkCapabilities.Builder()
+                .addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
+                .setTransportInfo(nonPrimaryWifiInfo)
+                .addCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)
+                .build();
+        Network nonPrimaryNetwork = Mockito.mock(Network.class);
+        int nonPrimaryNetworkId = 2;
+        when(nonPrimaryNetwork.getNetId()).thenReturn(nonPrimaryNetworkId);
+        mNetworkCallbackCaptor.getValue().onCapabilitiesChanged(nonPrimaryNetwork, nonPrimaryCap);
+
+        // Verify primary wifi info is still the one being used.
+        assertThat(wifiStatusTracker.connected).isTrue();
+        assertThat(wifiStatusTracker.rssi).isEqualTo(primaryRssi);
+
+        // Lose the non-primary network.
+        mNetworkCallbackCaptor.getValue().onLost(nonPrimaryNetwork);
+
+        // Verify primary wifi info is still the one being used.
+        assertThat(wifiStatusTracker.connected).isTrue();
+        assertThat(wifiStatusTracker.rssi).isEqualTo(primaryRssi);
+
+        // Lose the primary network.
+        mNetworkCallbackCaptor.getValue().onLost(primaryNetwork);
+
+        // Verify we aren't connected anymore.
+        assertThat(wifiStatusTracker.connected).isFalse();
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherController.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherController.java
index 2e66648..dfcdaef 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherController.java
@@ -62,7 +62,6 @@
 import com.android.internal.util.LatencyTracker;
 import com.android.settingslib.RestrictedLockUtilsInternal;
 import com.android.settingslib.users.UserCreatingDialog;
-import com.android.settingslib.utils.ThreadUtils;
 import com.android.systemui.Dumpable;
 import com.android.systemui.GuestResetOrExitSessionReceiver;
 import com.android.systemui.GuestResumeSessionReceiver;
@@ -167,6 +166,7 @@
     private final AtomicBoolean mGuestIsResetting;
     private final AtomicBoolean mGuestCreationScheduled;
     private FalsingManager mFalsingManager;
+    @Nullable
     private View mView;
     private String mCreateSupervisedUserPackage;
     private GlobalSettings mGlobalSettings;
@@ -572,9 +572,11 @@
 
     protected void switchToUserId(int id) {
         try {
-            mInteractionJankMonitor.begin(InteractionJankMonitor.Configuration.Builder
-                    .withView(InteractionJankMonitor.CUJ_USER_SWITCH, mView)
-                    .setTimeout(MULTI_USER_JOURNEY_TIMEOUT));
+            if (mView != null) {
+                mInteractionJankMonitor.begin(InteractionJankMonitor.Configuration.Builder
+                        .withView(InteractionJankMonitor.CUJ_USER_SWITCH, mView)
+                        .setTimeout(MULTI_USER_JOURNEY_TIMEOUT));
+            }
             mLatencyTracker.onActionStart(LatencyTracker.ACTION_USER_SWITCH);
             pauseRefreshUsers();
             mActivityManager.switchUser(id);
@@ -936,15 +938,17 @@
         guestCreationProgressDialog.show();
 
         // userManager.createGuest will block the thread so post is needed for the dialog to show
-        ThreadUtils.postOnMainThread(() -> {
+        mBgExecutor.execute(() -> {
             final int guestId = createGuest();
-            guestCreationProgressDialog.dismiss();
-            if (guestId == UserHandle.USER_NULL) {
-                Toast.makeText(mContext,
-                        com.android.settingslib.R.string.add_guest_failed,
-                        Toast.LENGTH_SHORT).show();
-            }
-            callback.accept(guestId);
+            mUiExecutor.execute(() -> {
+                guestCreationProgressDialog.dismiss();
+                if (guestId == UserHandle.USER_NULL) {
+                    Toast.makeText(mContext,
+                            com.android.settingslib.R.string.add_guest_failed,
+                            Toast.LENGTH_SHORT).show();
+                }
+                callback.accept(guestId);
+            });
         });
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/user/UserCreator.java b/packages/SystemUI/src/com/android/systemui/user/UserCreator.java
deleted file mode 100644
index 0686071c..0000000
--- a/packages/SystemUI/src/com/android/systemui/user/UserCreator.java
+++ /dev/null
@@ -1,85 +0,0 @@
-/*
- * Copyright (C) 2020 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.systemui.user;
-
-import android.app.Dialog;
-import android.content.Context;
-import android.content.pm.UserInfo;
-import android.content.res.Resources;
-import android.graphics.drawable.Drawable;
-import android.os.UserManager;
-
-import com.android.internal.util.UserIcons;
-import com.android.settingslib.users.UserCreatingDialog;
-import com.android.settingslib.utils.ThreadUtils;
-
-import java.util.function.Consumer;
-
-import javax.inject.Inject;
-
-/**
- * A class to do the user creation process. It shows a progress dialog, and manages the user
- * creation
- */
-public class UserCreator {
-
-    private final Context mContext;
-    private final UserManager mUserManager;
-
-    @Inject
-    public UserCreator(Context context, UserManager userManager) {
-        mContext = context;
-        mUserManager = userManager;
-    }
-
-    /**
-     * Shows a progress dialog then starts the user creation process on the main thread.
-     *
-     * @param successCallback is called when the user creation is successful.
-     * @param errorCallback   is called when userManager.createUser returns null.
-     *                        (Exceptions are not handled by this class)
-     */
-    public void createUser(String userName, Drawable userIcon, Consumer<UserInfo> successCallback,
-            Runnable errorCallback) {
-
-        Dialog userCreationProgressDialog = new UserCreatingDialog(mContext);
-        userCreationProgressDialog.show();
-
-        // userManager.createUser will block the thread so post is needed for the dialog to show
-        ThreadUtils.postOnMainThread(() -> {
-            UserInfo user =
-                    mUserManager.createUser(userName, UserManager.USER_TYPE_FULL_SECONDARY, 0);
-            if (user == null) {
-                // Couldn't create user for some reason
-                userCreationProgressDialog.dismiss();
-                errorCallback.run();
-                return;
-            }
-
-            Drawable newUserIcon = userIcon;
-            Resources res = mContext.getResources();
-            if (newUserIcon == null) {
-                newUserIcon = UserIcons.getDefaultUserIcon(res, user.id, false);
-            }
-            mUserManager.setUserIcon(
-                    user.id, UserIcons.convertToBitmapAtUserIconSize(res, newUserIcon));
-
-            userCreationProgressDialog.dismiss();
-            successCallback.accept(user);
-        });
-    }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/user/UserCreator.kt b/packages/SystemUI/src/com/android/systemui/user/UserCreator.kt
new file mode 100644
index 0000000..dcbbe74
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/user/UserCreator.kt
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2020 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.systemui.user
+
+import android.app.Dialog
+import android.content.Context
+import android.content.pm.UserInfo
+import android.graphics.drawable.Drawable
+import android.os.UserManager
+import com.android.internal.util.UserIcons
+import com.android.settingslib.users.UserCreatingDialog
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.dagger.qualifiers.Main
+import java.util.concurrent.Executor
+import java.util.function.Consumer
+import javax.inject.Inject
+
+/**
+ * A class to do the user creation process. It shows a progress dialog, and manages the user
+ * creation
+ */
+class UserCreator @Inject constructor(
+    private val context: Context,
+    private val userManager: UserManager,
+    @Main private val mainExecutor: Executor,
+    @Background private val bgExecutor: Executor
+) {
+    /**
+     * Shows a progress dialog then starts the user creation process on the main thread.
+     *
+     * @param successCallback is called when the user creation is successful.
+     * @param errorCallback is called when userManager.createUser returns null.
+     * (Exceptions are not handled by this class)
+     */
+    fun createUser(
+        userName: String?,
+        userIcon: Drawable?,
+        successCallback: Consumer<UserInfo?>,
+       errorCallback: Runnable
+    ) {
+        val userCreationProgressDialog: Dialog = UserCreatingDialog(context)
+        userCreationProgressDialog.show()
+
+        // userManager.createUser will block the thread so post is needed for the dialog to show
+        bgExecutor.execute {
+            val user = userManager.createUser(userName, UserManager.USER_TYPE_FULL_SECONDARY, 0)
+            mainExecutor.execute main@{
+                if (user == null) {
+                    // Couldn't create user for some reason
+                    userCreationProgressDialog.dismiss()
+                    errorCallback.run()
+                    return@main
+                }
+                bgExecutor.execute {
+                    var newUserIcon = userIcon
+                    val res = context.resources
+                    if (newUserIcon == null) {
+                        newUserIcon = UserIcons.getDefaultUserIcon(res, user.id, false)
+                    }
+                    userManager.setUserIcon(
+                        user.id, UserIcons.convertToBitmapAtUserIconSize(res, newUserIcon))
+                }
+                userCreationProgressDialog.dismiss()
+                successCallback.accept(user)
+            }
+        }
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/UserSwitcherControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/UserSwitcherControllerTest.kt
index 09d7c03..359a780 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/UserSwitcherControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/UserSwitcherControllerTest.kt
@@ -77,6 +77,7 @@
 import org.mockito.Mockito.doReturn
 import org.mockito.Mockito.eq
 import org.mockito.Mockito.mock
+import org.mockito.Mockito.never
 import org.mockito.Mockito.verify
 import org.mockito.Mockito.`when`
 import org.mockito.MockitoAnnotations
@@ -269,6 +270,8 @@
         `when`(userManager.createGuest(any())).thenReturn(guestInfo)
 
         userSwitcherController.onUserListItemClicked(emptyGuestUserRecord, null)
+        bgExecutor.runAllReady()
+        uiExecutor.runAllReady()
         testableLooper.processAllMessages()
         verify(interactionJankMonitor).begin(any())
         verify(latencyTracker).onActionStart(LatencyTracker.ACTION_USER_SWITCH)
@@ -294,6 +297,8 @@
         `when`(userManager.createGuest(any())).thenReturn(guestInfo)
 
         userSwitcherController.onUserListItemClicked(emptyGuestUserRecord, dialogShower)
+        bgExecutor.runAllReady()
+        uiExecutor.runAllReady()
         testableLooper.processAllMessages()
         verify(dialogShower).dismiss()
     }
@@ -584,4 +589,24 @@
         broadcastReceiverCaptor.value.onReceive(context, intent)
         verify(cb).onUserSwitched()
     }
+
+    @Test
+    fun onUserItemClicked_guest_runsOnBgThread() {
+        val dialogShower = mock(UserSwitchDialogController.DialogShower::class.java)
+        val guestUserRecord = UserSwitcherController.UserRecord(
+            null,
+            picture,
+            true /* guest */,
+            false /* current */,
+            false /* isAddUser */,
+            false /* isRestricted */,
+            true /* isSwitchToEnabled */,
+            false /* isAddSupervisedUser */)
+
+        userSwitcherController.onUserListItemClicked(guestUserRecord, dialogShower)
+        assertTrue(bgExecutor.numPending() > 0)
+        verify(userManager, never()).createGuest(context)
+        bgExecutor.runAllReady()
+        verify(userManager).createGuest(context)
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/user/UserCreatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/user/UserCreatorTest.kt
new file mode 100644
index 0000000..a85ae7df
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/user/UserCreatorTest.kt
@@ -0,0 +1,73 @@
+package com.android.systemui.user
+
+import android.content.pm.UserInfo
+import android.graphics.Bitmap
+import android.os.UserManager
+import android.test.suitebuilder.annotation.SmallTest
+import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.util.concurrency.FakeExecutor
+import com.android.systemui.util.mockito.any
+import com.android.systemui.util.time.FakeSystemClock
+import java.util.function.Consumer
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.Mock
+import org.mockito.Mockito
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+@TestableLooper.RunWithLooper(setAsMainLooper = true)
+class UserCreatorTest : SysuiTestCase() {
+    companion object {
+        const val USER_NAME = "abc"
+    }
+
+    @Mock
+    private lateinit var userCreator: UserCreator
+    @Mock
+    private lateinit var userManager: UserManager
+    private lateinit var mainExecutor: FakeExecutor
+    private lateinit var bgExecutor: FakeExecutor
+    private lateinit var user: UserInfo
+
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+        mainExecutor = FakeExecutor(FakeSystemClock())
+        bgExecutor = FakeExecutor(FakeSystemClock())
+        userCreator = UserCreator(context, userManager, mainExecutor, bgExecutor)
+        user = Mockito.mock(UserInfo::class.java)
+        Mockito.`when`(userManager.createUser(USER_NAME, UserManager.USER_TYPE_FULL_SECONDARY, 0))
+            .thenReturn(user)
+    }
+
+    @Test
+    fun testCreateUser_threadingOrder() {
+        val successCallback = Mockito.mock(Consumer::class.java)
+        val errorCallback = Mockito.mock(Runnable::class.java)
+
+        userCreator.createUser(
+            USER_NAME,
+            null,
+            successCallback as Consumer<UserInfo?>,
+            errorCallback)
+
+        verify(userManager, never()).createUser(USER_NAME, UserManager.USER_TYPE_FULL_SECONDARY, 0)
+        bgExecutor.runAllReady()
+        verify(successCallback, never()).accept(user)
+        mainExecutor.runAllReady()
+        verify(userManager, never()).setUserIcon(anyInt(), any(Bitmap::class.java))
+        bgExecutor.runAllReady()
+
+        verify(userManager).createUser(USER_NAME, UserManager.USER_TYPE_FULL_SECONDARY, 0)
+        verify(userManager).setUserIcon(anyInt(), any(Bitmap::class.java))
+        verify(successCallback).accept(user)
+    }
+}
diff --git a/services/core/java/com/android/server/locksettings/SyntheticPasswordCrypto.java b/services/core/java/com/android/server/locksettings/SyntheticPasswordCrypto.java
index 371ef76..b06af8e 100644
--- a/services/core/java/com/android/server/locksettings/SyntheticPasswordCrypto.java
+++ b/services/core/java/com/android/server/locksettings/SyntheticPasswordCrypto.java
@@ -17,6 +17,7 @@
 package com.android.server.locksettings;
 
 import android.security.AndroidKeyStoreMaintenance;
+import android.security.keymaster.KeymasterDefs;
 import android.security.keystore.KeyProperties;
 import android.security.keystore.KeyProtection;
 import android.security.keystore2.AndroidKeyStoreLoadStoreParameter;
@@ -210,10 +211,26 @@
                         .setBoundToSpecificSecureUserId(sid)
                         .setUserAuthenticationValidityDurationSeconds(USER_AUTHENTICATION_VALIDITY);
             }
+            final KeyProtection protNonRollbackResistant = builder.build();
+            builder.setRollbackResistant(true);
+            final KeyProtection protRollbackResistant = builder.build();
+            final KeyStore.SecretKeyEntry entry = new KeyStore.SecretKeyEntry(keyStoreKey);
+            try {
+                keyStore.setEntry(keyAlias, entry, protRollbackResistant);
+                Slog.i(TAG, "Using rollback-resistant key");
+            } catch (KeyStoreException e) {
+                if (!(e.getCause() instanceof android.security.KeyStoreException)) {
+                    throw e;
+                }
+                int errorCode = ((android.security.KeyStoreException) e.getCause()).getErrorCode();
+                if (errorCode != KeymasterDefs.KM_ERROR_ROLLBACK_RESISTANCE_UNAVAILABLE) {
+                    throw e;
+                }
+                Slog.w(TAG, "Rollback-resistant keys unavailable.  Falling back to "
+                        + "non-rollback-resistant key");
+                keyStore.setEntry(keyAlias, entry, protNonRollbackResistant);
+            }
 
-            keyStore.setEntry(keyAlias,
-                    new KeyStore.SecretKeyEntry(keyStoreKey),
-                    builder.build());
             byte[] intermediate = encrypt(protectorSecret, PROTECTOR_SECRET_PERSONALIZATION, data);
             return encrypt(keyStoreKey, intermediate);
         } catch (CertificateException | IOException | BadPaddingException
diff --git a/services/core/java/com/android/server/pm/ApexPackageInfo.java b/services/core/java/com/android/server/pm/ApexPackageInfo.java
index 1fd9f8e..f959a52 100644
--- a/services/core/java/com/android/server/pm/ApexPackageInfo.java
+++ b/services/core/java/com/android/server/pm/ApexPackageInfo.java
@@ -22,9 +22,10 @@
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.apex.ApexInfo;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageInfo;
 import android.content.pm.PackageManager;
 import android.util.ArrayMap;
-import android.util.Pair;
 import android.util.PrintWriterPrinter;
 
 import com.android.internal.annotations.GuardedBy;
@@ -32,9 +33,8 @@
 import com.android.internal.util.Preconditions;
 import com.android.server.pm.parsing.PackageParser2;
 import com.android.server.pm.parsing.pkg.AndroidPackage;
-import com.android.server.pm.parsing.pkg.AndroidPackageUtils;
 import com.android.server.pm.parsing.pkg.ParsedPackage;
-import com.android.server.pm.pkg.PackageStateInternal;
+import com.android.server.pm.pkg.parsing.PackageInfoWithoutStateUtils;
 import com.android.server.pm.pkg.parsing.ParsingPackageUtils;
 
 import java.io.File;
@@ -59,7 +59,17 @@
     private final Object mLock = new Object();
 
     @GuardedBy("mLock")
-    private List<Pair<ApexInfo, AndroidPackage>> mAllPackagesCache;
+    private List<PackageInfo> mAllPackagesCache;
+
+    /**
+     * Whether an APEX package is active or not.
+     *
+     * @param packageInfo the package to check
+     * @return {@code true} if this package is active, {@code false} otherwise.
+     */
+    private static boolean isActive(PackageInfo packageInfo) {
+        return packageInfo.isActiveApex;
+    }
 
     /**
      * Called by package manager service to scan apex package files when device boots up.
@@ -95,23 +105,20 @@
      *         is not found.
      */
     @Nullable
-    Pair<ApexInfo, AndroidPackage> getPackageInfo(String packageName,
-            @ApexManager.PackageInfoFlags int flags) {
+    PackageInfo getPackageInfo(String packageName, @ApexManager.PackageInfoFlags int flags) {
         synchronized (mLock) {
             Preconditions.checkState(mAllPackagesCache != null,
                     "APEX packages have not been scanned");
             boolean matchActive = (flags & MATCH_ACTIVE_PACKAGE) != 0;
             boolean matchFactory = (flags & MATCH_FACTORY_PACKAGE) != 0;
             for (int i = 0, size = mAllPackagesCache.size(); i < size; i++) {
-                final Pair<ApexInfo, AndroidPackage> pair = mAllPackagesCache.get(i);
-                var apexInfo = pair.first;
-                var pkg = pair.second;
-                if (!pkg.getPackageName().equals(packageName)) {
+                final PackageInfo packageInfo = mAllPackagesCache.get(i);
+                if (!packageInfo.packageName.equals(packageName)) {
                     continue;
                 }
-                if ((matchActive && apexInfo.isActive)
-                        || (matchFactory && apexInfo.isFactory)) {
-                    return pair;
+                if ((matchActive && isActive(packageInfo))
+                        || (matchFactory && isFactory(packageInfo))) {
+                    return packageInfo;
                 }
             }
             return null;
@@ -121,18 +128,18 @@
     /**
      * Retrieves information about all active APEX packages.
      *
-     * @return list containing information about different active packages.
+     * @return a List of PackageInfo object, each one containing information about a different
+     *         active package.
      */
-    @NonNull
-    List<Pair<ApexInfo, AndroidPackage>> getActivePackages() {
+    List<PackageInfo> getActivePackages() {
         synchronized (mLock) {
             Preconditions.checkState(mAllPackagesCache != null,
                     "APEX packages have not been scanned");
-            final List<Pair<ApexInfo, AndroidPackage>> activePackages = new ArrayList<>();
+            final List<PackageInfo> activePackages = new ArrayList<>();
             for (int i = 0; i < mAllPackagesCache.size(); i++) {
-                final var pair = mAllPackagesCache.get(i);
-                if (pair.first.isActive) {
-                    activePackages.add(pair);
+                final PackageInfo packageInfo = mAllPackagesCache.get(i);
+                if (isActive(packageInfo)) {
+                    activePackages.add(packageInfo);
                 }
             }
             return activePackages;
@@ -140,20 +147,20 @@
     }
 
     /**
-     * Retrieves information about all pre-installed APEX packages.
+     * Retrieves information about all active pre-installed APEX packages.
      *
-     * @return list containing information about different pre-installed packages.
+     * @return a List of PackageInfo object, each one containing information about a different
+     *         active pre-installed package.
      */
-    @NonNull
-    List<Pair<ApexInfo, AndroidPackage>> getFactoryPackages() {
+    List<PackageInfo> getFactoryPackages() {
         synchronized (mLock) {
             Preconditions.checkState(mAllPackagesCache != null,
                     "APEX packages have not been scanned");
-            final List<Pair<ApexInfo, AndroidPackage>> factoryPackages = new ArrayList<>();
+            final List<PackageInfo> factoryPackages = new ArrayList<>();
             for (int i = 0; i < mAllPackagesCache.size(); i++) {
-                final var pair = mAllPackagesCache.get(i);
-                if (pair.first.isFactory) {
-                    factoryPackages.add(pair);
+                final PackageInfo packageInfo = mAllPackagesCache.get(i);
+                if (isFactory(packageInfo)) {
+                    factoryPackages.add(packageInfo);
                 }
             }
             return factoryPackages;
@@ -163,18 +170,18 @@
     /**
      * Retrieves information about all inactive APEX packages.
      *
-     * @return list containing information about different inactive packages.
+     * @return a List of PackageInfo object, each one containing information about a different
+     *         inactive package.
      */
-    @NonNull
-    List<Pair<ApexInfo, AndroidPackage>> getInactivePackages() {
+    List<PackageInfo> getInactivePackages() {
         synchronized (mLock) {
             Preconditions.checkState(mAllPackagesCache != null,
                     "APEX packages have not been scanned");
-            final List<Pair<ApexInfo, AndroidPackage>> inactivePackages = new ArrayList<>();
+            final List<PackageInfo> inactivePackages = new ArrayList<>();
             for (int i = 0; i < mAllPackagesCache.size(); i++) {
-                final var pair = mAllPackagesCache.get(i);
-                if (!pair.first.isActive) {
-                    inactivePackages.add(pair);
+                final PackageInfo packageInfo = mAllPackagesCache.get(i);
+                if (!isActive(packageInfo)) {
+                    inactivePackages.add(packageInfo);
                 }
             }
             return inactivePackages;
@@ -192,8 +199,8 @@
             Preconditions.checkState(mAllPackagesCache != null,
                     "APEX packages have not been scanned");
             for (int i = 0, size = mAllPackagesCache.size(); i < size; i++) {
-                final var pair = mAllPackagesCache.get(i);
-                if (pair.second.getPackageName().equals(packageName)) {
+                final PackageInfo packageInfo = mAllPackagesCache.get(i);
+                if (packageInfo.packageName.equals(packageName)) {
                     return true;
                 }
             }
@@ -215,18 +222,21 @@
     }
 
     void notifyPackageInstalled(ApexInfo apexInfo, AndroidPackage pkg) {
-        final String packageName = pkg.getPackageName();
+        final int flags = PackageManager.GET_META_DATA
+                | PackageManager.GET_SIGNING_CERTIFICATES
+                | PackageManager.GET_SIGNATURES;
+        final PackageInfo newApexPkg = PackageInfoWithoutStateUtils.generate(
+                pkg, apexInfo, flags);
+        final String packageName = newApexPkg.packageName;
         synchronized (mLock) {
             for (int i = 0, size = mAllPackagesCache.size(); i < size; i++) {
-                var pair = mAllPackagesCache.get(i);
-                var oldApexInfo = pair.first;
-                var oldApexPkg = pair.second;
-                if (oldApexInfo.isActive && oldApexPkg.getPackageName().equals(packageName)) {
-                    if (oldApexInfo.isFactory) {
-                        oldApexInfo.isActive = false;
-                        mAllPackagesCache.add(Pair.create(apexInfo, pkg));
+                PackageInfo oldApexPkg = mAllPackagesCache.get(i);
+                if (oldApexPkg.isActiveApex && oldApexPkg.packageName.equals(packageName)) {
+                    if (isFactory(oldApexPkg)) {
+                        oldApexPkg.isActiveApex = false;
+                        mAllPackagesCache.add(newApexPkg);
                     } else {
-                        mAllPackagesCache.set(i, Pair.create(apexInfo, pkg));
+                        mAllPackagesCache.set(i, newApexPkg);
                     }
                     break;
                 }
@@ -235,6 +245,16 @@
     }
 
     /**
+     * Whether the APEX package is pre-installed or not.
+     *
+     * @param packageInfo the package to check
+     * @return {@code true} if this package is pre-installed, {@code false} otherwise.
+     */
+    private static boolean isFactory(@NonNull PackageInfo packageInfo) {
+        return (packageInfo.applicationInfo.flags & ApplicationInfo.FLAG_UPDATED_SYSTEM_APP) == 0;
+    }
+
+    /**
      * Dumps various state information to the provided {@link PrintWriter} object.
      *
      * @param pw the {@link PrintWriter} object to send information to.
@@ -268,25 +288,33 @@
         HashSet<String> factoryPackagesSet = new HashSet<>();
         for (ApexManager.ScanResult result : scanResults) {
             ApexInfo ai = result.apexInfo;
-            String packageName = result.pkg.getPackageName();
-            if (!packageName.equals(result.packageName)) {
+
+            final PackageInfo packageInfo = PackageInfoWithoutStateUtils.generate(
+                    result.pkg, ai, flags);
+            if (packageInfo == null) {
+                throw new IllegalStateException("Unable to generate package info: "
+                        + ai.modulePath);
+            }
+            if (!packageInfo.packageName.equals(result.packageName)) {
                 throw new IllegalStateException("Unmatched package name: "
-                        + result.packageName + " != " + packageName
+                        + result.packageName + " != " + packageInfo.packageName
                         + ", path=" + ai.modulePath);
             }
-            mAllPackagesCache.add(Pair.create(ai, result.pkg));
+            mAllPackagesCache.add(packageInfo);
             if (ai.isActive) {
-                if (!activePackagesSet.add(packageName)) {
+                if (!activePackagesSet.add(packageInfo.packageName)) {
                     throw new IllegalStateException(
-                            "Two active packages have the same name: " + packageName);
+                            "Two active packages have the same name: "
+                                    + packageInfo.packageName);
                 }
             }
             if (ai.isFactory) {
                 // Don't throw when the duplicating APEX is VNDK APEX
-                if (!factoryPackagesSet.add(packageName)
+                if (!factoryPackagesSet.add(packageInfo.packageName)
                         && !ai.moduleName.startsWith(VNDK_APEX_MODULE_NAME_PREFIX)) {
                     throw new IllegalStateException(
-                            "Two factory packages have the same name: " + packageName);
+                            "Two factory packages have the same name: "
+                                    + packageInfo.packageName);
                 }
             }
         }
@@ -335,65 +363,30 @@
     }
 
     /**
-     * @see #dumpPackages(List, String, IndentingPrintWriter)
-     */
-    static void dumpPackageStates(List<PackageStateInternal> packageStates, boolean isActive,
-            @Nullable String packageName, IndentingPrintWriter ipw) {
-        ipw.println();
-        ipw.increaseIndent();
-        for (int i = 0, size = packageStates.size(); i < size; i++) {
-            final var packageState = packageStates.get(i);
-            var pkg = packageState.getPkg();
-            if (packageName != null && !packageName.equals(pkg.getPackageName())) {
-                continue;
-            }
-            ipw.println(pkg.getPackageName());
-            ipw.increaseIndent();
-            ipw.println("Version: " + pkg.getLongVersionCode());
-            ipw.println("Path: " + pkg.getBaseApkPath());
-            ipw.println("IsActive: " + isActive);
-            ipw.println("IsFactory: " + !packageState.isUpdatedSystemApp());
-            ipw.println("ApplicationInfo: ");
-            ipw.increaseIndent();
-            // TODO: Dump the package manually
-            AndroidPackageUtils.generateAppInfoWithoutState(pkg)
-                    .dump(new PrintWriterPrinter(ipw), "");
-            ipw.decreaseIndent();
-            ipw.decreaseIndent();
-        }
-        ipw.decreaseIndent();
-        ipw.println();
-    }
-
-    /**
      * Dump information about the packages contained in a particular cache
      * @param packagesCache the cache to print information about.
      * @param packageName a {@link String} containing a package name, or {@code null}. If set,
      *                    only information about that specific package will be dumped.
      * @param ipw the {@link IndentingPrintWriter} object to send information to.
      */
-    static void dumpPackages(List<Pair<ApexInfo, AndroidPackage>> packagesCache,
+    static void dumpPackages(List<PackageInfo> packagesCache,
             @Nullable String packageName, IndentingPrintWriter ipw) {
         ipw.println();
         ipw.increaseIndent();
         for (int i = 0, size = packagesCache.size(); i < size; i++) {
-            final var pair = packagesCache.get(i);
-            var apexInfo = pair.first;
-            var pkg = pair.second;
-            if (packageName != null && !packageName.equals(pkg.getPackageName())) {
+            final PackageInfo pi = packagesCache.get(i);
+            if (packageName != null && !packageName.equals(pi.packageName)) {
                 continue;
             }
-            ipw.println(pkg.getPackageName());
+            ipw.println(pi.packageName);
             ipw.increaseIndent();
-            ipw.println("Version: " + pkg.getLongVersionCode());
-            ipw.println("Path: " + pkg.getBaseApkPath());
-            ipw.println("IsActive: " + apexInfo.isActive);
-            ipw.println("IsFactory: " + apexInfo.isFactory);
+            ipw.println("Version: " + pi.versionCode);
+            ipw.println("Path: " + pi.applicationInfo.sourceDir);
+            ipw.println("IsActive: " + isActive(pi));
+            ipw.println("IsFactory: " + isFactory(pi));
             ipw.println("ApplicationInfo: ");
             ipw.increaseIndent();
-            // TODO: Dump the package manually
-            AndroidPackageUtils.generateAppInfoWithoutState(pkg)
-                    .dump(new PrintWriterPrinter(ipw), "");
+            pi.applicationInfo.dump(new PrintWriterPrinter(ipw), "");
             ipw.decreaseIndent();
             ipw.decreaseIndent();
         }
diff --git a/services/core/java/com/android/server/pm/ComputerEngine.java b/services/core/java/com/android/server/pm/ComputerEngine.java
index 8ec3d2b..63c25ea 100644
--- a/services/core/java/com/android/server/pm/ComputerEngine.java
+++ b/services/core/java/com/android/server/pm/ComputerEngine.java
@@ -65,7 +65,6 @@
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.annotation.UserIdInt;
-import android.apex.ApexInfo;
 import android.app.ActivityManager;
 import android.content.ComponentName;
 import android.content.Context;
@@ -1019,12 +1018,11 @@
                 if ((flags & PackageManager.MATCH_SYSTEM_ONLY) != 0) {
                     apexFlags = ApexManager.MATCH_FACTORY_PACKAGE;
                 }
-                final var pair = mApexPackageInfo.getPackageInfo(packageName, apexFlags);
-                if (pair == null) {
+                final PackageInfo pi = mApexPackageInfo.getPackageInfo(packageName, apexFlags);
+                if (pi == null) {
                     return null;
                 }
-                return PackageInfoUtils.generateApplicationInfo(pair.second, flags,
-                        PackageUserStateInternal.DEFAULT, userId, null);
+                return pi.applicationInfo;
             }
         }
         if ("android".equals(packageName) || "system".equals(packageName)) {
@@ -1720,12 +1718,8 @@
             // Instant app filtering for APEX modules is ignored
             if (!ApexPackageInfo.ENABLE_FEATURE_SCAN_APEX) {
                 if (matchApex) {
-                    final var pair = mApexPackageInfo.getPackageInfo(packageName,
+                    return mApexPackageInfo.getPackageInfo(packageName,
                             ApexManager.MATCH_FACTORY_PACKAGE);
-                    if (pair == null) {
-                        return null;
-                    }
-                    return PackageInfoUtils.generate(pair.second, pair.first, flags, null, userId);
                 }
             }
             final PackageStateInternal ps = mSettings.getDisabledSystemPkg(packageName);
@@ -1781,12 +1775,8 @@
         }
         if (!ApexPackageInfo.ENABLE_FEATURE_SCAN_APEX) {
             if (matchApex) {
-                final var pair = mApexPackageInfo.getPackageInfo(packageName,
+                return mApexPackageInfo.getPackageInfo(packageName,
                         ApexManager.MATCH_ACTIVE_PACKAGE);
-                if (pair == null) {
-                    return null;
-                }
-                return PackageInfoUtils.generate(pair.second, pair.first, flags, null, userId);
             }
         }
         return null;
@@ -1893,17 +1883,10 @@
         }
         if (!ApexPackageInfo.ENABLE_FEATURE_SCAN_APEX) {
             if (listApex) {
-                List<Pair<ApexInfo, AndroidPackage>> pairs;
                 if (listFactory) {
-                    pairs = mApexPackageInfo.getFactoryPackages();
+                    list.addAll(mApexPackageInfo.getFactoryPackages());
                 } else {
-                    pairs = mApexPackageInfo.getActivePackages();
-                }
-
-                for (int index = 0; index < pairs.size(); index++) {
-                    var pair = pairs.get(index);
-                    list.add(PackageInfoUtils.generate(pair.second, pair.first, flags, null,
-                            userId));
+                    list.addAll(mApexPackageInfo.getActivePackages());
                 }
             }
         }
@@ -3421,23 +3404,29 @@
         } // switch
     }
 
-    private void generateApexPackageInfo(@NonNull List<PackageStateInternal> activePackages,
-            @NonNull List<PackageStateInternal> inactivePackages,
-            @NonNull List<PackageStateInternal> factoryActivePackages,
-            @NonNull List<PackageStateInternal> factoryInactivePackages) {
+    private void generateApexPackageInfo(List<PackageInfo> activePackages,
+            List<PackageInfo> inactivePackages, List<PackageInfo> factoryPackages) {
         for (AndroidPackage p : mPackages.values()) {
             final String packageName = p.getPackageName();
             PackageStateInternal ps = mSettings.getPackage(packageName);
             if (!p.isApex() || ps == null) {
                 continue;
             }
-            activePackages.add(ps);
+            PackageInfo pi = generatePackageInfo(ps, 0, 0);
+            if (pi == null) {
+                continue;
+            }
+            pi.isActiveApex = true;
+            activePackages.add(pi);
             if (!ps.isUpdatedSystemApp()) {
-                factoryActivePackages.add(ps);
+                factoryPackages.add(pi);
             } else {
                 PackageStateInternal psDisabled = mSettings.getDisabledSystemPkg(packageName);
-                factoryInactivePackages.add(psDisabled);
-                inactivePackages.add(psDisabled);
+                pi = generatePackageInfo(psDisabled, 0, 0);
+                if (pi != null) {
+                    factoryPackages.add(pi);
+                    inactivePackages.add(pi);
+                }
             }
         }
     }
@@ -3445,19 +3434,16 @@
     private void dumpApex(PrintWriter pw, String packageName) {
         if (ApexPackageInfo.ENABLE_FEATURE_SCAN_APEX) {
             final IndentingPrintWriter ipw = new IndentingPrintWriter(pw, "  ", 120);
-            List<PackageStateInternal> activePackages = new ArrayList<>();
-            List<PackageStateInternal> inactivePackages = new ArrayList<>();
-            List<PackageStateInternal> factoryActivePackages = new ArrayList<>();
-            List<PackageStateInternal> factoryInactivePackages = new ArrayList<>();
-            generateApexPackageInfo(activePackages, inactivePackages, factoryActivePackages,
-                    factoryInactivePackages);
+            List<PackageInfo> activePackages = new ArrayList<>();
+            List<PackageInfo> inactivePackages = new ArrayList<>();
+            List<PackageInfo> factoryPackages = new ArrayList<>();
+            generateApexPackageInfo(activePackages, inactivePackages, factoryPackages);
             ipw.println("Active APEX packages:");
-            ApexPackageInfo.dumpPackageStates(activePackages, true, packageName, ipw);
+            ApexPackageInfo.dumpPackages(activePackages, packageName, ipw);
             ipw.println("Inactive APEX packages:");
-            ApexPackageInfo.dumpPackageStates(inactivePackages, false, packageName, ipw);
+            ApexPackageInfo.dumpPackages(inactivePackages, packageName, ipw);
             ipw.println("Factory APEX packages:");
-            ApexPackageInfo.dumpPackageStates(factoryActivePackages, true, packageName, ipw);
-            ApexPackageInfo.dumpPackageStates(factoryInactivePackages, false, packageName, ipw);
+            ApexPackageInfo.dumpPackages(factoryPackages, packageName, ipw);
         } else {
             mApexPackageInfo.dump(pw, packageName);
         }
diff --git a/services/core/java/com/android/server/pm/PackageManagerService.java b/services/core/java/com/android/server/pm/PackageManagerService.java
index 32b3e6a..2bdf62bd 100644
--- a/services/core/java/com/android/server/pm/PackageManagerService.java
+++ b/services/core/java/com/android/server/pm/PackageManagerService.java
@@ -1611,7 +1611,7 @@
         mSharedLibraries = injector.getSharedLibrariesImpl();
 
         mApexManager = testParams.apexManager;
-        mApexPackageInfo = new ApexPackageInfo(this);
+        mApexPackageInfo = new ApexPackageInfo();
         mArtManagerService = testParams.artManagerService;
         mAvailableFeatures = testParams.availableFeatures;
         mBackgroundDexOptService = testParams.backgroundDexOptService;
@@ -1811,7 +1811,7 @@
         mProtectedPackages = new ProtectedPackages(mContext);
 
         mApexManager = injector.getApexManager();
-        mApexPackageInfo = new ApexPackageInfo(this);
+        mApexPackageInfo = new ApexPackageInfo();
         mAppsFilter = mInjector.getAppsFilter();
 
         mInstantAppRegistry = new InstantAppRegistry(mContext, mPermissionManager,
diff --git a/services/core/java/com/android/server/pm/ScanPackageUtils.java b/services/core/java/com/android/server/pm/ScanPackageUtils.java
index 86affdd..0e57c91 100644
--- a/services/core/java/com/android/server/pm/ScanPackageUtils.java
+++ b/services/core/java/com/android/server/pm/ScanPackageUtils.java
@@ -823,8 +823,8 @@
      * ideally be static, but, it requires locks to read system state.
      */
     public static void applyPolicy(ParsedPackage parsedPackage,
-            final @PackageManagerService.ScanFlags int scanFlags,
-            @Nullable AndroidPackage platformPkg, boolean isUpdatedSystemApp) {
+            final @PackageManagerService.ScanFlags int scanFlags, AndroidPackage platformPkg,
+            boolean isUpdatedSystemApp) {
         if ((scanFlags & SCAN_AS_SYSTEM) != 0) {
             parsedPackage.setSystem(true);
             // TODO(b/135203078): Can this be done in PackageParser? Or just inferred when the flag
diff --git a/services/core/java/com/android/server/pm/UserSystemPackageInstaller.java b/services/core/java/com/android/server/pm/UserSystemPackageInstaller.java
index 937b2cf..b57d4d5 100644
--- a/services/core/java/com/android/server/pm/UserSystemPackageInstaller.java
+++ b/services/core/java/com/android/server/pm/UserSystemPackageInstaller.java
@@ -219,38 +219,21 @@
 
         // Install/uninstall system packages per user.
         for (int userId : mUm.getUserIds()) {
-            final Set<String> userWhitelist = getInstallablePackagesForUserId(userId);
+            final Set<String> userAllowlist = getInstallablePackagesForUserId(userId);
 
-            // If null, run for all packages
-            if (userWhitelist == null) {
-                pmInt.forEachPackageState(packageState -> {
-                    if (packageState.getPkg() == null) {
-                        return;
-                    }
-                    final boolean install = !packageState.getTransientState()
-                            .isHiddenUntilInstalled();
-                    if (packageState.getUserStateOrDefault(userId).isInstalled() != install
-                            && shouldChangeInstallationState(packageState, install, userId,
-                            isFirstBoot, isConsideredUpgrade, preExistingPackages)) {
-                        changesToCommit.add(userId, packageState.getPackageName(), install);
-                    }
-                });
-            } else {
-                for (String packageName : userWhitelist) {
-                    PackageStateInternal packageState = pmInt.getPackageStateInternal(packageName);
-                    if (packageState.getPkg() == null) {
-                        continue;
-                    }
-
-                    final boolean install = !packageState.getTransientState()
-                            .isHiddenUntilInstalled();
-                    if (packageState.getUserStateOrDefault(userId).isInstalled() != install
-                            && shouldChangeInstallationState(packageState, install, userId,
-                            isFirstBoot, isConsideredUpgrade, preExistingPackages)) {
-                        changesToCommit.add(userId, packageState.getPackageName(), install);
-                    }
+            pmInt.forEachPackageState(packageState -> {
+                if (packageState.getPkg() == null) {
+                    return;
                 }
-            }
+                boolean install = (userAllowlist == null
+                                || userAllowlist.contains(packageState.getPackageName()))
+                        && !packageState.getTransientState().isHiddenUntilInstalled();
+                if (packageState.getUserStateOrDefault(userId).isInstalled() != install
+                        && shouldChangeInstallationState(packageState, install, userId,
+                        isFirstBoot, isConsideredUpgrade, preExistingPackages)) {
+                    changesToCommit.add(userId, packageState.getPackageName(), install);
+                }
+            });
         }
 
         pmInt.commitPackageStateMutation(null, packageStateMutator -> {
diff --git a/services/core/java/com/android/server/pm/parsing/PackageInfoUtils.java b/services/core/java/com/android/server/pm/parsing/PackageInfoUtils.java
index a44def8..9c620c4 100644
--- a/services/core/java/com/android/server/pm/parsing/PackageInfoUtils.java
+++ b/services/core/java/com/android/server/pm/parsing/PackageInfoUtils.java
@@ -93,14 +93,12 @@
 
     /**
      * @param pkgSetting See {@link PackageInfoUtils} for description of pkgSetting usage.
-     * @deprecated Once ENABLE_FEATURE_SCAN_APEX is removed, this should also be removed.
      */
-    @Deprecated
     @Nullable
-    public static PackageInfo generate(AndroidPackage pkg, ApexInfo apexInfo, long flags,
-            @Nullable PackageStateInternal pkgSetting, @UserIdInt int userId) {
+    public static PackageInfo generate(AndroidPackage pkg, ApexInfo apexInfo, int flags,
+            @Nullable PackageStateInternal pkgSetting) {
         return generateWithComponents(pkg, EmptyArray.INT, flags, 0, 0, Collections.emptySet(),
-                PackageUserStateInternal.DEFAULT, userId, apexInfo, pkgSetting);
+                PackageUserStateInternal.DEFAULT, UserHandle.getCallingUserId(), apexInfo, pkgSetting);
     }
 
     /**
diff --git a/services/tests/mockingservicestests/src/com/android/server/tare/ScribeTest.java b/services/tests/mockingservicestests/src/com/android/server/tare/ScribeTest.java
index 721777c..13510adb 100644
--- a/services/tests/mockingservicestests/src/com/android/server/tare/ScribeTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/tare/ScribeTest.java
@@ -30,6 +30,7 @@
 import android.os.UserHandle;
 import android.util.Log;
 import android.util.SparseArrayMap;
+import android.util.SparseLongArray;
 
 import androidx.test.InstrumentationRegistry;
 import androidx.test.filters.SmallTest;
@@ -174,10 +175,13 @@
         when(mIrs.getConsumptionLimitLocked()).thenReturn(consumptionLimit);
 
         Ledger ledger = mScribeUnderTest.getLedgerLocked(TEST_USER_ID, TEST_PACKAGE);
-        ledger.recordTransaction(new Ledger.Transaction(0, 1000L, 1, null, 2000, 0));
+        ledger.recordTransaction(
+                new Ledger.Transaction(0, 1000L, EconomicPolicy.TYPE_REWARD | 1, null, 2000, 0));
         // 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, 3000));
+        ledger.recordTransaction(
+                new Ledger.Transaction(0, 1000L,
+                        EconomicPolicy.TYPE_ACTION | 1, null, -5000, 3000));
         mScribeUnderTest.setLastReclamationTimeLocked(lastReclamationTime);
         mScribeUnderTest.setConsumptionLimitLocked(consumptionLimit);
         mScribeUnderTest.adjustRemainingConsumableCakesLocked(
@@ -209,9 +213,13 @@
     @Test
     public void testWritingPopulatedLedgerToDisk() {
         final Ledger ogLedger = mScribeUnderTest.getLedgerLocked(TEST_USER_ID, TEST_PACKAGE);
-        ogLedger.recordTransaction(new Ledger.Transaction(0, 1000, 1, null, 51, 0));
-        ogLedger.recordTransaction(new Ledger.Transaction(1500, 2000, 2, "green", 52, -1));
-        ogLedger.recordTransaction(new Ledger.Transaction(2500, 3000, 3, "blue", 3, 12));
+        ogLedger.recordTransaction(
+                new Ledger.Transaction(0, 1000, EconomicPolicy.TYPE_REWARD | 1, null, 51, 0));
+        ogLedger.recordTransaction(
+                new Ledger.Transaction(1500, 2000,
+                        EconomicPolicy.TYPE_REWARD | 2, "green", 52, -1));
+        ogLedger.recordTransaction(
+                new Ledger.Transaction(2500, 3000, EconomicPolicy.TYPE_REWARD | 3, "blue", 3, 12));
         mScribeUnderTest.writeImmediatelyForTesting();
 
         mScribeUnderTest.loadFromDiskLocked();
@@ -230,11 +238,13 @@
                 addInstalledPackage(userId, pkgName);
                 final Ledger ledger = mScribeUnderTest.getLedgerLocked(userId, pkgName);
                 ledger.recordTransaction(new Ledger.Transaction(
-                        0, 1000L * u + l, 1, null, -51L * u + l, 50));
+                        0, 1000L * u + l, EconomicPolicy.TYPE_ACTION | 1, null, -51L * u + l, 50));
                 ledger.recordTransaction(new Ledger.Transaction(
-                        1500L * u + l, 2000L * u + l, 2 * u + l, "green" + u + l, 52L * u + l, 0));
+                        1500L * u + l, 2000L * u + l,
+                        EconomicPolicy.TYPE_REWARD | 2 * u + l, "green" + u + l, 52L * u + l, 0));
                 ledger.recordTransaction(new Ledger.Transaction(
-                        2500L * u + l, 3000L * u + l, 3 * u + l, "blue" + u + l, 3L * u + l, 0));
+                        2500L * u + l, 3000L * u + l,
+                        EconomicPolicy.TYPE_REWARD | 3 * u + l, "blue" + u + l, 3L * u + l, 0));
                 ledgers.add(userId, pkgName, ledger);
             }
         }
@@ -248,9 +258,12 @@
     @Test
     public void testDiscardLedgerFromDisk() {
         final Ledger ogLedger = mScribeUnderTest.getLedgerLocked(TEST_USER_ID, TEST_PACKAGE);
-        ogLedger.recordTransaction(new Ledger.Transaction(0, 1000, 1, null, 51, 1));
-        ogLedger.recordTransaction(new Ledger.Transaction(1500, 2000, 2, "green", 52, 0));
-        ogLedger.recordTransaction(new Ledger.Transaction(2500, 3000, 3, "blue", 3, 1));
+        ogLedger.recordTransaction(
+                new Ledger.Transaction(0, 1000, EconomicPolicy.TYPE_REWARD | 1, null, 51, 1));
+        ogLedger.recordTransaction(
+                new Ledger.Transaction(1500, 2000, EconomicPolicy.TYPE_REWARD | 2, "green", 52, 0));
+        ogLedger.recordTransaction(
+                new Ledger.Transaction(2500, 3000, EconomicPolicy.TYPE_REWARD | 3, "blue", 3, 1));
         mScribeUnderTest.writeImmediatelyForTesting();
 
         mScribeUnderTest.loadFromDiskLocked();
@@ -269,9 +282,12 @@
     public void testLoadingMissingPackageFromDisk() {
         final String pkgName = TEST_PACKAGE + ".uninstalled";
         final Ledger ogLedger = mScribeUnderTest.getLedgerLocked(TEST_USER_ID, pkgName);
-        ogLedger.recordTransaction(new Ledger.Transaction(0, 1000, 1, null, 51, 1));
-        ogLedger.recordTransaction(new Ledger.Transaction(1500, 2000, 2, "green", 52, 2));
-        ogLedger.recordTransaction(new Ledger.Transaction(2500, 3000, 3, "blue", 3, 3));
+        ogLedger.recordTransaction(
+                new Ledger.Transaction(0, 1000, EconomicPolicy.TYPE_REGULATION | 1, null, 51, 1));
+        ogLedger.recordTransaction(
+                new Ledger.Transaction(1500, 2000, EconomicPolicy.TYPE_REWARD | 2, "green", 52, 2));
+        ogLedger.recordTransaction(
+                new Ledger.Transaction(2500, 3000, EconomicPolicy.TYPE_ACTION | 3, "blue", -3, 3));
         mScribeUnderTest.writeImmediatelyForTesting();
 
         // Package isn't installed, so make sure it's not saved to memory after loading.
@@ -283,9 +299,13 @@
     public void testLoadingMissingUserFromDisk() {
         final int userId = TEST_USER_ID + 1;
         final Ledger ogLedger = mScribeUnderTest.getLedgerLocked(userId, TEST_PACKAGE);
-        ogLedger.recordTransaction(new Ledger.Transaction(0, 1000, 1, null, 51, 0));
-        ogLedger.recordTransaction(new Ledger.Transaction(1500, 2000, 2, "green", 52, 1));
-        ogLedger.recordTransaction(new Ledger.Transaction(2500, 3000, 3, "blue", 3, 3));
+        ogLedger.recordTransaction(
+                new Ledger.Transaction(0, 1000, EconomicPolicy.TYPE_REWARD | 1, null, 51, 0));
+        ogLedger.recordTransaction(
+                new Ledger.Transaction(1500, 2000, EconomicPolicy.TYPE_REWARD | 2, "green", 52, 1));
+        ogLedger.recordTransaction(
+                new Ledger.Transaction(2500, 3000,
+                        EconomicPolicy.TYPE_REGULATION | 3, "blue", 3, 3));
         mScribeUnderTest.writeImmediatelyForTesting();
 
         // User doesn't show up with any packages, so make sure nothing is saved after loading.
@@ -331,12 +351,34 @@
         }
         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));
         }
+
+        List<Ledger.RewardBucket> expectedRewardBuckets = expected.getRewardBuckets();
+        List<Ledger.RewardBucket> actualRewardBuckets = actual.getRewardBuckets();
+        assertEquals(expectedRewardBuckets.size(), actualRewardBuckets.size());
+        for (int i = 0; i < expectedRewardBuckets.size(); ++i) {
+            assertRewardBucketsEqual(expectedRewardBuckets.get(i), actualRewardBuckets.get(i));
+        }
+    }
+
+    private void assertSparseLongArraysEqual(SparseLongArray expected, SparseLongArray actual) {
+        if (expected == null) {
+            assertNull(actual);
+            return;
+        }
+        assertNotNull(actual);
+        final int size = expected.size();
+        assertEquals(size, actual.size());
+        for (int i = 0; i < size; ++i) {
+            assertEquals(expected.keyAt(i), actual.keyAt(i));
+            assertEquals(expected.valueAt(i), actual.valueAt(i));
+        }
     }
 
     private void assertReportListsEqual(List<Analyst.Report> expected,
@@ -382,6 +424,17 @@
         }
     }
 
+    private void assertRewardBucketsEqual(Ledger.RewardBucket expected,
+            Ledger.RewardBucket actual) {
+        if (expected == null) {
+            assertNull(actual);
+            return;
+        }
+        assertNotNull(actual);
+        assertEquals(expected.startTimeMs, actual.startTimeMs);
+        assertSparseLongArraysEqual(expected.cumulativeDelta, actual.cumulativeDelta);
+    }
+
     private void assertTransactionsEqual(Ledger.Transaction expected, Ledger.Transaction actual) {
         if (expected == null) {
             assertNull(actual);
diff --git a/services/tests/servicestests/src/com/android/server/pm/ApexManagerTest.java b/services/tests/servicestests/src/com/android/server/pm/ApexManagerTest.java
index 503ca69..20482af 100644
--- a/services/tests/servicestests/src/com/android/server/pm/ApexManagerTest.java
+++ b/services/tests/servicestests/src/com/android/server/pm/ApexManagerTest.java
@@ -36,6 +36,8 @@
 import android.apex.ApexSessionParams;
 import android.apex.IApexService;
 import android.content.Context;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageInfo;
 import android.os.Environment;
 import android.os.RemoteException;
 import android.os.ServiceSpecificException;
@@ -91,16 +93,16 @@
         ApexPackageInfo apexPackageInfo = new ApexPackageInfo();
         apexPackageInfo.scanApexPackages(
                 apexInfo, mPackageParser2, ParallelPackageParser.makeExecutorService());
-        final var activePair = apexPackageInfo.getPackageInfo(TEST_APEX_PKG,
+        final PackageInfo activePkgPi = apexPackageInfo.getPackageInfo(TEST_APEX_PKG,
                 ApexManager.MATCH_ACTIVE_PACKAGE);
 
-        assertThat(activePair).isNotNull();
-        assertThat(activePair.second.getPackageName()).contains(TEST_APEX_PKG);
+        assertThat(activePkgPi).isNotNull();
+        assertThat(activePkgPi.packageName).contains(TEST_APEX_PKG);
 
-        final var factoryPair = apexPackageInfo.getPackageInfo(TEST_APEX_PKG,
+        final PackageInfo factoryPkgPi = apexPackageInfo.getPackageInfo(TEST_APEX_PKG,
                 ApexManager.MATCH_FACTORY_PACKAGE);
 
-        assertThat(factoryPair).isNull();
+        assertThat(factoryPkgPi).isNull();
     }
 
     @Test
@@ -109,16 +111,16 @@
         ApexPackageInfo apexPackageInfo = new ApexPackageInfo();
         apexPackageInfo.scanApexPackages(
                 apexInfo, mPackageParser2, ParallelPackageParser.makeExecutorService());
-        var factoryPair = apexPackageInfo.getPackageInfo(TEST_APEX_PKG,
+        PackageInfo factoryPkgPi = apexPackageInfo.getPackageInfo(TEST_APEX_PKG,
                 ApexManager.MATCH_FACTORY_PACKAGE);
 
-        assertThat(factoryPair).isNotNull();
-        assertThat(factoryPair.second.getPackageName()).contains(TEST_APEX_PKG);
+        assertThat(factoryPkgPi).isNotNull();
+        assertThat(factoryPkgPi.packageName).contains(TEST_APEX_PKG);
 
-        final var activePair = apexPackageInfo.getPackageInfo(TEST_APEX_PKG,
+        final PackageInfo activePkgPi = apexPackageInfo.getPackageInfo(TEST_APEX_PKG,
                 ApexManager.MATCH_ACTIVE_PACKAGE);
 
-        assertThat(activePair).isNull();
+        assertThat(activePkgPi).isNull();
     }
 
     @Test
@@ -386,16 +388,23 @@
         newApexInfo = mApexManager.installPackage(installedApex);
         apexPackageInfo.notifyPackageInstalled(newApexInfo, mPackageParser2);
 
-        var newInfo = apexPackageInfo.getPackageInfo("test.apex.rebootless",
+        PackageInfo newInfo = apexPackageInfo.getPackageInfo("test.apex.rebootless",
                 ApexManager.MATCH_ACTIVE_PACKAGE);
-        assertThat(newInfo.second.getBaseApkPath()).isEqualTo(finalApex.getAbsolutePath());
-        assertThat(newInfo.second.getLongVersionCode()).isEqualTo(2);
+        assertThat(newInfo.applicationInfo.sourceDir).isEqualTo(finalApex.getAbsolutePath());
+        assertThat(newInfo.applicationInfo.longVersionCode).isEqualTo(2);
+        assertThat(newInfo.applicationInfo.flags & ApplicationInfo.FLAG_UPDATED_SYSTEM_APP)
+                .isEqualTo(ApplicationInfo.FLAG_UPDATED_SYSTEM_APP);
+        assertThat(newInfo.applicationInfo.flags & ApplicationInfo.FLAG_INSTALLED)
+            .isEqualTo(ApplicationInfo.FLAG_INSTALLED);
 
-        var factoryInfo = apexPackageInfo.getPackageInfo("test.apex.rebootless",
+        PackageInfo factoryInfo = apexPackageInfo.getPackageInfo("test.apex.rebootless",
                 ApexManager.MATCH_FACTORY_PACKAGE);
-        assertThat(factoryInfo.second.getBaseApkPath()).isEqualTo(activeApexInfo.modulePath);
-        assertThat(factoryInfo.second.getLongVersionCode()).isEqualTo(1);
-        assertThat(factoryInfo.second.isSystem()).isTrue();
+        assertThat(factoryInfo.applicationInfo.sourceDir).isEqualTo(activeApexInfo.modulePath);
+        assertThat(factoryInfo.applicationInfo.longVersionCode).isEqualTo(1);
+        assertThat(factoryInfo.applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM)
+            .isEqualTo(ApplicationInfo.FLAG_SYSTEM);
+        assertThat(factoryInfo.applicationInfo.flags & ApplicationInfo.FLAG_INSTALLED)
+                .isEqualTo(ApplicationInfo.FLAG_INSTALLED);
     }
 
     @Test
@@ -420,16 +429,23 @@
         newApexInfo = mApexManager.installPackage(installedApex);
         apexPackageInfo.notifyPackageInstalled(newApexInfo, mPackageParser2);
 
-        var newInfo = apexPackageInfo.getPackageInfo("test.apex.rebootless",
+        PackageInfo newInfo = apexPackageInfo.getPackageInfo("test.apex.rebootless",
                 ApexManager.MATCH_ACTIVE_PACKAGE);
-        assertThat(newInfo.second.getBaseApkPath()).isEqualTo(finalApex.getAbsolutePath());
-        assertThat(newInfo.second.getLongVersionCode()).isEqualTo(2);
+        assertThat(newInfo.applicationInfo.sourceDir).isEqualTo(finalApex.getAbsolutePath());
+        assertThat(newInfo.applicationInfo.longVersionCode).isEqualTo(2);
+        assertThat(newInfo.applicationInfo.flags & ApplicationInfo.FLAG_UPDATED_SYSTEM_APP)
+                .isEqualTo(ApplicationInfo.FLAG_UPDATED_SYSTEM_APP);
+        assertThat(newInfo.applicationInfo.flags & ApplicationInfo.FLAG_INSTALLED)
+            .isEqualTo(ApplicationInfo.FLAG_INSTALLED);
 
-        var factoryInfo = apexPackageInfo.getPackageInfo("test.apex.rebootless",
+        PackageInfo factoryInfo = apexPackageInfo.getPackageInfo("test.apex.rebootless",
                 ApexManager.MATCH_FACTORY_PACKAGE);
-        assertThat(factoryInfo.second.getBaseApkPath()).isEqualTo(factoryApexInfo.modulePath);
-        assertThat(factoryInfo.second.getLongVersionCode()).isEqualTo(1);
-        assertThat(factoryInfo.second.isSystem()).isTrue();
+        assertThat(factoryInfo.applicationInfo.sourceDir).isEqualTo(factoryApexInfo.modulePath);
+        assertThat(factoryInfo.applicationInfo.longVersionCode).isEqualTo(1);
+        assertThat(factoryInfo.applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM)
+            .isEqualTo(ApplicationInfo.FLAG_SYSTEM);
+        assertThat(factoryInfo.applicationInfo.flags & ApplicationInfo.FLAG_INSTALLED)
+                .isEqualTo(ApplicationInfo.FLAG_INSTALLED);
     }
 
     @Test
diff --git a/services/tests/servicestests/src/com/android/server/tare/LedgerTest.java b/services/tests/servicestests/src/com/android/server/tare/LedgerTest.java
index 22dcf84..54566c3 100644
--- a/services/tests/servicestests/src/com/android/server/tare/LedgerTest.java
+++ b/services/tests/servicestests/src/com/android/server/tare/LedgerTest.java
@@ -22,7 +22,12 @@
 import static com.android.server.tare.TareUtils.getCurrentTimeMillis;
 
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+import android.util.SparseLongArray;
 
 import androidx.test.filters.SmallTest;
 import androidx.test.runner.AndroidJUnit4;
@@ -32,7 +37,10 @@
 import org.junit.runner.RunWith;
 
 import java.time.Clock;
+import java.time.Duration;
 import java.time.ZoneOffset;
+import java.util.ArrayList;
+import java.util.List;
 
 /** Test that the ledger records transactions correctly. */
 @RunWith(AndroidJUnit4.class)
@@ -44,6 +52,11 @@
         TareUtils.sSystemClock = Clock.fixed(Clock.systemUTC().instant(), ZoneOffset.UTC);
     }
 
+    private void shiftSystemTime(long incrementMs) {
+        TareUtils.sSystemClock =
+                Clock.offset(TareUtils.sSystemClock, Duration.ofMillis(incrementMs));
+    }
+
     @Test
     public void testInitialState() {
         final Ledger ledger = new Ledger();
@@ -52,37 +65,149 @@
     }
 
     @Test
+    public void testInitialization_FullLists() {
+        final long balance = 1234567890L;
+        List<Ledger.Transaction> transactions = new ArrayList<>();
+        List<Ledger.RewardBucket> rewardBuckets = new ArrayList<>();
+
+        final long now = getCurrentTimeMillis();
+        Ledger.Transaction secondTxn = null;
+        Ledger.RewardBucket remainingBucket = null;
+        for (int i = 0; i < Ledger.MAX_TRANSACTION_COUNT; ++i) {
+            final long start = now - 10 * HOUR_IN_MILLIS + i * MINUTE_IN_MILLIS;
+            Ledger.Transaction transaction = new Ledger.Transaction(
+                    start, start + MINUTE_IN_MILLIS, 1, null, 400, 0);
+            if (i == 1) {
+                secondTxn = transaction;
+            }
+            transactions.add(transaction);
+        }
+        for (int b = 0; b < Ledger.NUM_REWARD_BUCKET_WINDOWS; ++b) {
+            final long start = now - (Ledger.NUM_REWARD_BUCKET_WINDOWS - b) * 24 * HOUR_IN_MILLIS;
+            Ledger.RewardBucket rewardBucket = new Ledger.RewardBucket();
+            rewardBucket.startTimeMs = start;
+            for (int r = 0; r < 5; ++r) {
+                rewardBucket.cumulativeDelta.put(EconomicPolicy.TYPE_REWARD | r, b * start + r);
+            }
+            if (b == Ledger.NUM_REWARD_BUCKET_WINDOWS - 1) {
+                remainingBucket = rewardBucket;
+            }
+            rewardBuckets.add(rewardBucket);
+        }
+        final Ledger ledger = new Ledger(balance, transactions, rewardBuckets);
+        assertEquals(balance, ledger.getCurrentBalance());
+        assertEquals(transactions, ledger.getTransactions());
+        // Everything but the last bucket is old, so the returned list should only contain that
+        // bucket.
+        rewardBuckets.clear();
+        rewardBuckets.add(remainingBucket);
+        assertEquals(rewardBuckets, ledger.getRewardBuckets());
+
+        // Make sure the ledger can properly record new transactions.
+        final long start = now - MINUTE_IN_MILLIS;
+        final long delta = 400;
+        final Ledger.Transaction transaction = new Ledger.Transaction(
+                start, start + MINUTE_IN_MILLIS, EconomicPolicy.TYPE_REWARD | 1, null, delta, 0);
+        ledger.recordTransaction(transaction);
+        assertEquals(balance + delta, ledger.getCurrentBalance());
+        transactions = ledger.getTransactions();
+        assertEquals(secondTxn, transactions.get(0));
+        assertEquals(transaction, transactions.get(Ledger.MAX_TRANSACTION_COUNT - 1));
+        final Ledger.RewardBucket rewardBucket = new Ledger.RewardBucket();
+        rewardBucket.startTimeMs = now;
+        rewardBucket.cumulativeDelta.put(EconomicPolicy.TYPE_REWARD | 1, delta);
+        rewardBuckets = ledger.getRewardBuckets();
+        assertRewardBucketsEqual(remainingBucket, rewardBuckets.get(0));
+        assertRewardBucketsEqual(rewardBucket, rewardBuckets.get(1));
+    }
+
+    @Test
+    public void testInitialization_OverflowingLists() {
+        final long balance = 1234567890L;
+        final List<Ledger.Transaction> transactions = new ArrayList<>();
+        final List<Ledger.RewardBucket> rewardBuckets = new ArrayList<>();
+
+        final long now = getCurrentTimeMillis();
+        for (int i = 0; i < 2 * Ledger.MAX_TRANSACTION_COUNT; ++i) {
+            final long start = now - 20 * HOUR_IN_MILLIS + i * MINUTE_IN_MILLIS;
+            Ledger.Transaction transaction = new Ledger.Transaction(
+                    start, start + MINUTE_IN_MILLIS, 1, null, 400, 0);
+            transactions.add(transaction);
+        }
+        for (int b = 0; b < 2 * Ledger.NUM_REWARD_BUCKET_WINDOWS; ++b) {
+            final long start = now
+                    - (2 * Ledger.NUM_REWARD_BUCKET_WINDOWS - b) * 6 * HOUR_IN_MILLIS;
+            Ledger.RewardBucket rewardBucket = new Ledger.RewardBucket();
+            rewardBucket.startTimeMs = start;
+            for (int r = 0; r < 5; ++r) {
+                rewardBucket.cumulativeDelta.put(EconomicPolicy.TYPE_REWARD | r, b * start + r);
+            }
+            rewardBuckets.add(rewardBucket);
+        }
+        final Ledger ledger = new Ledger(balance, transactions, rewardBuckets);
+        assertEquals(balance, ledger.getCurrentBalance());
+        assertEquals(transactions.subList(Ledger.MAX_TRANSACTION_COUNT,
+                        2 * Ledger.MAX_TRANSACTION_COUNT),
+                ledger.getTransactions());
+        assertEquals(rewardBuckets.subList(Ledger.NUM_REWARD_BUCKET_WINDOWS,
+                        2 * Ledger.NUM_REWARD_BUCKET_WINDOWS),
+                ledger.getRewardBuckets());
+    }
+
+    @Test
     public void testMultipleTransactions() {
         final Ledger ledger = new Ledger();
         ledger.recordTransaction(new Ledger.Transaction(0, 1000, 1, null, 5, 0));
         assertEquals(5, ledger.getCurrentBalance());
-        assertEquals(5, ledger.get24HourSum(1, 60_000));
         ledger.recordTransaction(new Ledger.Transaction(2000, 2000, 1, null, 25, 0));
         assertEquals(30, ledger.getCurrentBalance());
-        assertEquals(30, ledger.get24HourSum(1, 60_000));
         ledger.recordTransaction(new Ledger.Transaction(5000, 5500, 1, null, -10, 5));
         assertEquals(20, ledger.getCurrentBalance());
-        assertEquals(20, ledger.get24HourSum(1, 60_000));
     }
 
     @Test
     public void test24HourSum() {
+        final long now = getCurrentTimeMillis();
+        final long end = now + 24 * HOUR_IN_MILLIS;
+        final int reward1 = EconomicPolicy.TYPE_REWARD | 1;
+        final int reward2 = EconomicPolicy.TYPE_REWARD | 2;
         final Ledger ledger = new Ledger();
-        ledger.recordTransaction(new Ledger.Transaction(0, 1000, 1, null, 500, 0));
-        assertEquals(500, ledger.get24HourSum(1, 24 * HOUR_IN_MILLIS));
+
+        // First bucket
+        assertEquals(0, ledger.get24HourSum(reward1, end));
+        ledger.recordTransaction(new Ledger.Transaction(now, now + 1000, reward1, null, 500, 0));
+        assertEquals(500, ledger.get24HourSum(reward1, end));
+        assertEquals(0, ledger.get24HourSum(reward2, end));
         ledger.recordTransaction(
-                new Ledger.Transaction(2 * HOUR_IN_MILLIS, 3 * HOUR_IN_MILLIS, 1, null, 2500, 0));
-        assertEquals(3000, ledger.get24HourSum(1, 24 * HOUR_IN_MILLIS));
+                new Ledger.Transaction(now + 2 * HOUR_IN_MILLIS, now + 3 * HOUR_IN_MILLIS,
+                        reward1, null, 2500, 0));
+        assertEquals(3000, ledger.get24HourSum(reward1, end));
+        // Second bucket
+        shiftSystemTime(7 * HOUR_IN_MILLIS); // now + 7
         ledger.recordTransaction(
-                new Ledger.Transaction(4 * HOUR_IN_MILLIS, 4 * HOUR_IN_MILLIS, 1, null, 1, 0));
-        assertEquals(3001, ledger.get24HourSum(1, 24 * HOUR_IN_MILLIS));
-        assertEquals(2501, ledger.get24HourSum(1, 25 * HOUR_IN_MILLIS));
-        assertEquals(2501, ledger.get24HourSum(1, 26 * HOUR_IN_MILLIS));
-        // Pro-rated as the second transaction phases out
-        assertEquals(1251,
-                ledger.get24HourSum(1, 26 * HOUR_IN_MILLIS + 30 * MINUTE_IN_MILLIS));
-        assertEquals(1, ledger.get24HourSum(1, 27 * HOUR_IN_MILLIS));
-        assertEquals(0, ledger.get24HourSum(1, 28 * HOUR_IN_MILLIS));
+                new Ledger.Transaction(now + 7 * HOUR_IN_MILLIS, now + 7 * HOUR_IN_MILLIS,
+                        reward1, null, 1, 0));
+        ledger.recordTransaction(
+                new Ledger.Transaction(now + 7 * HOUR_IN_MILLIS, now + 7 * HOUR_IN_MILLIS,
+                        reward2, null, 42, 0));
+        assertEquals(3001, ledger.get24HourSum(reward1, end));
+        assertEquals(42, ledger.get24HourSum(reward2, end));
+        // Third bucket
+        shiftSystemTime(12 * HOUR_IN_MILLIS); // now + 19
+        ledger.recordTransaction(
+                new Ledger.Transaction(now + 12 * HOUR_IN_MILLIS, now + 13 * HOUR_IN_MILLIS,
+                        reward1, null, 300, 0));
+        assertEquals(3301, ledger.get24HourSum(reward1, end));
+        assertRewardBucketsInOrder(ledger.getRewardBuckets());
+        // Older buckets should be excluded
+        assertEquals(301, ledger.get24HourSum(reward1, end + HOUR_IN_MILLIS));
+        assertEquals(301, ledger.get24HourSum(reward1, end + 2 * HOUR_IN_MILLIS));
+        // 2nd bucket should still be included since it started at the 7 hour mark
+        assertEquals(301, ledger.get24HourSum(reward1, end + 6 * HOUR_IN_MILLIS));
+        assertEquals(42, ledger.get24HourSum(reward2, end + 6 * HOUR_IN_MILLIS));
+        assertEquals(300, ledger.get24HourSum(reward1, end + 7 * HOUR_IN_MILLIS + 1));
+        assertEquals(0, ledger.get24HourSum(reward2, end + 8 * HOUR_IN_MILLIS));
+        assertEquals(0, ledger.get24HourSum(reward1, end + 19 * HOUR_IN_MILLIS + 1));
     }
 
     @Test
@@ -125,4 +250,127 @@
         ledger.removeOldTransactions(0);
         assertNull(ledger.getEarliestTransaction());
     }
+
+    @Test
+    public void testTransactionsAlwaysInOrder() {
+        final Ledger ledger = new Ledger();
+        List<Ledger.Transaction> transactions = ledger.getTransactions();
+        assertTrue(transactions.isEmpty());
+
+        final long now = getCurrentTimeMillis();
+        Ledger.Transaction transaction1 = new Ledger.Transaction(
+                now - 48 * HOUR_IN_MILLIS, now - 40 * HOUR_IN_MILLIS, 1, null, 4800, 0);
+        Ledger.Transaction transaction2 = new Ledger.Transaction(
+                now - 24 * HOUR_IN_MILLIS, now - 23 * HOUR_IN_MILLIS, 1, null, 600, 0);
+        Ledger.Transaction transaction3 = new Ledger.Transaction(
+                now - 22 * HOUR_IN_MILLIS, now - 21 * HOUR_IN_MILLIS, 1, null, 600, 0);
+        // Instant event
+        Ledger.Transaction transaction4 = new Ledger.Transaction(
+                now - 20 * HOUR_IN_MILLIS, now - 20 * HOUR_IN_MILLIS, 1, null, 500, 0);
+
+        Ledger.Transaction transaction5 = new Ledger.Transaction(
+                now - 15 * HOUR_IN_MILLIS, now - 15 * HOUR_IN_MILLIS + MINUTE_IN_MILLIS,
+                1, null, 400, 0);
+        ledger.recordTransaction(transaction1);
+        ledger.recordTransaction(transaction2);
+        ledger.recordTransaction(transaction3);
+        ledger.recordTransaction(transaction4);
+        ledger.recordTransaction(transaction5);
+
+        transactions = ledger.getTransactions();
+        assertEquals(5, transactions.size());
+        assertTransactionsInOrder(transactions);
+
+        for (int i = 0; i < Ledger.MAX_TRANSACTION_COUNT - 5; ++i) {
+            final long start = now - 10 * HOUR_IN_MILLIS + i * MINUTE_IN_MILLIS;
+            Ledger.Transaction transaction = new Ledger.Transaction(
+                    start, start + MINUTE_IN_MILLIS, 1, null, 400, 0);
+            ledger.recordTransaction(transaction);
+        }
+        transactions = ledger.getTransactions();
+        assertEquals(Ledger.MAX_TRANSACTION_COUNT, transactions.size());
+        assertTransactionsInOrder(transactions);
+
+        long start = now - 5 * HOUR_IN_MILLIS;
+        Ledger.Transaction transactionLast5 = new Ledger.Transaction(
+                start, start + MINUTE_IN_MILLIS, 1, null, 4800, 0);
+        start = now - 4 * HOUR_IN_MILLIS;
+        Ledger.Transaction transactionLast4 = new Ledger.Transaction(
+                start, start + MINUTE_IN_MILLIS, 1, null, 600, 0);
+        start = now - 3 * HOUR_IN_MILLIS;
+        Ledger.Transaction transactionLast3 = new Ledger.Transaction(
+                start, start + MINUTE_IN_MILLIS, 1, null, 600, 0);
+        // Instant event
+        start = now - 2 * HOUR_IN_MILLIS;
+        Ledger.Transaction transactionLast2 = new Ledger.Transaction(
+                start, start, 1, null, 500, 0);
+        Ledger.Transaction transactionLast1 = new Ledger.Transaction(
+                start, start + MINUTE_IN_MILLIS, 1, null, 400, 0);
+        ledger.recordTransaction(transactionLast5);
+        ledger.recordTransaction(transactionLast4);
+        ledger.recordTransaction(transactionLast3);
+        ledger.recordTransaction(transactionLast2);
+        ledger.recordTransaction(transactionLast1);
+
+        transactions = ledger.getTransactions();
+        assertEquals(Ledger.MAX_TRANSACTION_COUNT, transactions.size());
+        assertTransactionsInOrder(transactions);
+        assertEquals(transactionLast1, transactions.get(Ledger.MAX_TRANSACTION_COUNT - 1));
+        assertEquals(transactionLast2, transactions.get(Ledger.MAX_TRANSACTION_COUNT - 2));
+        assertEquals(transactionLast3, transactions.get(Ledger.MAX_TRANSACTION_COUNT - 3));
+        assertEquals(transactionLast4, transactions.get(Ledger.MAX_TRANSACTION_COUNT - 4));
+        assertEquals(transactionLast5, transactions.get(Ledger.MAX_TRANSACTION_COUNT - 5));
+        assertFalse(transactions.contains(transaction1));
+        assertFalse(transactions.contains(transaction2));
+        assertFalse(transactions.contains(transaction3));
+        assertFalse(transactions.contains(transaction4));
+        assertFalse(transactions.contains(transaction5));
+    }
+
+    private void assertSparseLongArraysEqual(SparseLongArray expected, SparseLongArray actual) {
+        if (expected == null) {
+            assertNull(actual);
+            return;
+        }
+        assertNotNull(actual);
+        final int size = expected.size();
+        assertEquals(size, actual.size());
+        for (int i = 0; i < size; ++i) {
+            assertEquals(expected.keyAt(i), actual.keyAt(i));
+            assertEquals(expected.valueAt(i), actual.valueAt(i));
+        }
+    }
+
+    private void assertRewardBucketsEqual(Ledger.RewardBucket expected,
+            Ledger.RewardBucket actual) {
+        if (expected == null) {
+            assertNull(actual);
+            return;
+        }
+        assertNotNull(actual);
+        assertEquals(expected.startTimeMs, actual.startTimeMs);
+        assertSparseLongArraysEqual(expected.cumulativeDelta, actual.cumulativeDelta);
+    }
+
+    private void assertRewardBucketsInOrder(List<Ledger.RewardBucket> rewardBuckets) {
+        assertNotNull(rewardBuckets);
+        for (int i = 1; i < rewardBuckets.size(); ++i) {
+            final Ledger.RewardBucket prev = rewardBuckets.get(i - 1);
+            final Ledger.RewardBucket cur = rewardBuckets.get(i);
+            assertTrue("Newer bucket stored before older bucket @ index " + i
+                            + ": " + prev.startTimeMs + " vs " + cur.startTimeMs,
+                    prev.startTimeMs <= cur.startTimeMs);
+        }
+    }
+
+    private void assertTransactionsInOrder(List<Ledger.Transaction> transactions) {
+        assertNotNull(transactions);
+        for (int i = 1; i < transactions.size(); ++i) {
+            final Ledger.Transaction prev = transactions.get(i - 1);
+            final Ledger.Transaction cur = transactions.get(i);
+            assertTrue("Newer transaction stored before older transaction @ index " + i
+                            + ": " + prev.endTimeMs + " vs " + cur.endTimeMs,
+                    prev.endTimeMs <= cur.endTimeMs);
+        }
+    }
 }