Merge "Disable media route scanning"
diff --git a/api/current.txt b/api/current.txt
index 1ffba87..3c3cfcc 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -16296,7 +16296,7 @@
 
   public final class TotalCaptureResult extends android.hardware.camera2.CaptureResult {
     method public java.util.List<android.hardware.camera2.CaptureResult> getPartialResults();
-    method public <T> T getPhysicalCameraKey(android.hardware.camera2.CaptureResult.Key<T>, java.lang.String);
+    method public java.util.Map<java.lang.String, android.hardware.camera2.CaptureResult> getPhysicalCameraResults();
   }
 
 }
diff --git a/api/system-current.txt b/api/system-current.txt
index 70148d9..2d3b65a 100644
--- a/api/system-current.txt
+++ b/api/system-current.txt
@@ -2516,6 +2516,7 @@
     method public int requestAudioFocus(android.media.AudioManager.OnAudioFocusChangeListener, android.media.AudioAttributes, int, int) throws java.lang.IllegalArgumentException;
     method public deprecated int requestAudioFocus(android.media.AudioManager.OnAudioFocusChangeListener, android.media.AudioAttributes, int, int, android.media.audiopolicy.AudioPolicy) throws java.lang.IllegalArgumentException;
     method public int requestAudioFocus(android.media.AudioFocusRequest, android.media.audiopolicy.AudioPolicy);
+    method public void setFocusRequestResult(android.media.AudioFocusInfo, int, android.media.audiopolicy.AudioPolicy);
     method public void unregisterAudioPolicyAsync(android.media.audiopolicy.AudioPolicy);
     field public static final int AUDIOFOCUS_FLAG_DELAY_OK = 1; // 0x1
     field public static final int AUDIOFOCUS_FLAG_LOCK = 4; // 0x4
diff --git a/api/test-current.txt b/api/test-current.txt
index ac4ce3b..c30c056 100644
--- a/api/test-current.txt
+++ b/api/test-current.txt
@@ -211,6 +211,7 @@
     method public abstract boolean isPermissionReviewModeEnabled();
     field public static final java.lang.String FEATURE_ADOPTABLE_STORAGE = "android.software.adoptable_storage";
     field public static final java.lang.String FEATURE_FILE_BASED_ENCRYPTION = "android.software.file_based_encryption";
+    field public static final int MATCH_FACTORY_ONLY = 2097152; // 0x200000
   }
 
   public class PermissionInfo extends android.content.pm.PackageItemInfo implements android.os.Parcelable {
diff --git a/cmds/statsd/src/StatsLogProcessor.h b/cmds/statsd/src/StatsLogProcessor.h
index 7642aafa..17741a8 100644
--- a/cmds/statsd/src/StatsLogProcessor.h
+++ b/cmds/statsd/src/StatsLogProcessor.h
@@ -100,7 +100,8 @@
     FRIEND_TEST(StatsLogProcessorTest, TestRateLimitBroadcast);
     FRIEND_TEST(StatsLogProcessorTest, TestDropWhenByteSizeTooLarge);
     FRIEND_TEST(StatsLogProcessorTest, TestDropWhenByteSizeTooLarge);
-    FRIEND_TEST(WakelockDurationE2eTest, TestAggregatedPredicateDimensions);
+    FRIEND_TEST(WakelockDurationE2eTest, TestAggregatedPredicateDimensionsForSumDuration);
+    FRIEND_TEST(WakelockDurationE2eTest, TestAggregatedPredicateDimensionsForMaxDuration);
     FRIEND_TEST(MetricConditionLinkE2eTest, TestMultiplePredicatesAndLinks);
     FRIEND_TEST(AttributionE2eTest, TestAttributionMatchAndSlice);
     FRIEND_TEST(GaugeMetricE2eTest, TestMultipleFieldsForPushedEvent);
diff --git a/cmds/statsd/src/anomaly/AnomalyTracker.cpp b/cmds/statsd/src/anomaly/AnomalyTracker.cpp
index c84a5b4..79b7d9c 100644
--- a/cmds/statsd/src/anomaly/AnomalyTracker.cpp
+++ b/cmds/statsd/src/anomaly/AnomalyTracker.cpp
@@ -35,9 +35,7 @@
 
 // TODO: Get rid of bucketNumbers, and return to the original circular array method.
 AnomalyTracker::AnomalyTracker(const Alert& alert, const ConfigKey& configKey)
-    : mAlert(alert),
-      mConfigKey(configKey),
-      mNumOfPastBuckets(mAlert.num_buckets() - 1) {
+    : mAlert(alert), mConfigKey(configKey), mNumOfPastBuckets(mAlert.num_buckets() - 1) {
     VLOG("AnomalyTracker() called");
     if (mAlert.num_buckets() <= 0) {
         ALOGE("Cannot create AnomalyTracker with %lld buckets",
diff --git a/cmds/statsd/src/atoms.proto b/cmds/statsd/src/atoms.proto
index ed570e7..e58c535 100644
--- a/cmds/statsd/src/atoms.proto
+++ b/cmds/statsd/src/atoms.proto
@@ -97,6 +97,7 @@
         BootSequenceReported boot_sequence_reported = 57;
         DaveyOccurred davey_occurred = 58;
         OverlayStateChanged overlay_state_changed = 59;
+        ForegroundServiceStateChanged foreground_service_state_changed = 60;
         // TODO: Reorder the numbering so that the most frequent occur events occur in the first 15.
     }
 
@@ -998,6 +999,25 @@
     optional State state = 4;
 }
 
+/*
+ * Logs foreground service starts and stops.
+ * Note that this is not when a service starts or stops, but when it is
+ * considered foreground.
+ * Logged from
+ *     //frameworks/base/services/core/java/com/android/server/am/ActiveServices.java
+ */
+message ForegroundServiceStateChanged {
+    optional int32 uid = 1;
+    // package_name + "/" + class_name
+    optional string short_name = 2;
+
+    enum State {
+        ENTER = 1;
+        EXIT = 2;
+    }
+    optional State state = 3;
+}
+
 /**
  * Pulls bytes transferred via wifi (Sum of foreground and background usage).
  *
@@ -1463,3 +1483,4 @@
 message FullBatteryCapacity {
     optional int32 capacity_uAh = 1;
 }
+
diff --git a/cmds/statsd/src/metrics/CountMetricProducer.cpp b/cmds/statsd/src/metrics/CountMetricProducer.cpp
index ae4df3e..5b5b57b 100644
--- a/cmds/statsd/src/metrics/CountMetricProducer.cpp
+++ b/cmds/statsd/src/metrics/CountMetricProducer.cpp
@@ -170,7 +170,6 @@
     protoOutput->write(FIELD_TYPE_INT64 | FIELD_ID_END_REPORT_NANOS, (long long)dumpTimeNs);
 
     mPastBuckets.clear();
-    mStartTimeNs = mCurrentBucketStartTimeNs;
 
     // TODO: Clear mDimensionKeyMap once the report is dumped.
 }
@@ -214,13 +213,11 @@
     }
 
     auto it = mCurrentSlicedCounter->find(eventKey);
-
     if (it == mCurrentSlicedCounter->end()) {
         // ===========GuardRail==============
         if (hitGuardRailLocked(eventKey)) {
             return;
         }
-
         // create a counter for the new key
         (*mCurrentSlicedCounter)[eventKey] = 1;
     } else {
@@ -228,10 +225,14 @@
         auto& count = it->second;
         count++;
     }
-
     for (auto& tracker : mAnomalyTrackers) {
+        int64_t countWholeBucket = mCurrentSlicedCounter->find(eventKey)->second;
+        auto prev = mCurrentFullCounters->find(eventKey);
+        if (prev != mCurrentFullCounters->end()) {
+            countWholeBucket += prev->second;
+        }
         tracker->detectAndDeclareAnomaly(eventTimeNs, mCurrentBucketNum, eventKey,
-                                         mCurrentSlicedCounter->find(eventKey)->second);
+                                         countWholeBucket);
     }
 
     VLOG("metric %lld %s->%lld", (long long)mMetricId, eventKey.c_str(),
@@ -241,33 +242,65 @@
 // When a new matched event comes in, we check if event falls into the current
 // bucket. If not, flush the old counter to past buckets and initialize the new bucket.
 void CountMetricProducer::flushIfNeededLocked(const uint64_t& eventTimeNs) {
-    if (eventTimeNs < mCurrentBucketStartTimeNs + mBucketSizeNs) {
+    uint64_t currentBucketEndTimeNs = getCurrentBucketEndTimeNs();
+    if (eventTimeNs < currentBucketEndTimeNs) {
         return;
     }
 
+    flushCurrentBucketLocked(eventTimeNs);
+    // Setup the bucket start time and number.
+    uint64_t numBucketsForward = 1 + (eventTimeNs - currentBucketEndTimeNs) / mBucketSizeNs;
+    mCurrentBucketStartTimeNs = currentBucketEndTimeNs + (numBucketsForward - 1) * mBucketSizeNs;
+    mCurrentBucketNum += numBucketsForward;
+    VLOG("metric %lld: new bucket start time: %lld", (long long)mMetricId,
+         (long long)mCurrentBucketStartTimeNs);
+}
+
+void CountMetricProducer::flushCurrentBucketLocked(const uint64_t& eventTimeNs) {
+    uint64_t fullBucketEndTimeNs = getCurrentBucketEndTimeNs();
     CountBucket info;
     info.mBucketStartNs = mCurrentBucketStartTimeNs;
-    info.mBucketEndNs = mCurrentBucketStartTimeNs + mBucketSizeNs;
+    if (eventTimeNs < fullBucketEndTimeNs) {
+        info.mBucketEndNs = eventTimeNs;
+    } else {
+        info.mBucketEndNs = fullBucketEndTimeNs;
+    }
     info.mBucketNum = mCurrentBucketNum;
     for (const auto& counter : *mCurrentSlicedCounter) {
         info.mCount = counter.second;
         auto& bucketList = mPastBuckets[counter.first];
         bucketList.push_back(info);
-        VLOG("metric %lld, dump key value: %s -> %lld",
-            (long long)mMetricId, counter.first.c_str(), (long long)counter.second);
+        VLOG("metric %lld, dump key value: %s -> %lld", (long long)mMetricId, counter.first.c_str(),
+             (long long)counter.second);
     }
 
-    for (auto& tracker : mAnomalyTrackers) {
-        tracker->addPastBucket(mCurrentSlicedCounter, mCurrentBucketNum);
+    // If we have finished a full bucket, then send this to anomaly tracker.
+    if (eventTimeNs > fullBucketEndTimeNs) {
+        // Accumulate partial buckets with current value and then send to anomaly tracker.
+        if (mCurrentFullCounters->size() > 0) {
+            for (const auto& keyValuePair : *mCurrentSlicedCounter) {
+                (*mCurrentFullCounters)[keyValuePair.first] += keyValuePair.second;
+            }
+            for (auto& tracker : mAnomalyTrackers) {
+                tracker->addPastBucket(mCurrentFullCounters, mCurrentBucketNum);
+            }
+            mCurrentFullCounters = std::make_shared<DimToValMap>();
+        } else {
+            // Skip aggregating the partial buckets since there's no previous partial bucket.
+            for (auto& tracker : mAnomalyTrackers) {
+                tracker->addPastBucket(mCurrentSlicedCounter, mCurrentBucketNum);
+            }
+        }
+    } else {
+        // Accumulate partial bucket.
+        for (const auto& keyValuePair : *mCurrentSlicedCounter) {
+            (*mCurrentFullCounters)[keyValuePair.first] += keyValuePair.second;
+        }
     }
 
-    // Reset counters (do not clear, since the old one is still referenced in mAnomalyTrackers).
+    // Only resets the counters, but doesn't setup the times nor numbers.
+    // (Do not clear since the old one is still referenced in mAnomalyTrackers).
     mCurrentSlicedCounter = std::make_shared<DimToValMap>();
-    uint64_t numBucketsForward = (eventTimeNs - mCurrentBucketStartTimeNs) / mBucketSizeNs;
-    mCurrentBucketStartTimeNs = mCurrentBucketStartTimeNs + numBucketsForward * mBucketSizeNs;
-    mCurrentBucketNum += numBucketsForward;
-    VLOG("metric %lld: new bucket start time: %lld", (long long)mMetricId,
-         (long long)mCurrentBucketStartTimeNs);
 }
 
 // Rough estimate of CountMetricProducer buffer stored. This number will be
diff --git a/cmds/statsd/src/metrics/CountMetricProducer.h b/cmds/statsd/src/metrics/CountMetricProducer.h
index 8659d47..b06c77b 100644
--- a/cmds/statsd/src/metrics/CountMetricProducer.h
+++ b/cmds/statsd/src/metrics/CountMetricProducer.h
@@ -71,14 +71,20 @@
     void dumpStatesLocked(FILE* out, bool verbose) const override{};
 
     // Util function to flush the old packet.
-    void flushIfNeededLocked(const uint64_t& newEventTime);
+    void flushIfNeededLocked(const uint64_t& newEventTime) override;
+
+    void flushCurrentBucketLocked(const uint64_t& eventTimeNs) override;
 
     // TODO: Add a lock to mPastBuckets.
     std::unordered_map<MetricDimensionKey, std::vector<CountBucket>> mPastBuckets;
 
-    // The current bucket.
+    // The current bucket (may be a partial bucket).
     std::shared_ptr<DimToValMap> mCurrentSlicedCounter = std::make_shared<DimToValMap>();
 
+    // The sum of previous partial buckets in the current full bucket (excluding the current
+    // partial bucket). This is only updated while flushing the current bucket.
+    std::shared_ptr<DimToValMap> mCurrentFullCounters = std::make_shared<DimToValMap>();
+
     static const size_t kBucketSize = sizeof(CountBucket{});
 
     bool hitGuardRailLocked(const MetricDimensionKey& newKey);
@@ -87,6 +93,8 @@
     FRIEND_TEST(CountMetricProducerTest, TestEventsWithNonSlicedCondition);
     FRIEND_TEST(CountMetricProducerTest, TestEventsWithSlicedCondition);
     FRIEND_TEST(CountMetricProducerTest, TestAnomalyDetectionUnSliced);
+    FRIEND_TEST(CountMetricProducerTest, TestEventWithAppUpgrade);
+    FRIEND_TEST(CountMetricProducerTest, TestEventWithAppUpgradeInNextBucket);
 };
 
 }  // namespace statsd
diff --git a/cmds/statsd/src/metrics/DurationMetricProducer.cpp b/cmds/statsd/src/metrics/DurationMetricProducer.cpp
index efbdae1..2400eba1 100644
--- a/cmds/statsd/src/metrics/DurationMetricProducer.cpp
+++ b/cmds/statsd/src/metrics/DurationMetricProducer.cpp
@@ -121,13 +121,13 @@
         case DurationMetric_AggregationType_SUM:
             return make_unique<OringDurationTracker>(
                     mConfigKey, mMetricId, eventKey, mWizard, mConditionTrackerIndex,
-                    mDimensionsInCondition, mNested,
-                    mCurrentBucketStartTimeNs, mBucketSizeNs, mConditionSliced, mAnomalyTrackers);
+                    mDimensionsInCondition, mNested, mCurrentBucketStartTimeNs, mCurrentBucketNum,
+                    mStartTimeNs, mBucketSizeNs, mConditionSliced, mAnomalyTrackers);
         case DurationMetric_AggregationType_MAX_SPARSE:
             return make_unique<MaxDurationTracker>(
                     mConfigKey, mMetricId, eventKey, mWizard, mConditionTrackerIndex,
-                    mDimensionsInCondition, mNested,
-                    mCurrentBucketStartTimeNs, mBucketSizeNs, mConditionSliced, mAnomalyTrackers);
+                    mDimensionsInCondition, mNested, mCurrentBucketStartTimeNs, mCurrentBucketNum,
+                    mStartTimeNs, mBucketSizeNs, mConditionSliced, mAnomalyTrackers);
     }
 }
 
@@ -252,17 +252,18 @@
     protoOutput->end(protoToken);
     protoOutput->write(FIELD_TYPE_INT64 | FIELD_ID_END_REPORT_NANOS, (long long)dumpTimeNs);
     mPastBuckets.clear();
-    mStartTimeNs = mCurrentBucketStartTimeNs;
 }
 
-void DurationMetricProducer::flushIfNeededLocked(const uint64_t& eventTime) {
-    if (mCurrentBucketStartTimeNs + mBucketSizeNs > eventTime) {
+void DurationMetricProducer::flushIfNeededLocked(const uint64_t& eventTimeNs) {
+    uint64_t currentBucketEndTimeNs = getCurrentBucketEndTimeNs();
+
+    if (currentBucketEndTimeNs > eventTimeNs) {
         return;
     }
     VLOG("flushing...........");
     for (auto it = mCurrentSlicedDurationTrackerMap.begin();
             it != mCurrentSlicedDurationTrackerMap.end();) {
-        if (it->second->flushIfNeeded(eventTime, &mPastBuckets)) {
+        if (it->second->flushIfNeeded(eventTimeNs, &mPastBuckets)) {
             VLOG("erase bucket for key %s", it->first.c_str());
             it = mCurrentSlicedDurationTrackerMap.erase(it);
         } else {
@@ -270,11 +271,23 @@
         }
     }
 
-    int numBucketsForward = (eventTime - mCurrentBucketStartTimeNs) / mBucketSizeNs;
-    mCurrentBucketStartTimeNs += numBucketsForward * mBucketSizeNs;
+    int numBucketsForward = 1 + (eventTimeNs - currentBucketEndTimeNs) / mBucketSizeNs;
+    mCurrentBucketStartTimeNs = currentBucketEndTimeNs + (numBucketsForward - 1) * mBucketSizeNs;
     mCurrentBucketNum += numBucketsForward;
 }
 
+void DurationMetricProducer::flushCurrentBucketLocked(const uint64_t& eventTimeNs) {
+    for (auto it = mCurrentSlicedDurationTrackerMap.begin();
+         it != mCurrentSlicedDurationTrackerMap.end();) {
+        if (it->second->flushCurrentBucket(eventTimeNs, &mPastBuckets)) {
+            VLOG("erase bucket for key %s", it->first.c_str());
+            it = mCurrentSlicedDurationTrackerMap.erase(it);
+        } else {
+            ++it;
+        }
+    }
+}
+
 void DurationMetricProducer::dumpStatesLocked(FILE* out, bool verbose) const {
     if (mCurrentSlicedDurationTrackerMap.size() == 0) {
         return;
diff --git a/cmds/statsd/src/metrics/DurationMetricProducer.h b/cmds/statsd/src/metrics/DurationMetricProducer.h
index 152e570..a496016 100644
--- a/cmds/statsd/src/metrics/DurationMetricProducer.h
+++ b/cmds/statsd/src/metrics/DurationMetricProducer.h
@@ -73,6 +73,8 @@
     // Util function to flush the old packet.
     void flushIfNeededLocked(const uint64_t& eventTime);
 
+    void flushCurrentBucketLocked(const uint64_t& eventTimeNs) override;
+
     const DurationMetric_AggregationType mAggregationType;
 
     // Index of the SimpleAtomMatcher which defines the start.
@@ -112,6 +114,10 @@
 
     FRIEND_TEST(DurationMetricTrackerTest, TestNoCondition);
     FRIEND_TEST(DurationMetricTrackerTest, TestNonSlicedCondition);
+    FRIEND_TEST(DurationMetricTrackerTest, TestSumDurationWithUpgrade);
+    FRIEND_TEST(DurationMetricTrackerTest, TestSumDurationWithUpgradeInFollowingBucket);
+    FRIEND_TEST(DurationMetricTrackerTest, TestMaxDurationWithUpgrade);
+    FRIEND_TEST(DurationMetricTrackerTest, TestMaxDurationWithUpgradeInNextBucket);
     FRIEND_TEST(WakelockDurationE2eTest, TestAggregatedPredicates);
 };
 
diff --git a/cmds/statsd/src/metrics/EventMetricProducer.cpp b/cmds/statsd/src/metrics/EventMetricProducer.cpp
index 820d591..a021e0a 100644
--- a/cmds/statsd/src/metrics/EventMetricProducer.cpp
+++ b/cmds/statsd/src/metrics/EventMetricProducer.cpp
@@ -118,7 +118,6 @@
                        reinterpret_cast<char*>(buffer.get()->data()), buffer.get()->size());
 
     startNewProtoOutputStreamLocked();
-    mStartTimeNs = dumpTimeNs;
 }
 
 void EventMetricProducer::onConditionChangedLocked(const bool conditionMet,
diff --git a/cmds/statsd/src/metrics/GaugeMetricProducer.cpp b/cmds/statsd/src/metrics/GaugeMetricProducer.cpp
index d6cb189..4190f00 100644
--- a/cmds/statsd/src/metrics/GaugeMetricProducer.cpp
+++ b/cmds/statsd/src/metrics/GaugeMetricProducer.cpp
@@ -191,10 +191,20 @@
     protoOutput->write(FIELD_TYPE_INT64 | FIELD_ID_END_REPORT_NANOS, (long long)dumpTimeNs);
 
     mPastBuckets.clear();
-    mStartTimeNs = mCurrentBucketStartTimeNs;
     // TODO: Clear mDimensionKeyMap once the report is dumped.
 }
 
+void GaugeMetricProducer::pullLocked() {
+    vector<std::shared_ptr<LogEvent>> allData;
+    if (!mStatsPullerManager->Pull(mPullTagId, &allData)) {
+        ALOGE("Stats puller failed for tag: %d", mPullTagId);
+        return;
+    }
+    for (const auto& data : allData) {
+        onMatchedLogEventLocked(0, *data);
+    }
+}
+
 void GaugeMetricProducer::onConditionChangedLocked(const bool conditionMet,
                                                    const uint64_t eventTime) {
     VLOG("Metric %lld onConditionChanged", (long long)mMetricId);
@@ -326,7 +336,6 @@
 }
 
 void GaugeMetricProducer::updateCurrentSlicedBucketForAnomaly() {
-    mCurrentSlicedBucketForAnomaly->clear();
     status_t err = NO_ERROR;
     for (const auto& slice : *mCurrentSlicedBucket) {
         if (slice.second.empty() || slice.second.front().mFields->empty()) {
@@ -349,42 +358,57 @@
 // if data is pushed, onMatchedLogEvent will only be called through onConditionChanged() inside
 // the GaugeMetricProducer while holding the lock.
 void GaugeMetricProducer::flushIfNeededLocked(const uint64_t& eventTimeNs) {
-    if (eventTimeNs < mCurrentBucketStartTimeNs + mBucketSizeNs) {
+    uint64_t currentBucketEndTimeNs = getCurrentBucketEndTimeNs();
+
+    if (eventTimeNs < currentBucketEndTimeNs) {
         VLOG("eventTime is %lld, less than next bucket start time %lld", (long long)eventTimeNs,
              (long long)(mCurrentBucketStartTimeNs + mBucketSizeNs));
         return;
     }
 
+    flushCurrentBucketLocked(eventTimeNs);
+
+    // Adjusts the bucket start and end times.
+    int64_t numBucketsForward = 1 + (eventTimeNs - currentBucketEndTimeNs) / mBucketSizeNs;
+    mCurrentBucketStartTimeNs = currentBucketEndTimeNs + (numBucketsForward - 1) * mBucketSizeNs;
+    mCurrentBucketNum += numBucketsForward;
+    VLOG("metric %lld: new bucket start time: %lld", (long long)mMetricId,
+         (long long)mCurrentBucketStartTimeNs);
+}
+
+void GaugeMetricProducer::flushCurrentBucketLocked(const uint64_t& eventTimeNs) {
+    uint64_t fullBucketEndTimeNs = getCurrentBucketEndTimeNs();
+
     GaugeBucket info;
     info.mBucketStartNs = mCurrentBucketStartTimeNs;
-    info.mBucketEndNs = mCurrentBucketStartTimeNs + mBucketSizeNs;
+    if (eventTimeNs < fullBucketEndTimeNs) {
+        info.mBucketEndNs = eventTimeNs;
+    } else {
+        info.mBucketEndNs = fullBucketEndTimeNs;
+    }
     info.mBucketNum = mCurrentBucketNum;
 
     for (const auto& slice : *mCurrentSlicedBucket) {
         info.mGaugeAtoms = slice.second;
         auto& bucketList = mPastBuckets[slice.first];
         bucketList.push_back(info);
-        VLOG("gauge metric %lld, dump key value: %s",
-            (long long)mMetricId, slice.first.c_str());
+        VLOG("gauge metric %lld, dump key value: %s", (long long)mMetricId, slice.first.c_str());
     }
 
-    // Reset counters
+    // If we have anomaly trackers, we need to update the partial bucket values.
     if (mAnomalyTrackers.size() > 0) {
         updateCurrentSlicedBucketForAnomaly();
-        for (auto& tracker : mAnomalyTrackers) {
-            tracker->addPastBucket(mCurrentSlicedBucketForAnomaly, mCurrentBucketNum);
+
+        if (eventTimeNs > fullBucketEndTimeNs) {
+            // This is known to be a full bucket, so send this data to the anomaly tracker.
+            for (auto& tracker : mAnomalyTrackers) {
+                tracker->addPastBucket(mCurrentSlicedBucketForAnomaly, mCurrentBucketNum);
+            }
+            mCurrentSlicedBucketForAnomaly = std::make_shared<DimToValMap>();
         }
     }
 
-    mCurrentSlicedBucketForAnomaly = std::make_shared<DimToValMap>();
     mCurrentSlicedBucket = std::make_shared<DimToGaugeAtomsMap>();
-
-    // Adjusts the bucket start time
-    int64_t numBucketsForward = (eventTimeNs - mCurrentBucketStartTimeNs) / mBucketSizeNs;
-    mCurrentBucketStartTimeNs = mCurrentBucketStartTimeNs + numBucketsForward * mBucketSizeNs;
-    mCurrentBucketNum += numBucketsForward;
-    VLOG("metric %lld: new bucket start time: %lld", (long long)mMetricId,
-         (long long)mCurrentBucketStartTimeNs);
 }
 
 size_t GaugeMetricProducer::byteSizeLocked() const {
diff --git a/cmds/statsd/src/metrics/GaugeMetricProducer.h b/cmds/statsd/src/metrics/GaugeMetricProducer.h
index 86d0ccd..d5d34be 100644
--- a/cmds/statsd/src/metrics/GaugeMetricProducer.h
+++ b/cmds/statsd/src/metrics/GaugeMetricProducer.h
@@ -62,6 +62,22 @@
     // Handles when the pulled data arrives.
     void onDataPulled(const std::vector<std::shared_ptr<LogEvent>>& data) override;
 
+    // GaugeMetric needs to immediately trigger another pull when we create the partial bucket.
+    void notifyAppUpgrade(const uint64_t& eventTimeNs, const string& apk, const int uid,
+                          const int64_t version) override {
+        std::lock_guard<std::mutex> lock(mMutex);
+
+        if (eventTimeNs > getCurrentBucketEndTimeNs()) {
+            // Flush full buckets on the normal path up to the latest bucket boundary.
+            flushIfNeededLocked(eventTimeNs);
+        }
+        flushCurrentBucketLocked(eventTimeNs);
+        mCurrentBucketStartTimeNs = eventTimeNs;
+        if (mPullTagId != -1) {
+            pullLocked();
+        }
+    };
+
 protected:
     void onMatchedLogEventInternalLocked(
             const size_t matcherIndex, const MetricDimensionKey& eventKey,
@@ -91,7 +107,11 @@
     void dumpStatesLocked(FILE* out, bool verbose) const override{};
 
     // Util function to flush the old packet.
-    void flushIfNeededLocked(const uint64_t& eventTime);
+    void flushIfNeededLocked(const uint64_t& eventTime) override;
+
+    void flushCurrentBucketLocked(const uint64_t& eventTimeNs) override;
+
+    void pullLocked();
 
     std::shared_ptr<StatsPullerManager> mStatsPullerManager;
     // tagId for pulled data. -1 if this is not pulled
@@ -101,13 +121,15 @@
     // TODO: Add a lock to mPastBuckets.
     std::unordered_map<MetricDimensionKey, std::vector<GaugeBucket>> mPastBuckets;
 
-    // The current bucket.
+    // The current partial bucket.
     std::shared_ptr<DimToGaugeAtomsMap> mCurrentSlicedBucket;
 
-    // The current bucket for anomaly detection.
+    // The current full bucket for anomaly detection. This is updated to the latest value seen for
+    // this slice (ie, for partial buckets, we use the last partial bucket in this full bucket).
     std::shared_ptr<DimToValMap> mCurrentSlicedBucketForAnomaly;
 
-    // Translate Atom based bucket to single numeric value bucket for anomaly
+    // Translate Atom based bucket to single numeric value bucket for anomaly and updates the map
+    // for each slice with the latest value.
     void updateCurrentSlicedBucketForAnomaly();
 
     // Whitelist of fields to report. Empty means all are reported.
@@ -125,6 +147,8 @@
 
     FRIEND_TEST(GaugeMetricProducerTest, TestWithCondition);
     FRIEND_TEST(GaugeMetricProducerTest, TestNoCondition);
+    FRIEND_TEST(GaugeMetricProducerTest, TestPushedEventsWithUpgrade);
+    FRIEND_TEST(GaugeMetricProducerTest, TestPulledWithUpgrade);
     FRIEND_TEST(GaugeMetricProducerTest, TestAnomalyDetection);
 };
 
diff --git a/cmds/statsd/src/metrics/MetricProducer.h b/cmds/statsd/src/metrics/MetricProducer.h
index 3b1498f..542dd8a 100644
--- a/cmds/statsd/src/metrics/MetricProducer.h
+++ b/cmds/statsd/src/metrics/MetricProducer.h
@@ -53,15 +53,32 @@
 
     virtual ~MetricProducer(){};
 
-    void notifyAppUpgrade(const string& apk, const int uid, const int64_t version) override{
+    /**
+     * Forces this metric to split into a partial bucket right now. If we're past a full bucket, we
+     * first call the standard flushing code to flush up to the latest full bucket. Then we call
+     * the flush again when the end timestamp is forced to be now, and then after flushing, update
+     * the start timestamp to be now.
+     */
+    void notifyAppUpgrade(const uint64_t& eventTimeNs, const string& apk, const int uid,
+                          const int64_t version) override {
+        std::lock_guard<std::mutex> lock(mMutex);
+
+        if (eventTimeNs > getCurrentBucketEndTimeNs()) {
+            // Flush full buckets on the normal path up to the latest bucket boundary.
+            flushIfNeededLocked(eventTimeNs);
+        }
+        // Now flush a partial bucket.
+        flushCurrentBucketLocked(eventTimeNs);
+        mCurrentBucketStartTimeNs = eventTimeNs;
+        // Don't update the current bucket number so that the anomaly tracker knows this bucket
+        // is a partial bucket and can merge it with the previous bucket.
+    };
+
+    void notifyAppRemoved(const uint64_t& eventTimeNs, const string& apk, const int uid) override{
             // TODO: Implement me.
     };
 
-    void notifyAppRemoved(const string& apk, const int uid) override{
-            // TODO: Implement me.
-    };
-
-    void onUidMapReceived() override{
+    void onUidMapReceived(const uint64_t& eventTimeNs) override{
             // TODO: Implement me.
     };
 
@@ -87,11 +104,12 @@
     };
 
     // Output the metrics data to [protoOutput]. All metrics reports end with the same timestamp.
+    // This method clears all the past buckets.
     void onDumpReport(const uint64_t dumpTimeNs, android::util::ProtoOutputStream* protoOutput) {
         std::lock_guard<std::mutex> lock(mMutex);
         return onDumpReportLocked(dumpTimeNs, protoOutput);
     }
-
+    // This method does not clear the past buckets.
     void onDumpReport(const uint64_t dumpTimeNs, StatsLogReport* report) {
         std::lock_guard<std::mutex> lock(mMutex);
         return onDumpReportLocked(dumpTimeNs, report);
@@ -136,15 +154,43 @@
     virtual size_t byteSizeLocked() const = 0;
     virtual void dumpStatesLocked(FILE* out, bool verbose) const = 0;
 
+    /**
+     * Flushes the current bucket if the eventTime is after the current bucket's end time.
+     */
+    virtual void flushIfNeededLocked(const uint64_t& eventTime){};
+
+    /**
+     * For metrics that aggregate (ie, every metric producer except for EventMetricProducer),
+     * we need to be able to flush the current buckets on demand (ie, end the current bucket and
+     * start new bucket). If this function is called when eventTimeNs is greater than the current
+     * bucket's end timestamp, than we flush up to the end of the latest full bucket; otherwise,
+     * we assume that we want to flush a partial bucket. The bucket start timestamp and bucket
+     * number are not changed by this function. This method should only be called by
+     * flushIfNeededLocked or the app upgrade handler; the caller MUST update the bucket timestamp
+     * and bucket number as needed.
+     */
+    virtual void flushCurrentBucketLocked(const uint64_t& eventTimeNs){};
+
+    // Convenience to compute the current bucket's end time, which is always aligned with the
+    // start time of the metric.
+    uint64_t getCurrentBucketEndTimeNs() {
+        return mStartTimeNs + (mCurrentBucketNum + 1) * mBucketSizeNs;
+    }
+
     const int64_t mMetricId;
 
     const ConfigKey mConfigKey;
 
-    // The start time for the current in memory metrics data.
+    // The time when this metric producer was first created. The end time for the current bucket
+    // can be computed from this based on mCurrentBucketNum.
     uint64_t mStartTimeNs;
 
+    // Start time may not be aligned with the start of statsd if there is an app upgrade in the
+    // middle of a bucket.
     uint64_t mCurrentBucketStartTimeNs;
 
+    // Used by anomaly detector to track which bucket we are in. This is not sent with the produced
+    // report.
     uint64_t mCurrentBucketNum;
 
     int64_t mBucketSizeNs;
diff --git a/cmds/statsd/src/metrics/MetricsManager.cpp b/cmds/statsd/src/metrics/MetricsManager.cpp
index 417145c..6573a89 100644
--- a/cmds/statsd/src/metrics/MetricsManager.cpp
+++ b/cmds/statsd/src/metrics/MetricsManager.cpp
@@ -28,6 +28,7 @@
 #include "stats_util.h"
 
 #include <log/logprint.h>
+#include <private/android_filesystem_config.h>
 
 using android::util::FIELD_COUNT_REPEATED;
 using android::util::FIELD_TYPE_MESSAGE;
@@ -47,7 +48,7 @@
 
 MetricsManager::MetricsManager(const ConfigKey& key, const StatsdConfig& config,
                                const long timeBaseSec, sp<UidMap> uidMap)
-    : mConfigKey(key), mUidMap(uidMap), mStatsdUid(getStatsdUid()) {
+    : mConfigKey(key), mUidMap(uidMap) {
     mConfigValid =
             initStatsdConfig(key, config, *uidMap, timeBaseSec, mTagIds, mAllAtomMatchers, mAllConditionTrackers,
                              mAllMetricProducers, mAllAnomalyTrackers, mConditionToMetricMap,
@@ -59,9 +60,9 @@
         // mConfigValid = false;
         // ALOGE("Log source white list is empty! This config won't get any data.");
 
-        mAllowedUid.push_back(1000);
-        mAllowedUid.push_back(0);
-        mAllowedUid.push_back(mStatsdUid);
+        mAllowedUid.push_back(AID_ROOT);
+        mAllowedUid.push_back(AID_STATSD);
+        mAllowedUid.push_back(AID_SYSTEM);
         mAllowedLogSources.insert(mAllowedUid.begin(), mAllowedUid.end());
     } else {
         for (const auto& source : config.allowed_log_source()) {
@@ -120,7 +121,8 @@
     return mConfigValid;
 }
 
-void MetricsManager::notifyAppUpgrade(const string& apk, const int uid, const int64_t version) {
+void MetricsManager::notifyAppUpgrade(const uint64_t& eventTimeNs, const string& apk, const int uid,
+                                      const int64_t version) {
     // check if we care this package
     if (std::find(mAllowedPkg.begin(), mAllowedPkg.end(), apk) == mAllowedPkg.end()) {
         return;
@@ -130,7 +132,8 @@
     initLogSourceWhiteList();
 }
 
-void MetricsManager::notifyAppRemoved(const string& apk, const int uid) {
+void MetricsManager::notifyAppRemoved(const uint64_t& eventTimeNs, const string& apk,
+                                      const int uid) {
     // check if we care this package
     if (std::find(mAllowedPkg.begin(), mAllowedPkg.end(), apk) == mAllowedPkg.end()) {
         return;
@@ -140,7 +143,7 @@
     initLogSourceWhiteList();
 }
 
-void MetricsManager::onUidMapReceived() {
+void MetricsManager::onUidMapReceived(const uint64_t& eventTimeNs) {
     if (mAllowedPkg.size() == 0) {
         return;
     }
@@ -198,7 +201,7 @@
         // unless that caller is statsd itself (statsd is allowed to spoof uids).
         long appHookUid = event.GetLong(event.size()-2, &err);
         int32_t loggerUid = event.GetUid();
-        if (err != NO_ERROR || (loggerUid != appHookUid && loggerUid != mStatsdUid)) {
+        if (err != NO_ERROR || (loggerUid != appHookUid && loggerUid != AID_STATSD)) {
             VLOG("AppHook has invalid uid: claimed %ld but caller is %d", appHookUid, loggerUid);
             return;
         }
@@ -333,16 +336,6 @@
     return totalSize;
 }
 
-int32_t MetricsManager::getStatsdUid() {
-    auto suit = UidMap::sAidToUidMapping.find("AID_STATSD");
-    if (suit != UidMap::sAidToUidMapping.end()) {
-        return suit->second;
-    } else {
-        ALOGE("Statsd failed to find its own uid!");
-        return -1;
-    }
-}
-
 }  // namespace statsd
 }  // namespace os
 }  // namespace android
diff --git a/cmds/statsd/src/metrics/MetricsManager.h b/cmds/statsd/src/metrics/MetricsManager.h
index a1220f9..9cae70a 100644
--- a/cmds/statsd/src/metrics/MetricsManager.h
+++ b/cmds/statsd/src/metrics/MetricsManager.h
@@ -51,11 +51,12 @@
 
     void setAnomalyMonitor(const sp<AnomalyMonitor>& anomalyMonitor);
 
-    void notifyAppUpgrade(const string& apk, const int uid, const int64_t version) override;
+    void notifyAppUpgrade(const uint64_t& eventTimeNs, const string& apk, const int uid,
+                          const int64_t version) override;
 
-    void notifyAppRemoved(const string& apk, const int uid) override;
+    void notifyAppRemoved(const uint64_t& eventTimeNs, const string& apk, const int uid) override;
 
-    void onUidMapReceived() override;
+    void onUidMapReceived(const uint64_t& eventTimeNs) override;
 
     bool shouldAddUidMapListener() const {
         return !mAllowedPkg.empty();
@@ -75,9 +76,6 @@
 
     sp<UidMap> mUidMap;
 
-    // The uid of statsd.
-    const int32_t mStatsdUid;
-
     bool mConfigValid = false;
 
     // The uid log sources from StatsdConfig.
@@ -139,9 +137,6 @@
 
     void initLogSourceWhiteList();
 
-    // Fetches the uid of statsd from UidMap.
-    static int32_t getStatsdUid();
-
     // The metrics that don't need to be uploaded or even reported.
     std::set<int64_t> mNoReportMetricIds;
 
diff --git a/cmds/statsd/src/metrics/ValueMetricProducer.cpp b/cmds/statsd/src/metrics/ValueMetricProducer.cpp
index c9cc7bb..31d9ff8 100644
--- a/cmds/statsd/src/metrics/ValueMetricProducer.cpp
+++ b/cmds/statsd/src/metrics/ValueMetricProducer.cpp
@@ -190,7 +190,6 @@
 
     VLOG("metric %lld dump report now...", (long long)mMetricId);
     mPastBuckets.clear();
-    mStartTimeNs = mCurrentBucketStartTimeNs;
     // TODO: Clear mDimensionKeyMap once the report is dumped.
 }
 
@@ -320,8 +319,13 @@
         interval.sum += value;
     }
 
+    long wholeBucketVal = interval.sum;
+    auto prev = mCurrentFullBucket.find(eventKey);
+    if (prev != mCurrentFullBucket.end()) {
+        wholeBucketVal += prev->second;
+    }
     for (auto& tracker : mAnomalyTrackers) {
-        tracker->detectAndDeclareAnomaly(eventTimeNs, mCurrentBucketNum, eventKey, interval.sum);
+        tracker->detectAndDeclareAnomaly(eventTimeNs, mCurrentBucketNum, eventKey, wholeBucketVal);
     }
 }
 
@@ -333,16 +337,39 @@
 }
 
 void ValueMetricProducer::flushIfNeededLocked(const uint64_t& eventTimeNs) {
-    if (mCurrentBucketStartTimeNs + mBucketSizeNs > eventTimeNs) {
+    uint64_t currentBucketEndTimeNs = getCurrentBucketEndTimeNs();
+
+    if (currentBucketEndTimeNs > eventTimeNs) {
         VLOG("eventTime is %lld, less than next bucket start time %lld", (long long)eventTimeNs,
-             (long long)(mCurrentBucketStartTimeNs + mBucketSizeNs));
+             (long long)(currentBucketEndTimeNs));
         return;
     }
+
+    flushCurrentBucketLocked(eventTimeNs);
+
+    int64_t numBucketsForward = 1 + (eventTimeNs - currentBucketEndTimeNs) / mBucketSizeNs;
+    mCurrentBucketStartTimeNs = currentBucketEndTimeNs + (numBucketsForward - 1) * mBucketSizeNs;
+    mCurrentBucketNum += numBucketsForward;
+
+    if (numBucketsForward > 1) {
+        VLOG("Skipping forward %lld buckets", (long long)numBucketsForward);
+    }
+    VLOG("metric %lld: new bucket start time: %lld", (long long)mMetricId,
+         (long long)mCurrentBucketStartTimeNs);
+}
+
+void ValueMetricProducer::flushCurrentBucketLocked(const uint64_t& eventTimeNs) {
     VLOG("finalizing bucket for %ld, dumping %d slices", (long)mCurrentBucketStartTimeNs,
          (int)mCurrentSlicedBucket.size());
+    uint64_t fullBucketEndTimeNs = getCurrentBucketEndTimeNs();
+
     ValueBucket info;
     info.mBucketStartNs = mCurrentBucketStartTimeNs;
-    info.mBucketEndNs = mCurrentBucketStartTimeNs + mBucketSizeNs;
+    if (eventTimeNs < fullBucketEndTimeNs) {
+        info.mBucketEndNs = eventTimeNs;
+    } else {
+        info.mBucketEndNs = fullBucketEndTimeNs;
+    }
     info.mBucketNum = mCurrentBucketNum;
 
     int tainted = 0;
@@ -352,27 +379,42 @@
         // it will auto create new vector of ValuebucketInfo if the key is not found.
         auto& bucketList = mPastBuckets[slice.first];
         bucketList.push_back(info);
-
-        for (auto& tracker : mAnomalyTrackers) {
-            if (tracker != nullptr) {
-                tracker->addPastBucket(slice.first, info.mValue, info.mBucketNum);
-            }
-        }
     }
     VLOG("%d tainted pairs in the bucket", tainted);
 
+    if (eventTimeNs > fullBucketEndTimeNs) {  // If full bucket, send to anomaly tracker.
+        // Accumulate partial buckets with current value and then send to anomaly tracker.
+        if (mCurrentFullBucket.size() > 0) {
+            for (const auto& slice : mCurrentSlicedBucket) {
+                mCurrentFullBucket[slice.first] += slice.second.sum;
+            }
+            for (const auto& slice : mCurrentFullBucket) {
+                for (auto& tracker : mAnomalyTrackers) {
+                    if (tracker != nullptr) {
+                        tracker->addPastBucket(slice.first, slice.second, mCurrentBucketNum);
+                    }
+                }
+            }
+            mCurrentFullBucket.clear();
+        } else {
+            // Skip aggregating the partial buckets since there's no previous partial bucket.
+            for (const auto& slice : mCurrentSlicedBucket) {
+                for (auto& tracker : mAnomalyTrackers) {
+                    if (tracker != nullptr) {
+                        tracker->addPastBucket(slice.first, slice.second.sum, mCurrentBucketNum);
+                    }
+                }
+            }
+        }
+    } else {
+        // Accumulate partial bucket.
+        for (const auto& slice : mCurrentSlicedBucket) {
+            mCurrentFullBucket[slice.first] += slice.second.sum;
+        }
+    }
+
     // Reset counters
     mCurrentSlicedBucket.clear();
-
-    int64_t numBucketsForward = (eventTimeNs - mCurrentBucketStartTimeNs) / mBucketSizeNs;
-    mCurrentBucketStartTimeNs = mCurrentBucketStartTimeNs + numBucketsForward * mBucketSizeNs;
-    mCurrentBucketNum += numBucketsForward;
-
-    if (numBucketsForward > 1) {
-        VLOG("Skipping forward %lld buckets", (long long)numBucketsForward);
-    }
-    VLOG("metric %lld: new bucket start time: %lld", (long long)mMetricId,
-         (long long)mCurrentBucketStartTimeNs);
 }
 
 size_t ValueMetricProducer::byteSizeLocked() const {
diff --git a/cmds/statsd/src/metrics/ValueMetricProducer.h b/cmds/statsd/src/metrics/ValueMetricProducer.h
index 121ec7d..bf5b7df 100644
--- a/cmds/statsd/src/metrics/ValueMetricProducer.h
+++ b/cmds/statsd/src/metrics/ValueMetricProducer.h
@@ -47,6 +47,39 @@
 
     void onDataPulled(const std::vector<std::shared_ptr<LogEvent>>& data) override;
 
+    // ValueMetric needs special logic if it's a pulled atom.
+    void notifyAppUpgrade(const uint64_t& eventTimeNs, const string& apk, const int uid,
+                          const int64_t version) override {
+        std::lock_guard<std::mutex> lock(mMutex);
+
+        if (mPullTagId != -1) {
+            vector<shared_ptr<LogEvent>> allData;
+            mStatsPullerManager->Pull(mPullTagId, &allData);
+            if (allData.size() == 0) {
+                // This shouldn't happen since this valuemetric is not useful now.
+            }
+
+            // Pretend the pulled data occurs right before the app upgrade event.
+            mCondition = false;
+            for (const auto& data : allData) {
+                data->setTimestampNs(eventTimeNs - 1);
+                onMatchedLogEventLocked(0, *data);
+            }
+
+            flushCurrentBucketLocked(eventTimeNs);
+            mCurrentBucketStartTimeNs = eventTimeNs;
+
+            mCondition = true;
+            for (const auto& data : allData) {
+                data->setTimestampNs(eventTimeNs);
+                onMatchedLogEventLocked(0, *data);
+            }
+        } else {  // For pushed value metric, we simply flush and reset the current bucket start.
+            flushCurrentBucketLocked(eventTimeNs);
+            mCurrentBucketStartTimeNs = eventTimeNs;
+        }
+    };
+
 protected:
     void onMatchedLogEventInternalLocked(
             const size_t matcherIndex, const MetricDimensionKey& eventKey,
@@ -70,7 +103,9 @@
     void dumpStatesLocked(FILE* out, bool verbose) const override{};
 
     // Util function to flush the old packet.
-    void flushIfNeededLocked(const uint64_t& eventTime);
+    void flushIfNeededLocked(const uint64_t& eventTime) override;
+
+    void flushCurrentBucketLocked(const uint64_t& eventTimeNs) override;
 
     const FieldMatcher mValueField;
 
@@ -101,6 +136,8 @@
 
     std::unordered_map<MetricDimensionKey, Interval> mCurrentSlicedBucket;
 
+    std::unordered_map<MetricDimensionKey, long> mCurrentFullBucket;
+
     // Save the past buckets and we can clear when the StatsLogReport is dumped.
     // TODO: Add a lock to mPastBuckets.
     std::unordered_map<MetricDimensionKey, std::vector<ValueBucket>> mPastBuckets;
@@ -114,6 +151,8 @@
 
     FRIEND_TEST(ValueMetricProducerTest, TestNonDimensionalEvents);
     FRIEND_TEST(ValueMetricProducerTest, TestEventsWithNonSlicedCondition);
+    FRIEND_TEST(ValueMetricProducerTest, TestPushedEventsWithUpgrade);
+    FRIEND_TEST(ValueMetricProducerTest, TestPulledValueWithUpgrade);
     FRIEND_TEST(ValueMetricProducerTest, TestPushedEventsWithoutCondition);
     FRIEND_TEST(ValueMetricProducerTest, TestAnomalyDetection);
 };
diff --git a/cmds/statsd/src/metrics/duration_helper/DurationTracker.h b/cmds/statsd/src/metrics/duration_helper/DurationTracker.h
index 45735a8..356a81c 100644
--- a/cmds/statsd/src/metrics/duration_helper/DurationTracker.h
+++ b/cmds/statsd/src/metrics/duration_helper/DurationTracker.h
@@ -63,7 +63,8 @@
     DurationTracker(const ConfigKey& key, const int64_t& id, const MetricDimensionKey& eventKey,
                     sp<ConditionWizard> wizard, int conditionIndex,
                     const FieldMatcher& dimensionInCondition, bool nesting,
-                    uint64_t currentBucketStartNs, uint64_t bucketSizeNs, bool conditionSliced,
+                    uint64_t currentBucketStartNs, uint64_t currentBucketNum, uint64_t startTimeNs,
+                    uint64_t bucketSizeNs, bool conditionSliced,
                     const std::vector<sp<DurationAnomalyTracker>>& anomalyTrackers)
         : mConfigKey(key),
           mTrackerId(id),
@@ -75,7 +76,9 @@
           mNested(nesting),
           mCurrentBucketStartTimeNs(currentBucketStartNs),
           mDuration(0),
-          mCurrentBucketNum(0),
+          mDurationFullBucket(0),
+          mCurrentBucketNum(currentBucketNum),
+          mStartTimeNs(startTimeNs),
           mConditionSliced(conditionSliced),
           mAnomalyTrackers(anomalyTrackers){};
 
@@ -98,6 +101,12 @@
             uint64_t timestampNs,
             std::unordered_map<MetricDimensionKey, std::vector<DurationBucket>>* output) = 0;
 
+    // Should only be called during an app upgrade or from this tracker's flushIfNeeded. If from
+    // an app upgrade, we assume that we're trying to form a partial bucket.
+    virtual bool flushCurrentBucket(
+            const uint64_t& eventTimeNs,
+            std::unordered_map<MetricDimensionKey, std::vector<DurationBucket>>* output) = 0;
+
     // Predict the anomaly timestamp given the current status.
     virtual int64_t predictAnomalyTimestampNs(const DurationAnomalyTracker& anomalyTracker,
                                               const uint64_t currentTimestamp) const = 0;
@@ -153,6 +162,13 @@
             }
         }
     }
+
+    // Convenience to compute the current bucket's end time, which is always aligned with the
+    // start time of the metric.
+    uint64_t getCurrentBucketEndTimeNs() {
+        return mStartTimeNs + (mCurrentBucketNum + 1) * mBucketSizeNs;
+    }
+
     // A reference to the DurationMetricProducer's config key.
     const ConfigKey& mConfigKey;
 
@@ -172,10 +188,14 @@
 
     uint64_t mCurrentBucketStartTimeNs;
 
-    int64_t mDuration;  // current recorded duration result
+    int64_t mDuration;  // current recorded duration result (for partial bucket)
+
+    int64_t mDurationFullBucket;  // Sum of past partial buckets in current full bucket.
 
     uint64_t mCurrentBucketNum;
 
+    const uint64_t mStartTimeNs;
+
     const bool mConditionSliced;
 
     std::vector<sp<DurationAnomalyTracker>> mAnomalyTrackers;
diff --git a/cmds/statsd/src/metrics/duration_helper/MaxDurationTracker.cpp b/cmds/statsd/src/metrics/duration_helper/MaxDurationTracker.cpp
index db7dea4..c3bafc6 100644
--- a/cmds/statsd/src/metrics/duration_helper/MaxDurationTracker.cpp
+++ b/cmds/statsd/src/metrics/duration_helper/MaxDurationTracker.cpp
@@ -28,11 +28,13 @@
                                        const MetricDimensionKey& eventKey,
                                        sp<ConditionWizard> wizard, int conditionIndex,
                                        const FieldMatcher& dimensionInCondition, bool nesting,
-                                       uint64_t currentBucketStartNs, uint64_t bucketSizeNs,
+                                       uint64_t currentBucketStartNs, uint64_t currentBucketNum,
+                                       uint64_t startTimeNs, uint64_t bucketSizeNs,
                                        bool conditionSliced,
                                        const vector<sp<DurationAnomalyTracker>>& anomalyTrackers)
     : DurationTracker(key, id, eventKey, wizard, conditionIndex, dimensionInCondition, nesting,
-                      currentBucketStartNs, bucketSizeNs, conditionSliced, anomalyTrackers) {
+                      currentBucketStartNs, currentBucketNum, startTimeNs, bucketSizeNs,
+                      conditionSliced, anomalyTrackers) {
 }
 
 unique_ptr<DurationTracker> MaxDurationTracker::clone(const uint64_t eventTime) {
@@ -102,7 +104,6 @@
 
 void MaxDurationTracker::noteStop(const HashableDimensionKey& key, const uint64_t eventTime,
                                   bool forceStop) {
-    declareAnomalyIfAlarmExpired(eventTime);
     VLOG("MaxDuration: key %s stop", key.c_str());
     if (mInfos.find(key) == mInfos.end()) {
         // we didn't see a start event before. do nothing.
@@ -122,7 +123,7 @@
                 VLOG("Max, key %s, Stop %lld %lld %lld", key.c_str(),
                      (long long)duration.lastStartTime, (long long)eventTime,
                      (long long)durationTime);
-                duration.lastDuration = duration.lastDuration + durationTime;
+                duration.lastDuration += durationTime;
                 VLOG("  record duration: %lld ", (long long)duration.lastDuration);
             }
             break;
@@ -138,7 +139,6 @@
 
     if (duration.lastDuration > mDuration) {
         mDuration = duration.lastDuration;
-        detectAndDeclareAnomaly(eventTime, mCurrentBucketNum, mDuration);
         VLOG("Max: new max duration: %lld", (long long)mDuration);
     }
     // Once an atom duration ends, we erase it. Next time, if we see another atom event with the
@@ -158,88 +158,67 @@
     }
 }
 
-bool MaxDurationTracker::flushIfNeeded(
-        uint64_t eventTime, unordered_map<MetricDimensionKey, vector<DurationBucket>>* output) {
-    if (mCurrentBucketStartTimeNs + mBucketSizeNs > eventTime) {
-        return false;
-    }
-
+bool MaxDurationTracker::flushCurrentBucket(
+        const uint64_t& eventTimeNs,
+        std::unordered_map<MetricDimensionKey, std::vector<DurationBucket>>* output) {
     VLOG("MaxDurationTracker flushing.....");
 
     // adjust the bucket start time
-    int numBucketsForward = (eventTime - mCurrentBucketStartTimeNs) / mBucketSizeNs;
+    int numBucketsForward = 0;
+    uint64_t fullBucketEnd = getCurrentBucketEndTimeNs();
+    uint64_t currentBucketEndTimeNs;
+    if (eventTimeNs >= fullBucketEnd) {
+        numBucketsForward = 1 + (eventTimeNs - fullBucketEnd) / mBucketSizeNs;
+        currentBucketEndTimeNs = fullBucketEnd;
+    } else {
+        // This must be a partial bucket.
+        currentBucketEndTimeNs = eventTimeNs;
+    }
 
-    uint64_t endTime = mCurrentBucketStartTimeNs + mBucketSizeNs;
-
-    DurationBucket info;
-    info.mBucketStartNs = mCurrentBucketStartTimeNs;
-    info.mBucketEndNs = endTime;
-    info.mBucketNum = mCurrentBucketNum;
-
-    uint64_t oldBucketStartTimeNs = mCurrentBucketStartTimeNs;
-    mCurrentBucketStartTimeNs += (numBucketsForward)*mBucketSizeNs;
-
-    bool hasOnGoingStartedEvent = false;  // a kStarted event last across bucket boundaries.
     bool hasPendingEvent =
             false;  // has either a kStarted or kPaused event across bucket boundaries
-                    // meaning we need to carry them over to the new bucket.
+    // meaning we need to carry them over to the new bucket.
     for (auto it = mInfos.begin(); it != mInfos.end(); ++it) {
         int64_t finalDuration = it->second.lastDuration;
-        if (it->second.state == kStarted) {
-            // the event is still on-going, duration needs to be updated.
-            // |..lastDurationTime_recorded...last_start -----|bucket_end. We need to record the
-            // duration between lastStartTime and bucketEnd.
-            int64_t durationTime = endTime - it->second.lastStartTime;
-
-            finalDuration += durationTime;
-            VLOG("  unrecorded %lld -> %lld", (long long)(durationTime), (long long)finalDuration);
-            // if the event is still on-going, we need to fill the buckets between prev_bucket and
-            // now_bucket. |prev_bucket|...|..|...|now_bucket|
-            hasOnGoingStartedEvent = true;
-        }
-
-        if (finalDuration > mDuration) {
-            mDuration = finalDuration;
-        }
-
         if (it->second.state == DurationState::kStopped) {
             // No need to keep buckets for events that were stopped before.
             mInfos.erase(it);
         } else {
             hasPendingEvent = true;
-            // for kPaused, and kStarted event, we will keep track of them, and reset the start time
-            // and duration.
-            it->second.lastStartTime = mCurrentBucketStartTimeNs;
-            it->second.lastDuration = 0;
         }
     }
 
+    // mDuration is updated in noteStop to the maximum duration that ended in the current bucket.
     if (mDuration != 0) {
+        DurationBucket info;
+        info.mBucketStartNs = mCurrentBucketStartTimeNs;
+        info.mBucketEndNs = currentBucketEndTimeNs;
+        info.mBucketNum = mCurrentBucketNum;
         info.mDuration = mDuration;
         (*output)[mEventKey].push_back(info);
-        addPastBucketToAnomalyTrackers(info.mDuration, info.mBucketNum);
         VLOG("  final duration for last bucket: %lld", (long long)mDuration);
     }
 
-    mDuration = 0;
-    if (hasOnGoingStartedEvent) {
-        for (int i = 1; i < numBucketsForward; i++) {
-            DurationBucket info;
-            info.mBucketStartNs = oldBucketStartTimeNs + mBucketSizeNs * i;
-            info.mBucketEndNs = endTime + mBucketSizeNs * i;
-            info.mBucketNum = mCurrentBucketNum + i;
-            info.mDuration = mBucketSizeNs;
-            (*output)[mEventKey].push_back(info);
-            addPastBucketToAnomalyTrackers(info.mDuration, info.mBucketNum);
-            VLOG("  filling gap bucket with duration %lld", (long long)mBucketSizeNs);
-        }
+    if (numBucketsForward > 0) {
+        mCurrentBucketStartTimeNs = fullBucketEnd + (numBucketsForward - 1) * mBucketSizeNs;
+        mCurrentBucketNum += numBucketsForward;
+    } else {  // We must be forming a partial bucket.
+        mCurrentBucketStartTimeNs = eventTimeNs;
     }
 
-    mCurrentBucketNum += numBucketsForward;
+    mDuration = 0;
     // If this tracker has no pending events, tell owner to remove.
     return !hasPendingEvent;
 }
 
+bool MaxDurationTracker::flushIfNeeded(
+        uint64_t eventTimeNs, unordered_map<MetricDimensionKey, vector<DurationBucket>>* output) {
+    if (eventTimeNs < getCurrentBucketEndTimeNs()) {
+        return false;
+    }
+    return flushCurrentBucket(eventTimeNs, output);
+}
+
 void MaxDurationTracker::onSlicedConditionMayChange(const uint64_t timestamp) {
     // Now for each of the on-going event, check if the condition has changed for them.
     for (auto& pair : mInfos) {
@@ -267,7 +246,6 @@
 
 void MaxDurationTracker::noteConditionChanged(const HashableDimensionKey& key, bool conditionMet,
                                               const uint64_t timestamp) {
-    declareAnomalyIfAlarmExpired(timestamp);
     auto it = mInfos.find(key);
     if (it == mInfos.end()) {
         return;
@@ -297,7 +275,6 @@
     }
     if (it->second.lastDuration > mDuration) {
         mDuration = it->second.lastDuration;
-        detectAndDeclareAnomaly(timestamp, mCurrentBucketNum, mDuration);
     }
 }
 
diff --git a/cmds/statsd/src/metrics/duration_helper/MaxDurationTracker.h b/cmds/statsd/src/metrics/duration_helper/MaxDurationTracker.h
index 4d32a06..fba4119 100644
--- a/cmds/statsd/src/metrics/duration_helper/MaxDurationTracker.h
+++ b/cmds/statsd/src/metrics/duration_helper/MaxDurationTracker.h
@@ -28,10 +28,11 @@
 // they stop or bucket expires.
 class MaxDurationTracker : public DurationTracker {
 public:
-    MaxDurationTracker(const ConfigKey& key, const int64_t& id,
-                       const MetricDimensionKey& eventKey, sp<ConditionWizard> wizard,
-                       int conditionIndex, const FieldMatcher& dimensionInCondition, bool nesting,
-                       uint64_t currentBucketStartNs, uint64_t bucketSizeNs, bool conditionSliced,
+    MaxDurationTracker(const ConfigKey& key, const int64_t& id, const MetricDimensionKey& eventKey,
+                       sp<ConditionWizard> wizard, int conditionIndex,
+                       const FieldMatcher& dimensionInCondition, bool nesting,
+                       uint64_t currentBucketStartNs, uint64_t currentBucketNum,
+                       uint64_t startTimeNs, uint64_t bucketSizeNs, bool conditionSliced,
                        const std::vector<sp<DurationAnomalyTracker>>& anomalyTrackers);
 
     MaxDurationTracker(const MaxDurationTracker& tracker) = default;
@@ -47,6 +48,9 @@
     bool flushIfNeeded(
             uint64_t timestampNs,
             std::unordered_map<MetricDimensionKey, std::vector<DurationBucket>>* output) override;
+    bool flushCurrentBucket(
+            const uint64_t& eventTimeNs,
+            std::unordered_map<MetricDimensionKey, std::vector<DurationBucket>>*) override;
 
     void onSlicedConditionMayChange(const uint64_t timestamp) override;
     void onConditionChanged(bool condition, const uint64_t timestamp) override;
@@ -68,7 +72,6 @@
     FRIEND_TEST(MaxDurationTrackerTest, TestCrossBucketBoundary);
     FRIEND_TEST(MaxDurationTrackerTest, TestMaxDurationWithCondition);
     FRIEND_TEST(MaxDurationTrackerTest, TestStopAll);
-    FRIEND_TEST(MaxDurationTrackerTest, TestAnomalyDetection);
 };
 
 }  // namespace statsd
diff --git a/cmds/statsd/src/metrics/duration_helper/OringDurationTracker.cpp b/cmds/statsd/src/metrics/duration_helper/OringDurationTracker.cpp
index 0feae36..85f7b7c 100644
--- a/cmds/statsd/src/metrics/duration_helper/OringDurationTracker.cpp
+++ b/cmds/statsd/src/metrics/duration_helper/OringDurationTracker.cpp
@@ -26,12 +26,13 @@
 
 OringDurationTracker::OringDurationTracker(
         const ConfigKey& key, const int64_t& id, const MetricDimensionKey& eventKey,
-        sp<ConditionWizard> wizard, int conditionIndex,
-        const FieldMatcher& dimensionInCondition, bool nesting, uint64_t currentBucketStartNs,
-        uint64_t bucketSizeNs, bool conditionSliced,
+        sp<ConditionWizard> wizard, int conditionIndex, const FieldMatcher& dimensionInCondition,
+        bool nesting, uint64_t currentBucketStartNs, uint64_t currentBucketNum,
+        uint64_t startTimeNs, uint64_t bucketSizeNs, bool conditionSliced,
         const vector<sp<DurationAnomalyTracker>>& anomalyTrackers)
     : DurationTracker(key, id, eventKey, wizard, conditionIndex, dimensionInCondition, nesting,
-                      currentBucketStartNs, bucketSizeNs, conditionSliced, anomalyTrackers),
+                      currentBucketStartNs, currentBucketNum, startTimeNs, bucketSizeNs,
+                      conditionSliced, anomalyTrackers),
       mStarted(),
       mPaused() {
     mLastStartTime = 0;
@@ -100,7 +101,7 @@
         }
         if (mStarted.empty()) {
             mDuration += (timestamp - mLastStartTime);
-            detectAndDeclareAnomaly(timestamp, mCurrentBucketNum, mDuration);
+            detectAndDeclareAnomaly(timestamp, mCurrentBucketNum, mDuration + mDurationFullBucket);
             VLOG("record duration %lld, total %lld ", (long long)timestamp - mLastStartTime,
                  (long long)mDuration);
         }
@@ -125,7 +126,7 @@
         mDuration += (timestamp - mLastStartTime);
         VLOG("Oring Stop all: record duration %lld %lld ", (long long)timestamp - mLastStartTime,
              (long long)mDuration);
-        detectAndDeclareAnomaly(timestamp, mCurrentBucketNum, mDuration);
+        detectAndDeclareAnomaly(timestamp, mCurrentBucketNum, mDuration + mDurationFullBucket);
     }
 
     stopAnomalyAlarm();
@@ -134,51 +135,83 @@
     mConditionKeyMap.clear();
 }
 
-bool OringDurationTracker::flushIfNeeded(
-        uint64_t eventTime, unordered_map<MetricDimensionKey, vector<DurationBucket>>* output) {
-    if (eventTime < mCurrentBucketStartTimeNs + mBucketSizeNs) {
-        return false;
-    }
+bool OringDurationTracker::flushCurrentBucket(
+        const uint64_t& eventTimeNs,
+        std::unordered_map<MetricDimensionKey, std::vector<DurationBucket>>* output) {
     VLOG("OringDurationTracker Flushing.............");
-    // adjust the bucket start time
-    int numBucketsForward = (eventTime - mCurrentBucketStartTimeNs) / mBucketSizeNs;
-    DurationBucket current_info;
-    current_info.mBucketStartNs = mCurrentBucketStartTimeNs;
-    current_info.mBucketEndNs = current_info.mBucketStartNs + mBucketSizeNs;
-    current_info.mBucketNum = mCurrentBucketNum;
+
+    // Note that we have to mimic the bucket time changes we do in the
+    // MetricProducer#notifyAppUpgrade.
+
+    int numBucketsForward = 0;
+    uint64_t fullBucketEnd = getCurrentBucketEndTimeNs();
+    uint64_t currentBucketEndTimeNs;
+
+    if (eventTimeNs >= fullBucketEnd) {
+        numBucketsForward = 1 + (eventTimeNs - fullBucketEnd) / mBucketSizeNs;
+        currentBucketEndTimeNs = fullBucketEnd;
+    } else {
+        // This must be a partial bucket.
+        currentBucketEndTimeNs = eventTimeNs;
+    }
+
     // Process the current bucket.
     if (mStarted.size() > 0) {
-        mDuration += (current_info.mBucketEndNs - mLastStartTime);
+        mDuration += (currentBucketEndTimeNs - mLastStartTime);
     }
     if (mDuration > 0) {
+        DurationBucket current_info;
+        current_info.mBucketStartNs = mCurrentBucketStartTimeNs;
+        current_info.mBucketEndNs = currentBucketEndTimeNs;
+        current_info.mBucketNum = mCurrentBucketNum;
         current_info.mDuration = mDuration;
         (*output)[mEventKey].push_back(current_info);
-        addPastBucketToAnomalyTrackers(current_info.mDuration, current_info.mBucketNum);
+        mDurationFullBucket += mDuration;
+        if (eventTimeNs > fullBucketEnd) {
+            // End of full bucket, can send to anomaly tracker now.
+            addPastBucketToAnomalyTrackers(mDurationFullBucket, current_info.mBucketNum);
+            mDurationFullBucket = 0;
+        }
         VLOG("  duration: %lld", (long long)current_info.mDuration);
     }
 
     if (mStarted.size() > 0) {
         for (int i = 1; i < numBucketsForward; i++) {
             DurationBucket info;
-            info.mBucketStartNs = mCurrentBucketStartTimeNs + mBucketSizeNs * i;
+            info.mBucketStartNs = fullBucketEnd + mBucketSizeNs * (i - 1);
             info.mBucketEndNs = info.mBucketStartNs + mBucketSizeNs;
             info.mBucketNum = mCurrentBucketNum + i;
             info.mDuration = mBucketSizeNs;
             (*output)[mEventKey].push_back(info);
+            // Safe to send these buckets to anomaly tracker since they must be full buckets.
+            // If it's a partial bucket, numBucketsForward would be 0.
             addPastBucketToAnomalyTrackers(info.mDuration, info.mBucketNum);
             VLOG("  add filling bucket with duration %lld", (long long)info.mDuration);
         }
     }
-    mCurrentBucketStartTimeNs += numBucketsForward * mBucketSizeNs;
-    mCurrentBucketNum += numBucketsForward;
 
-    mLastStartTime = mCurrentBucketStartTimeNs;
     mDuration = 0;
 
+    if (numBucketsForward > 0) {
+        mCurrentBucketStartTimeNs = fullBucketEnd + (numBucketsForward - 1) * mBucketSizeNs;
+        mCurrentBucketNum += numBucketsForward;
+    } else {  // We must be forming a partial bucket.
+        mCurrentBucketStartTimeNs = eventTimeNs;
+    }
+    mLastStartTime = mCurrentBucketStartTimeNs;
+
     // if all stopped, then tell owner it's safe to remove this tracker.
     return mStarted.empty() && mPaused.empty();
 }
 
+bool OringDurationTracker::flushIfNeeded(
+        uint64_t eventTimeNs, unordered_map<MetricDimensionKey, vector<DurationBucket>>* output) {
+    if (eventTimeNs < getCurrentBucketEndTimeNs()) {
+        return false;
+    }
+    return flushCurrentBucket(eventTimeNs, output);
+}
+
 void OringDurationTracker::onSlicedConditionMayChange(const uint64_t timestamp) {
     declareAnomalyIfAlarmExpired(timestamp);
     vector<pair<HashableDimensionKey, int>> startedToPaused;
@@ -211,7 +244,7 @@
             mDuration += (timestamp - mLastStartTime);
             VLOG("Duration add %lld , to %lld ", (long long)(timestamp - mLastStartTime),
                  (long long)mDuration);
-            detectAndDeclareAnomaly(timestamp, mCurrentBucketNum, mDuration);
+            detectAndDeclareAnomaly(timestamp, mCurrentBucketNum, mDuration + mDurationFullBucket);
         }
     }
 
@@ -275,7 +308,7 @@
             mDuration += (timestamp - mLastStartTime);
             mPaused.insert(mStarted.begin(), mStarted.end());
             mStarted.clear();
-            detectAndDeclareAnomaly(timestamp, mCurrentBucketNum, mDuration);
+            detectAndDeclareAnomaly(timestamp, mCurrentBucketNum, mDuration + mDurationFullBucket);
         }
     }
     if (mStarted.empty()) {
@@ -298,7 +331,7 @@
     // As we move into the future, old buckets get overwritten (so their old data is erased).
 
     // Sum of past durations. Will change as we overwrite old buckets.
-    int64_t pastNs = mDuration;
+    int64_t pastNs = mDuration + mDurationFullBucket;
     pastNs += anomalyTracker.getSumOverPastBuckets(mEventKey);
 
     // How much of the threshold is still unaccounted after considering pastNs.
diff --git a/cmds/statsd/src/metrics/duration_helper/OringDurationTracker.h b/cmds/statsd/src/metrics/duration_helper/OringDurationTracker.h
index 75b5a81..73e50e0 100644
--- a/cmds/statsd/src/metrics/duration_helper/OringDurationTracker.h
+++ b/cmds/statsd/src/metrics/duration_helper/OringDurationTracker.h
@@ -30,7 +30,8 @@
     OringDurationTracker(const ConfigKey& key, const int64_t& id,
                          const MetricDimensionKey& eventKey, sp<ConditionWizard> wizard,
                          int conditionIndex, const FieldMatcher& dimensionInCondition, bool nesting,
-                         uint64_t currentBucketStartNs, uint64_t bucketSizeNs, bool conditionSliced,
+                         uint64_t currentBucketStartNs, uint64_t currentBucketNum,
+                         uint64_t startTimeNs, uint64_t bucketSizeNs, bool conditionSliced,
                          const std::vector<sp<DurationAnomalyTracker>>& anomalyTrackers);
 
     OringDurationTracker(const OringDurationTracker& tracker) = default;
@@ -46,6 +47,9 @@
     void onSlicedConditionMayChange(const uint64_t timestamp) override;
     void onConditionChanged(bool condition, const uint64_t timestamp) override;
 
+    bool flushCurrentBucket(
+            const uint64_t& eventTimeNs,
+            std::unordered_map<MetricDimensionKey, std::vector<DurationBucket>>* output) override;
     bool flushIfNeeded(
             uint64_t timestampNs,
             std::unordered_map<MetricDimensionKey, std::vector<DurationBucket>>* output) override;
diff --git a/cmds/statsd/src/packages/PackageInfoListener.h b/cmds/statsd/src/packages/PackageInfoListener.h
index df29eb0..03cb364 100644
--- a/cmds/statsd/src/packages/PackageInfoListener.h
+++ b/cmds/statsd/src/packages/PackageInfoListener.h
@@ -28,13 +28,15 @@
 public:
     // Uid map will notify this listener that the app with apk name and uid has been upgraded to
     // the specified version.
-    virtual void notifyAppUpgrade(const std::string& apk, const int uid, const int64_t version) = 0;
+    virtual void notifyAppUpgrade(const uint64_t& eventTimeNs, const std::string& apk,
+                                  const int uid, const int64_t version) = 0;
 
     // Notify interested listeners that the given apk and uid combination no longer exits.
-    virtual void notifyAppRemoved(const std::string& apk, const int uid) = 0;
+    virtual void notifyAppRemoved(const uint64_t& eventTimeNs, const std::string& apk,
+                                  const int uid) = 0;
 
     // Notify the listener that the UidMap snapshot is available.
-    virtual void onUidMapReceived() = 0;
+    virtual void onUidMapReceived(const uint64_t& eventTimeNs) = 0;
 };
 
 }  // namespace statsd
diff --git a/cmds/statsd/src/packages/UidMap.cpp b/cmds/statsd/src/packages/UidMap.cpp
index 91279661..691423e 100644
--- a/cmds/statsd/src/packages/UidMap.cpp
+++ b/cmds/statsd/src/packages/UidMap.cpp
@@ -119,7 +119,7 @@
     for (auto weakPtr : broadcastList) {
         auto strongPtr = weakPtr.promote();
         if (strongPtr != NULL) {
-            strongPtr->onUidMapReceived();
+            strongPtr->onUidMapReceived(timestamp);
         }
     }
 }
@@ -166,7 +166,7 @@
     for (auto weakPtr : broadcastList) {
         auto strongPtr = weakPtr.promote();
         if (strongPtr != NULL) {
-            strongPtr->notifyAppUpgrade(appName, uid, versionCode);
+            strongPtr->notifyAppUpgrade(timestamp, appName, uid, versionCode);
         }
     }
 }
@@ -239,7 +239,7 @@
     for (auto weakPtr : broadcastList) {
         auto strongPtr = weakPtr.promote();
         if (strongPtr != NULL) {
-            strongPtr->notifyAppRemoved(app, uid);
+            strongPtr->notifyAppRemoved(timestamp, app, uid);
         }
     }
 }
@@ -316,13 +316,13 @@
     mLastUpdatePerConfigKey[key] = timestamp;
     int64_t newMin = getMinimumTimestampNs();
 
-    if (newMin > prevMin) {
+    if (newMin > prevMin) {  // Delete anything possible now that the minimum has moved forward.
         int64_t cutoff_nanos = newMin;
         auto snapshots = mOutput.mutable_snapshots();
         auto it_snapshots = snapshots->cbegin();
         while (it_snapshots != snapshots->cend()) {
             if (it_snapshots->timestamp_nanos() < cutoff_nanos) {
-                // it_snapshots now points to the following element.
+                // it_snapshots points to the following element after erasing.
                 it_snapshots = snapshots->erase(it_snapshots);
             } else {
                 ++it_snapshots;
@@ -332,7 +332,7 @@
         auto it_deltas = deltas->cbegin();
         while (it_deltas != deltas->cend()) {
             if (it_deltas->timestamp_nanos() < cutoff_nanos) {
-                // it_deltas now points to the following element.
+                // it_snapshots points to the following element after erasing.
                 it_deltas = deltas->erase(it_deltas);
             } else {
                 ++it_deltas;
diff --git a/cmds/statsd/tests/e2e/WakelockDuration_e2e_test.cpp b/cmds/statsd/tests/e2e/WakelockDuration_e2e_test.cpp
index 1186a16..a99dbe8 100644
--- a/cmds/statsd/tests/e2e/WakelockDuration_e2e_test.cpp
+++ b/cmds/statsd/tests/e2e/WakelockDuration_e2e_test.cpp
@@ -40,9 +40,11 @@
 
     auto holdingWakelockPredicate = CreateHoldingWakelockPredicate();
     // The predicate is dimensioning by any attribution node and both by uid and tag.
-    *holdingWakelockPredicate.mutable_simple_predicate()->mutable_dimensions() =
-        CreateAttributionUidAndTagDimensions(
+    FieldMatcher dimensions = CreateAttributionUidAndTagDimensions(
             android::util::WAKELOCK_STATE_CHANGED, {Position::FIRST, Position::LAST});
+    // Also slice by the wakelock tag
+    dimensions.add_child()->set_field(3);  // The wakelock tag is set in field 3 of the wakelock.
+    *holdingWakelockPredicate.mutable_simple_predicate()->mutable_dimensions() = dimensions;
     *config.add_predicate() = holdingWakelockPredicate;
 
     auto durationMetric = config.add_duration_metric();
@@ -58,133 +60,198 @@
     return config;
 }
 
-}  // namespace
+std::vector<AttributionNode> attributions1 = {CreateAttribution(111, "App1"),
+                                              CreateAttribution(222, "GMSCoreModule1"),
+                                              CreateAttribution(222, "GMSCoreModule2")};
 
-TEST(WakelockDurationE2eTest, TestAggregatedPredicateDimensions) {
-    ConfigKey cfgKey;
-    for (auto aggregationType : { DurationMetric::SUM, DurationMetric::MAX_SPARSE }) {
-        auto config = CreateStatsdConfig(aggregationType);
-        uint64_t bucketStartTimeNs = 10000000000;
-        uint64_t bucketSizeNs =
+std::vector<AttributionNode> attributions2 = {CreateAttribution(111, "App2"),
+                                              CreateAttribution(222, "GMSCoreModule1"),
+                                              CreateAttribution(222, "GMSCoreModule2")};
+
+/*
+Events:
+Screen off is met from (200ns,1 min+500ns].
+Acquire event for wl1 from 2ns to 1min+2ns
+Acquire event for wl2 from 1min-10ns to 2min-15ns
+*/
+void FeedEvents(StatsdConfig config, sp<StatsLogProcessor> processor) {
+    uint64_t bucketStartTimeNs = 10000000000;
+    uint64_t bucketSizeNs =
             TimeUnitToBucketSizeInMillis(config.duration_metric(0).bucket()) * 1000000LL;
 
-        auto processor = CreateStatsLogProcessor(bucketStartTimeNs / NS_PER_SEC, config, cfgKey);
-        EXPECT_EQ(processor->mMetricsManagers.size(), 1u);
-        EXPECT_TRUE(processor->mMetricsManagers.begin()->second->isConfigValid());
-
-        auto screenTurnedOnEvent =
-            CreateScreenStateChangedEvent(android::view::DisplayStateEnum::DISPLAY_STATE_ON,
-                                          bucketStartTimeNs + 1);
-        auto screenTurnedOffEvent =
-            CreateScreenStateChangedEvent(android::view::DisplayStateEnum::DISPLAY_STATE_OFF,
-                                          bucketStartTimeNs + 200);
-        auto screenTurnedOnEvent2 =
+    auto screenTurnedOnEvent = CreateScreenStateChangedEvent(
+            android::view::DisplayStateEnum::DISPLAY_STATE_ON, bucketStartTimeNs + 1);
+    auto screenTurnedOffEvent = CreateScreenStateChangedEvent(
+            android::view::DisplayStateEnum::DISPLAY_STATE_OFF, bucketStartTimeNs + 200);
+    auto screenTurnedOnEvent2 =
             CreateScreenStateChangedEvent(android::view::DisplayStateEnum::DISPLAY_STATE_ON,
                                           bucketStartTimeNs + bucketSizeNs + 500);
 
-        std::vector<AttributionNode> attributions1 =
-            {CreateAttribution(111, "App1"), CreateAttribution(222, "GMSCoreModule1"),
-             CreateAttribution(222, "GMSCoreModule2")};
+    auto acquireEvent1 = CreateAcquireWakelockEvent(attributions1, "wl1", bucketStartTimeNs + 2);
+    auto releaseEvent1 =
+            CreateReleaseWakelockEvent(attributions1, "wl1", bucketStartTimeNs + bucketSizeNs + 2);
+    auto acquireEvent2 =
+            CreateAcquireWakelockEvent(attributions2, "wl2", bucketStartTimeNs + bucketSizeNs - 10);
+    auto releaseEvent2 = CreateReleaseWakelockEvent(attributions2, "wl2",
+                                                    bucketStartTimeNs + 2 * bucketSizeNs - 15);
 
-        std::vector<AttributionNode> attributions2 =
-            {CreateAttribution(111, "App2"), CreateAttribution(222, "GMSCoreModule1"),
-             CreateAttribution(222, "GMSCoreModule2")};
+    std::vector<std::unique_ptr<LogEvent>> events;
 
-        auto acquireEvent1 = CreateAcquireWakelockEvent(
-            attributions1, "wl1", bucketStartTimeNs + 2);
-        auto acquireEvent2 = CreateAcquireWakelockEvent(
-            attributions2, "wl2", bucketStartTimeNs + bucketSizeNs - 10);
+    events.push_back(std::move(screenTurnedOnEvent));
+    events.push_back(std::move(screenTurnedOffEvent));
+    events.push_back(std::move(screenTurnedOnEvent2));
+    events.push_back(std::move(acquireEvent1));
+    events.push_back(std::move(acquireEvent2));
+    events.push_back(std::move(releaseEvent1));
+    events.push_back(std::move(releaseEvent2));
 
-        auto releaseEvent1 = CreateReleaseWakelockEvent(
-            attributions1, "wl1", bucketStartTimeNs + bucketSizeNs + 2);
-        auto releaseEvent2 = CreateReleaseWakelockEvent(
-            attributions2, "wl2", bucketStartTimeNs + 2 * bucketSizeNs - 15);
+    sortLogEventsByTimestamp(&events);
 
-
-        std::vector<std::unique_ptr<LogEvent>> events;
-
-        events.push_back(std::move(screenTurnedOnEvent));
-        events.push_back(std::move(screenTurnedOffEvent));
-        events.push_back(std::move(screenTurnedOnEvent2));
-        events.push_back(std::move(acquireEvent1));
-        events.push_back(std::move(acquireEvent2));
-        events.push_back(std::move(releaseEvent1));
-        events.push_back(std::move(releaseEvent2));
-
-        sortLogEventsByTimestamp(&events);
-
-        for (const auto& event : events) {
-            processor->OnLogEvent(event.get());
-        }
-
-        ConfigMetricsReportList reports;
-        processor->onDumpReport(cfgKey, bucketStartTimeNs + 2 * bucketSizeNs - 1, &reports);
-        EXPECT_EQ(reports.reports_size(), 1);
-        EXPECT_EQ(reports.reports(0).metrics_size(), 1);
-        // Only 1 dimension output. The tag dimension in the predicate has been aggregated.
-        EXPECT_EQ(reports.reports(0).metrics(0).duration_metrics().data_size(), 1);
-
-        auto data = reports.reports(0).metrics(0).duration_metrics().data(0);
-        // Validate dimension value.
-        ValidateAttributionUidDimension(
-            data.dimensions_in_what(),
-            android::util::WAKELOCK_STATE_CHANGED, 111);
-        // Validate bucket info.
-        EXPECT_EQ(reports.reports(0).metrics(0).duration_metrics().data(0).bucket_info_size(), 1);
-        data = reports.reports(0).metrics(0).duration_metrics().data(0);
-        // The wakelock holding interval starts from the screen off event and to the end of the 1st
-        // bucket.
-        EXPECT_EQ((unsigned long long)data.bucket_info(0).duration_nanos(), bucketSizeNs - 200);
-
-        reports.Clear();
-        processor->onDumpReport(cfgKey, bucketStartTimeNs + 2 * bucketSizeNs + 1, &reports);
-        EXPECT_EQ(reports.reports_size(), 1);
-        EXPECT_EQ(reports.reports(0).metrics_size(), 1);
-        EXPECT_EQ(reports.reports(0).metrics(0).duration_metrics().data_size(), 1);
-        // Dump the report after the end of 2nd bucket.
-        EXPECT_EQ(reports.reports(0).metrics(0).duration_metrics().data(0).bucket_info_size(), 2);
-        data = reports.reports(0).metrics(0).duration_metrics().data(0);
-        // Validate dimension value.
-        ValidateAttributionUidDimension(
-            data.dimensions_in_what(), android::util::WAKELOCK_STATE_CHANGED, 111);
-        // Two output buckets.
-        // The wakelock holding interval in the 1st bucket starts from the screen off event and to
-        // the end of the 1st bucket.
-        EXPECT_EQ((unsigned long long)data.bucket_info(0).duration_nanos(),
-            bucketStartTimeNs + bucketSizeNs - (bucketStartTimeNs + 200));
-        // The wakelock holding interval in the 2nd bucket starts at the beginning of the bucket and
-        // ends at the second screen on event.
-        EXPECT_EQ((unsigned long long)data.bucket_info(1).duration_nanos(), 500UL);
-
-        events.clear();
-        events.push_back(CreateScreenStateChangedEvent(
-            android::view::DisplayStateEnum::DISPLAY_STATE_OFF,
-            bucketStartTimeNs + 2 * bucketSizeNs + 90));
-        events.push_back(CreateAcquireWakelockEvent(
-            attributions1, "wl3", bucketStartTimeNs + 2 * bucketSizeNs + 100));
-        events.push_back(CreateReleaseWakelockEvent(
-            attributions1, "wl3", bucketStartTimeNs + 5 * bucketSizeNs + 100));
-        sortLogEventsByTimestamp(&events);
-        for (const auto& event : events) {
-            processor->OnLogEvent(event.get());
-        }
-        reports.Clear();
-        processor->onDumpReport(cfgKey, bucketStartTimeNs + 6 * bucketSizeNs + 1, &reports);
-        EXPECT_EQ(reports.reports_size(), 1);
-        EXPECT_EQ(reports.reports(0).metrics_size(), 1);
-        EXPECT_EQ(reports.reports(0).metrics(0).duration_metrics().data_size(), 1);
-        EXPECT_EQ(reports.reports(0).metrics(0).duration_metrics().data(0).bucket_info_size(), 6);
-        data = reports.reports(0).metrics(0).duration_metrics().data(0);
-        ValidateAttributionUidDimension(
-            data.dimensions_in_what(), android::util::WAKELOCK_STATE_CHANGED, 111);
-        // The last wakelock holding spans 4 buckets.
-        EXPECT_EQ((unsigned long long)data.bucket_info(2).duration_nanos(), bucketSizeNs - 100);
-        EXPECT_EQ((unsigned long long)data.bucket_info(3).duration_nanos(), bucketSizeNs);
-        EXPECT_EQ((unsigned long long)data.bucket_info(4).duration_nanos(), bucketSizeNs);
-        EXPECT_EQ((unsigned long long)data.bucket_info(5).duration_nanos(), 100UL);
+    for (const auto& event : events) {
+        processor->OnLogEvent(event.get());
     }
 }
 
+}  // namespace
+
+TEST(WakelockDurationE2eTest, TestAggregatedPredicateDimensionsForSumDuration) {
+    ConfigKey cfgKey;
+    auto config = CreateStatsdConfig(DurationMetric::SUM);
+    uint64_t bucketStartTimeNs = 10000000000;
+    uint64_t bucketSizeNs =
+            TimeUnitToBucketSizeInMillis(config.duration_metric(0).bucket()) * 1000000LL;
+    auto processor = CreateStatsLogProcessor(bucketStartTimeNs / NS_PER_SEC, config, cfgKey);
+    EXPECT_EQ(processor->mMetricsManagers.size(), 1u);
+    EXPECT_TRUE(processor->mMetricsManagers.begin()->second->isConfigValid());
+    FeedEvents(config, processor);
+    ConfigMetricsReportList reports;
+    processor->onDumpReport(cfgKey, bucketStartTimeNs + 2 * bucketSizeNs - 1, &reports);
+
+    EXPECT_EQ(reports.reports_size(), 1);
+    EXPECT_EQ(reports.reports(0).metrics_size(), 1);
+    // Only 1 dimension output. The tag dimension in the predicate has been aggregated.
+    EXPECT_EQ(reports.reports(0).metrics(0).duration_metrics().data_size(), 1);
+
+    auto data = reports.reports(0).metrics(0).duration_metrics().data(0);
+    // Validate dimension value.
+    ValidateAttributionUidDimension(data.dimensions_in_what(),
+                                    android::util::WAKELOCK_STATE_CHANGED, 111);
+    // Validate bucket info.
+    EXPECT_EQ(reports.reports(0).metrics(0).duration_metrics().data(0).bucket_info_size(), 1);
+    data = reports.reports(0).metrics(0).duration_metrics().data(0);
+    // The wakelock holding interval starts from the screen off event and to the end of the 1st
+    // bucket.
+    EXPECT_EQ((unsigned long long)data.bucket_info(0).duration_nanos(), bucketSizeNs - 200);
+
+    reports.Clear();
+    processor->onDumpReport(cfgKey, bucketStartTimeNs + 2 * bucketSizeNs + 1, &reports);
+    EXPECT_EQ(reports.reports_size(), 1);
+    EXPECT_EQ(reports.reports(0).metrics_size(), 1);
+    EXPECT_EQ(reports.reports(0).metrics(0).duration_metrics().data_size(), 1);
+    // Dump the report after the end of 2nd bucket.
+    EXPECT_EQ(reports.reports(0).metrics(0).duration_metrics().data(0).bucket_info_size(), 2);
+    data = reports.reports(0).metrics(0).duration_metrics().data(0);
+    // Validate dimension value.
+    ValidateAttributionUidDimension(data.dimensions_in_what(),
+                                    android::util::WAKELOCK_STATE_CHANGED, 111);
+    // Two output buckets.
+    // The wakelock holding interval in the 1st bucket starts from the screen off event and to
+    // the end of the 1st bucket.
+    EXPECT_EQ((unsigned long long)data.bucket_info(0).duration_nanos(),
+              bucketStartTimeNs + bucketSizeNs - (bucketStartTimeNs + 200));
+    // The wakelock holding interval in the 2nd bucket starts at the beginning of the bucket and
+    // ends at the second screen on event.
+    EXPECT_EQ((unsigned long long)data.bucket_info(1).duration_nanos(), 500UL);
+
+    std::vector<std::unique_ptr<LogEvent>> events;
+    events.push_back(
+            CreateScreenStateChangedEvent(android::view::DisplayStateEnum::DISPLAY_STATE_OFF,
+                                          bucketStartTimeNs + 2 * bucketSizeNs + 90));
+    events.push_back(CreateAcquireWakelockEvent(attributions1, "wl3",
+                                                bucketStartTimeNs + 2 * bucketSizeNs + 100));
+    events.push_back(CreateReleaseWakelockEvent(attributions1, "wl3",
+                                                bucketStartTimeNs + 5 * bucketSizeNs + 100));
+    sortLogEventsByTimestamp(&events);
+    for (const auto& event : events) {
+        processor->OnLogEvent(event.get());
+    }
+    reports.Clear();
+    processor->onDumpReport(cfgKey, bucketStartTimeNs + 6 * bucketSizeNs + 1, &reports);
+    EXPECT_EQ(reports.reports_size(), 1);
+    EXPECT_EQ(reports.reports(0).metrics_size(), 1);
+    EXPECT_EQ(reports.reports(0).metrics(0).duration_metrics().data_size(), 1);
+    EXPECT_EQ(reports.reports(0).metrics(0).duration_metrics().data(0).bucket_info_size(), 6);
+    data = reports.reports(0).metrics(0).duration_metrics().data(0);
+    ValidateAttributionUidDimension(data.dimensions_in_what(),
+                                    android::util::WAKELOCK_STATE_CHANGED, 111);
+    // The last wakelock holding spans 4 buckets.
+    EXPECT_EQ((unsigned long long)data.bucket_info(2).duration_nanos(), bucketSizeNs - 100);
+    EXPECT_EQ((unsigned long long)data.bucket_info(3).duration_nanos(), bucketSizeNs);
+    EXPECT_EQ((unsigned long long)data.bucket_info(4).duration_nanos(), bucketSizeNs);
+    EXPECT_EQ((unsigned long long)data.bucket_info(5).duration_nanos(), 100UL);
+}
+
+TEST(WakelockDurationE2eTest, TestAggregatedPredicateDimensionsForMaxDuration) {
+    ConfigKey cfgKey;
+    auto config = CreateStatsdConfig(DurationMetric::MAX_SPARSE);
+    uint64_t bucketStartTimeNs = 10000000000;
+    uint64_t bucketSizeNs =
+            TimeUnitToBucketSizeInMillis(config.duration_metric(0).bucket()) * 1000000LL;
+    auto processor = CreateStatsLogProcessor(bucketStartTimeNs / NS_PER_SEC, config, cfgKey);
+    EXPECT_EQ(processor->mMetricsManagers.size(), 1u);
+    EXPECT_TRUE(processor->mMetricsManagers.begin()->second->isConfigValid());
+    FeedEvents(config, processor);
+    ConfigMetricsReportList reports;
+    processor->onDumpReport(cfgKey, bucketStartTimeNs + 2 * bucketSizeNs - 1, &reports);
+
+    EXPECT_EQ(reports.reports_size(), 1);
+    EXPECT_EQ(reports.reports(0).metrics_size(), 1);
+    // Nothing has ended in the first bucket.
+    EXPECT_EQ(reports.reports(0).metrics(0).duration_metrics().data_size(), 0);
+
+    reports.Clear();
+    processor->onDumpReport(cfgKey, bucketStartTimeNs + 2 * bucketSizeNs + 1, &reports);
+    EXPECT_EQ(reports.reports_size(), 1);
+    EXPECT_EQ(reports.reports(0).metrics_size(), 1);
+    EXPECT_EQ(reports.reports(0).metrics(0).duration_metrics().data_size(), 1);
+    // Dump the report after the end of 2nd bucket. One dimension with one bucket.
+    EXPECT_EQ(reports.reports(0).metrics(0).duration_metrics().data(0).bucket_info_size(), 1);
+    auto data = reports.reports(0).metrics(0).duration_metrics().data(0);
+    // Validate dimension value.
+    ValidateAttributionUidDimension(data.dimensions_in_what(),
+                                    android::util::WAKELOCK_STATE_CHANGED, 111);
+    // The max is acquire event for wl1 to screen off start.
+    EXPECT_EQ((unsigned long long)data.bucket_info(0).duration_nanos(), bucketSizeNs + 2 - 200);
+
+    std::vector<std::unique_ptr<LogEvent>> events;
+    events.push_back(
+            CreateScreenStateChangedEvent(android::view::DisplayStateEnum::DISPLAY_STATE_OFF,
+                                          bucketStartTimeNs + 2 * bucketSizeNs + 90));
+    events.push_back(CreateAcquireWakelockEvent(attributions1, "wl3",
+                                                bucketStartTimeNs + 2 * bucketSizeNs + 100));
+    events.push_back(CreateReleaseWakelockEvent(attributions1, "wl3",
+                                                bucketStartTimeNs + 5 * bucketSizeNs + 100));
+    sortLogEventsByTimestamp(&events);
+    for (const auto& event : events) {
+        processor->OnLogEvent(event.get());
+    }
+    reports.Clear();
+    processor->onDumpReport(cfgKey, bucketStartTimeNs + 6 * bucketSizeNs + 1, &reports);
+    EXPECT_EQ(reports.reports_size(), 1);
+    EXPECT_EQ(reports.reports(0).metrics_size(), 1);
+    EXPECT_EQ(reports.reports(0).metrics(0).duration_metrics().data_size(), 1);
+    EXPECT_EQ(reports.reports(0).metrics(0).duration_metrics().data(0).bucket_info_size(), 2);
+    data = reports.reports(0).metrics(0).duration_metrics().data(0);
+    ValidateAttributionUidDimension(data.dimensions_in_what(),
+                                    android::util::WAKELOCK_STATE_CHANGED, 111);
+    // The last wakelock holding spans 4 buckets.
+    EXPECT_EQ((unsigned long long)data.bucket_info(1).duration_nanos(), 3 * bucketSizeNs);
+    EXPECT_EQ((unsigned long long)data.bucket_info(1).start_bucket_nanos(),
+              bucketStartTimeNs + 5 * bucketSizeNs);
+    EXPECT_EQ((unsigned long long)data.bucket_info(1).end_bucket_nanos(),
+              bucketStartTimeNs + 6 * bucketSizeNs);
+}
+
 #else
 GTEST_LOG_(INFO) << "This test does nothing.\n";
 #endif
diff --git a/cmds/statsd/tests/metrics/CountMetricProducer_test.cpp b/cmds/statsd/tests/metrics/CountMetricProducer_test.cpp
index 50b3532..87a1079 100644
--- a/cmds/statsd/tests/metrics/CountMetricProducer_test.cpp
+++ b/cmds/statsd/tests/metrics/CountMetricProducer_test.cpp
@@ -191,6 +191,117 @@
     EXPECT_EQ(1LL, bucketInfo.mCount);
 }
 
+TEST(CountMetricProducerTest, TestEventWithAppUpgrade) {
+    uint64_t bucketStartTimeNs = 10000000000;
+    uint64_t bucketSizeNs = TimeUnitToBucketSizeInMillis(ONE_MINUTE) * 1000000LL;
+    uint64_t eventUpgradeTimeNs = bucketStartTimeNs + 15 * NS_PER_SEC;
+
+    int tagId = 1;
+    int conditionTagId = 2;
+
+    CountMetric metric;
+    metric.set_id(1);
+    metric.set_bucket(ONE_MINUTE);
+    Alert alert;
+    alert.set_num_buckets(3);
+    alert.set_trigger_if_sum_gt(2);
+    LogEvent event1(tagId, bucketStartTimeNs + 1);
+    event1.write("111");  // uid
+    event1.init();
+    sp<MockConditionWizard> wizard = new NaggyMock<MockConditionWizard>();
+    CountMetricProducer countProducer(kConfigKey, metric, -1 /* no condition */, wizard,
+                                      bucketStartTimeNs);
+    sp<AnomalyTracker> anomalyTracker = countProducer.addAnomalyTracker(alert);
+    EXPECT_TRUE(anomalyTracker != nullptr);
+
+    // Bucket is flushed yet.
+    countProducer.onMatchedLogEvent(1 /*log matcher index*/, event1);
+    EXPECT_EQ(0UL, countProducer.mPastBuckets.size());
+    EXPECT_EQ(0, anomalyTracker->getSumOverPastBuckets(DEFAULT_METRIC_DIMENSION_KEY));
+
+    // App upgrade forces bucket flush.
+    // Check that there's a past bucket and the bucket end is not adjusted.
+    countProducer.notifyAppUpgrade(eventUpgradeTimeNs, "ANY.APP", 1, 1);
+    EXPECT_EQ(1UL, countProducer.mPastBuckets[DEFAULT_METRIC_DIMENSION_KEY].size());
+    EXPECT_EQ((long long)bucketStartTimeNs,
+              countProducer.mPastBuckets[DEFAULT_METRIC_DIMENSION_KEY][0].mBucketStartNs);
+    EXPECT_EQ((long long)eventUpgradeTimeNs,
+              countProducer.mPastBuckets[DEFAULT_METRIC_DIMENSION_KEY][0].mBucketEndNs);
+    EXPECT_EQ(eventUpgradeTimeNs, countProducer.mCurrentBucketStartTimeNs);
+    // Anomaly tracker only contains full buckets.
+    EXPECT_EQ(0, anomalyTracker->getSumOverPastBuckets(DEFAULT_METRIC_DIMENSION_KEY));
+
+    uint64_t lastEndTimeNs = countProducer.getCurrentBucketEndTimeNs();
+    // Next event occurs in same bucket as partial bucket created.
+    LogEvent event2(tagId, bucketStartTimeNs + 59 * NS_PER_SEC + 10);
+    event2.write("222");  // uid
+    event2.init();
+    countProducer.onMatchedLogEvent(1 /*log matcher index*/, event2);
+    EXPECT_EQ(1UL, countProducer.mPastBuckets[DEFAULT_METRIC_DIMENSION_KEY].size());
+    EXPECT_EQ(eventUpgradeTimeNs, countProducer.mCurrentBucketStartTimeNs);
+    EXPECT_EQ(0, anomalyTracker->getSumOverPastBuckets(DEFAULT_METRIC_DIMENSION_KEY));
+
+    // Third event in following bucket.
+    LogEvent event3(tagId, bucketStartTimeNs + 62 * NS_PER_SEC + 10);
+    event3.write("333");  // uid
+    event3.init();
+    countProducer.onMatchedLogEvent(1 /*log matcher index*/, event3);
+    EXPECT_EQ(2UL, countProducer.mPastBuckets[DEFAULT_METRIC_DIMENSION_KEY].size());
+    EXPECT_EQ(lastEndTimeNs, countProducer.mCurrentBucketStartTimeNs);
+    EXPECT_EQ(2, anomalyTracker->getSumOverPastBuckets(DEFAULT_METRIC_DIMENSION_KEY));
+}
+
+TEST(CountMetricProducerTest, TestEventWithAppUpgradeInNextBucket) {
+    uint64_t bucketStartTimeNs = 10000000000;
+    uint64_t bucketSizeNs = TimeUnitToBucketSizeInMillis(ONE_MINUTE) * 1000000LL;
+    uint64_t eventUpgradeTimeNs = bucketStartTimeNs + 65 * NS_PER_SEC;
+
+    int tagId = 1;
+    int conditionTagId = 2;
+
+    CountMetric metric;
+    metric.set_id(1);
+    metric.set_bucket(ONE_MINUTE);
+    LogEvent event1(tagId, bucketStartTimeNs + 1);
+    event1.write("111");  // uid
+    event1.init();
+    sp<MockConditionWizard> wizard = new NaggyMock<MockConditionWizard>();
+    CountMetricProducer countProducer(kConfigKey, metric, -1 /* no condition */, wizard,
+                                      bucketStartTimeNs);
+
+    // Bucket is flushed yet.
+    countProducer.onMatchedLogEvent(1 /*log matcher index*/, event1);
+    EXPECT_EQ(0UL, countProducer.mPastBuckets.size());
+
+    // App upgrade forces bucket flush.
+    // Check that there's a past bucket and the bucket end is not adjusted.
+    countProducer.notifyAppUpgrade(eventUpgradeTimeNs, "ANY.APP", 1, 1);
+    EXPECT_EQ(1UL, countProducer.mPastBuckets[DEFAULT_METRIC_DIMENSION_KEY].size());
+    EXPECT_EQ((int64_t)bucketStartTimeNs,
+              countProducer.mPastBuckets[DEFAULT_METRIC_DIMENSION_KEY][0].mBucketStartNs);
+    EXPECT_EQ(bucketStartTimeNs + bucketSizeNs,
+              (uint64_t)countProducer.mPastBuckets[DEFAULT_METRIC_DIMENSION_KEY][0].mBucketEndNs);
+    EXPECT_EQ(eventUpgradeTimeNs, countProducer.mCurrentBucketStartTimeNs);
+
+    // Next event occurs in same bucket as partial bucket created.
+    LogEvent event2(tagId, bucketStartTimeNs + 70 * NS_PER_SEC + 10);
+    event2.write("222");  // uid
+    event2.init();
+    countProducer.onMatchedLogEvent(1 /*log matcher index*/, event2);
+    EXPECT_EQ(1UL, countProducer.mPastBuckets[DEFAULT_METRIC_DIMENSION_KEY].size());
+
+    // Third event in following bucket.
+    LogEvent event3(tagId, bucketStartTimeNs + 121 * NS_PER_SEC + 10);
+    event3.write("333");  // uid
+    event3.init();
+    countProducer.onMatchedLogEvent(1 /*log matcher index*/, event3);
+    EXPECT_EQ(2UL, countProducer.mPastBuckets[DEFAULT_METRIC_DIMENSION_KEY].size());
+    EXPECT_EQ((int64_t)eventUpgradeTimeNs,
+              countProducer.mPastBuckets[DEFAULT_METRIC_DIMENSION_KEY][1].mBucketStartNs);
+    EXPECT_EQ(bucketStartTimeNs + 2 * bucketSizeNs,
+              (uint64_t)countProducer.mPastBuckets[DEFAULT_METRIC_DIMENSION_KEY][1].mBucketEndNs);
+}
+
 TEST(CountMetricProducerTest, TestAnomalyDetectionUnSliced) {
     Alert alert;
     alert.set_id(11);
diff --git a/cmds/statsd/tests/metrics/DurationMetricProducer_test.cpp b/cmds/statsd/tests/metrics/DurationMetricProducer_test.cpp
index c9fe252..23e15f7 100644
--- a/cmds/statsd/tests/metrics/DurationMetricProducer_test.cpp
+++ b/cmds/statsd/tests/metrics/DurationMetricProducer_test.cpp
@@ -116,6 +116,231 @@
     EXPECT_EQ(1ULL, buckets2[0].mDuration);
 }
 
+TEST(DurationMetricTrackerTest, TestSumDurationWithUpgrade) {
+    /**
+     * The duration starts from the first bucket, through the two partial buckets (10-70sec),
+     * another bucket, and ends at the beginning of the next full bucket.
+     * Expected buckets:
+     *  - [10,25]: 14 secs
+     *  - [25,70]: All 45 secs
+     *  - [70,130]: All 60 secs
+     *  - [130, 210]: Only 5 secs (event ended at 135sec)
+     */
+    uint64_t bucketStartTimeNs = 10000000000;
+    uint64_t bucketSizeNs = TimeUnitToBucketSizeInMillis(ONE_MINUTE) * 1000000LL;
+    uint64_t eventUpgradeTimeNs = bucketStartTimeNs + 15 * NS_PER_SEC;
+    uint64_t startTimeNs = bucketStartTimeNs + 1 * NS_PER_SEC;
+    uint64_t endTimeNs = startTimeNs + 125 * NS_PER_SEC;
+
+    int tagId = 1;
+
+    DurationMetric metric;
+    metric.set_id(1);
+    metric.set_bucket(ONE_MINUTE);
+    metric.set_aggregation_type(DurationMetric_AggregationType_SUM);
+    sp<MockConditionWizard> wizard = new NaggyMock<MockConditionWizard>();
+    FieldMatcher dimensions;
+    DurationMetricProducer durationProducer(
+            kConfigKey, metric, -1 /* no condition */, 1 /* start index */, 2 /* stop index */,
+            3 /* stop_all index */, false /*nesting*/, wizard, dimensions, bucketStartTimeNs);
+
+    durationProducer.onMatchedLogEvent(1 /* start index*/, LogEvent(tagId, startTimeNs));
+    EXPECT_EQ(0UL, durationProducer.mPastBuckets.size());
+    EXPECT_EQ(bucketStartTimeNs, durationProducer.mCurrentBucketStartTimeNs);
+
+    durationProducer.notifyAppUpgrade(eventUpgradeTimeNs, "ANY.APP", 1, 1);
+    EXPECT_EQ(1UL, durationProducer.mPastBuckets[DEFAULT_METRIC_DIMENSION_KEY].size());
+    std::vector<DurationBucket> buckets =
+            durationProducer.mPastBuckets[DEFAULT_METRIC_DIMENSION_KEY];
+    EXPECT_EQ(bucketStartTimeNs, buckets[0].mBucketStartNs);
+    EXPECT_EQ(eventUpgradeTimeNs, buckets[0].mBucketEndNs);
+    EXPECT_EQ(eventUpgradeTimeNs - startTimeNs, buckets[0].mDuration);
+    EXPECT_EQ(eventUpgradeTimeNs, durationProducer.mCurrentBucketStartTimeNs);
+
+    // We skip ahead one bucket, so we fill in the first two partial buckets and one full bucket.
+    durationProducer.onMatchedLogEvent(2 /* stop index*/, LogEvent(tagId, endTimeNs));
+    buckets = durationProducer.mPastBuckets[DEFAULT_METRIC_DIMENSION_KEY];
+    EXPECT_EQ(3UL, buckets.size());
+    EXPECT_EQ(eventUpgradeTimeNs, buckets[1].mBucketStartNs);
+    EXPECT_EQ(bucketStartTimeNs + bucketSizeNs, buckets[1].mBucketEndNs);
+    EXPECT_EQ(bucketStartTimeNs + bucketSizeNs - eventUpgradeTimeNs, buckets[1].mDuration);
+    EXPECT_EQ(bucketStartTimeNs + bucketSizeNs, buckets[2].mBucketStartNs);
+    EXPECT_EQ(bucketStartTimeNs + 2 * bucketSizeNs, buckets[2].mBucketEndNs);
+    EXPECT_EQ(bucketSizeNs, buckets[2].mDuration);
+}
+
+TEST(DurationMetricTrackerTest, TestSumDurationWithUpgradeInFollowingBucket) {
+    /**
+     * Expected buckets (start at 11s, upgrade at 75s, end at 135s):
+     *  - [10,70]: 59 secs
+     *  - [70,75]: 5 sec
+     *  - [75,130]: 55 secs
+     */
+    uint64_t bucketStartTimeNs = 10000000000;
+    uint64_t bucketSizeNs = TimeUnitToBucketSizeInMillis(ONE_MINUTE) * 1000000LL;
+    uint64_t eventUpgradeTimeNs = bucketStartTimeNs + 65 * NS_PER_SEC;
+    uint64_t startTimeNs = bucketStartTimeNs + 1 * NS_PER_SEC;
+    uint64_t endTimeNs = startTimeNs + 125 * NS_PER_SEC;
+
+    int tagId = 1;
+
+    DurationMetric metric;
+    metric.set_id(1);
+    metric.set_bucket(ONE_MINUTE);
+    metric.set_aggregation_type(DurationMetric_AggregationType_SUM);
+    sp<MockConditionWizard> wizard = new NaggyMock<MockConditionWizard>();
+    FieldMatcher dimensions;
+    DurationMetricProducer durationProducer(
+            kConfigKey, metric, -1 /* no condition */, 1 /* start index */, 2 /* stop index */,
+            3 /* stop_all index */, false /*nesting*/, wizard, dimensions, bucketStartTimeNs);
+
+    durationProducer.onMatchedLogEvent(1 /* start index*/, LogEvent(tagId, startTimeNs));
+    EXPECT_EQ(0UL, durationProducer.mPastBuckets.size());
+    EXPECT_EQ(bucketStartTimeNs, durationProducer.mCurrentBucketStartTimeNs);
+
+    durationProducer.notifyAppUpgrade(eventUpgradeTimeNs, "ANY.APP", 1, 1);
+    EXPECT_EQ(2UL, durationProducer.mPastBuckets[DEFAULT_METRIC_DIMENSION_KEY].size());
+    std::vector<DurationBucket> buckets =
+            durationProducer.mPastBuckets[DEFAULT_METRIC_DIMENSION_KEY];
+    EXPECT_EQ(bucketStartTimeNs, buckets[0].mBucketStartNs);
+    EXPECT_EQ(bucketStartTimeNs + bucketSizeNs, buckets[0].mBucketEndNs);
+    EXPECT_EQ(bucketStartTimeNs + bucketSizeNs - startTimeNs, buckets[0].mDuration);
+    EXPECT_EQ(bucketStartTimeNs + bucketSizeNs, buckets[1].mBucketStartNs);
+    EXPECT_EQ(eventUpgradeTimeNs, buckets[1].mBucketEndNs);
+    EXPECT_EQ(eventUpgradeTimeNs - (bucketStartTimeNs + bucketSizeNs), buckets[1].mDuration);
+    EXPECT_EQ(eventUpgradeTimeNs, durationProducer.mCurrentBucketStartTimeNs);
+
+    // We skip ahead one bucket, so we fill in the first two partial buckets and one full bucket.
+    durationProducer.onMatchedLogEvent(2 /* stop index*/, LogEvent(tagId, endTimeNs));
+    buckets = durationProducer.mPastBuckets[DEFAULT_METRIC_DIMENSION_KEY];
+    EXPECT_EQ(3UL, buckets.size());
+    EXPECT_EQ(eventUpgradeTimeNs, buckets[2].mBucketStartNs);
+    EXPECT_EQ(bucketStartTimeNs + 2 * bucketSizeNs, buckets[2].mBucketEndNs);
+    EXPECT_EQ(bucketStartTimeNs + 2 * bucketSizeNs - eventUpgradeTimeNs, buckets[2].mDuration);
+}
+
+TEST(DurationMetricTrackerTest, TestSumDurationAnomalyWithUpgrade) {
+    uint64_t bucketStartTimeNs = 10000000000;
+    uint64_t bucketSizeNs = TimeUnitToBucketSizeInMillis(ONE_MINUTE) * 1000000LL;
+    uint64_t eventUpgradeTimeNs = bucketStartTimeNs + 15 * NS_PER_SEC;
+    uint64_t startTimeNs = bucketStartTimeNs + 1;
+    uint64_t endTimeNs = startTimeNs + 65 * NS_PER_SEC;
+
+    int tagId = 1;
+
+    // Setup metric with alert.
+    DurationMetric metric;
+    metric.set_id(1);
+    metric.set_bucket(ONE_MINUTE);
+    metric.set_aggregation_type(DurationMetric_AggregationType_SUM);
+    Alert alert;
+    alert.set_num_buckets(3);
+    alert.set_trigger_if_sum_gt(2);
+
+    sp<MockConditionWizard> wizard = new NaggyMock<MockConditionWizard>();
+    FieldMatcher dimensions;
+    DurationMetricProducer durationProducer(
+            kConfigKey, metric, -1 /* no condition */, 1 /* start index */, 2 /* stop index */,
+            3 /* stop_all index */, false /*nesting*/, wizard, dimensions, bucketStartTimeNs);
+    sp<AnomalyTracker> anomalyTracker = durationProducer.addAnomalyTracker(alert);
+    EXPECT_TRUE(anomalyTracker != nullptr);
+
+    durationProducer.onMatchedLogEvent(1 /* start index*/, LogEvent(tagId, startTimeNs));
+    durationProducer.notifyAppUpgrade(eventUpgradeTimeNs, "ANY.APP", 1, 1);
+    // We skip ahead one bucket, so we fill in the first two partial buckets and one full bucket.
+    durationProducer.onMatchedLogEvent(2 /* stop index*/, LogEvent(tagId, endTimeNs));
+
+    EXPECT_EQ(bucketStartTimeNs + bucketSizeNs - startTimeNs,
+              (uint64_t)anomalyTracker->getSumOverPastBuckets(DEFAULT_METRIC_DIMENSION_KEY));
+}
+
+TEST(DurationMetricTrackerTest, TestMaxDurationWithUpgrade) {
+    uint64_t bucketStartTimeNs = 10000000000;
+    uint64_t bucketSizeNs = TimeUnitToBucketSizeInMillis(ONE_MINUTE) * 1000000LL;
+    uint64_t eventUpgradeTimeNs = bucketStartTimeNs + 15 * NS_PER_SEC;
+    uint64_t startTimeNs = bucketStartTimeNs + 1;
+    uint64_t endTimeNs = startTimeNs + 125 * NS_PER_SEC;
+
+    int tagId = 1;
+
+    DurationMetric metric;
+    metric.set_id(1);
+    metric.set_bucket(ONE_MINUTE);
+    metric.set_aggregation_type(DurationMetric_AggregationType_MAX_SPARSE);
+    LogEvent event1(tagId, startTimeNs);
+    event1.write("111");  // uid
+    event1.init();
+    sp<MockConditionWizard> wizard = new NaggyMock<MockConditionWizard>();
+    FieldMatcher dimensions;
+    DurationMetricProducer durationProducer(
+            kConfigKey, metric, -1 /* no condition */, 1 /* start index */, 2 /* stop index */,
+            3 /* stop_all index */, false /*nesting*/, wizard, dimensions, bucketStartTimeNs);
+
+    durationProducer.onMatchedLogEvent(1 /* start index*/, LogEvent(tagId, startTimeNs));
+    EXPECT_EQ(0UL, durationProducer.mPastBuckets.size());
+    EXPECT_EQ(bucketStartTimeNs, durationProducer.mCurrentBucketStartTimeNs);
+
+    durationProducer.notifyAppUpgrade(eventUpgradeTimeNs, "ANY.APP", 1, 1);
+    EXPECT_EQ(0UL, durationProducer.mPastBuckets[DEFAULT_METRIC_DIMENSION_KEY].size());
+    EXPECT_EQ(eventUpgradeTimeNs, durationProducer.mCurrentBucketStartTimeNs);
+
+    // We skip ahead one bucket, so we fill in the first two partial buckets and one full bucket.
+    durationProducer.onMatchedLogEvent(2 /* stop index*/, LogEvent(tagId, endTimeNs));
+    EXPECT_EQ(0UL, durationProducer.mPastBuckets[DEFAULT_METRIC_DIMENSION_KEY].size());
+
+    durationProducer.flushIfNeededLocked(bucketStartTimeNs + 3 * bucketSizeNs + 1);
+    std::vector<DurationBucket> buckets =
+            durationProducer.mPastBuckets[DEFAULT_METRIC_DIMENSION_KEY];
+    EXPECT_EQ(1UL, buckets.size());
+    EXPECT_EQ(bucketStartTimeNs + 2 * bucketSizeNs, buckets[0].mBucketStartNs);
+    EXPECT_EQ(bucketStartTimeNs + 3 * bucketSizeNs, buckets[0].mBucketEndNs);
+    EXPECT_EQ(endTimeNs - startTimeNs, buckets[0].mDuration);
+}
+
+TEST(DurationMetricTrackerTest, TestMaxDurationWithUpgradeInNextBucket) {
+    uint64_t bucketStartTimeNs = 10000000000;
+    uint64_t bucketSizeNs = TimeUnitToBucketSizeInMillis(ONE_MINUTE) * 1000000LL;
+    uint64_t eventUpgradeTimeNs = bucketStartTimeNs + 65 * NS_PER_SEC;
+    uint64_t startTimeNs = bucketStartTimeNs + 1;
+    uint64_t endTimeNs = startTimeNs + 115 * NS_PER_SEC;
+
+    int tagId = 1;
+
+    DurationMetric metric;
+    metric.set_id(1);
+    metric.set_bucket(ONE_MINUTE);
+    metric.set_aggregation_type(DurationMetric_AggregationType_MAX_SPARSE);
+    LogEvent event1(tagId, startTimeNs);
+    event1.write("111");  // uid
+    event1.init();
+    sp<MockConditionWizard> wizard = new NaggyMock<MockConditionWizard>();
+    FieldMatcher dimensions;
+    DurationMetricProducer durationProducer(
+            kConfigKey, metric, -1 /* no condition */, 1 /* start index */, 2 /* stop index */,
+            3 /* stop_all index */, false /*nesting*/, wizard, dimensions, bucketStartTimeNs);
+
+    durationProducer.onMatchedLogEvent(1 /* start index*/, LogEvent(tagId, startTimeNs));
+    EXPECT_EQ(0UL, durationProducer.mPastBuckets.size());
+    EXPECT_EQ(bucketStartTimeNs, durationProducer.mCurrentBucketStartTimeNs);
+
+    durationProducer.notifyAppUpgrade(eventUpgradeTimeNs, "ANY.APP", 1, 1);
+    EXPECT_EQ(0UL, durationProducer.mPastBuckets[DEFAULT_METRIC_DIMENSION_KEY].size());
+    EXPECT_EQ(eventUpgradeTimeNs, durationProducer.mCurrentBucketStartTimeNs);
+
+    // Stop occurs in the same partial bucket as created for the app upgrade.
+    durationProducer.onMatchedLogEvent(2 /* stop index*/, LogEvent(tagId, endTimeNs));
+    EXPECT_EQ(0UL, durationProducer.mPastBuckets[DEFAULT_METRIC_DIMENSION_KEY].size());
+    EXPECT_EQ(eventUpgradeTimeNs, durationProducer.mCurrentBucketStartTimeNs);
+
+    durationProducer.flushIfNeededLocked(bucketStartTimeNs + 2 * bucketSizeNs + 1);
+    std::vector<DurationBucket> buckets =
+            durationProducer.mPastBuckets[DEFAULT_METRIC_DIMENSION_KEY];
+    EXPECT_EQ(1UL, buckets.size());
+    EXPECT_EQ(eventUpgradeTimeNs, buckets[0].mBucketStartNs);
+    EXPECT_EQ(bucketStartTimeNs + 2 * bucketSizeNs, buckets[0].mBucketEndNs);
+    EXPECT_EQ(endTimeNs - startTimeNs, buckets[0].mDuration);
+}
+
 }  // namespace statsd
 }  // namespace os
 }  // namespace android
diff --git a/cmds/statsd/tests/metrics/GaugeMetricProducer_test.cpp b/cmds/statsd/tests/metrics/GaugeMetricProducer_test.cpp
index 58be5b0..470d4d0 100644
--- a/cmds/statsd/tests/metrics/GaugeMetricProducer_test.cpp
+++ b/cmds/statsd/tests/metrics/GaugeMetricProducer_test.cpp
@@ -44,6 +44,7 @@
 const int64_t bucket2StartTimeNs = bucketStartTimeNs + bucketSizeNs;
 const int64_t bucket3StartTimeNs = bucketStartTimeNs + 2 * bucketSizeNs;
 const int64_t bucket4StartTimeNs = bucketStartTimeNs + 3 * bucketSizeNs;
+const uint64_t eventUpgradeTimeNs = bucketStartTimeNs + 15 * NS_PER_SEC;
 
 TEST(GaugeMetricProducerTest, TestNoCondition) {
     GaugeMetric metric;
@@ -119,6 +120,143 @@
     EXPECT_EQ(2UL, gaugeProducer.mPastBuckets.begin()->second.back().mBucketNum);
 }
 
+TEST(GaugeMetricProducerTest, TestPushedEventsWithUpgrade) {
+    GaugeMetric metric;
+    metric.set_id(metricId);
+    metric.set_bucket(ONE_MINUTE);
+    metric.mutable_gauge_fields_filter()->set_include_all(true);
+
+    Alert alert;
+    alert.set_id(101);
+    alert.set_metric_id(metricId);
+    alert.set_trigger_if_sum_gt(25);
+    alert.set_num_buckets(100);
+    sp<MockConditionWizard> wizard = new NaggyMock<MockConditionWizard>();
+    shared_ptr<MockStatsPullerManager> pullerManager =
+            make_shared<StrictMock<MockStatsPullerManager>>();
+    GaugeMetricProducer gaugeProducer(kConfigKey, metric, -1 /*-1 meaning no condition*/, wizard,
+                                      -1 /* -1 means no pulling */, bucketStartTimeNs,
+                                      pullerManager);
+    sp<AnomalyTracker> anomalyTracker = gaugeProducer.addAnomalyTracker(alert);
+    EXPECT_TRUE(anomalyTracker != nullptr);
+
+    shared_ptr<LogEvent> event1 = make_shared<LogEvent>(tagId, bucketStartTimeNs + 10);
+    event1->write(1);
+    event1->write(10);
+    event1->init();
+    gaugeProducer.onMatchedLogEvent(1 /*log matcher index*/, *event1);
+    EXPECT_EQ(1UL, (*gaugeProducer.mCurrentSlicedBucket).count(DEFAULT_METRIC_DIMENSION_KEY));
+
+    gaugeProducer.notifyAppUpgrade(eventUpgradeTimeNs, "ANY.APP", 1, 1);
+    EXPECT_EQ(0UL, (*gaugeProducer.mCurrentSlicedBucket).count(DEFAULT_METRIC_DIMENSION_KEY));
+    EXPECT_EQ(1UL, gaugeProducer.mPastBuckets[DEFAULT_METRIC_DIMENSION_KEY].size());
+    EXPECT_EQ(0UL, gaugeProducer.mCurrentBucketNum);
+    EXPECT_EQ(eventUpgradeTimeNs, gaugeProducer.mCurrentBucketStartTimeNs);
+    // Partial buckets are not sent to anomaly tracker.
+    EXPECT_EQ(0, anomalyTracker->getSumOverPastBuckets(DEFAULT_METRIC_DIMENSION_KEY));
+
+    // Create an event in the same partial bucket.
+    shared_ptr<LogEvent> event2 = make_shared<LogEvent>(tagId, bucketStartTimeNs + 59 * NS_PER_SEC);
+    event2->write(1);
+    event2->write(10);
+    event2->init();
+    gaugeProducer.onMatchedLogEvent(1 /*log matcher index*/, *event2);
+    EXPECT_EQ(0UL, gaugeProducer.mCurrentBucketNum);
+    EXPECT_EQ(1UL, gaugeProducer.mPastBuckets[DEFAULT_METRIC_DIMENSION_KEY].size());
+    EXPECT_EQ((uint64_t)eventUpgradeTimeNs, gaugeProducer.mCurrentBucketStartTimeNs);
+    // Partial buckets are not sent to anomaly tracker.
+    EXPECT_EQ(0, anomalyTracker->getSumOverPastBuckets(DEFAULT_METRIC_DIMENSION_KEY));
+
+    // Next event should trigger creation of new bucket and send previous full bucket to anomaly
+    // tracker.
+    shared_ptr<LogEvent> event3 = make_shared<LogEvent>(tagId, bucketStartTimeNs + 65 * NS_PER_SEC);
+    event3->write(1);
+    event3->write(10);
+    event3->init();
+    gaugeProducer.onMatchedLogEvent(1 /*log matcher index*/, *event3);
+    EXPECT_EQ(1UL, gaugeProducer.mCurrentBucketNum);
+    EXPECT_EQ(2UL, gaugeProducer.mPastBuckets[DEFAULT_METRIC_DIMENSION_KEY].size());
+    EXPECT_EQ((uint64_t)bucketStartTimeNs + bucketSizeNs, gaugeProducer.mCurrentBucketStartTimeNs);
+    EXPECT_EQ(1, anomalyTracker->getSumOverPastBuckets(DEFAULT_METRIC_DIMENSION_KEY));
+
+    // Next event should trigger creation of new bucket.
+    shared_ptr<LogEvent> event4 =
+            make_shared<LogEvent>(tagId, bucketStartTimeNs + 125 * NS_PER_SEC);
+    event4->write(1);
+    event4->write(10);
+    event4->init();
+    gaugeProducer.onMatchedLogEvent(1 /*log matcher index*/, *event4);
+    EXPECT_EQ(2UL, gaugeProducer.mCurrentBucketNum);
+    EXPECT_EQ(3UL, gaugeProducer.mPastBuckets[DEFAULT_METRIC_DIMENSION_KEY].size());
+    EXPECT_EQ(2, anomalyTracker->getSumOverPastBuckets(DEFAULT_METRIC_DIMENSION_KEY));
+}
+
+TEST(GaugeMetricProducerTest, TestPulledWithUpgrade) {
+    GaugeMetric metric;
+    metric.set_id(metricId);
+    metric.set_bucket(ONE_MINUTE);
+    auto gaugeFieldMatcher = metric.mutable_gauge_fields_filter()->mutable_fields();
+    gaugeFieldMatcher->set_field(tagId);
+    gaugeFieldMatcher->add_child()->set_field(2);
+
+    sp<MockConditionWizard> wizard = new NaggyMock<MockConditionWizard>();
+
+    shared_ptr<MockStatsPullerManager> pullerManager =
+            make_shared<StrictMock<MockStatsPullerManager>>();
+    EXPECT_CALL(*pullerManager, RegisterReceiver(tagId, _, _)).WillOnce(Return());
+    EXPECT_CALL(*pullerManager, UnRegisterReceiver(tagId, _)).WillOnce(Return());
+    EXPECT_CALL(*pullerManager, Pull(tagId, _))
+            .WillOnce(Invoke([](int tagId, vector<std::shared_ptr<LogEvent>>* data) {
+                data->clear();
+                shared_ptr<LogEvent> event = make_shared<LogEvent>(tagId, eventUpgradeTimeNs);
+                event->write("some value");
+                event->write(2);
+                event->init();
+                data->push_back(event);
+                return true;
+            }));
+
+    GaugeMetricProducer gaugeProducer(kConfigKey, metric, -1 /*-1 meaning no condition*/, wizard,
+                                      tagId, bucketStartTimeNs, pullerManager);
+
+    vector<shared_ptr<LogEvent>> allData;
+    shared_ptr<LogEvent> event = make_shared<LogEvent>(tagId, bucketStartTimeNs + 1);
+    event->write("some value");
+    event->write(1);
+    event->init();
+    allData.push_back(event);
+    gaugeProducer.onDataPulled(allData);
+    EXPECT_EQ(1UL, gaugeProducer.mCurrentSlicedBucket->size());
+    EXPECT_EQ(1, gaugeProducer.mCurrentSlicedBucket->begin()
+                         ->second.front()
+                         .mFields->begin()
+                         ->second.value_int());
+
+    gaugeProducer.notifyAppUpgrade(eventUpgradeTimeNs, "ANY.APP", 1, 1);
+    EXPECT_EQ(1UL, gaugeProducer.mPastBuckets[DEFAULT_METRIC_DIMENSION_KEY].size());
+    EXPECT_EQ(0UL, gaugeProducer.mCurrentBucketNum);
+    EXPECT_EQ((uint64_t)eventUpgradeTimeNs, gaugeProducer.mCurrentBucketStartTimeNs);
+    EXPECT_EQ(1UL, gaugeProducer.mCurrentSlicedBucket->size());
+    EXPECT_EQ(2, gaugeProducer.mCurrentSlicedBucket->begin()
+                         ->second.front()
+                         .mFields->begin()
+                         ->second.value_int());
+
+    allData.clear();
+    event = make_shared<LogEvent>(tagId, bucketStartTimeNs + bucketSizeNs + 1);
+    event->write("some value");
+    event->write(3);
+    event->init();
+    allData.push_back(event);
+    gaugeProducer.onDataPulled(allData);
+    EXPECT_EQ(2UL, gaugeProducer.mPastBuckets[DEFAULT_METRIC_DIMENSION_KEY].size());
+    EXPECT_EQ(1UL, gaugeProducer.mCurrentSlicedBucket->size());
+    EXPECT_EQ(3, gaugeProducer.mCurrentSlicedBucket->begin()
+                         ->second.front()
+                         .mFields->begin()
+                         ->second.value_int());
+}
+
 TEST(GaugeMetricProducerTest, TestWithCondition) {
     GaugeMetric metric;
     metric.set_id(metricId);
diff --git a/cmds/statsd/tests/metrics/MaxDurationTracker_test.cpp b/cmds/statsd/tests/metrics/MaxDurationTracker_test.cpp
index 203f028..2658e4e 100644
--- a/cmds/statsd/tests/metrics/MaxDurationTracker_test.cpp
+++ b/cmds/statsd/tests/metrics/MaxDurationTracker_test.cpp
@@ -41,6 +41,11 @@
 
 const int TagId = 1;
 
+const HashableDimensionKey eventKey = getMockedDimensionKey(TagId, 0, "1");
+const std::vector<HashableDimensionKey> conditionKey = {getMockedDimensionKey(TagId, 4, "1")};
+const HashableDimensionKey key1 = getMockedDimensionKey(TagId, 1, "1");
+const HashableDimensionKey key2 = getMockedDimensionKey(TagId, 1, "2");
+const uint64_t bucketSizeNs = 30 * 1000 * 1000 * 1000LL;
 
 TEST(MaxDurationTrackerTest, TestSimpleMaxDuration) {
     const MetricDimensionKey eventKey = getMockedMetricDimensionKey(TagId, 0, "1");
@@ -54,11 +59,13 @@
     unordered_map<MetricDimensionKey, vector<DurationBucket>> buckets;
 
     uint64_t bucketStartTimeNs = 10000000000;
-    uint64_t bucketSizeNs = 30 * 1000 * 1000 * 1000LL;
+    uint64_t bucketEndTimeNs = bucketStartTimeNs + bucketSizeNs;
+    uint64_t bucketNum = 0;
 
     int64_t metricId = 1;
     MaxDurationTracker tracker(kConfigKey, metricId, eventKey, wizard, -1, dimensionInCondition,
-                               false, bucketStartTimeNs, bucketSizeNs, false, {});
+                               false, bucketStartTimeNs, bucketNum, bucketStartTimeNs, bucketSizeNs,
+                               false, {});
 
     tracker.noteStart(key1, true, bucketStartTimeNs, ConditionKey());
     // Event starts again. This would not change anything as it already starts.
@@ -87,12 +94,15 @@
 
     unordered_map<MetricDimensionKey, vector<DurationBucket>> buckets;
 
-    uint64_t bucketStartTimeNs = 10000000000;
     uint64_t bucketSizeNs = 30 * 1000 * 1000 * 1000LL;
+    uint64_t bucketStartTimeNs = 10000000000;
+    uint64_t bucketEndTimeNs = bucketStartTimeNs + bucketSizeNs;
+    uint64_t bucketNum = 0;
 
     int64_t metricId = 1;
     MaxDurationTracker tracker(kConfigKey, metricId, eventKey, wizard, -1, dimensionInCondition,
-                               false, bucketStartTimeNs, bucketSizeNs, false, {});
+                               false, bucketStartTimeNs, bucketNum, bucketStartTimeNs, bucketSizeNs,
+                               false, {});
 
     tracker.noteStart(key1, true, bucketStartTimeNs + 1, ConditionKey());
 
@@ -101,15 +111,14 @@
     tracker.flushIfNeeded(bucketStartTimeNs + bucketSizeNs + 40, &buckets);
     tracker.noteStopAll(bucketStartTimeNs + bucketSizeNs + 40);
     EXPECT_TRUE(tracker.mInfos.empty());
-
-    EXPECT_TRUE(buckets.find(eventKey) != buckets.end());
-    EXPECT_EQ(1u, buckets[eventKey].size());
-    EXPECT_EQ(bucketSizeNs - 1, buckets[eventKey][0].mDuration);
+    EXPECT_TRUE(buckets.find(eventKey) == buckets.end());
 
     tracker.flushIfNeeded(bucketStartTimeNs + 3 * bucketSizeNs + 40, &buckets);
-    EXPECT_EQ(2u, buckets[eventKey].size());
-    EXPECT_EQ(bucketSizeNs - 1, buckets[eventKey][0].mDuration);
-    EXPECT_EQ(40ULL, buckets[eventKey][1].mDuration);
+    EXPECT_TRUE(buckets.find(eventKey) != buckets.end());
+    EXPECT_EQ(1u, buckets[eventKey].size());
+    EXPECT_EQ(bucketSizeNs + 40 - 1, buckets[eventKey][0].mDuration);
+    EXPECT_EQ(bucketStartTimeNs + bucketSizeNs, buckets[eventKey][0].mBucketStartNs);
+    EXPECT_EQ(bucketStartTimeNs + 2 * bucketSizeNs, buckets[eventKey][0].mBucketEndNs);
 }
 
 TEST(MaxDurationTrackerTest, TestCrossBucketBoundary) {
@@ -122,12 +131,15 @@
 
     unordered_map<MetricDimensionKey, vector<DurationBucket>> buckets;
 
-    uint64_t bucketStartTimeNs = 10000000000;
     uint64_t bucketSizeNs = 30 * 1000 * 1000 * 1000LL;
+    uint64_t bucketStartTimeNs = 10000000000;
+    uint64_t bucketEndTimeNs = bucketStartTimeNs + bucketSizeNs;
+    uint64_t bucketNum = 0;
 
     int64_t metricId = 1;
     MaxDurationTracker tracker(kConfigKey, metricId, eventKey, wizard, -1, dimensionInCondition,
-                               false, bucketStartTimeNs, bucketSizeNs, false, {});
+                               false, bucketStartTimeNs, bucketNum, bucketStartTimeNs, bucketSizeNs,
+                               false, {});
 
     // The event starts.
     tracker.noteStart(DEFAULT_DIMENSION_KEY, true, bucketStartTimeNs + 1, ConditionKey());
@@ -137,14 +149,18 @@
                       ConditionKey());
 
     // The event stops at early 4th bucket.
+    // Notestop is called from DurationMetricProducer's onMatchedLogEvent, which calls
+    // flushIfneeded.
     tracker.flushIfNeeded(bucketStartTimeNs + (3 * bucketSizeNs) + 20, &buckets);
     tracker.noteStop(DEFAULT_DIMENSION_KEY, bucketStartTimeNs + (3 * bucketSizeNs) + 20,
                      false /*stop all*/);
-    EXPECT_TRUE(buckets.find(eventKey) != buckets.end());
-    EXPECT_EQ(3u, buckets[eventKey].size());
-    EXPECT_EQ((unsigned long long)(bucketSizeNs - 1), buckets[eventKey][0].mDuration);
-    EXPECT_EQ((unsigned long long)bucketSizeNs, buckets[eventKey][1].mDuration);
-    EXPECT_EQ((unsigned long long)bucketSizeNs, buckets[eventKey][2].mDuration);
+    EXPECT_TRUE(buckets.find(eventKey) == buckets.end());
+
+    tracker.flushIfNeeded(bucketStartTimeNs + 4 * bucketSizeNs, &buckets);
+    EXPECT_EQ(1u, buckets[eventKey].size());
+    EXPECT_EQ((3 * bucketSizeNs) + 20 - 1, buckets[eventKey][0].mDuration);
+    EXPECT_EQ(bucketStartTimeNs + 3 * bucketSizeNs, buckets[eventKey][0].mBucketStartNs);
+    EXPECT_EQ(bucketStartTimeNs + 4 * bucketSizeNs, buckets[eventKey][0].mBucketEndNs);
 }
 
 TEST(MaxDurationTrackerTest, TestCrossBucketBoundary_nested) {
@@ -157,12 +173,15 @@
 
     unordered_map<MetricDimensionKey, vector<DurationBucket>> buckets;
 
-    uint64_t bucketStartTimeNs = 10000000000;
     uint64_t bucketSizeNs = 30 * 1000 * 1000 * 1000LL;
+    uint64_t bucketStartTimeNs = 10000000000;
+    uint64_t bucketEndTimeNs = bucketStartTimeNs + bucketSizeNs;
+    uint64_t bucketNum = 0;
 
     int64_t metricId = 1;
     MaxDurationTracker tracker(kConfigKey, metricId, eventKey, wizard, -1, dimensionInCondition,
-                               true, bucketStartTimeNs, bucketSizeNs, false, {});
+                               true, bucketStartTimeNs, bucketNum, bucketStartTimeNs, bucketSizeNs,
+                               false, {});
 
     // 2 starts
     tracker.noteStart(DEFAULT_DIMENSION_KEY, true, bucketStartTimeNs + 1, ConditionKey());
@@ -171,21 +190,16 @@
     tracker.noteStop(DEFAULT_DIMENSION_KEY, bucketStartTimeNs + 20, false /*stop all*/);
 
     tracker.flushIfNeeded(bucketStartTimeNs + (2 * bucketSizeNs) + 1, &buckets);
-
-    EXPECT_TRUE(buckets.find(eventKey) != buckets.end());
-    EXPECT_EQ(2u, buckets[eventKey].size());
-    EXPECT_EQ(bucketSizeNs - 1, buckets[eventKey][0].mDuration);
-    EXPECT_EQ(bucketSizeNs, buckets[eventKey][1].mDuration);
+    // Because of nesting, still not stopped.
+    EXPECT_TRUE(buckets.find(eventKey) == buckets.end());
 
     // real stop now.
     tracker.noteStop(DEFAULT_DIMENSION_KEY,
                      bucketStartTimeNs + (2 * bucketSizeNs) + 5, false);
     tracker.flushIfNeeded(bucketStartTimeNs + (3 * bucketSizeNs) + 1, &buckets);
 
-    EXPECT_EQ(3u, buckets[eventKey].size());
-    EXPECT_EQ(bucketSizeNs - 1, buckets[eventKey][0].mDuration);
-    EXPECT_EQ(bucketSizeNs, buckets[eventKey][1].mDuration);
-    EXPECT_EQ(5ULL, buckets[eventKey][2].mDuration);
+    EXPECT_EQ(1u, buckets[eventKey].size());
+    EXPECT_EQ(2 * bucketSizeNs + 5 - 1, buckets[eventKey][0].mDuration);
 }
 
 TEST(MaxDurationTrackerTest, TestMaxDurationWithCondition) {
@@ -204,14 +218,17 @@
 
     unordered_map<MetricDimensionKey, vector<DurationBucket>> buckets;
 
-    uint64_t bucketStartTimeNs = 10000000000;
-    uint64_t eventStartTimeNs = bucketStartTimeNs + 1;
     uint64_t bucketSizeNs = 30 * 1000 * 1000 * 1000LL;
+    uint64_t bucketStartTimeNs = 10000000000;
+    uint64_t bucketEndTimeNs = bucketStartTimeNs + bucketSizeNs;
+    uint64_t bucketNum = 0;
+    uint64_t eventStartTimeNs = bucketStartTimeNs + 1;
     int64_t durationTimeNs = 2 * 1000;
 
     int64_t metricId = 1;
     MaxDurationTracker tracker(kConfigKey, metricId, eventKey, wizard, 1, dimensionInCondition,
-                               false, bucketStartTimeNs, bucketSizeNs, true, {});
+                               false, bucketStartTimeNs, bucketNum, bucketStartTimeNs, bucketSizeNs,
+                               true, {});
     EXPECT_TRUE(tracker.mAnomalyTrackers.empty());
 
     tracker.noteStart(key1, true, eventStartTimeNs, conditionKey1);
@@ -226,45 +243,6 @@
     EXPECT_EQ(5ULL, buckets[eventKey][0].mDuration);
 }
 
-TEST(MaxDurationTrackerTest, TestAnomalyDetection) {
-    const MetricDimensionKey eventKey = getMockedMetricDimensionKey(TagId, 0, "1");
-    const HashableDimensionKey key1 = getMockedDimensionKey(TagId, 1, "1");
-    const HashableDimensionKey key2 = getMockedDimensionKey(TagId, 1, "2");
-    FieldMatcher dimensionInCondition;
-    int64_t metricId = 1;
-    Alert alert;
-    alert.set_id(101);
-    alert.set_metric_id(metricId);
-    alert.set_trigger_if_sum_gt(32 * NS_PER_SEC);
-    alert.set_num_buckets(2);
-    const int32_t refPeriodSec = 1;
-    alert.set_refractory_period_secs(refPeriodSec);
-
-    unordered_map<MetricDimensionKey, vector<DurationBucket>> buckets;
-    sp<MockConditionWizard> wizard = new NaggyMock<MockConditionWizard>();
-
-    uint64_t bucketStartTimeNs = 10 * NS_PER_SEC;
-    uint64_t eventStartTimeNs = bucketStartTimeNs + NS_PER_SEC + 1;
-    uint64_t bucketSizeNs = 30 * NS_PER_SEC;
-
-    sp<DurationAnomalyTracker> anomalyTracker = new DurationAnomalyTracker(alert, kConfigKey);
-    MaxDurationTracker tracker(kConfigKey, metricId, eventKey, wizard, -1, dimensionInCondition,
-                               true, bucketStartTimeNs, bucketSizeNs, false, {anomalyTracker});
-
-    tracker.noteStart(key1, true, eventStartTimeNs, ConditionKey());
-    tracker.noteStop(key1, eventStartTimeNs + 10, false);
-    EXPECT_EQ(anomalyTracker->getRefractoryPeriodEndsSec(eventKey), 0U);
-    EXPECT_EQ(10LL, tracker.mDuration);
-
-    tracker.noteStart(key2, true, eventStartTimeNs + 20, ConditionKey());
-    tracker.flushIfNeeded(eventStartTimeNs + 2 * bucketSizeNs + 3 * NS_PER_SEC, &buckets);
-    tracker.noteStop(key2, eventStartTimeNs + 2 * bucketSizeNs + 3 * NS_PER_SEC, false);
-    EXPECT_EQ((long long)(4 * NS_PER_SEC + 1LL), tracker.mDuration);
-
-    EXPECT_EQ(anomalyTracker->getRefractoryPeriodEndsSec(eventKey),
-              (eventStartTimeNs + 2 * bucketSizeNs) / NS_PER_SEC  + 3 + refPeriodSec);
-}
-
 }  // namespace statsd
 }  // namespace os
 }  // namespace android
diff --git a/cmds/statsd/tests/metrics/OringDurationTracker_test.cpp b/cmds/statsd/tests/metrics/OringDurationTracker_test.cpp
index 80e16a1..4b579b1 100644
--- a/cmds/statsd/tests/metrics/OringDurationTracker_test.cpp
+++ b/cmds/statsd/tests/metrics/OringDurationTracker_test.cpp
@@ -38,6 +38,12 @@
 const ConfigKey kConfigKey(0, 12345);
 const int TagId = 1;
 const int64_t metricId = 123;
+const HashableDimensionKey eventKey = getMockedDimensionKey(TagId, 0, "event");
+
+const std::vector<HashableDimensionKey> kConditionKey1 = {getMockedDimensionKey(TagId, 1, "maps")};
+const HashableDimensionKey kEventKey1 = getMockedDimensionKey(TagId, 2, "maps");
+const HashableDimensionKey kEventKey2 = getMockedDimensionKey(TagId, 3, "maps");
+const uint64_t bucketSizeNs = 30 * 1000 * 1000 * 1000LL;
 
 TEST(OringDurationTrackerTest, TestDurationOverlap) {
     const MetricDimensionKey eventKey = getMockedMetricDimensionKey(TagId, 0, "event");
@@ -51,13 +57,16 @@
 
     unordered_map<MetricDimensionKey, vector<DurationBucket>> buckets;
 
-    uint64_t bucketStartTimeNs = 10000000000;
-    uint64_t eventStartTimeNs = bucketStartTimeNs + 1;
     uint64_t bucketSizeNs = 30 * 1000 * 1000 * 1000LL;
+    uint64_t bucketStartTimeNs = 10000000000;
+    uint64_t bucketEndTimeNs = bucketStartTimeNs + bucketSizeNs;
+    uint64_t bucketNum = 0;
+    uint64_t eventStartTimeNs = bucketStartTimeNs + 1;
     uint64_t durationTimeNs = 2 * 1000;
 
     OringDurationTracker tracker(kConfigKey, metricId, eventKey, wizard, 1, dimensionInCondition,
-                                 false, bucketStartTimeNs, bucketSizeNs, false, {});
+                                 false, bucketStartTimeNs, bucketNum, bucketStartTimeNs,
+                                 bucketSizeNs, false, {});
 
     tracker.noteStart(kEventKey1, true, eventStartTimeNs, ConditionKey());
     EXPECT_EQ((long long)eventStartTimeNs, tracker.mLastStartTime);
@@ -84,12 +93,15 @@
 
     unordered_map<MetricDimensionKey, vector<DurationBucket>> buckets;
 
-    uint64_t bucketStartTimeNs = 10000000000;
-    uint64_t eventStartTimeNs = bucketStartTimeNs + 1;
     uint64_t bucketSizeNs = 30 * 1000 * 1000 * 1000LL;
+    uint64_t bucketStartTimeNs = 10000000000;
+    uint64_t bucketEndTimeNs = bucketStartTimeNs + bucketSizeNs;
+    uint64_t bucketNum = 0;
+    uint64_t eventStartTimeNs = bucketStartTimeNs + 1;
 
     OringDurationTracker tracker(kConfigKey, metricId, eventKey, wizard, 1, dimensionInCondition,
-                                 true, bucketStartTimeNs, bucketSizeNs, false, {});
+                                 true, bucketStartTimeNs, bucketNum, bucketStartTimeNs,
+                                 bucketSizeNs, false, {});
 
     tracker.noteStart(kEventKey1, true, eventStartTimeNs, ConditionKey());
     tracker.noteStart(kEventKey1, true, eventStartTimeNs + 10, ConditionKey());  // overlapping wl
@@ -115,12 +127,15 @@
 
     unordered_map<MetricDimensionKey, vector<DurationBucket>> buckets;
 
-    uint64_t bucketStartTimeNs = 10000000000;
-    uint64_t eventStartTimeNs = bucketStartTimeNs + 1;
     uint64_t bucketSizeNs = 30 * 1000 * 1000 * 1000LL;
+    uint64_t bucketStartTimeNs = 10000000000;
+    uint64_t bucketEndTimeNs = bucketStartTimeNs + bucketSizeNs;
+    uint64_t bucketNum = 0;
+    uint64_t eventStartTimeNs = bucketStartTimeNs + 1;
 
     OringDurationTracker tracker(kConfigKey, metricId, eventKey, wizard, 1, dimensionInCondition,
-                                 true, bucketStartTimeNs, bucketSizeNs, false, {});
+                                 true, bucketStartTimeNs, bucketNum, bucketStartTimeNs,
+                                 bucketSizeNs, false, {});
 
     tracker.noteStart(kEventKey1, true, eventStartTimeNs, ConditionKey());
     tracker.noteStart(kEventKey2, true, eventStartTimeNs + 10, ConditionKey());  // overlapping wl
@@ -145,13 +160,16 @@
 
     unordered_map<MetricDimensionKey, vector<DurationBucket>> buckets;
 
-    uint64_t bucketStartTimeNs = 10000000000;
-    uint64_t eventStartTimeNs = bucketStartTimeNs + 1;
     uint64_t bucketSizeNs = 30 * 1000 * 1000 * 1000LL;
+    uint64_t bucketStartTimeNs = 10000000000;
+    uint64_t bucketEndTimeNs = bucketStartTimeNs + bucketSizeNs;
+    uint64_t bucketNum = 0;
+    uint64_t eventStartTimeNs = bucketStartTimeNs + 1;
     uint64_t durationTimeNs = 2 * 1000;
 
     OringDurationTracker tracker(kConfigKey, metricId, eventKey, wizard, 1, dimensionInCondition,
-                                 true, bucketStartTimeNs, bucketSizeNs, false, {});
+                                 true, bucketStartTimeNs, bucketNum, bucketStartTimeNs,
+                                 bucketSizeNs, false, {});
 
     tracker.noteStart(kEventKey1, true, eventStartTimeNs, ConditionKey());
     EXPECT_EQ((long long)eventStartTimeNs, tracker.mLastStartTime);
@@ -190,13 +208,16 @@
 
     unordered_map<MetricDimensionKey, vector<DurationBucket>> buckets;
 
-    uint64_t bucketStartTimeNs = 10000000000;
-    uint64_t eventStartTimeNs = bucketStartTimeNs + 1;
     uint64_t bucketSizeNs = 30 * 1000 * 1000 * 1000LL;
+    uint64_t bucketStartTimeNs = 10000000000;
+    uint64_t bucketEndTimeNs = bucketStartTimeNs + bucketSizeNs;
+    uint64_t bucketNum = 0;
+    uint64_t eventStartTimeNs = bucketStartTimeNs + 1;
     uint64_t durationTimeNs = 2 * 1000;
 
     OringDurationTracker tracker(kConfigKey, metricId, eventKey, wizard, 1, dimensionInCondition,
-                                 false, bucketStartTimeNs, bucketSizeNs, true, {});
+                                 false, bucketStartTimeNs, bucketNum, bucketStartTimeNs,
+                                 bucketSizeNs, true, {});
 
     tracker.noteStart(kEventKey1, true, eventStartTimeNs, key1);
 
@@ -231,12 +252,15 @@
     unordered_map<MetricDimensionKey, vector<DurationBucket>> buckets;
 
     uint64_t bucketStartTimeNs = 10000000000;
-    uint64_t eventStartTimeNs = bucketStartTimeNs + 1;
     uint64_t bucketSizeNs = 30 * 1000 * 1000 * 1000LL;
+    uint64_t bucketEndTimeNs = bucketStartTimeNs + bucketSizeNs;
+    uint64_t bucketNum = 0;
+    uint64_t eventStartTimeNs = bucketStartTimeNs + 1;
     uint64_t durationTimeNs = 2 * 1000;
 
     OringDurationTracker tracker(kConfigKey, metricId, eventKey, wizard, 1, dimensionInCondition,
-                                 false, bucketStartTimeNs, bucketSizeNs, true, {});
+                                 false, bucketStartTimeNs, bucketNum, bucketStartTimeNs,
+                                 bucketSizeNs, true, {});
 
     tracker.noteStart(kEventKey1, true, eventStartTimeNs, key1);
     // condition to false; record duration 5n
@@ -271,11 +295,14 @@
     unordered_map<MetricDimensionKey, vector<DurationBucket>> buckets;
 
     uint64_t bucketStartTimeNs = 10000000000;
-    uint64_t eventStartTimeNs = bucketStartTimeNs + 1;
     uint64_t bucketSizeNs = 30 * 1000 * 1000 * 1000LL;
+    uint64_t bucketEndTimeNs = bucketStartTimeNs + bucketSizeNs;
+    uint64_t bucketNum = 0;
+    uint64_t eventStartTimeNs = bucketStartTimeNs + 1;
 
     OringDurationTracker tracker(kConfigKey, metricId, eventKey, wizard, 1, dimensionInCondition,
-                                 true, bucketStartTimeNs, bucketSizeNs, true, {});
+                                 true, bucketStartTimeNs, bucketNum, bucketStartTimeNs,
+                                 bucketSizeNs, true, {});
 
     tracker.noteStart(kEventKey1, true, eventStartTimeNs, key1);
     tracker.noteStart(kEventKey1, true, eventStartTimeNs + 2, key1);
@@ -311,12 +338,14 @@
     sp<MockConditionWizard> wizard = new NaggyMock<MockConditionWizard>();
 
     uint64_t bucketStartTimeNs = 10 * NS_PER_SEC;
+    uint64_t bucketEndTimeNs = bucketStartTimeNs + bucketSizeNs;
+    uint64_t bucketNum = 0;
     uint64_t eventStartTimeNs = bucketStartTimeNs + NS_PER_SEC + 1;
-    uint64_t bucketSizeNs = 30 * NS_PER_SEC;
 
     sp<DurationAnomalyTracker> anomalyTracker = new DurationAnomalyTracker(alert, kConfigKey);
     OringDurationTracker tracker(kConfigKey, metricId, eventKey, wizard, 1, dimensionInCondition,
-                                 true, bucketStartTimeNs, bucketSizeNs, true, {anomalyTracker});
+                                 true, bucketStartTimeNs, bucketNum, bucketStartTimeNs,
+                                 bucketSizeNs, true, {anomalyTracker});
 
     // Nothing in the past bucket.
     tracker.noteStart(DEFAULT_DIMENSION_KEY, true, eventStartTimeNs, ConditionKey());
@@ -379,12 +408,14 @@
     sp<MockConditionWizard> wizard = new NaggyMock<MockConditionWizard>();
 
     uint64_t bucketStartTimeNs = 10 * NS_PER_SEC;
+    uint64_t bucketEndTimeNs = bucketStartTimeNs + bucketSizeNs;
+    uint64_t bucketNum = 0;
     uint64_t eventStartTimeNs = bucketStartTimeNs + NS_PER_SEC + 1;
-    uint64_t bucketSizeNs = 30 * NS_PER_SEC;
 
     sp<DurationAnomalyTracker> anomalyTracker = new DurationAnomalyTracker(alert, kConfigKey);
     OringDurationTracker tracker(kConfigKey, metricId, eventKey, wizard, 1, dimensionInCondition,
-                                 true /*nesting*/, bucketStartTimeNs, bucketSizeNs, false, {anomalyTracker});
+                                 true /*nesting*/, bucketStartTimeNs, bucketNum, bucketStartTimeNs,
+                                 bucketSizeNs, false, {anomalyTracker});
 
     tracker.noteStart(kEventKey1, true, eventStartTimeNs, ConditionKey());
     tracker.noteStop(kEventKey1, eventStartTimeNs + 10, false);
@@ -434,8 +465,8 @@
 
     sp<DurationAnomalyTracker> anomalyTracker = new DurationAnomalyTracker(alert, kConfigKey);
     OringDurationTracker tracker(kConfigKey, metricId, eventKey, wizard, 1, dimensionInCondition,
-                                 true /*nesting*/, bucketStartTimeNs, bucketSizeNs, false,
-                                 {anomalyTracker});
+                                 true /*nesting*/, bucketStartTimeNs, 0, bucketStartTimeNs,
+                                 bucketSizeNs, false, {anomalyTracker});
 
     tracker.noteStart(kEventKey1, true, 15 * NS_PER_SEC, conkey); // start key1
     EXPECT_EQ(1u, anomalyTracker->mAlarms.size());
diff --git a/cmds/statsd/tests/metrics/ValueMetricProducer_test.cpp b/cmds/statsd/tests/metrics/ValueMetricProducer_test.cpp
index 55c078d..325a372 100644
--- a/cmds/statsd/tests/metrics/ValueMetricProducer_test.cpp
+++ b/cmds/statsd/tests/metrics/ValueMetricProducer_test.cpp
@@ -44,6 +44,7 @@
 const int64_t bucket2StartTimeNs = bucketStartTimeNs + bucketSizeNs;
 const int64_t bucket3StartTimeNs = bucketStartTimeNs + 2 * bucketSizeNs;
 const int64_t bucket4StartTimeNs = bucketStartTimeNs + 3 * bucketSizeNs;
+const int64_t eventUpgradeTimeNs = bucketStartTimeNs + 15 * NS_PER_SEC;
 
 /*
  * Tests pulled atoms with no conditions
@@ -202,6 +203,101 @@
     EXPECT_EQ(false, curInterval.startUpdated);
 }
 
+TEST(ValueMetricProducerTest, TestPushedEventsWithUpgrade) {
+    ValueMetric metric;
+    metric.set_id(metricId);
+    metric.set_bucket(ONE_MINUTE);
+    metric.mutable_value_field()->set_field(tagId);
+    metric.mutable_value_field()->add_child()->set_field(2);
+
+    sp<MockConditionWizard> wizard = new NaggyMock<MockConditionWizard>();
+    shared_ptr<MockStatsPullerManager> pullerManager =
+            make_shared<StrictMock<MockStatsPullerManager>>();
+    ValueMetricProducer valueProducer(kConfigKey, metric, -1, wizard, -1, bucketStartTimeNs,
+                                      pullerManager);
+
+    shared_ptr<LogEvent> event1 = make_shared<LogEvent>(tagId, bucketStartTimeNs + 10);
+    event1->write(1);
+    event1->write(10);
+    event1->init();
+    valueProducer.onMatchedLogEvent(1 /*log matcher index*/, *event1);
+    EXPECT_EQ(1UL, valueProducer.mCurrentSlicedBucket.size());
+
+    valueProducer.notifyAppUpgrade(eventUpgradeTimeNs, "ANY.APP", 1, 1);
+    EXPECT_EQ(1UL, valueProducer.mPastBuckets[DEFAULT_METRIC_DIMENSION_KEY].size());
+    EXPECT_EQ((uint64_t)eventUpgradeTimeNs, valueProducer.mCurrentBucketStartTimeNs);
+
+    shared_ptr<LogEvent> event2 = make_shared<LogEvent>(tagId, bucketStartTimeNs + 59 * NS_PER_SEC);
+    event2->write(1);
+    event2->write(10);
+    event2->init();
+    valueProducer.onMatchedLogEvent(1 /*log matcher index*/, *event2);
+    EXPECT_EQ(1UL, valueProducer.mPastBuckets[DEFAULT_METRIC_DIMENSION_KEY].size());
+    EXPECT_EQ((uint64_t)eventUpgradeTimeNs, valueProducer.mCurrentBucketStartTimeNs);
+
+    // Next value should create a new bucket.
+    shared_ptr<LogEvent> event3 = make_shared<LogEvent>(tagId, bucketStartTimeNs + 65 * NS_PER_SEC);
+    event3->write(1);
+    event3->write(10);
+    event3->init();
+    valueProducer.onMatchedLogEvent(1 /*log matcher index*/, *event3);
+    EXPECT_EQ(2UL, valueProducer.mPastBuckets[DEFAULT_METRIC_DIMENSION_KEY].size());
+    EXPECT_EQ((uint64_t)bucketStartTimeNs + bucketSizeNs, valueProducer.mCurrentBucketStartTimeNs);
+}
+
+TEST(ValueMetricProducerTest, TestPulledValueWithUpgrade) {
+    ValueMetric metric;
+    metric.set_id(metricId);
+    metric.set_bucket(ONE_MINUTE);
+    metric.mutable_value_field()->set_field(tagId);
+    metric.mutable_value_field()->add_child()->set_field(2);
+
+    sp<MockConditionWizard> wizard = new NaggyMock<MockConditionWizard>();
+    shared_ptr<MockStatsPullerManager> pullerManager =
+            make_shared<StrictMock<MockStatsPullerManager>>();
+    EXPECT_CALL(*pullerManager, RegisterReceiver(tagId, _, _)).WillOnce(Return());
+    EXPECT_CALL(*pullerManager, UnRegisterReceiver(tagId, _)).WillOnce(Return());
+    EXPECT_CALL(*pullerManager, Pull(tagId, _))
+            .WillOnce(Invoke([](int tagId, vector<std::shared_ptr<LogEvent>>* data) {
+                data->clear();
+                shared_ptr<LogEvent> event = make_shared<LogEvent>(tagId, bucketStartTimeNs + 10);
+                event->write(tagId);
+                event->write(120);
+                event->init();
+                data->push_back(event);
+                return true;
+            }));
+    ValueMetricProducer valueProducer(kConfigKey, metric, -1, wizard, tagId, bucketStartTimeNs,
+                                      pullerManager);
+
+    vector<shared_ptr<LogEvent>> allData;
+    allData.clear();
+    shared_ptr<LogEvent> event = make_shared<LogEvent>(tagId, bucketStartTimeNs + 1);
+    event->write(tagId);
+    event->write(100);
+    event->init();
+    allData.push_back(event);
+
+    valueProducer.onDataPulled(allData);
+    EXPECT_EQ(1UL, valueProducer.mCurrentSlicedBucket.size());
+
+    valueProducer.notifyAppUpgrade(eventUpgradeTimeNs, "ANY.APP", 1, 1);
+    EXPECT_EQ(1UL, valueProducer.mPastBuckets[DEFAULT_METRIC_DIMENSION_KEY].size());
+    EXPECT_EQ((uint64_t)eventUpgradeTimeNs, valueProducer.mCurrentBucketStartTimeNs);
+    EXPECT_EQ(20L, valueProducer.mPastBuckets[DEFAULT_METRIC_DIMENSION_KEY][0].mValue);
+
+    allData.clear();
+    event = make_shared<LogEvent>(tagId, bucket2StartTimeNs + 1);
+    event->write(tagId);
+    event->write(150);
+    event->init();
+    allData.push_back(event);
+    valueProducer.onDataPulled(allData);
+    EXPECT_EQ(2UL, valueProducer.mPastBuckets[DEFAULT_METRIC_DIMENSION_KEY].size());
+    EXPECT_EQ((uint64_t)bucket2StartTimeNs, valueProducer.mCurrentBucketStartTimeNs);
+    EXPECT_EQ(30L, valueProducer.mPastBuckets[DEFAULT_METRIC_DIMENSION_KEY][1].mValue);
+}
+
 TEST(ValueMetricProducerTest, TestPushedEventsWithoutCondition) {
     ValueMetric metric;
     metric.set_id(metricId);
diff --git a/core/java/android/app/InstantAppResolverService.java b/core/java/android/app/InstantAppResolverService.java
index 76a3682..2ba4c00 100644
--- a/core/java/android/app/InstantAppResolverService.java
+++ b/core/java/android/app/InstantAppResolverService.java
@@ -88,7 +88,7 @@
     public void onGetInstantAppResolveInfo(Intent sanitizedIntent, int[] hostDigestPrefix,
             String token, InstantAppResolutionCallback callback) {
         // if not overridden, forward to old methods and filter out non-web intents
-        if (sanitizedIntent.isBrowsableWebIntent()) {
+        if (sanitizedIntent.isWebIntent()) {
             onGetInstantAppResolveInfo(hostDigestPrefix, token, callback);
         } else {
             callback.onInstantAppResolveInfo(Collections.emptyList());
@@ -107,7 +107,7 @@
             String token, InstantAppResolutionCallback callback) {
         Log.e(TAG, "New onGetInstantAppIntentFilter is not overridden");
         // if not overridden, forward to old methods and filter out non-web intents
-        if (sanitizedIntent.isBrowsableWebIntent()) {
+        if (sanitizedIntent.isWebIntent()) {
             onGetInstantAppIntentFilter(hostDigestPrefix, token, callback);
         } else {
             callback.onInstantAppResolveInfo(Collections.emptyList());
diff --git a/core/java/android/app/Notification.java b/core/java/android/app/Notification.java
index 6e40986..8393caa 100644
--- a/core/java/android/app/Notification.java
+++ b/core/java/android/app/Notification.java
@@ -1055,11 +1055,10 @@
     /**
      * {@link #extras} key: A
      * {@link android.content.ContentUris content URI} pointing to an image that can be displayed
-     * in the background when the notification is selected. The URI must point to an image stream
-     * suitable for passing into
+     * in the background when the notification is selected. Used on television platforms.
+     * The URI must point to an image stream suitable for passing into
      * {@link android.graphics.BitmapFactory#decodeStream(java.io.InputStream)
-     * BitmapFactory.decodeStream}; all other content types will be ignored. The content provider
-     * URI used for this purpose must require no permissions to read the image data.
+     * BitmapFactory.decodeStream}; all other content types will be ignored.
      */
     public static final String EXTRA_BACKGROUND_IMAGE_URI = "android.backgroundImageUri";
 
diff --git a/core/java/android/app/NotificationManager.java b/core/java/android/app/NotificationManager.java
index 538623f..b10e608 100644
--- a/core/java/android/app/NotificationManager.java
+++ b/core/java/android/app/NotificationManager.java
@@ -24,6 +24,7 @@
 import android.app.Notification.Builder;
 import android.content.ComponentName;
 import android.content.Context;
+import android.content.Intent;
 import android.content.pm.ParceledListSlice;
 import android.graphics.drawable.Icon;
 import android.net.Uri;
@@ -350,6 +351,14 @@
      * the same tag and id has already been posted by your application and has not yet been
      * canceled, it will be replaced by the updated information.
      *
+     * All {@link android.service.notification.NotificationListenerService listener services} will
+     * be granted {@link Intent#FLAG_GRANT_READ_URI_PERMISSION} access to any {@link Uri uris}
+     * provided on this notification or the
+     * {@link NotificationChannel} this notification is posted to using
+     * {@link Context#grantUriPermission(String, Uri, int)}. Permission will be revoked when the
+     * notification is canceled, or you can revoke permissions with
+     * {@link Context#revokeUriPermission(Uri, int)}.
+     *
      * @param tag A string identifier for this notification.  May be {@code null}.
      * @param id An identifier for this notification.  The pair (tag, id) must be unique
      *        within your application.
@@ -370,11 +379,13 @@
         String pkg = mContext.getPackageName();
         // Fix the notification as best we can.
         Notification.addFieldsFromContext(mContext, notification);
+
         if (notification.sound != null) {
             notification.sound = notification.sound.getCanonicalUri();
             if (StrictMode.vmFileUriExposureEnabled()) {
                 notification.sound.checkFileUriExposed("Notification.sound");
             }
+
         }
         fixLegacySmallIcon(notification, pkg);
         if (mContext.getApplicationInfo().targetSdkVersion > Build.VERSION_CODES.LOLLIPOP_MR1) {
@@ -385,6 +396,7 @@
         }
         if (localLOGV) Log.v(TAG, pkg + ": notify(" + id + ", " + notification + ")");
         notification.reduceImageSizes(mContext);
+
         ActivityManager am = (ActivityManager) mContext.getSystemService(Context.ACTIVITY_SERVICE);
         boolean isLowRam = am.isLowRamDevice();
         final Notification copy = Builder.maybeCloneStrippedForDelivery(notification, isLowRam);
diff --git a/core/java/android/bluetooth/BluetoothSocket.java b/core/java/android/bluetooth/BluetoothSocket.java
index 09f9684..09a5b59 100644
--- a/core/java/android/bluetooth/BluetoothSocket.java
+++ b/core/java/android/bluetooth/BluetoothSocket.java
@@ -676,6 +676,35 @@
         mExcludeSdp = excludeSdp;
     }
 
+    /**
+     * Set the LE Transmit Data Length to be the maximum that the BT Controller is capable of. This
+     * parameter is used by the BT Controller to set the maximum transmission packet size on this
+     * connection. This function is currently used for testing only.
+     * @hide
+     */
+    public void requestMaximumTxDataLength() throws IOException {
+        if (mDevice == null) {
+            throw new IOException("requestMaximumTxDataLength is called on null device");
+        }
+
+        try {
+            if (mSocketState == SocketState.CLOSED) {
+                throw new IOException("socket closed");
+            }
+            IBluetooth bluetoothProxy =
+                    BluetoothAdapter.getDefaultAdapter().getBluetoothService(null);
+            if (bluetoothProxy == null) {
+                throw new IOException("Bluetooth is off");
+            }
+
+            if (DBG) Log.d(TAG, "requestMaximumTxDataLength");
+            bluetoothProxy.getSocketManager().requestMaximumTxDataLength(mDevice);
+        } catch (RemoteException e) {
+            Log.e(TAG, Log.getStackTraceString(new Throwable()));
+            throw new IOException("unable to send RPC: " + e.getMessage());
+        }
+    }
+
     private String convertAddr(final byte[] addr) {
         return String.format(Locale.US, "%02X:%02X:%02X:%02X:%02X:%02X",
                 addr[0], addr[1], addr[2], addr[3], addr[4], addr[5]);
diff --git a/core/java/android/content/Intent.java b/core/java/android/content/Intent.java
index 9b62f19..fa73e3c 100644
--- a/core/java/android/content/Intent.java
+++ b/core/java/android/content/Intent.java
@@ -10089,9 +10089,8 @@
     }
 
     /** @hide */
-    public boolean isBrowsableWebIntent() {
+    public boolean isWebIntent() {
         return ACTION_VIEW.equals(mAction)
-                && hasCategory(CATEGORY_BROWSABLE)
                 && hasWebURI();
     }
 
diff --git a/core/java/android/content/om/OverlayInfo.java b/core/java/android/content/om/OverlayInfo.java
index 1a207ba..8464e26 100644
--- a/core/java/android/content/om/OverlayInfo.java
+++ b/core/java/android/content/om/OverlayInfo.java
@@ -16,10 +16,14 @@
 
 package android.content.om;
 
+import android.annotation.IntDef;
 import android.annotation.NonNull;
 import android.os.Parcel;
 import android.os.Parcelable;
 
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
 /**
  * Immutable overlay information about a package. All PackageInfos that
  * represent an overlay package will have a corresponding OverlayInfo.
@@ -27,6 +31,19 @@
  * @hide
  */
 public final class OverlayInfo implements Parcelable {
+
+    @IntDef(prefix = "STATE_", value = {
+            STATE_UNKNOWN,
+            STATE_MISSING_TARGET,
+            STATE_NO_IDMAP,
+            STATE_DISABLED,
+            STATE_ENABLED,
+            STATE_TARGET_UPGRADING,
+            STATE_OVERLAY_UPGRADING,
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface State {}
+
     /**
      * An internal state used as the initial state of an overlay. OverlayInfo
      * objects exposed outside the {@link
@@ -61,6 +78,18 @@
     public static final int STATE_ENABLED = 3;
 
     /**
+     * The target package is currently being upgraded; the state will change
+     * once the package installation has finished.
+     */
+    public static final int STATE_TARGET_UPGRADING = 4;
+
+    /**
+     * The overlay package is currently being upgraded; the state will change
+     * once the package installation has finished.
+     */
+    public static final int STATE_OVERLAY_UPGRADING = 5;
+
+    /**
      * Package name of the overlay package
      */
     public final String packageName;
@@ -77,13 +106,8 @@
 
     /**
      * The state of this OverlayInfo as defined by the STATE_* constants in this class.
-     *
-     * @see #STATE_MISSING_TARGET
-     * @see #STATE_NO_IDMAP
-     * @see #STATE_DISABLED
-     * @see #STATE_ENABLED
      */
-    public final int state;
+    public final @State int state;
 
     /**
      * User handle for which this overlay applies
@@ -96,13 +120,13 @@
      * @param source the source OverlayInfo to base the new instance on
      * @param state the new state for the source OverlayInfo
      */
-    public OverlayInfo(@NonNull OverlayInfo source, int state) {
+    public OverlayInfo(@NonNull OverlayInfo source, @State int state) {
         this(source.packageName, source.targetPackageName, source.baseCodePath, state,
                 source.userId);
     }
 
     public OverlayInfo(@NonNull String packageName, @NonNull String targetPackageName,
-            @NonNull String baseCodePath, int state, int userId) {
+            @NonNull String baseCodePath, @State int state, int userId) {
         this.packageName = packageName;
         this.targetPackageName = targetPackageName;
         this.baseCodePath = baseCodePath;
@@ -136,6 +160,8 @@
             case STATE_NO_IDMAP:
             case STATE_DISABLED:
             case STATE_ENABLED:
+            case STATE_TARGET_UPGRADING:
+            case STATE_OVERLAY_UPGRADING:
                 break;
             default:
                 throw new IllegalArgumentException("State " + state + " is not a valid state");
@@ -156,7 +182,8 @@
         dest.writeInt(userId);
     }
 
-    public static final Parcelable.Creator<OverlayInfo> CREATOR = new Parcelable.Creator<OverlayInfo>() {
+    public static final Parcelable.Creator<OverlayInfo> CREATOR =
+            new Parcelable.Creator<OverlayInfo>() {
         @Override
         public OverlayInfo createFromParcel(Parcel source) {
             return new OverlayInfo(source);
@@ -189,14 +216,9 @@
      * Translate a state to a human readable string. Only intended for
      * debugging purposes.
      *
-     * @see #STATE_MISSING_TARGET
-     * @see #STATE_NO_IDMAP
-     * @see #STATE_DISABLED
-     * @see #STATE_ENABLED
-     *
      * @return a human readable String representing the state.
      */
-    public static String stateToString(int state) {
+    public static String stateToString(@State int state) {
         switch (state) {
             case STATE_UNKNOWN:
                 return "STATE_UNKNOWN";
@@ -208,6 +230,10 @@
                 return "STATE_DISABLED";
             case STATE_ENABLED:
                 return "STATE_ENABLED";
+            case STATE_TARGET_UPGRADING:
+                return "STATE_TARGET_UPGRADING";
+            case STATE_OVERLAY_UPGRADING:
+                return "STATE_OVERLAY_UPGRADING";
             default:
                 return "<unknown state>";
         }
diff --git a/core/java/android/content/pm/ApplicationInfo.java b/core/java/android/content/pm/ApplicationInfo.java
index b61a6d9..db1630b 100644
--- a/core/java/android/content/pm/ApplicationInfo.java
+++ b/core/java/android/content/pm/ApplicationInfo.java
@@ -1601,7 +1601,7 @@
      * @hide
      */
     public boolean isAllowedToUseHiddenApi() {
-        return false;
+        return isSystemApp();
     }
 
     /**
diff --git a/core/java/android/content/pm/PackageManager.java b/core/java/android/content/pm/PackageManager.java
index 4e5f835..486c86c 100644
--- a/core/java/android/content/pm/PackageManager.java
+++ b/core/java/android/content/pm/PackageManager.java
@@ -441,6 +441,7 @@
      * @hide
      */
     @SystemApi
+    @TestApi
     public static final int MATCH_FACTORY_ONLY = 0x00200000;
 
     /**
diff --git a/core/java/android/content/pm/PackageParser.java b/core/java/android/content/pm/PackageParser.java
index 2da8937..3afc346 100644
--- a/core/java/android/content/pm/PackageParser.java
+++ b/core/java/android/content/pm/PackageParser.java
@@ -6738,31 +6738,6 @@
                 + " " + packageName + "}";
         }
 
-        public String dumpState_temp() {
-            String flags = "";
-            flags += ((applicationInfo.flags & ApplicationInfo.FLAG_UPDATED_SYSTEM_APP) != 0 ? "U" : "");
-            flags += ((applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0 ? "S" : "");
-            if ("".equals(flags)) {
-                flags = "-";
-            }
-            String privFlags = "";
-            privFlags += ((applicationInfo.privateFlags & ApplicationInfo.PRIVATE_FLAG_PRIVILEGED) != 0 ? "P" : "");
-            privFlags += ((applicationInfo.privateFlags & ApplicationInfo.PRIVATE_FLAG_OEM) != 0 ? "O" : "");
-            privFlags += ((applicationInfo.privateFlags & ApplicationInfo.PRIVATE_FLAG_VENDOR) != 0 ? "V" : "");
-            if ("".equals(privFlags)) {
-                privFlags = "-";
-            }
-            return "Package{"
-            + Integer.toHexString(System.identityHashCode(this))
-            + " " + packageName
-            + ", ver:" + getLongVersionCode()
-            + ", path: " + codePath
-            + ", flags: " + flags
-            + ", privFlags: " + privFlags
-            + ", extra: " + (mExtras == null ? "<<NULL>>" : Integer.toHexString(System.identityHashCode(mExtras)) + "}")
-            + "}";
-        }
-
         @Override
         public int describeContents() {
             return 0;
diff --git a/core/java/android/hardware/camera2/CameraDevice.java b/core/java/android/hardware/camera2/CameraDevice.java
index 40ee834..df64401 100644
--- a/core/java/android/hardware/camera2/CameraDevice.java
+++ b/core/java/android/hardware/camera2/CameraDevice.java
@@ -923,6 +923,14 @@
      * {@link CameraCaptureSession#setRepeatingRequest} or
      * {@link CameraCaptureSession#setRepeatingBurst}</p>
      *
+     * <p>Individual physical camera settings will only be honored for camera session
+     * that was initialiazed with corresponding physical camera id output configuration
+     * {@link OutputConfiguration#setPhysicalCameraId} and the same output targets are
+     * also attached in the request by {@link CaptureRequest.Builder#addTarget}.</p>
+     *
+     * <p>The output is undefined for any logical camera streams in case valid physical camera
+     * settings are attached.</p>
+     *
      * @param templateType An enumeration selecting the use case for this request. Not all template
      * types are supported on every device. See the documentation for each template type for
      * details.
diff --git a/core/java/android/hardware/camera2/TotalCaptureResult.java b/core/java/android/hardware/camera2/TotalCaptureResult.java
index bae2d04..0be45a0 100644
--- a/core/java/android/hardware/camera2/TotalCaptureResult.java
+++ b/core/java/android/hardware/camera2/TotalCaptureResult.java
@@ -17,7 +17,6 @@
 package android.hardware.camera2;
 
 import android.annotation.NonNull;
-import android.annotation.Nullable;
 import android.hardware.camera2.impl.CameraMetadataNative;
 import android.hardware.camera2.impl.CaptureResultExtras;
 import android.hardware.camera2.impl.PhysicalCaptureResultInfo;
@@ -26,6 +25,7 @@
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.List;
+import java.util.Map;
 
 /**
  * <p>The total assembled results of a single image capture from the image sensor.</p>
@@ -49,9 +49,9 @@
  *
  * <p>For a logical multi-camera device, if the CaptureRequest contains a surface for an underlying
  * physical camera, the corresponding {@link TotalCaptureResult} object will include the metadata
- * for that physical camera. And its keys and values can be accessed by
- * {@link #getPhysicalCameraKey}. If all requested surfaces are for the logical camera, no
- * metadata for physical camera will be included.</p>
+ * for that physical camera. And the mapping between the physical camera id and result metadata
+ * can be accessed via {@link #getPhysicalCameraResults}. If all requested surfaces are for the
+ * logical camera, no metadata for physical camera will be included.</p>
  *
  * <p>{@link TotalCaptureResult} objects are immutable.</p>
  *
@@ -62,7 +62,7 @@
     private final List<CaptureResult> mPartialResults;
     private final int mSessionId;
     // The map between physical camera id and capture result
-    private final HashMap<String, CameraMetadataNative> mPhysicalCaptureResults;
+    private final HashMap<String, CaptureResult> mPhysicalCaptureResults;
 
     /**
      * Takes ownership of the passed-in camera metadata and the partial results
@@ -83,10 +83,12 @@
 
         mSessionId = sessionId;
 
-        mPhysicalCaptureResults = new HashMap<String, CameraMetadataNative>();
+        mPhysicalCaptureResults = new HashMap<String, CaptureResult>();
         for (PhysicalCaptureResultInfo onePhysicalResult : physicalResults) {
+            CaptureResult physicalResult = new CaptureResult(
+                    onePhysicalResult.getCameraMetadata(), parent, extras);
             mPhysicalCaptureResults.put(onePhysicalResult.getCameraId(),
-                    onePhysicalResult.getCameraMetadata());
+                    physicalResult);
         }
     }
 
@@ -101,7 +103,7 @@
 
         mPartialResults = new ArrayList<>();
         mSessionId = CameraCaptureSession.SESSION_ID_NONE;
-        mPhysicalCaptureResults = new HashMap<String, CameraMetadataNative>();
+        mPhysicalCaptureResults = new HashMap<String, CaptureResult>();
     }
 
     /**
@@ -132,36 +134,20 @@
     }
 
     /**
-     * Get a capture result field value for a particular physical camera id.
+     * Get the map between physical camera ids and their capture result metadata
      *
-     * <p>The field definitions can be found in {@link CaptureResult}.</p>
-     *
-     * <p>This function can be called for logical camera devices, which are devices that have
+     * <p>This function can be called for logical multi-camera devices, which are devices that have
      * REQUEST_AVAILABLE_CAPABILITIES_LOGICAL_MULTI_CAMERA capability and calls to {@link
      * CameraCharacteristics#getPhysicalCameraIds} return a non-empty list of physical devices that
-     * are backing the logical camera. The camera id included in physicalCameraId argument
-     * selects an individual physical device, and returns its specific capture result field.</p>
+     * are backing the logical camera.</p>
      *
-     * <p>This function should only be called if one or more streams from the underlying
-     * 'physicalCameraId' was requested by the corresponding capture request.</p>
-     *
-     * @throws IllegalArgumentException if the key was not valid, or the physicalCameraId is not
-     * applicable to the current camera, or a stream from 'physicalCameraId' is not requested by the
-     * corresponding capture request.
-     *
-     * @param key The result field to read.
-     * @param physicalCameraId The physical camera the result originates from.
-     *
-     * @return The value of that key, or {@code null} if the field is not set.
-     */
-    @Nullable
-    public <T> T getPhysicalCameraKey(Key<T> key, @NonNull String physicalCameraId) {
-        if (!mPhysicalCaptureResults.containsKey(physicalCameraId)) {
-            throw new IllegalArgumentException(
-                    "No TotalCaptureResult exists for physical camera " + physicalCameraId);
-        }
+     * <p>If one or more streams from the underlying physical cameras were requested by the
+     * corresponding capture request, this function returns the result metadata for those physical
+     * cameras. Otherwise, an empty map is returned.</p>
 
-        CameraMetadataNative physicalMetadata = mPhysicalCaptureResults.get(physicalCameraId);
-        return physicalMetadata.get(key);
+     * @return unmodifiable map between physical camera ids and their capture result metadata
+     */
+    public Map<String, CaptureResult> getPhysicalCameraResults() {
+        return Collections.unmodifiableMap(mPhysicalCaptureResults);
     }
 }
diff --git a/core/java/android/hardware/camera2/params/OutputConfiguration.java b/core/java/android/hardware/camera2/params/OutputConfiguration.java
index eb4bced..b205e2c 100644
--- a/core/java/android/hardware/camera2/params/OutputConfiguration.java
+++ b/core/java/android/hardware/camera2/params/OutputConfiguration.java
@@ -368,7 +368,7 @@
      * desirable for the camera application to request streams from individual physical cameras.
      * This call achieves it by mapping the OutputConfiguration to the physical camera id.</p>
      *
-     * <p>The valid physical camera id can be queried by {@link
+     * <p>The valid physical camera ids can be queried by {@link
      * android.hardware.camera2.CameraCharacteristics#getPhysicalCameraIds}.
      * </p>
      *
diff --git a/core/java/android/os/Binder.java b/core/java/android/os/Binder.java
index 4aadc5b..682fdb7 100644
--- a/core/java/android/os/Binder.java
+++ b/core/java/android/os/Binder.java
@@ -784,7 +784,7 @@
         private static final int MAIN_INDEX_SIZE = 1 <<  LOG_MAIN_INDEX_SIZE;
         private static final int MAIN_INDEX_MASK = MAIN_INDEX_SIZE - 1;
         // Debuggable builds will throw an AssertionError if the number of map entries exceeds:
-        private static final int CRASH_AT_SIZE = 5_000;
+        private static final int CRASH_AT_SIZE = 20_000;
 
         /**
          * We next warn when we exceed this bucket size.
diff --git a/core/java/android/os/IServiceManager.java b/core/java/android/os/IServiceManager.java
index 2176a78..89bf7b9 100644
--- a/core/java/android/os/IServiceManager.java
+++ b/core/java/android/os/IServiceManager.java
@@ -76,9 +76,15 @@
     int DUMP_FLAG_PRIORITY_CRITICAL = 1 << 0;
     int DUMP_FLAG_PRIORITY_HIGH = 1 << 1;
     int DUMP_FLAG_PRIORITY_NORMAL = 1 << 2;
+    /**
+     * Services are by default registered with a DEFAULT dump priority. DEFAULT priority has the
+     * same priority as NORMAL priority but the services are not called with dump priority
+     * arguments.
+     */
+    int DUMP_FLAG_PRIORITY_DEFAULT = 1 << 3;
     int DUMP_FLAG_PRIORITY_ALL = DUMP_FLAG_PRIORITY_CRITICAL | DUMP_FLAG_PRIORITY_HIGH
-            | DUMP_FLAG_PRIORITY_NORMAL;
+            | DUMP_FLAG_PRIORITY_NORMAL | DUMP_FLAG_PRIORITY_DEFAULT;
     /* Allows services to dump sections in protobuf format. */
-    int DUMP_FLAG_PROTO = 1 << 3;
+    int DUMP_FLAG_PROTO = 1 << 4;
 
 }
diff --git a/core/java/android/os/ServiceManager.java b/core/java/android/os/ServiceManager.java
index 42ec315..3be76d6 100644
--- a/core/java/android/os/ServiceManager.java
+++ b/core/java/android/os/ServiceManager.java
@@ -83,7 +83,7 @@
      * @param service the service object
      */
     public static void addService(String name, IBinder service) {
-        addService(name, service, false, IServiceManager.DUMP_FLAG_PRIORITY_NORMAL);
+        addService(name, service, false, IServiceManager.DUMP_FLAG_PRIORITY_DEFAULT);
     }
 
     /**
@@ -96,7 +96,7 @@
      * to access this service
      */
     public static void addService(String name, IBinder service, boolean allowIsolated) {
-        addService(name, service, allowIsolated, IServiceManager.DUMP_FLAG_PRIORITY_NORMAL);
+        addService(name, service, allowIsolated, IServiceManager.DUMP_FLAG_PRIORITY_DEFAULT);
     }
 
     /**
diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java
index 8c65041..f179371 100644
--- a/core/java/android/provider/Settings.java
+++ b/core/java/android/provider/Settings.java
@@ -3692,6 +3692,20 @@
                 new SettingsValidators.InclusiveIntegerRangeValidator(0, 3);
 
         /**
+         * User-selected RTT mode
+         * 0 = OFF
+         * 1 = FULL
+         * 2 = VCO
+         * 3 = HCO
+         * Uses the same constants as TTY (e.g. {@link android.telecom.TelecomManager#TTY_MODE_OFF})
+         * @hide
+         */
+        public static final String RTT_CALLING_MODE = "rtt_calling_mode";
+
+        /** @hide */
+        public static final Validator RTT_CALLING_MODE_VALIDATOR = TTY_MODE_VALIDATOR;
+
+        /**
          * Whether the sounds effects (key clicks, lid open ...) are enabled. The value is
          * boolean (1 or 0).
          */
@@ -4016,6 +4030,7 @@
             DTMF_TONE_WHEN_DIALING,
             DTMF_TONE_TYPE_WHEN_DIALING,
             HEARING_AID,
+            RTT_CALLING_MODE,
             TTY_MODE,
             MASTER_MONO,
             SOUND_EFFECTS_ENABLED,
@@ -4214,6 +4229,7 @@
             VALIDATORS.put(DTMF_TONE_TYPE_WHEN_DIALING, DTMF_TONE_TYPE_WHEN_DIALING_VALIDATOR);
             VALIDATORS.put(HEARING_AID, HEARING_AID_VALIDATOR);
             VALIDATORS.put(TTY_MODE, TTY_MODE_VALIDATOR);
+            VALIDATORS.put(RTT_CALLING_MODE, RTT_CALLING_MODE_VALIDATOR);
             VALIDATORS.put(NOTIFICATION_LIGHT_PULSE, NOTIFICATION_LIGHT_PULSE_VALIDATOR);
             VALIDATORS.put(POINTER_LOCATION, POINTER_LOCATION_VALIDATOR);
             VALIDATORS.put(SHOW_TOUCHES, SHOW_TOUCHES_VALIDATOR);
@@ -10255,6 +10271,20 @@
         public static final String BATTERY_TIP_CONSTANTS = "battery_tip_constants";
 
         /**
+         * An integer to show the version of the anomaly config. Ex: 1, which means
+         * current version is 1.
+         * @hide
+         */
+        public static final String ANOMALY_CONFIG_VERSION = "anomaly_config_version";
+
+        /**
+         * A base64-encoded string represents anomaly stats config, used for
+         * {@link android.app.StatsManager}.
+         * @hide
+         */
+        public static final String ANOMALY_CONFIG = "anomaly_config";
+
+        /**
          * Always on display(AOD) specific settings
          * This is encoded as a key=value list, separated by commas. Ex:
          *
diff --git a/core/java/android/service/autofill/AutofillServiceInfo.java b/core/java/android/service/autofill/AutofillServiceInfo.java
index 29c6cea..7383de7 100644
--- a/core/java/android/service/autofill/AutofillServiceInfo.java
+++ b/core/java/android/service/autofill/AutofillServiceInfo.java
@@ -44,6 +44,7 @@
 import org.xmlpull.v1.XmlPullParserException;
 
 import java.io.IOException;
+import java.io.PrintWriter;
 import java.util.Map;
 
 /**
@@ -247,4 +248,13 @@
                 && !mCompatibilityPackages.isEmpty()).append("]");
         return builder.toString();
     }
+
+    /**
+     * Dumps it!
+     */
+    public void dump(String prefix, PrintWriter pw) {
+        pw.print(prefix); pw.print("Component: "); pw.println(getServiceInfo().getComponentName());
+        pw.print(prefix); pw.print("Settings: "); pw.println(mSettingsActivity);
+        pw.print(prefix); pw.print("Compat packages: "); pw.println(mCompatibilityPackages);
+    }
 }
diff --git a/core/java/android/transition/TransitionUtils.java b/core/java/android/transition/TransitionUtils.java
index 084b79d..60b77bc 100644
--- a/core/java/android/transition/TransitionUtils.java
+++ b/core/java/android/transition/TransitionUtils.java
@@ -163,10 +163,14 @@
     public static Bitmap createViewBitmap(View view, Matrix matrix, RectF bounds,
             ViewGroup sceneRoot) {
         final boolean addToOverlay = !view.isAttachedToWindow();
+        ViewGroup parent = null;
+        int indexInParent = 0;
         if (addToOverlay) {
             if (sceneRoot == null || !sceneRoot.isAttachedToWindow()) {
                 return null;
             }
+            parent = (ViewGroup) view.getParent();
+            indexInParent = parent.indexOfChild(view);
             sceneRoot.getOverlay().add(view);
         }
         Bitmap bitmap = null;
@@ -190,6 +194,7 @@
         }
         if (addToOverlay) {
             sceneRoot.getOverlay().remove(view);
+            parent.addView(view, indexInParent);
         }
         return bitmap;
     }
diff --git a/core/java/android/transition/Visibility.java b/core/java/android/transition/Visibility.java
index f0838a1..77c652e 100644
--- a/core/java/android/transition/Visibility.java
+++ b/core/java/android/transition/Visibility.java
@@ -402,8 +402,11 @@
                 // Becoming GONE
                 if (startView == endView) {
                     viewToKeep = endView;
-                } else {
+                } else if (mCanRemoveViews) {
                     overlayView = startView;
+                } else {
+                    overlayView = TransitionUtils.copyViewImage(sceneRoot, startView,
+                            (View) startView.getParent());
                 }
             }
         }
diff --git a/core/java/android/view/textclassifier/SmartSelection.java b/core/java/android/view/textclassifier/SmartSelection.java
index 8edf97e..69c38ee 100644
--- a/core/java/android/view/textclassifier/SmartSelection.java
+++ b/core/java/android/view/textclassifier/SmartSelection.java
@@ -108,9 +108,9 @@
     }
 
     /**
-     * Returns the language of the model.
+     * Returns a comma separated list of locales supported by the model as BCP 47 tags.
      */
-    public static String getLanguage(int fd) {
+    public static String getLanguages(int fd) {
         return nativeGetLanguage(fd);
     }
 
diff --git a/core/java/android/view/textclassifier/TextClassifierImpl.java b/core/java/android/view/textclassifier/TextClassifierImpl.java
index 9f389ba..fc03493 100644
--- a/core/java/android/view/textclassifier/TextClassifierImpl.java
+++ b/core/java/android/view/textclassifier/TextClassifierImpl.java
@@ -27,8 +27,10 @@
 import android.content.pm.ResolveInfo;
 import android.graphics.drawable.Drawable;
 import android.net.Uri;
+import android.os.Bundle;
 import android.os.LocaleList;
 import android.os.ParcelFileDescriptor;
+import android.os.UserManager;
 import android.provider.Browser;
 import android.provider.CalendarContract;
 import android.provider.ContactsContract;
@@ -56,6 +58,7 @@
 import java.util.Locale;
 import java.util.Map;
 import java.util.Objects;
+import java.util.StringJoiner;
 import java.util.concurrent.TimeUnit;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
@@ -99,11 +102,9 @@
 
     private final Object mLock = new Object();
     @GuardedBy("mLock") // Do not access outside this lock.
-    private Map<Locale, String> mModelFilePaths;
+    private List<ModelFile> mAllModelFiles;
     @GuardedBy("mLock") // Do not access outside this lock.
-    private Locale mLocale;
-    @GuardedBy("mLock") // Do not access outside this lock.
-    private int mVersion;
+    private ModelFile mModel;
     @GuardedBy("mLock") // Do not access outside this lock.
     private SmartSelection mSmartSelection;
 
@@ -279,18 +280,18 @@
     private SmartSelection getSmartSelection(LocaleList localeList) throws FileNotFoundException {
         synchronized (mLock) {
             localeList = localeList == null ? LocaleList.getEmptyLocaleList() : localeList;
-            final Locale locale = findBestSupportedLocaleLocked(localeList);
-            if (locale == null) {
-                throw new FileNotFoundException("No file for null locale");
+            final ModelFile bestModel = findBestModelLocked(localeList);
+            if (bestModel == null) {
+                throw new FileNotFoundException("No model for " + localeList.toLanguageTags());
             }
-            if (mSmartSelection == null || !Objects.equals(mLocale, locale)) {
+            if (mSmartSelection == null || !Objects.equals(mModel, bestModel)) {
+                Log.d(DEFAULT_LOG_TAG, "Loading " + bestModel);
                 destroySmartSelectionIfExistsLocked();
-                final ParcelFileDescriptor fd = getFdLocked(locale);
-                final int modelFd = fd.getFd();
-                mVersion = SmartSelection.getVersion(modelFd);
-                mSmartSelection = new SmartSelection(modelFd);
+                final ParcelFileDescriptor fd = ParcelFileDescriptor.open(
+                        new File(bestModel.getPath()), ParcelFileDescriptor.MODE_READ_ONLY);
+                mSmartSelection = new SmartSelection(fd.getFd());
                 closeAndLogError(fd);
-                mLocale = locale;
+                mModel = bestModel;
             }
             return mSmartSelection;
         }
@@ -298,74 +299,8 @@
 
     private String getSignature(String text, int start, int end) {
         synchronized (mLock) {
-            return DefaultLogger.createSignature(text, start, end, mContext, mVersion, mLocale);
-        }
-    }
-
-    @GuardedBy("mLock") // Do not call outside this lock.
-    private ParcelFileDescriptor getFdLocked(Locale locale) throws FileNotFoundException {
-        ParcelFileDescriptor updateFd;
-        int updateVersion = -1;
-        try {
-            updateFd = ParcelFileDescriptor.open(
-                    new File(UPDATED_MODEL_FILE_PATH), ParcelFileDescriptor.MODE_READ_ONLY);
-            if (updateFd != null) {
-                updateVersion = SmartSelection.getVersion(updateFd.getFd());
-            }
-        } catch (FileNotFoundException e) {
-            updateFd = null;
-        }
-        ParcelFileDescriptor factoryFd;
-        int factoryVersion = -1;
-        try {
-            final String factoryModelFilePath = getFactoryModelFilePathsLocked().get(locale);
-            if (factoryModelFilePath != null) {
-                factoryFd = ParcelFileDescriptor.open(
-                        new File(factoryModelFilePath), ParcelFileDescriptor.MODE_READ_ONLY);
-                if (factoryFd != null) {
-                    factoryVersion = SmartSelection.getVersion(factoryFd.getFd());
-                }
-            } else {
-                factoryFd = null;
-            }
-        } catch (FileNotFoundException e) {
-            factoryFd = null;
-        }
-
-        if (updateFd == null) {
-            if (factoryFd != null) {
-                return factoryFd;
-            } else {
-                throw new FileNotFoundException(
-                        String.format(Locale.US, "No model file found for %s", locale));
-            }
-        }
-
-        final int updateFdInt = updateFd.getFd();
-        final boolean localeMatches = Objects.equals(
-                locale.getLanguage().trim().toLowerCase(),
-                SmartSelection.getLanguage(updateFdInt).trim().toLowerCase());
-        if (factoryFd == null) {
-            if (localeMatches) {
-                return updateFd;
-            } else {
-                closeAndLogError(updateFd);
-                throw new FileNotFoundException(
-                        String.format(Locale.US, "No model file found for %s", locale));
-            }
-        }
-
-        if (!localeMatches) {
-            closeAndLogError(updateFd);
-            return factoryFd;
-        }
-
-        if (updateVersion > factoryVersion) {
-            closeAndLogError(factoryFd);
-            return updateFd;
-        } else {
-            closeAndLogError(updateFd);
-            return factoryFd;
+            return DefaultLogger.createSignature(text, start, end, mContext, mModel.getVersion(),
+                    mModel.getSupportedLocales());
         }
     }
 
@@ -377,60 +312,66 @@
         }
     }
 
+    /**
+     * Finds the most appropriate model to use for the given target locale list.
+     *
+     * The basic logic is: we ignore all models that don't support any of the target locales. For
+     * the remaining candidates, we take the update model unless its version number is lower than
+     * the factory version. It's assumed that factory models do not have overlapping locale ranges
+     * and conflict resolution between these models hence doesn't matter.
+     */
     @GuardedBy("mLock") // Do not call outside this lock.
     @Nullable
-    private Locale findBestSupportedLocaleLocked(LocaleList localeList) {
+    private ModelFile findBestModelLocked(LocaleList localeList) {
         // Specified localeList takes priority over the system default, so it is listed first.
         final String languages = localeList.isEmpty()
                 ? LocaleList.getDefault().toLanguageTags()
                 : localeList.toLanguageTags() + "," + LocaleList.getDefault().toLanguageTags();
         final List<Locale.LanguageRange> languageRangeList = Locale.LanguageRange.parse(languages);
 
-        final List<Locale> supportedLocales =
-                new ArrayList<>(getFactoryModelFilePathsLocked().keySet());
-        final Locale updatedModelLocale = getUpdatedModelLocale();
-        if (updatedModelLocale != null) {
-            supportedLocales.add(updatedModelLocale);
+        ModelFile bestModel = null;
+        int bestModelVersion = -1;
+        for (ModelFile model : listAllModelsLocked()) {
+            if (model.isAnyLanguageSupported(languageRangeList)) {
+                if (model.getVersion() >= bestModelVersion) {
+                    bestModel = model;
+                    bestModelVersion = model.getVersion();
+                }
+            }
         }
-        return Locale.lookup(languageRangeList, supportedLocales);
+        return bestModel;
     }
 
+    /** Returns a list of all model files available, in order of precedence. */
     @GuardedBy("mLock") // Do not call outside this lock.
-    private Map<Locale, String> getFactoryModelFilePathsLocked() {
-        if (mModelFilePaths == null) {
-            final Map<Locale, String> modelFilePaths = new HashMap<>();
+    private List<ModelFile> listAllModelsLocked() {
+        if (mAllModelFiles == null) {
+            final List<ModelFile> allModels = new ArrayList<>();
+            // The update model has the highest precedence.
+            if (new File(UPDATED_MODEL_FILE_PATH).exists()) {
+                final ModelFile updatedModel = ModelFile.fromPath(UPDATED_MODEL_FILE_PATH);
+                if (updatedModel != null) {
+                    allModels.add(updatedModel);
+                }
+            }
+            // Factory models should never have overlapping locales, so the order doesn't matter.
             final File modelsDir = new File(MODEL_DIR);
             if (modelsDir.exists() && modelsDir.isDirectory()) {
-                final File[] models = modelsDir.listFiles();
+                final File[] modelFiles = modelsDir.listFiles();
                 final Pattern modelFilenamePattern = Pattern.compile(MODEL_FILE_REGEX);
-                final int size = models.length;
-                for (int i = 0; i < size; i++) {
-                    final File modelFile = models[i];
+                for (File modelFile : modelFiles) {
                     final Matcher matcher = modelFilenamePattern.matcher(modelFile.getName());
                     if (matcher.matches() && modelFile.isFile()) {
-                        final String language = matcher.group(1);
-                        final Locale locale = Locale.forLanguageTag(language);
-                        modelFilePaths.put(locale, modelFile.getAbsolutePath());
+                        final ModelFile model = ModelFile.fromPath(modelFile.getAbsolutePath());
+                        if (model != null) {
+                            allModels.add(model);
+                        }
                     }
                 }
             }
-            mModelFilePaths = modelFilePaths;
+            mAllModelFiles = allModels;
         }
-        return mModelFilePaths;
-    }
-
-    @Nullable
-    private Locale getUpdatedModelLocale() {
-        try {
-            final ParcelFileDescriptor updateFd = ParcelFileDescriptor.open(
-                    new File(UPDATED_MODEL_FILE_PATH), ParcelFileDescriptor.MODE_READ_ONLY);
-            final Locale locale = Locale.forLanguageTag(
-                    SmartSelection.getLanguage(updateFd.getFd()));
-            closeAndLogError(updateFd);
-            return locale;
-        } catch (FileNotFoundException e) {
-            return null;
-        }
+        return mAllModelFiles;
     }
 
     private TextClassification createClassificationResult(
@@ -520,6 +461,95 @@
     }
 
     /**
+     * Describes TextClassifier model files on disk.
+     */
+    private static final class ModelFile {
+
+        private final String mPath;
+        private final String mName;
+        private final int mVersion;
+        private final List<Locale> mSupportedLocales;
+
+        /** Returns null if the path did not point to a compatible model. */
+        static @Nullable ModelFile fromPath(String path) {
+            final File file = new File(path);
+            try {
+                final ParcelFileDescriptor modelFd = ParcelFileDescriptor.open(
+                        file, ParcelFileDescriptor.MODE_READ_ONLY);
+                final int version = SmartSelection.getVersion(modelFd.getFd());
+                final String supportedLocalesStr = SmartSelection.getLanguages(modelFd.getFd());
+                if (supportedLocalesStr.isEmpty()) {
+                    Log.d(DEFAULT_LOG_TAG, "Ignoring " + file.getAbsolutePath());
+                    return null;
+                }
+                final List<Locale> supportedLocales = new ArrayList<>();
+                for (String langTag : supportedLocalesStr.split(",")) {
+                    supportedLocales.add(Locale.forLanguageTag(langTag));
+                }
+                closeAndLogError(modelFd);
+                return new ModelFile(path, file.getName(), version, supportedLocales);
+            } catch (FileNotFoundException e) {
+                Log.e(DEFAULT_LOG_TAG, "Failed to peek " + file.getAbsolutePath(), e);
+                return null;
+            }
+        }
+
+        /** The absolute path to the model file. */
+        String getPath() {
+            return mPath;
+        }
+
+        /** A name to use for signature generation. Effectively the name of the model file. */
+        String getName() {
+            return mName;
+        }
+
+        /** Returns the version tag in the model's metadata. */
+        int getVersion() {
+            return mVersion;
+        }
+
+        /** Returns whether the language supports any language in the given ranges. */
+        boolean isAnyLanguageSupported(List<Locale.LanguageRange> languageRanges) {
+            return Locale.lookup(languageRanges, mSupportedLocales) != null;
+        }
+
+        /** All locales supported by the model. */
+        List<Locale> getSupportedLocales() {
+            return Collections.unmodifiableList(mSupportedLocales);
+        }
+
+        @Override
+        public boolean equals(Object other) {
+            if (this == other) {
+                return true;
+            } else if (other == null || !ModelFile.class.isAssignableFrom(other.getClass())) {
+                return false;
+            } else {
+                final ModelFile otherModel = (ModelFile) other;
+                return mPath.equals(otherModel.mPath);
+            }
+        }
+
+        @Override
+        public String toString() {
+            final StringJoiner localesJoiner = new StringJoiner(",");
+            for (Locale locale : mSupportedLocales) {
+                localesJoiner.add(locale.toLanguageTag());
+            }
+            return String.format(Locale.US, "ModelFile { path=%s name=%s version=%d locales=%s }",
+                    mPath, mName, mVersion, localesJoiner.toString());
+        }
+
+        private ModelFile(String path, String name, int version, List<Locale> supportedLocales) {
+            mPath = path;
+            mName = name;
+            mVersion = version;
+            mSupportedLocales = supportedLocales;
+        }
+    }
+
+    /**
      * Creates intents based on the classification type.
      */
     static final class IntentFactory {
@@ -541,7 +571,7 @@
                 case TextClassifier.TYPE_EMAIL:
                     return createForEmail(text);
                 case TextClassifier.TYPE_PHONE:
-                    return createForPhone(text);
+                    return createForPhone(context, text);
                 case TextClassifier.TYPE_ADDRESS:
                     return createForAddress(text);
                 case TextClassifier.TYPE_URL:
@@ -573,15 +603,23 @@
         }
 
         @NonNull
-        private static List<Intent> createForPhone(String text) {
-            return Arrays.asList(
-                    new Intent(Intent.ACTION_DIAL)
-                            .setData(Uri.parse(String.format("tel:%s", text))),
-                    new Intent(Intent.ACTION_INSERT_OR_EDIT)
-                            .setType(ContactsContract.Contacts.CONTENT_ITEM_TYPE)
-                            .putExtra(ContactsContract.Intents.Insert.PHONE, text),
-                    new Intent(Intent.ACTION_SENDTO)
-                            .setData(Uri.parse(String.format("smsto:%s", text))));
+        private static List<Intent> createForPhone(Context context, String text) {
+            final List<Intent> intents = new ArrayList<>();
+            final UserManager userManager = context.getSystemService(UserManager.class);
+            final Bundle userRestrictions = userManager != null
+                    ? userManager.getUserRestrictions() : new Bundle();
+            if (!userRestrictions.getBoolean(UserManager.DISALLOW_OUTGOING_CALLS, false)) {
+                intents.add(new Intent(Intent.ACTION_DIAL)
+                        .setData(Uri.parse(String.format("tel:%s", text))));
+            }
+            intents.add(new Intent(Intent.ACTION_INSERT_OR_EDIT)
+                    .setType(ContactsContract.Contacts.CONTENT_ITEM_TYPE)
+                    .putExtra(ContactsContract.Intents.Insert.PHONE, text));
+            if (!userRestrictions.getBoolean(UserManager.DISALLOW_SMS, false)) {
+                intents.add(new Intent(Intent.ACTION_SENDTO)
+                        .setData(Uri.parse(String.format("smsto:%s", text))));
+            }
+            return intents;
         }
 
         @NonNull
diff --git a/core/java/android/view/textclassifier/logging/DefaultLogger.java b/core/java/android/view/textclassifier/logging/DefaultLogger.java
index 6b84835..03a6d3a 100644
--- a/core/java/android/view/textclassifier/logging/DefaultLogger.java
+++ b/core/java/android/view/textclassifier/logging/DefaultLogger.java
@@ -17,7 +17,6 @@
 package android.view.textclassifier.logging;
 
 import android.annotation.NonNull;
-import android.annotation.Nullable;
 import android.content.Context;
 import android.metrics.LogMaker;
 import android.util.Log;
@@ -27,8 +26,10 @@
 import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
 import com.android.internal.util.Preconditions;
 
+import java.util.List;
 import java.util.Locale;
 import java.util.Objects;
+import java.util.StringJoiner;
 
 /**
  * Default Logger.
@@ -210,12 +211,16 @@
      */
     public static String createSignature(
             String text, int start, int end, Context context, int modelVersion,
-            @Nullable Locale locale) {
+            List<Locale> locales) {
         Preconditions.checkNotNull(text);
         Preconditions.checkNotNull(context);
-        final String modelName = (locale != null)
-                ? String.format(Locale.US, "%s_v%d", locale.toLanguageTag(), modelVersion)
-                : "";
+        Preconditions.checkNotNull(locales);
+        final StringJoiner localesJoiner = new StringJoiner(",");
+        for (Locale locale : locales) {
+            localesJoiner.add(locale.toLanguageTag());
+        }
+        final String modelName = String.format(Locale.US, "%s_v%d", localesJoiner.toString(),
+                modelVersion);
         final int hash = Objects.hash(text, start, end, context.getPackageName());
         return SignatureParser.createSignature(CLASSIFIER_ID, modelName, hash);
     }
@@ -242,9 +247,9 @@
 
         static String getModelName(String signature) {
             Preconditions.checkNotNull(signature);
-            final int start = signature.indexOf("|");
+            final int start = signature.indexOf("|") + 1;
             final int end = signature.indexOf("|", start);
-            if (start >= 0 && end >= start) {
+            if (start >= 1 && end >= start) {
                 return signature.substring(start, end);
             }
             return "";
diff --git a/core/java/com/android/internal/app/ResolverListController.java b/core/java/com/android/internal/app/ResolverListController.java
index 1dfff5e..6bd6930 100644
--- a/core/java/com/android/internal/app/ResolverListController.java
+++ b/core/java/com/android/internal/app/ResolverListController.java
@@ -106,7 +106,7 @@
             int flags = PackageManager.MATCH_DEFAULT_ONLY
                     | (shouldGetResolvedFilter ? PackageManager.GET_RESOLVED_FILTER : 0)
                     | (shouldGetActivityMetadata ? PackageManager.GET_META_DATA : 0);
-            if (intent.isBrowsableWebIntent()
+            if (intent.isWebIntent()
                         || (intent.getFlags() & Intent.FLAG_ACTIVITY_MATCH_EXTERNAL) != 0) {
                 flags |= PackageManager.MATCH_INSTANT;
             }
diff --git a/core/java/com/android/internal/colorextraction/types/Tonal.java b/core/java/com/android/internal/colorextraction/types/Tonal.java
index 9b7383f..7b25a06 100644
--- a/core/java/com/android/internal/colorextraction/types/Tonal.java
+++ b/core/java/com/android/internal/colorextraction/types/Tonal.java
@@ -405,12 +405,13 @@
         return v - (float) Math.floor(v);
     }
 
-    static class TonalPalette {
-        final float[] h;
-        final float[] s;
-        final float[] l;
-        final float minHue;
-        final float maxHue;
+    @VisibleForTesting
+    public static class TonalPalette {
+        public final float[] h;
+        public final float[] s;
+        public final float[] l;
+        public final float minHue;
+        public final float maxHue;
 
         TonalPalette(float[] h, float[] s, float[] l) {
             if (h.length != s.length || s.length != l.length) {
diff --git a/core/java/com/android/internal/os/BatteryStatsImpl.java b/core/java/com/android/internal/os/BatteryStatsImpl.java
index 40dcf25b..cf3e073 100644
--- a/core/java/com/android/internal/os/BatteryStatsImpl.java
+++ b/core/java/com/android/internal/os/BatteryStatsImpl.java
@@ -3743,7 +3743,7 @@
 
         if (mNumHistoryItems == MAX_HISTORY_ITEMS
                 || mNumHistoryItems == MAX_MAX_HISTORY_ITEMS) {
-            addHistoryRecordLocked(elapsedRealtimeMs, HistoryItem.CMD_OVERFLOW);
+            addHistoryBufferLocked(elapsedRealtimeMs, uptimeMs, HistoryItem.CMD_OVERFLOW, cur);
         }
 
         if (mNumHistoryItems >= MAX_HISTORY_ITEMS) {
@@ -3760,7 +3760,7 @@
             }
         }
 
-        addHistoryRecordLocked(elapsedRealtimeMs, HistoryItem.CMD_UPDATE);
+        addHistoryBufferLocked(elapsedRealtimeMs, uptimeMs, HistoryItem.CMD_UPDATE, cur);
     }
 
     public void addHistoryEventLocked(long elapsedRealtimeMs, long uptimeMs, int code,
diff --git a/core/java/com/android/internal/view/InputBindResult.java b/core/java/com/android/internal/view/InputBindResult.java
index 74dbaba..f05bd32 100644
--- a/core/java/com/android/internal/view/InputBindResult.java
+++ b/core/java/com/android/internal/view/InputBindResult.java
@@ -139,6 +139,10 @@
          * @see com.android.server.wm.WindowManagerService#inputMethodClientHasFocus(IInputMethodClient)
          */
         int ERROR_NOT_IME_TARGET_WINDOW = 11;
+        /**
+         * Indicates that focused view in the current window is not an editor.
+         */
+        int ERROR_NO_EDITOR = 12;
     }
 
     @ResultCode
@@ -258,6 +262,8 @@
                 return "ERROR_NULL";
             case ResultCode.ERROR_NO_IME:
                 return "ERROR_NO_IME";
+            case ResultCode.ERROR_NO_EDITOR:
+                return "ERROR_NO_EDITOR";
             case ResultCode.ERROR_INVALID_PACKAGE_NAME:
                 return "ERROR_INVALID_PACKAGE_NAME";
             case ResultCode.ERROR_SYSTEM_NOT_READY:
@@ -288,6 +294,10 @@
      */
     public static final InputBindResult NO_IME = error(ResultCode.ERROR_NO_IME);
     /**
+     * Predefined error object for {@link ResultCode#NO_EDITOR}.
+     */
+    public static final InputBindResult NO_EDITOR = error(ResultCode.ERROR_NO_EDITOR);
+    /**
      * Predefined error object for {@link ResultCode#ERROR_INVALID_PACKAGE_NAME}.
      */
     public static final InputBindResult INVALID_PACKAGE_NAME =
diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml
index f627aaa..deefddb 100644
--- a/core/res/AndroidManifest.xml
+++ b/core/res/AndroidManifest.xml
@@ -3861,6 +3861,7 @@
                 android:excludeFromRecents="true"
                 android:label="@string/user_owner_label"
                 android:exported="true"
+                android:visibleToInstantApps="true"
                 >
         </activity>
         <activity-alias android:name="com.android.internal.app.ForwardIntentToParent"
diff --git a/core/res/res/xml/color_extraction.xml b/core/res/res/xml/color_extraction.xml
index 7d52b20..93ab0ff 100644
--- a/core/res/res/xml/color_extraction.xml
+++ b/core/res/res/xml/color_extraction.xml
@@ -257,52 +257,58 @@
         <!-- Red Orange -->
         <range h="20, 40"
                s="0.7, 1"
-               l="0.28, 0.643"/>
+               l="0.2, 0.643"/>
         <range h="20, 40"
                s="0.3, 0.7"
                l="0.414, 0.561"/>
         <range h="20, 40"
-               s="0, 3"
+               s="0, 0.3"
                l="0.343, 0.584"/>
         <!-- Orange -->
         <range h="40, 60"
                s="0.7, 1"
-               l="0.173, 0.349"/>
+               l="0.173, 0.38"/>
         <range h="40, 60"
                s="0.3, 0.7"
                l="0.233, 0.427"/>
         <range h="40, 60"
                s="0, 0.3"
-               l="0.231, 0.484"/>
+               l="0.231, 0.48"/>
         <!-- Yellow 60 -->
         <range h="60, 80"
                s="0.7, 1"
-               l="0.488, 0.737"/>
+               l="0.15, 0.40"/>
         <range h="60, 80"
                s="0.3, 0.7"
-               l="0.673, 0.837"/>
+               l="0.15, 0.42"/>
+        <range h="60, 80"
+               s="0, 0.3"
+               l="0.35, 0.57"/>
         <!-- Yellow Green 80 -->
         <range h="80, 100"
                s="0.7, 1"
-               l="0.469, 0.61"/>
+               l="0.36, 0.65"/>
+        <range h="80, 100"
+               s="0.3, 0.7"
+               l="0.48, 0.57"/>
         <!-- Yellow green 100 -->
         <range h="100, 120"
                s="0.7, 1"
-               l="0.388, 0.612"/>
+               l="0.388, 0.67"/>
         <range h="100, 120"
                s="0.3, 0.7"
-               l="0.424, 0.541"/>
+               l="0.424, 0.58"/>
         <!-- Green -->
         <range h="120, 140"
                s="0.7, 1"
-               l="0.375, 0.52"/>
+               l="0.37, 0.65"/>
         <range h="120, 140"
                s="0.3, 0.7"
-               l="0.435, 0.524"/>
+               l="0.435, 0.58"/>
         <!-- Green Blue 140 -->
         <range h="140, 160"
                s="0.7, 1"
-               l="0.496, 0.641"/>
+               l="0.43, 0.641"/>
         <!-- Seaoam -->
         <range h="160, 180"
                s="0.7, 1"
diff --git a/core/tests/coretests/src/android/provider/SettingsBackupTest.java b/core/tests/coretests/src/android/provider/SettingsBackupTest.java
index 3e62183..e1f95a3 100644
--- a/core/tests/coretests/src/android/provider/SettingsBackupTest.java
+++ b/core/tests/coretests/src/android/provider/SettingsBackupTest.java
@@ -109,6 +109,8 @@
                     Settings.Global.ALWAYS_ON_DISPLAY_CONSTANTS,
                     Settings.Global.ANIMATOR_DURATION_SCALE,
                     Settings.Global.ANOMALY_DETECTION_CONSTANTS,
+                    Settings.Global.ANOMALY_CONFIG,
+                    Settings.Global.ANOMALY_CONFIG_VERSION,
                     Settings.Global.APN_DB_UPDATE_CONTENT_URL,
                     Settings.Global.APN_DB_UPDATE_METADATA_URL,
                     Settings.Global.APP_IDLE_CONSTANTS,
diff --git a/core/tests/overlaytests/host/Android.mk b/core/tests/overlaytests/host/Android.mk
index d8e1fc1..b48a46b 100644
--- a/core/tests/overlaytests/host/Android.mk
+++ b/core/tests/overlaytests/host/Android.mk
@@ -24,7 +24,11 @@
     OverlayHostTests_BadSignatureOverlay \
     OverlayHostTests_PlatformSignatureStaticOverlay \
     OverlayHostTests_PlatformSignatureOverlay \
-    OverlayHostTests_PlatformSignatureOverlayV2
+    OverlayHostTests_UpdateOverlay \
+    OverlayHostTests_FrameworkOverlayV1 \
+    OverlayHostTests_FrameworkOverlayV2 \
+    OverlayHostTests_AppOverlayV1 \
+    OverlayHostTests_AppOverlayV2
 include $(BUILD_HOST_JAVA_LIBRARY)
 
 # Include to build test-apps.
diff --git a/core/tests/overlaytests/host/AndroidTest.xml b/core/tests/overlaytests/host/AndroidTest.xml
index 6884623..1f750a8 100644
--- a/core/tests/overlaytests/host/AndroidTest.xml
+++ b/core/tests/overlaytests/host/AndroidTest.xml
@@ -18,6 +18,11 @@
     <option name="test-tag" value="OverlayHostTests" />
     <option name="test-suite-tag" value="apct" />
 
+    <!-- Install the device tests that will be used to verify things on device. -->
+    <target_preparer class="com.android.tradefed.targetprep.TestAppInstallSetup">
+        <option name="test-file-name" value="OverlayHostTests_UpdateOverlay.apk" />
+    </target_preparer>
+
     <test class="com.android.tradefed.testtype.HostTest">
         <option name="class" value="com.android.server.om.hosttest.InstallOverlayTests" />
     </test>
diff --git a/core/tests/overlaytests/host/src/com/android/server/om/hosttest/InstallOverlayTests.java b/core/tests/overlaytests/host/src/com/android/server/om/hosttest/InstallOverlayTests.java
index 5093710..bf91a16 100644
--- a/core/tests/overlaytests/host/src/com/android/server/om/hosttest/InstallOverlayTests.java
+++ b/core/tests/overlaytests/host/src/com/android/server/om/hosttest/InstallOverlayTests.java
@@ -23,14 +23,48 @@
 import com.android.tradefed.testtype.DeviceJUnit4ClassRunner;
 import com.android.tradefed.testtype.junit4.BaseHostJUnit4Test;
 
+import org.junit.After;
+import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
 @RunWith(DeviceJUnit4ClassRunner.class)
 public class InstallOverlayTests extends BaseHostJUnit4Test {
-
-    private static final String OVERLAY_PACKAGE_NAME =
+    private static final String SIG_OVERLAY_PACKAGE_NAME =
             "com.android.server.om.hosttest.signature_overlay";
+    private static final String APP_OVERLAY_PACKAGE_NAME =
+            "com.android.server.om.hosttest.app_overlay";
+    private static final String FRAMEWORK_OVERLAY_PACKAGE_NAME =
+            "com.android.server.om.hosttest.framework_overlay";
+    private static final String[] ALL_PACKAGES = new String[] {
+            SIG_OVERLAY_PACKAGE_NAME, APP_OVERLAY_PACKAGE_NAME, FRAMEWORK_OVERLAY_PACKAGE_NAME
+    };
+
+    private static final String DEVICE_TEST_PKG =
+            "com.android.server.om.hosttest.update_overlay_test";
+    private static final String DEVICE_TEST_CLS = DEVICE_TEST_PKG + ".UpdateOverlayTest";
+
+    @Before
+    public void ensureNoOverlays() throws Exception {
+        // Make sure we're starting with a clean slate.
+        for (String pkg : ALL_PACKAGES) {
+            assertFalse(pkg + " should not be installed", isPackageInstalled(pkg));
+            assertFalse(pkg + " should not be registered with overlay manager service",
+                    overlayManagerContainsPackage(pkg));
+        }
+    }
+
+    /*
+    For some reason, SuiteApkInstaller is *not* uninstalling overlays, even though #installPackage()
+    claims it will auto-clean.
+    TODO(b/72877546): Remove when auto-clean is fixed.
+     */
+    @After
+    public void uninstallOverlays() throws Exception {
+        for (String pkg : ALL_PACKAGES) {
+            uninstallPackage(pkg);
+        }
+    }
 
     @Test
     public void failToInstallNonPlatformSignedOverlay() throws Exception {
@@ -40,7 +74,7 @@
         } catch (Exception e) {
             // Expected.
         }
-        assertFalse(overlayManagerContainsPackage());
+        assertFalse(overlayManagerContainsPackage(SIG_OVERLAY_PACKAGE_NAME));
     }
 
     @Test
@@ -51,28 +85,64 @@
         } catch (Exception e) {
             // Expected.
         }
-        assertFalse(overlayManagerContainsPackage());
+        assertFalse(overlayManagerContainsPackage(SIG_OVERLAY_PACKAGE_NAME));
     }
 
     @Test
-    public void succeedToInstallPlatformSignedOverlay() throws Exception {
+    public void installPlatformSignedOverlay() throws Exception {
         installPackage("OverlayHostTests_PlatformSignatureOverlay.apk");
-        assertTrue(overlayManagerContainsPackage());
+        assertTrue(overlayManagerContainsPackage(SIG_OVERLAY_PACKAGE_NAME));
     }
 
     @Test
-    public void succeedToInstallPlatformSignedOverlayAndUpdate() throws Exception {
-        installPackage("OverlayHostTests_PlatformSignatureOverlay.apk");
-        assertTrue(overlayManagerContainsPackage());
-        assertEquals("v1", getDevice().getAppPackageInfo(OVERLAY_PACKAGE_NAME).getVersionName());
+    public void installPlatformSignedAppOverlayAndUpdate() throws Exception {
+        assertTrue(runDeviceTests(DEVICE_TEST_PKG, DEVICE_TEST_CLS, "expectAppResource"));
 
-        installPackage("OverlayHostTests_PlatformSignatureOverlayV2.apk");
-        assertTrue(overlayManagerContainsPackage());
-        assertEquals("v2", getDevice().getAppPackageInfo(OVERLAY_PACKAGE_NAME).getVersionName());
+        installPackage("OverlayHostTests_AppOverlayV1.apk");
+        setOverlayEnabled(APP_OVERLAY_PACKAGE_NAME, true);
+        assertTrue(overlayManagerContainsPackage(APP_OVERLAY_PACKAGE_NAME));
+        assertEquals("v1", getDevice()
+                .getAppPackageInfo(APP_OVERLAY_PACKAGE_NAME)
+                .getVersionName());
+        assertTrue(runDeviceTests(DEVICE_TEST_PKG, DEVICE_TEST_CLS,
+                "expectAppOverlayV1Resource"));
+
+        installPackage("OverlayHostTests_AppOverlayV2.apk");
+        assertTrue(overlayManagerContainsPackage(APP_OVERLAY_PACKAGE_NAME));
+        assertEquals("v2", getDevice()
+                .getAppPackageInfo(APP_OVERLAY_PACKAGE_NAME)
+                .getVersionName());
+        assertTrue(runDeviceTests(DEVICE_TEST_PKG, DEVICE_TEST_CLS,
+                "expectAppOverlayV2Resource"));
     }
 
-    private boolean overlayManagerContainsPackage() throws Exception {
-        return getDevice().executeShellCommand("cmd overlay list")
-                .contains(OVERLAY_PACKAGE_NAME);
+    @Test
+    public void installPlatformSignedFrameworkOverlayAndUpdate() throws Exception {
+        assertTrue(runDeviceTests(DEVICE_TEST_PKG, DEVICE_TEST_CLS, "expectAppResource"));
+
+        installPackage("OverlayHostTests_FrameworkOverlayV1.apk");
+        setOverlayEnabled(FRAMEWORK_OVERLAY_PACKAGE_NAME, true);
+        assertTrue(overlayManagerContainsPackage(FRAMEWORK_OVERLAY_PACKAGE_NAME));
+        assertEquals("v1", getDevice()
+                .getAppPackageInfo(FRAMEWORK_OVERLAY_PACKAGE_NAME)
+                .getVersionName());
+        assertTrue(runDeviceTests(DEVICE_TEST_PKG, DEVICE_TEST_CLS,
+                "expectFrameworkOverlayV1Resource"));
+
+        installPackage("OverlayHostTests_FrameworkOverlayV2.apk");
+        assertTrue(overlayManagerContainsPackage(FRAMEWORK_OVERLAY_PACKAGE_NAME));
+        assertEquals("v2", getDevice()
+                .getAppPackageInfo(FRAMEWORK_OVERLAY_PACKAGE_NAME)
+                .getVersionName());
+        assertTrue(runDeviceTests(DEVICE_TEST_PKG, DEVICE_TEST_CLS,
+                "expectFrameworkOverlayV2Resource"));
+    }
+
+    private void setOverlayEnabled(String pkg, boolean enabled) throws Exception {
+        getDevice().executeShellCommand("cmd overlay " + (enabled ? "enable " : "disable ") + pkg);
+    }
+
+    private boolean overlayManagerContainsPackage(String pkg) throws Exception {
+        return getDevice().executeShellCommand("cmd overlay list").contains(pkg);
     }
 }
diff --git a/core/tests/overlaytests/host/test-apps/SignatureOverlay/Android.mk b/core/tests/overlaytests/host/test-apps/SignatureOverlay/Android.mk
index b051a82..4249549 100644
--- a/core/tests/overlaytests/host/test-apps/SignatureOverlay/Android.mk
+++ b/core/tests/overlaytests/host/test-apps/SignatureOverlay/Android.mk
@@ -40,13 +40,4 @@
 LOCAL_AAPT_FLAGS += --version-code 1 --version-name v1
 include $(BUILD_PACKAGE)
 
-include $(CLEAR_VARS)
-LOCAL_MODULE_TAGS := tests
-LOCAL_PACKAGE_NAME := OverlayHostTests_PlatformSignatureOverlayV2
-LOCAL_COMPATIBILITY_SUITE := general-tests
-LOCAL_CERTIFICATE := platform
-LOCAL_AAPT_FLAGS := --custom-package $(my_package_prefix)_v2
-LOCAL_AAPT_FLAGS += --version-code 2 --version-name v2
-include $(BUILD_PACKAGE)
-
 my_package_prefix :=
diff --git a/core/tests/overlaytests/host/test-apps/UpdateOverlay/Android.mk b/core/tests/overlaytests/host/test-apps/UpdateOverlay/Android.mk
new file mode 100644
index 0000000..bd6d73d
--- /dev/null
+++ b/core/tests/overlaytests/host/test-apps/UpdateOverlay/Android.mk
@@ -0,0 +1,73 @@
+# Copyright (C) 2018 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.
+
+LOCAL_PATH := $(call my-dir)
+
+include $(CLEAR_VARS)
+LOCAL_MODULE_TAGS := tests
+LOCAL_SRC_FILES := $(call all-java-files-under,src)
+LOCAL_PACKAGE_NAME := OverlayHostTests_UpdateOverlay
+LOCAL_COMPATIBILITY_SUITE := general-tests
+LOCAL_STATIC_JAVA_LIBRARIES := android-support-test
+include $(BUILD_PACKAGE)
+
+my_package_prefix := com.android.server.om.hosttest.framework_overlay
+
+include $(CLEAR_VARS)
+LOCAL_MODULE_TAGS := tests
+LOCAL_PACKAGE_NAME := OverlayHostTests_FrameworkOverlayV1
+LOCAL_COMPATIBILITY_SUITE := general-tests
+LOCAL_CERTIFICATE := platform
+LOCAL_AAPT_FLAGS := --custom-package $(my_package_prefix)_v1
+LOCAL_AAPT_FLAGS += --version-code 1 --version-name v1
+LOCAL_RESOURCE_DIR := $(LOCAL_PATH)/framework/v1/res
+LOCAL_MANIFEST_FILE := framework/AndroidManifest.xml
+include $(BUILD_PACKAGE)
+
+include $(CLEAR_VARS)
+LOCAL_MODULE_TAGS := tests
+LOCAL_PACKAGE_NAME := OverlayHostTests_FrameworkOverlayV2
+LOCAL_COMPATIBILITY_SUITE := general-tests
+LOCAL_CERTIFICATE := platform
+LOCAL_AAPT_FLAGS := --custom-package $(my_package_prefix)_v2
+LOCAL_AAPT_FLAGS += --version-code 2 --version-name v2
+LOCAL_RESOURCE_DIR := $(LOCAL_PATH)/framework/v2/res
+LOCAL_MANIFEST_FILE := framework/AndroidManifest.xml
+include $(BUILD_PACKAGE)
+
+my_package_prefix := com.android.server.om.hosttest.app_overlay
+
+include $(CLEAR_VARS)
+LOCAL_MODULE_TAGS := tests
+LOCAL_PACKAGE_NAME := OverlayHostTests_AppOverlayV1
+LOCAL_COMPATIBILITY_SUITE := general-tests
+LOCAL_CERTIFICATE := platform
+LOCAL_AAPT_FLAGS := --custom-package $(my_package_prefix)_v1
+LOCAL_AAPT_FLAGS += --version-code 1 --version-name v1
+LOCAL_RESOURCE_DIR := $(LOCAL_PATH)/app/v1/res
+LOCAL_MANIFEST_FILE := app/AndroidManifest.xml
+include $(BUILD_PACKAGE)
+
+include $(CLEAR_VARS)
+LOCAL_MODULE_TAGS := tests
+LOCAL_PACKAGE_NAME := OverlayHostTests_AppOverlayV2
+LOCAL_COMPATIBILITY_SUITE := general-tests
+LOCAL_CERTIFICATE := platform
+LOCAL_AAPT_FLAGS := --custom-package $(my_package_prefix)_v2
+LOCAL_AAPT_FLAGS += --version-code 2 --version-name v2
+LOCAL_RESOURCE_DIR := $(LOCAL_PATH)/app/v2/res
+LOCAL_MANIFEST_FILE := app/AndroidManifest.xml
+include $(BUILD_PACKAGE)
+
+my_package_prefix :=
diff --git a/core/tests/overlaytests/host/test-apps/UpdateOverlay/AndroidManifest.xml b/core/tests/overlaytests/host/test-apps/UpdateOverlay/AndroidManifest.xml
new file mode 100644
index 0000000..06077a7
--- /dev/null
+++ b/core/tests/overlaytests/host/test-apps/UpdateOverlay/AndroidManifest.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2018 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.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.android.server.om.hosttest.update_overlay_test">
+
+    <application>
+        <uses-library android:name="android.test.runner" />
+    </application>
+
+    <instrumentation android:name="android.support.test.runner.AndroidJUnitRunner"
+        android:targetPackage="com.android.server.om.hosttest.update_overlay_test"
+        android:label="Update Overlay Test"/>
+</manifest>
diff --git a/core/tests/overlaytests/host/test-apps/UpdateOverlay/app/AndroidManifest.xml b/core/tests/overlaytests/host/test-apps/UpdateOverlay/app/AndroidManifest.xml
new file mode 100644
index 0000000..73804eb
--- /dev/null
+++ b/core/tests/overlaytests/host/test-apps/UpdateOverlay/app/AndroidManifest.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2018 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.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.android.server.om.hosttest.app_overlay">
+    <overlay android:targetPackage="com.android.server.om.hosttest.update_overlay_test" />
+</manifest>
diff --git a/core/tests/overlaytests/host/test-apps/UpdateOverlay/app/v1/res/values/values.xml b/core/tests/overlaytests/host/test-apps/UpdateOverlay/app/v1/res/values/values.xml
new file mode 100644
index 0000000..63f85c2
--- /dev/null
+++ b/core/tests/overlaytests/host/test-apps/UpdateOverlay/app/v1/res/values/values.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2018 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.
+-->
+
+<resources>
+    <string name="app_resource">App Resource Overlay V1</string>
+</resources>
diff --git a/core/tests/overlaytests/host/test-apps/UpdateOverlay/app/v2/res/values/values.xml b/core/tests/overlaytests/host/test-apps/UpdateOverlay/app/v2/res/values/values.xml
new file mode 100644
index 0000000..fa4a697
--- /dev/null
+++ b/core/tests/overlaytests/host/test-apps/UpdateOverlay/app/v2/res/values/values.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2018 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.
+-->
+
+<resources>
+    <string name="app_resource">App Resource Overlay V2</string>
+</resources>
diff --git a/core/tests/overlaytests/host/test-apps/UpdateOverlay/framework/AndroidManifest.xml b/core/tests/overlaytests/host/test-apps/UpdateOverlay/framework/AndroidManifest.xml
new file mode 100644
index 0000000..8c8fe94
--- /dev/null
+++ b/core/tests/overlaytests/host/test-apps/UpdateOverlay/framework/AndroidManifest.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2018 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.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.android.server.om.hosttest.framework_overlay">
+    <overlay android:targetPackage="android" />
+</manifest>
diff --git a/core/tests/overlaytests/host/test-apps/UpdateOverlay/framework/v1/res/values/values.xml b/core/tests/overlaytests/host/test-apps/UpdateOverlay/framework/v1/res/values/values.xml
new file mode 100644
index 0000000..fedb2c6
--- /dev/null
+++ b/core/tests/overlaytests/host/test-apps/UpdateOverlay/framework/v1/res/values/values.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2018 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.
+-->
+
+<resources>
+    <string name="ok">Framework Overlay V1</string>
+</resources>
diff --git a/core/tests/overlaytests/host/test-apps/UpdateOverlay/framework/v2/res/values/values.xml b/core/tests/overlaytests/host/test-apps/UpdateOverlay/framework/v2/res/values/values.xml
new file mode 100644
index 0000000..8aebf483
--- /dev/null
+++ b/core/tests/overlaytests/host/test-apps/UpdateOverlay/framework/v2/res/values/values.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2018 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.
+-->
+
+<resources>
+    <string name="ok">Framework Overlay V2</string>
+</resources>
diff --git a/core/tests/overlaytests/host/test-apps/UpdateOverlay/res/values/values.xml b/core/tests/overlaytests/host/test-apps/UpdateOverlay/res/values/values.xml
new file mode 100644
index 0000000..7393166
--- /dev/null
+++ b/core/tests/overlaytests/host/test-apps/UpdateOverlay/res/values/values.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2018 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.
+-->
+
+<resources>
+    <string name="app_resource">App Resource</string>
+</resources>
diff --git a/core/tests/overlaytests/host/test-apps/UpdateOverlay/src/com/android/server/om/hosttest/update_overlay_test/UpdateOverlayTest.java b/core/tests/overlaytests/host/test-apps/UpdateOverlay/src/com/android/server/om/hosttest/update_overlay_test/UpdateOverlayTest.java
new file mode 100644
index 0000000..d46bb37
--- /dev/null
+++ b/core/tests/overlaytests/host/test-apps/UpdateOverlay/src/com/android/server/om/hosttest/update_overlay_test/UpdateOverlayTest.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy
+ * of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+package com.android.server.om.hosttest.update_overlay_test;
+
+import static org.junit.Assert.assertEquals;
+
+import android.content.res.Configuration;
+import android.content.res.Resources;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.Locale;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class UpdateOverlayTest {
+    private Resources mResources;
+
+    @Before
+    public void setUp() throws Exception {
+        final Configuration defaultLocaleConfiguration = new Configuration();
+        defaultLocaleConfiguration.setLocale(Locale.US);
+        mResources = InstrumentationRegistry
+                .getInstrumentation()
+                .getContext()
+                .createConfigurationContext(defaultLocaleConfiguration)
+                .getResources();
+    }
+
+    @Test
+    public void expectAppResource() throws Exception {
+        assertEquals("App Resource", mResources.getString(R.string.app_resource));
+    }
+
+    @Test
+    public void expectAppOverlayV1Resource() throws Exception {
+        assertEquals("App Resource Overlay V1", mResources.getString(R.string.app_resource));
+    }
+
+    @Test
+    public void expectAppOverlayV2Resource() throws Exception {
+        assertEquals("App Resource Overlay V2", mResources.getString(R.string.app_resource));
+    }
+
+    @Test
+    public void expectFrameworkOverlayResource() throws Exception {
+        assertEquals("OK", mResources.getString(android.R.string.ok));
+    }
+
+    @Test
+    public void expectFrameworkOverlayV1Resource() throws Exception {
+        assertEquals("Framework Overlay V1", mResources.getString(android.R.string.ok));
+    }
+
+    @Test
+    public void expectFrameworkOverlayV2Resource() throws Exception {
+        assertEquals("Framework Overlay V2", mResources.getString(android.R.string.ok));
+    }
+}
diff --git a/graphics/java/android/graphics/Typeface.java b/graphics/java/android/graphics/Typeface.java
index 04c5295..f41267e 100644
--- a/graphics/java/android/graphics/Typeface.java
+++ b/graphics/java/android/graphics/Typeface.java
@@ -60,6 +60,7 @@
 import java.io.IOException;
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
+import java.io.InputStream;
 import java.nio.ByteBuffer;
 import java.nio.channels.FileChannel;
 import java.util.Arrays;
@@ -801,36 +802,18 @@
      * @return The new typeface.
      */
     public static Typeface createFromAsset(AssetManager mgr, String path) {
-        if (path == null) {
-            throw new NullPointerException();  // for backward compatibility
-        }
-        synchronized (sDynamicCacheLock) {
-            Typeface typeface = new Builder(mgr, path).build();
-            if (typeface != null) return typeface;
+        Preconditions.checkNotNull(path); // for backward compatibility
+        Preconditions.checkNotNull(mgr);
 
-            final String key = Builder.createAssetUid(mgr, path, 0 /* ttcIndex */,
-                    null /* axes */, RESOLVE_BY_FONT_TABLE, RESOLVE_BY_FONT_TABLE,
-                    DEFAULT_FAMILY);
-            typeface = sDynamicTypefaceCache.get(key);
-            if (typeface != null) return typeface;
-
-            final FontFamily fontFamily = new FontFamily();
-            if (fontFamily.addFontFromAssetManager(mgr, path, 0, true /* isAsset */,
-                    0 /* ttc index */, RESOLVE_BY_FONT_TABLE, RESOLVE_BY_FONT_TABLE,
-                    null /* axes */)) {
-                if (!fontFamily.freeze()) {
-                    return Typeface.DEFAULT;
-                }
-                final FontFamily[] families = { fontFamily };
-                typeface = createFromFamiliesWithDefault(families, DEFAULT_FAMILY,
-                        RESOLVE_BY_FONT_TABLE, RESOLVE_BY_FONT_TABLE);
-                sDynamicTypefaceCache.put(key, typeface);
-                return typeface;
-            } else {
-                fontFamily.abortCreation();
-            }
+        Typeface typeface = new Builder(mgr, path).build();
+        if (typeface != null) return typeface;
+        // check if the file exists, and throw an exception for backward compatibility
+        try (InputStream inputStream = mgr.open(path)) {
+        } catch (IOException e) {
+            throw new RuntimeException("Font asset not found " + path);
         }
-        throw new RuntimeException("Font asset not found " + path);
+
+        return Typeface.DEFAULT;
     }
 
     /**
@@ -848,13 +831,22 @@
     /**
      * Create a new typeface from the specified font file.
      *
-     * @param path The path to the font data.
+     * @param file The path to the font data.
      * @return The new typeface.
      */
-    public static Typeface createFromFile(@Nullable File path) {
+    public static Typeface createFromFile(@Nullable File file) {
         // For the compatibility reasons, leaving possible NPE here.
         // See android.graphics.cts.TypefaceTest#testCreateFromFileByFileReferenceNull
-        return createFromFile(path.getAbsolutePath());
+
+        Typeface typeface = new Builder(file).build();
+        if (typeface != null) return typeface;
+
+        // check if the file exists, and throw an exception for backward compatibility
+        if (!file.exists()) {
+            throw new RuntimeException("Font asset not found " + file.getAbsolutePath());
+        }
+
+        return Typeface.DEFAULT;
     }
 
     /**
@@ -864,19 +856,8 @@
      * @return The new typeface.
      */
     public static Typeface createFromFile(@Nullable String path) {
-        final FontFamily fontFamily = new FontFamily();
-        if (fontFamily.addFont(path, 0 /* ttcIndex */, null /* axes */,
-                  RESOLVE_BY_FONT_TABLE, RESOLVE_BY_FONT_TABLE)) {
-            if (!fontFamily.freeze()) {
-                return Typeface.DEFAULT;
-            }
-            FontFamily[] families = { fontFamily };
-            return createFromFamiliesWithDefault(families, DEFAULT_FAMILY,
-                    RESOLVE_BY_FONT_TABLE, RESOLVE_BY_FONT_TABLE);
-        } else {
-            fontFamily.abortCreation();
-        }
-        throw new RuntimeException("Font not found " + path);
+        Preconditions.checkNotNull(path); // for backward compatibility
+        return createFromFile(new File(path));
     }
 
     /**
diff --git a/location/java/android/location/LocationManager.java b/location/java/android/location/LocationManager.java
index c33dce1..d3c6edd 100644
--- a/location/java/android/location/LocationManager.java
+++ b/location/java/android/location/LocationManager.java
@@ -236,6 +236,62 @@
      */
     public static final String GNSS_HARDWARE_MODEL_NAME_UNKNOWN = "Model Name Unknown";
 
+    /**
+     * Broadcast intent action for Settings app to inject a footer at the bottom of location
+     * settings.
+     *
+     * <p>This broadcast is used for two things:
+     * <ol>
+     *     <li>For receivers to inject a footer with provided text. This is for use only by apps
+     *         that are included in the system image. </li>
+     *     <li>For receivers to know their footer is injected under location settings.</li>
+     * </ol>
+     *
+     * <p>To inject a footer to location settings, you must declare a broadcast receiver of
+     * {@link LocationManager#SETTINGS_FOOTER_DISPLAYED_ACTION} in the manifest as so:
+     * <pre>
+     *     &lt;receiver android:name="com.example.android.footer.MyFooterInjector"&gt;
+     *         &lt;intent-filter&gt;
+     *             &lt;action android:name="com.android.settings.location.INJECT_FOOTER" /&gt;
+     *         &lt;/intent-filter&gt;
+     *         &lt;meta-data
+     *             android:name="com.android.settings.location.FOOTER_STRING"
+     *             android:resource="@string/my_injected_footer_string" /&gt;
+     *     &lt;/receiver&gt;
+     * </pre>
+     *
+     * <p>On entering location settings, Settings app will send a
+     * {@link #SETTINGS_FOOTER_DISPLAYED_ACTION} broadcast to receivers whose footer is successfully
+     * injected. On leaving location settings, the footer becomes not visible to users. Settings app
+     * will send a {@link #SETTINGS_FOOTER_REMOVED_ACTION} broadcast to those receivers.
+     *
+     * @hide
+     */
+    public static final String SETTINGS_FOOTER_DISPLAYED_ACTION =
+            "com.android.settings.location.DISPLAYED_FOOTER";
+
+    /**
+     * Broadcast intent action when location settings footer is not visible to users.
+     *
+     * <p>See {@link #SETTINGS_FOOTER_DISPLAYED_ACTION} for more detail on how to use.
+     *
+     * @hide
+     */
+    public static final String SETTINGS_FOOTER_REMOVED_ACTION =
+            "com.android.settings.location.REMOVED_FOOTER";
+
+    /**
+     * Metadata name for {@link LocationManager#SETTINGS_FOOTER_DISPLAYED_ACTION} broadcast
+     * receivers to specify a string resource id as location settings footer text. This is for use
+     * only by apps that are included in the system image.
+     *
+     * <p>See {@link #SETTINGS_FOOTER_DISPLAYED_ACTION} for more detail on how to use.
+     *
+     * @hide
+     */
+    public static final String METADATA_SETTINGS_FOOTER_STRING =
+            "com.android.settings.location.FOOTER_STRING";
+
     // Map from LocationListeners to their associated ListenerTransport objects
     private HashMap<LocationListener,ListenerTransport> mListeners =
         new HashMap<LocationListener,ListenerTransport>();
diff --git a/media/java/android/media/AudioFocusInfo.java b/media/java/android/media/AudioFocusInfo.java
index 5d0c8e2..5467a69 100644
--- a/media/java/android/media/AudioFocusInfo.java
+++ b/media/java/android/media/AudioFocusInfo.java
@@ -38,6 +38,10 @@
     private int mLossReceived;
     private int mFlags;
 
+    // generation count for the validity of a request/response async exchange between
+    // external focus policy and MediaFocusControl
+    private long mGenCount = -1;
+
 
     /**
      * Class constructor
@@ -61,6 +65,16 @@
         mSdkTarget = sdk;
     }
 
+    /** @hide */
+    public void setGen(long g) {
+        mGenCount = g;
+    }
+
+    /** @hide */
+    public long getGen() {
+        return mGenCount;
+    }
+
 
     /**
      * The audio attributes for the audio focus request.
@@ -128,6 +142,7 @@
         dest.writeInt(mLossReceived);
         dest.writeInt(mFlags);
         dest.writeInt(mSdkTarget);
+        dest.writeLong(mGenCount);
     }
 
     @Override
@@ -168,6 +183,8 @@
         if (mSdkTarget != other.mSdkTarget) {
             return false;
         }
+        // mGenCount is not used to verify equality between two focus holds as multiple requests
+        // (hence of different generations) could correspond to the same hold
         return true;
     }
 
@@ -175,7 +192,7 @@
             = new Parcelable.Creator<AudioFocusInfo>() {
 
         public AudioFocusInfo createFromParcel(Parcel in) {
-            return new AudioFocusInfo(
+            final AudioFocusInfo afi = new AudioFocusInfo(
                     AudioAttributes.CREATOR.createFromParcel(in), //AudioAttributes aa
                     in.readInt(), // int clientUid
                     in.readString(), //String clientId
@@ -185,6 +202,8 @@
                     in.readInt(), //int flags
                     in.readInt()  //int sdkTarget
                     );
+            afi.setGen(in.readLong());
+            return afi;
         }
 
         public AudioFocusInfo[] newArray(int size) {
diff --git a/media/java/android/media/AudioManager.java b/media/java/android/media/AudioManager.java
index bf51d97..0be54ec 100644
--- a/media/java/android/media/AudioManager.java
+++ b/media/java/android/media/AudioManager.java
@@ -32,6 +32,7 @@
 import android.content.Context;
 import android.content.Intent;
 import android.media.audiopolicy.AudioPolicy;
+import android.media.audiopolicy.AudioPolicy.AudioPolicyFocusListener;
 import android.media.session.MediaController;
 import android.media.session.MediaSession;
 import android.media.session.MediaSessionLegacyHelper;
@@ -54,10 +55,13 @@
 import android.util.Slog;
 import android.view.KeyEvent;
 
+import com.android.internal.annotations.GuardedBy;
+
 import java.io.IOException;
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
 import java.util.ArrayList;
+import java.util.HashMap;
 import java.util.Iterator;
 import java.util.List;
 import java.util.concurrent.ConcurrentHashMap;
@@ -2338,6 +2342,20 @@
                 }
             }
         }
+
+        @Override
+        public void dispatchFocusResultFromExtPolicy(int requestResult, String clientId) {
+            synchronized (mFocusRequestsLock) {
+                // TODO use generation counter as the key instead
+                final BlockingFocusResultReceiver focusReceiver =
+                        mFocusRequestsAwaitingResult.remove(clientId);
+                if (focusReceiver != null) {
+                    focusReceiver.notifyResult(requestResult);
+                } else {
+                    Log.e(TAG, "dispatchFocusResultFromExtPolicy found no result receiver");
+                }
+            }
+        }
     };
 
     private String getIdForAudioFocusListener(OnAudioFocusChangeListener l) {
@@ -2390,6 +2408,40 @@
       */
     public static final int AUDIOFOCUS_REQUEST_DELAYED = 2;
 
+    /** @hide */
+    @IntDef(flag = false, prefix = "AUDIOFOCUS_REQUEST", value = {
+            AUDIOFOCUS_REQUEST_FAILED,
+            AUDIOFOCUS_REQUEST_GRANTED,
+            AUDIOFOCUS_REQUEST_DELAYED }
+    )
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface FocusRequestResult {}
+
+    /**
+     * @hide
+     * code returned when a synchronous focus request on the client-side is to be blocked
+     * until the external audio focus policy decides on the response for the client
+     */
+    public static final int AUDIOFOCUS_REQUEST_WAITING_FOR_EXT_POLICY = 100;
+
+    /**
+     * Timeout duration in ms when waiting on an external focus policy for the result for a
+     * focus request
+     */
+    private static final int EXT_FOCUS_POLICY_TIMEOUT_MS = 200;
+
+    private static final String FOCUS_CLIENT_ID_STRING = "android_audio_focus_client_id";
+
+    private final Object mFocusRequestsLock = new Object();
+    /**
+     * Map of all receivers of focus request results, one per unresolved focus request.
+     * Receivers are added before sending the request to the external focus policy,
+     * and are removed either after receiving the result, or after the timeout.
+     * This variable is lazily initialized.
+     */
+    @GuardedBy("mFocusRequestsLock")
+    private HashMap<String, BlockingFocusResultReceiver> mFocusRequestsAwaitingResult;
+
 
     /**
      *  Request audio focus.
@@ -2656,18 +2708,100 @@
             // some tests don't have a Context
             sdk = Build.VERSION.SDK_INT;
         }
-        try {
-            status = service.requestAudioFocus(afr.getAudioAttributes(),
-                    afr.getFocusGain(), mICallBack,
-                    mAudioFocusDispatcher,
-                    getIdForAudioFocusListener(afr.getOnAudioFocusChangeListener()),
-                    getContext().getOpPackageName() /* package name */, afr.getFlags(),
-                    ap != null ? ap.cb() : null,
-                    sdk);
-        } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
+
+        final String clientId = getIdForAudioFocusListener(afr.getOnAudioFocusChangeListener());
+        final BlockingFocusResultReceiver focusReceiver;
+        synchronized (mFocusRequestsLock) {
+            try {
+                // TODO status contains result and generation counter for ext policy
+                status = service.requestAudioFocus(afr.getAudioAttributes(),
+                        afr.getFocusGain(), mICallBack,
+                        mAudioFocusDispatcher,
+                        clientId,
+                        getContext().getOpPackageName() /* package name */, afr.getFlags(),
+                        ap != null ? ap.cb() : null,
+                        sdk);
+            } catch (RemoteException e) {
+                throw e.rethrowFromSystemServer();
+            }
+            if (status != AudioManager.AUDIOFOCUS_REQUEST_WAITING_FOR_EXT_POLICY) {
+                // default path with no external focus policy
+                return status;
+            }
+            if (mFocusRequestsAwaitingResult == null) {
+                mFocusRequestsAwaitingResult =
+                        new HashMap<String, BlockingFocusResultReceiver>(1);
+            }
+            focusReceiver = new BlockingFocusResultReceiver(clientId);
+            mFocusRequestsAwaitingResult.put(clientId, focusReceiver);
         }
-        return status;
+        focusReceiver.waitForResult(EXT_FOCUS_POLICY_TIMEOUT_MS);
+        if (DEBUG && !focusReceiver.receivedResult()) {
+            Log.e(TAG, "requestAudio response from ext policy timed out, denying request");
+        }
+        synchronized (mFocusRequestsLock) {
+            mFocusRequestsAwaitingResult.remove(clientId);
+        }
+        return focusReceiver.requestResult();
+    }
+
+    // helper class that abstracts out the handling of spurious wakeups in Object.wait()
+    private static final class SafeWaitObject {
+        private boolean mQuit = false;
+
+        public void safeNotify() {
+            synchronized (this) {
+                mQuit = true;
+                this.notify();
+            }
+        }
+
+        public void safeWait(long millis) throws InterruptedException {
+            final long timeOutTime = java.lang.System.currentTimeMillis() + millis;
+            synchronized (this) {
+                while (!mQuit) {
+                    final long timeToWait = timeOutTime - java.lang.System.currentTimeMillis();
+                    if (timeToWait < 0) { break; }
+                    this.wait(timeToWait);
+                }
+            }
+        }
+    }
+
+    private static final class BlockingFocusResultReceiver {
+        private final SafeWaitObject mLock = new SafeWaitObject();
+        @GuardedBy("mLock")
+        private boolean mResultReceived = false;
+        // request denied by default (e.g. timeout)
+        private int mFocusRequestResult = AudioManager.AUDIOFOCUS_REQUEST_FAILED;
+        private final String mFocusClientId;
+
+        BlockingFocusResultReceiver(String clientId) {
+            mFocusClientId = clientId;
+        }
+
+        boolean receivedResult() { return mResultReceived; }
+        int requestResult() { return mFocusRequestResult; }
+
+        void notifyResult(int requestResult) {
+            synchronized (mLock) {
+                mResultReceived = true;
+                mFocusRequestResult = requestResult;
+                mLock.safeNotify();
+            }
+        }
+
+        public void waitForResult(long timeOutMs) {
+            synchronized (mLock) {
+                if (mResultReceived) {
+                    // the result was received before waiting
+                    return;
+                }
+                try {
+                    mLock.safeWait(timeOutMs);
+                } catch (InterruptedException e) { }
+            }
+        }
     }
 
     /**
@@ -2714,6 +2848,32 @@
 
     /**
      * @hide
+     * Set the result to the audio focus request received through
+     * {@link AudioPolicyFocusListener#onAudioFocusRequest(AudioFocusInfo, int)}.
+     * @param afi the information about the focus requester
+     * @param requestResult the result to the focus request to be passed to the requester
+     * @param ap a valid registered {@link AudioPolicy} configured as a focus policy.
+     */
+    @SystemApi
+    @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING)
+    public void setFocusRequestResult(@NonNull AudioFocusInfo afi,
+            @FocusRequestResult int requestResult, @NonNull AudioPolicy ap) {
+        if (afi == null) {
+            throw new IllegalArgumentException("Illegal null AudioFocusInfo");
+        }
+        if (ap == null) {
+            throw new IllegalArgumentException("Illegal null AudioPolicy");
+        }
+        final IAudioService service = getService();
+        try {
+            service.setFocusRequestResultFromExtPolicy(afi, requestResult, ap.cb());
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * @hide
      * Notifies an application with a focus listener of gain or loss of audio focus.
      * This method can only be used by owners of an {@link AudioPolicy} configured with
      * {@link AudioPolicy.Builder#setIsAudioFocusPolicy(boolean)} set to true.
diff --git a/media/java/android/media/IAudioFocusDispatcher.aidl b/media/java/android/media/IAudioFocusDispatcher.aidl
index 09575f7..3b33c5b 100644
--- a/media/java/android/media/IAudioFocusDispatcher.aidl
+++ b/media/java/android/media/IAudioFocusDispatcher.aidl
@@ -25,4 +25,6 @@
 
     void dispatchAudioFocusChange(int focusChange, String clientId);
 
+    void dispatchFocusResultFromExtPolicy(int requestResult, String clientId);
+
 }
diff --git a/media/java/android/media/IAudioService.aidl b/media/java/android/media/IAudioService.aidl
index 88d0a60..cd4143c 100644
--- a/media/java/android/media/IAudioService.aidl
+++ b/media/java/android/media/IAudioService.aidl
@@ -207,5 +207,8 @@
     int setBluetoothA2dpDeviceConnectionStateSuppressNoisyIntent(in BluetoothDevice device,
             int state, int profile, boolean suppressNoisyIntent);
 
+    oneway void setFocusRequestResultFromExtPolicy(in AudioFocusInfo afi, int requestResult,
+            in IAudioPolicyCallback pcb);
+
     // WARNING: read warning at top of file, it is recommended to add new methods at the end
 }
diff --git a/media/java/android/media/audiopolicy/AudioPolicy.java b/media/java/android/media/audiopolicy/AudioPolicy.java
index 4de731a..2190635 100644
--- a/media/java/android/media/audiopolicy/AudioPolicy.java
+++ b/media/java/android/media/audiopolicy/AudioPolicy.java
@@ -463,9 +463,9 @@
          * Only ever called if the {@link AudioPolicy} was built with
          * {@link AudioPolicy.Builder#setIsAudioFocusPolicy(boolean)} set to {@code true}.
          * @param afi information about the focus request and the requester
-         * @param requestResult the result that was returned synchronously by the framework to the
-         *     application, {@link #AUDIOFOCUS_REQUEST_FAILED},or
-         *     {@link #AUDIOFOCUS_REQUEST_DELAYED}.
+         * @param requestResult deprecated after the addition of
+         *     {@link AudioManager#setFocusRequestResult(AudioFocusInfo, int, AudioPolicy)}
+         *     in Android P, always equal to {@link #AUDIOFOCUS_REQUEST_GRANTED}.
          */
         public void onAudioFocusRequest(AudioFocusInfo afi, int requestResult) {}
         /**
@@ -534,7 +534,7 @@
             sendMsg(MSG_FOCUS_REQUEST, afi, requestResult);
             if (DEBUG) {
                 Log.v(TAG, "notifyAudioFocusRequest: pack=" + afi.getPackageName() + " client="
-                        + afi.getClientId() + "reqRes=" + requestResult);
+                        + afi.getClientId() + " gen=" + afi.getGen());
             }
         }
 
diff --git a/packages/SettingsLib/common.mk b/packages/SettingsLib/common.mk
index 741db34..3c2ca2d 100644
--- a/packages/SettingsLib/common.mk
+++ b/packages/SettingsLib/common.mk
@@ -16,11 +16,11 @@
 ifeq ($(LOCAL_USE_AAPT2),true)
 LOCAL_STATIC_JAVA_LIBRARIES += \
     android-support-annotations \
-    android-arch-lifecycle-common
+    apptoolkit-lifecycle-common
 
 LOCAL_STATIC_ANDROID_LIBRARIES += \
     android-support-v4 \
-    android-arch-lifecycle-runtime \
+    apptoolkit-lifecycle-runtime \
     android-support-v7-recyclerview \
     android-support-v7-preference \
     android-support-v7-appcompat \
diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java
index adb4dbf..85a579d 100644
--- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java
+++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java
@@ -3017,7 +3017,7 @@
         }
 
         private final class UpgradeController {
-            private static final int SETTINGS_VERSION = 153;
+            private static final int SETTINGS_VERSION = 154;
 
             private final int mUserId;
 
@@ -3627,6 +3627,23 @@
                     currentVersion = 153;
                 }
 
+                if (currentVersion == 153) {
+                    // Version 154: Read notification badge configuration from config.
+                    // If user has already set the value, don't do anything.
+                    final SettingsState systemSecureSettings = getSecureSettingsLocked(userId);
+                    final Setting showNotificationBadges = systemSecureSettings.getSettingLocked(
+                            Settings.Secure.NOTIFICATION_BADGING);
+                    if (showNotificationBadges.isNull()) {
+                        final boolean defaultValue = getContext().getResources().getBoolean(
+                                com.android.internal.R.bool.config_notificationBadging);
+                        systemSecureSettings.insertSettingLocked(
+                                Secure.NOTIFICATION_BADGING,
+                                defaultValue ? "1" : "0",
+                                null, true, SettingsState.SYSTEM_PACKAGE_NAME);
+                    }
+                    currentVersion = 154;
+                }
+
                 // vXXX: Add new settings above this point.
 
                 if (currentVersion != newVersion) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusIconContainer.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusIconContainer.java
index 1897171..1da50ad 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusIconContainer.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusIconContainer.java
@@ -88,7 +88,8 @@
      * Layout is happening from end -> start
      */
     private void calculateIconTranslations() {
-        float translationX = getWidth();
+        float width = getWidth();
+        float translationX = width;
         float contentStart = getPaddingStart();
         int childCount = getChildCount();
         // Underflow === don't show content until that index
@@ -133,6 +134,15 @@
                 }
             }
         }
+
+        // Stole this from NotificationIconContainer. Not optimal but keeps the layout logic clean
+        if (isLayoutRtl()) {
+            for (int i = 0; i < childCount; i++) {
+                View child = getChildAt(i);
+                ViewState state = getViewStateFromChild(child);
+                state.xTranslation = width - state.xTranslation - child.getWidth();
+            }
+        }
     }
 
     private void applyIconStates() {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/NonPhoneDependencyTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/NonPhoneDependencyTest.java
index 5f763a4..1204dda 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/NonPhoneDependencyTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/NonPhoneDependencyTest.java
@@ -86,13 +86,13 @@
                 mHeadsUpManager);
         gutsManager.setUpWithPresenter(mPresenter, entryManager, mListContainer,
                 mCheckSaveListener, mOnClickListener);
-        notificationListener.setUpWithPresenter(mPresenter, entryManager);
         notificationLogger.setUpWithEntryManager(entryManager, mListContainer);
         mediaManager.setUpWithPresenter(mPresenter, entryManager);
         remoteInputManager.setUpWithPresenter(mPresenter, entryManager, mRemoteInputManagerCallback,
                 mDelegate);
         lockscreenUserManager.setUpWithPresenter(mPresenter, entryManager);
         viewHierarchyManager.setUpWithPresenter(mPresenter, entryManager, mListContainer);
+        notificationListener.setUpWithPresenter(mPresenter, entryManager);
 
         assertFalse(mDependency.hasInstantiatedDependency(StatusBarWindowManager.class));
     }
diff --git a/services/autofill/java/com/android/server/autofill/AutofillManagerServiceImpl.java b/services/autofill/java/com/android/server/autofill/AutofillManagerServiceImpl.java
index 297dcf1..989a7b5 100644
--- a/services/autofill/java/com/android/server/autofill/AutofillManagerServiceImpl.java
+++ b/services/autofill/java/com/android/server/autofill/AutofillManagerServiceImpl.java
@@ -861,8 +861,13 @@
 
         pw.print(prefix); pw.print("User: "); pw.println(mUserId);
         pw.print(prefix); pw.print("UID: "); pw.println(getServiceUidLocked());
-        pw.print(prefix); pw.print("Component: "); pw.println(mInfo != null
-                ? mInfo.getServiceInfo().getComponentName() : null);
+        pw.print(prefix); pw.print("Autofill Service Info: ");
+        if (mInfo == null) {
+            pw.println("N/A");
+        } else {
+            pw.println();
+            mInfo.dump(prefix2, pw);
+        }
         pw.print(prefix); pw.print("Component from settings: ");
             pw.println(getComponentNameFromSettings());
         pw.print(prefix); pw.print("Default component: ");
@@ -870,6 +875,7 @@
         pw.print(prefix); pw.print("Disabled: "); pw.println(mDisabled);
         pw.print(prefix); pw.print("Field classification enabled: ");
             pw.println(isFieldClassificationEnabledLocked());
+        pw.print(prefix); pw.print("Compat pkgs: "); pw.println(getWhitelistedCompatModePackages());
         pw.print(prefix); pw.print("Setup complete: "); pw.println(mSetupComplete);
         pw.print(prefix); pw.print("Last prune: "); pw.println(mLastPrune);
 
@@ -996,14 +1002,18 @@
         }
         if (!Build.IS_ENG) {
             // TODO: Build a map and watch for settings changes (this is called on app start)
-            final String whiteListedPackages = Settings.Global.getString(
-                    mContext.getContentResolver(),
-                    Settings.Global.AUTOFILL_COMPAT_ALLOWED_PACKAGES);
+            final String whiteListedPackages = getWhitelistedCompatModePackages();
             return whiteListedPackages != null && whiteListedPackages.contains(packageName);
         }
         return true;
     }
 
+    private String getWhitelistedCompatModePackages() {
+        return Settings.Global.getString(
+                mContext.getContentResolver(),
+                Settings.Global.AUTOFILL_COMPAT_ALLOWED_PACKAGES);
+    }
+
     private void sendStateToClients(boolean resetClient) {
         final RemoteCallbackList<IAutoFillManagerClient> clients;
         final int userClientCount;
diff --git a/services/core/java/com/android/server/AlarmManagerService.java b/services/core/java/com/android/server/AlarmManagerService.java
index f49cd67..b4af432 100644
--- a/services/core/java/com/android/server/AlarmManagerService.java
+++ b/services/core/java/com/android/server/AlarmManagerService.java
@@ -58,6 +58,7 @@
 import android.text.TextUtils;
 import android.text.format.DateFormat;
 import android.util.ArrayMap;
+import android.util.ArraySet;
 import android.util.KeyValueListParser;
 import android.util.Log;
 import android.util.Pair;
@@ -79,7 +80,6 @@
 import java.util.Comparator;
 import java.util.Date;
 import java.util.HashMap;
-import java.util.HashSet;
 import java.util.LinkedList;
 import java.util.Locale;
 import java.util.Random;
@@ -224,10 +224,12 @@
 
     interface Stats {
         int REBATCH_ALL_ALARMS = 0;
+        int REORDER_ALARMS_FOR_STANDBY = 1;
     }
 
     private final StatLogger mStatLogger = new StatLogger(new String[] {
             "REBATCH_ALL_ALARMS",
+            "REORDER_ALARMS_FOR_STANDBY",
     });
 
     /**
@@ -522,6 +524,10 @@
             return newStart;
         }
 
+        boolean remove(Alarm alarm) {
+            return remove(a -> (a == alarm));
+        }
+
         boolean remove(Predicate<Alarm> predicate) {
             boolean didRemove = false;
             long newStart = 0;  // recalculate endpoints as we go
@@ -741,6 +747,23 @@
         return (index == 0);
     }
 
+    private void insertAndBatchAlarmLocked(Alarm alarm) {
+        final int whichBatch = ((alarm.flags & AlarmManager.FLAG_STANDALONE) != 0) ? -1
+                : attemptCoalesceLocked(alarm.whenElapsed, alarm.maxWhenElapsed);
+
+        if (whichBatch < 0) {
+            addBatchLocked(mAlarmBatches, new Batch(alarm));
+        } else {
+            final Batch batch = mAlarmBatches.get(whichBatch);
+            if (batch.add(alarm)) {
+                // The start time of this batch advanced, so batch ordering may
+                // have just been broken.  Move it to where it now belongs.
+                mAlarmBatches.remove(whichBatch);
+                addBatchLocked(mAlarmBatches, batch);
+            }
+        }
+    }
+
     // Return the index of the matching batch, or -1 if none found.
     int attemptCoalesceLocked(long whenElapsed, long maxWhen) {
         final int N = mAlarmBatches.size();
@@ -794,7 +817,7 @@
     }
 
     void rebatchAllAlarmsLocked(boolean doValidate) {
-        long start = mStatLogger.getTime();
+        final long start = mStatLogger.getTime();
         final int oldCount =
                 getAlarmCount(mAlarmBatches) + ArrayUtils.size(mPendingWhileIdleAlarms);
         final boolean oldHasTick = haveBatchesTimeTickAlarm(mAlarmBatches)
@@ -837,6 +860,44 @@
         mStatLogger.logDurationStat(Stats.REBATCH_ALL_ALARMS, start);
     }
 
+    /**
+     * Re-orders the alarm batches based on newly evaluated send times based on the current
+     * app-standby buckets
+     * @param targetPackages [Package, User] pairs for which alarms need to be re-evaluated,
+     *                       null indicates all
+     * @return True if there was any reordering done to the current list.
+     */
+    boolean reorderAlarmsBasedOnStandbyBuckets(ArraySet<Pair<String, Integer>> targetPackages) {
+        final long start = mStatLogger.getTime();
+        final ArrayList<Alarm> rescheduledAlarms = new ArrayList<>();
+
+        for (int batchIndex = mAlarmBatches.size() - 1; batchIndex >= 0; batchIndex--) {
+            final Batch batch = mAlarmBatches.get(batchIndex);
+            for (int alarmIndex = batch.size() - 1; alarmIndex >= 0; alarmIndex--) {
+                final Alarm alarm = batch.get(alarmIndex);
+                final Pair<String, Integer> packageUser =
+                        Pair.create(alarm.sourcePackage, UserHandle.getUserId(alarm.creatorUid));
+                if (targetPackages != null && !targetPackages.contains(packageUser)) {
+                    continue;
+                }
+                if (adjustDeliveryTimeBasedOnStandbyBucketLocked(alarm)) {
+                    batch.remove(alarm);
+                    rescheduledAlarms.add(alarm);
+                }
+            }
+            if (batch.size() == 0) {
+                mAlarmBatches.remove(batchIndex);
+            }
+        }
+        for (int i = 0; i < rescheduledAlarms.size(); i++) {
+            final Alarm a = rescheduledAlarms.get(i);
+            insertAndBatchAlarmLocked(a);
+        }
+
+        mStatLogger.logDurationStat(Stats.REORDER_ALARMS_FOR_STANDBY, start);
+        return rescheduledAlarms.size() > 0;
+    }
+
     void reAddAlarmLocked(Alarm a, long nowElapsed, boolean doValidate) {
         a.when = a.origWhen;
         long whenElapsed = convertToElapsed(a.when, a.type);
@@ -1442,18 +1503,32 @@
         else return mConstants.APP_STANDBY_MIN_DELAYS[0];
     }
 
-    private void adjustDeliveryTimeBasedOnStandbyBucketLocked(Alarm alarm) {
+    /**
+     * Adjusts the alarm delivery time based on the current app standby bucket.
+     * @param alarm The alarm to adjust
+     * @return true if the alarm delivery time was updated.
+     * TODO: Reduce the number of calls to getAppStandbyBucket by batching the calls per
+     * {package, user} pairs
+     */
+    private boolean adjustDeliveryTimeBasedOnStandbyBucketLocked(Alarm alarm) {
         if (alarm.alarmClock != null || UserHandle.isCore(alarm.creatorUid)) {
-            return;
+            return false;
+        }
+        // TODO: short term fix for b/72816079, remove after a proper fix is in place
+        if ((alarm.flags & AlarmManager.FLAG_ALLOW_WHILE_IDLE) != 0) {
+            return false;
         }
         if (mAppStandbyParole) {
             if (alarm.whenElapsed > alarm.requestedWhenElapsed) {
-                // We did throttle this alarm earlier, restore original requirements
+                // We did defer this alarm earlier, restore original requirements
                 alarm.whenElapsed = alarm.requestedWhenElapsed;
                 alarm.maxWhenElapsed = alarm.requestedMaxWhenElapsed;
             }
-            return;
+            return true;
         }
+        final long oldWhenElapsed = alarm.whenElapsed;
+        final long oldMaxWhenElapsed = alarm.maxWhenElapsed;
+
         final String sourcePackage = alarm.sourcePackage;
         final int sourceUserId = UserHandle.getUserId(alarm.creatorUid);
         final int standbyBucket = mUsageStatsManagerInternal.getAppStandbyBucket(
@@ -1465,8 +1540,14 @@
             final long minElapsed = lastElapsed + getMinDelayForBucketLocked(standbyBucket);
             if (alarm.requestedWhenElapsed < minElapsed) {
                 alarm.whenElapsed = alarm.maxWhenElapsed = minElapsed;
+            } else {
+                // app is now eligible to run alarms at the originally requested window.
+                // Restore original requirements in case they were changed earlier.
+                alarm.whenElapsed = alarm.requestedWhenElapsed;
+                alarm.maxWhenElapsed = alarm.requestedMaxWhenElapsed;
             }
         }
+        return (oldWhenElapsed != alarm.whenElapsed || oldMaxWhenElapsed != alarm.maxWhenElapsed);
     }
 
     private void setImplLocked(Alarm a, boolean rebatching, boolean doValidate) {
@@ -1521,21 +1602,7 @@
             }
         }
         adjustDeliveryTimeBasedOnStandbyBucketLocked(a);
-
-        int whichBatch = ((a.flags&AlarmManager.FLAG_STANDALONE) != 0)
-                ? -1 : attemptCoalesceLocked(a.whenElapsed, a.maxWhenElapsed);
-        if (whichBatch < 0) {
-            Batch batch = new Batch(a);
-            addBatchLocked(mAlarmBatches, batch);
-        } else {
-            Batch batch = mAlarmBatches.get(whichBatch);
-            if (batch.add(a)) {
-                // The start time of this batch advanced, so batch ordering may
-                // have just been broken.  Move it to where it now belongs.
-                mAlarmBatches.remove(whichBatch);
-                addBatchLocked(mAlarmBatches, batch);
-            }
-        }
+        insertAndBatchAlarmLocked(a);
 
         if (a.alarmClock != null) {
             mNextAlarmClockMayChange = true;
@@ -3138,7 +3205,7 @@
             final boolean isRtc = (type == RTC || type == RTC_WAKEUP);
             pw.print(prefix); pw.print("tag="); pw.println(statsTag);
             pw.print(prefix); pw.print("type="); pw.print(type);
-                    pw.print(" requestedWhenELapsed="); TimeUtils.formatDuration(
+                    pw.print(" requestedWhenElapsed="); TimeUtils.formatDuration(
                             requestedWhenElapsed, nowELAPSED, pw);
                     pw.print(" whenElapsed="); TimeUtils.formatDuration(whenElapsed,
                             nowELAPSED, pw);
@@ -3391,28 +3458,19 @@
                                 }
                                 mPendingNonWakeupAlarms.clear();
                             }
-                            boolean needRebatch = false;
-                            final HashSet<String> triggerPackages = new HashSet<>();
-                            for (int i = triggerList.size() - 1; i >= 0; i--) {
-                                triggerPackages.add(triggerList.get(i).sourcePackage);
-                            }
-                            outer:
-                            for (int i = 0; i < mAlarmBatches.size(); i++) {
-                                final Batch batch = mAlarmBatches.get(i);
-                                for (int j = 0; j < batch.size(); j++) {
-                                    if (triggerPackages.contains(batch.get(j))) {
-                                        needRebatch = true;
-                                        break outer;
-                                    }
+                            final ArraySet<Pair<String, Integer>> triggerPackages =
+                                    new ArraySet<>();
+                            for (int i = 0; i < triggerList.size(); i++) {
+                                final Alarm a = triggerList.get(i);
+                                if (!UserHandle.isCore(a.creatorUid)) {
+                                    triggerPackages.add(Pair.create(
+                                            a.sourcePackage, UserHandle.getUserId(a.creatorUid)));
                                 }
                             }
-                            if (needRebatch) {
-                                rebatchAllAlarmsLocked(false);
-                            } else {
-                                rescheduleKernelAlarmsLocked();
-                                updateNextAlarmClockLocked();
-                            }
                             deliverAlarmsLocked(triggerList, nowELAPSED);
+                            reorderAlarmsBasedOnStandbyBuckets(triggerPackages);
+                            rescheduleKernelAlarmsLocked();
+                            updateNextAlarmClockLocked();
                         }
                     }
 
@@ -3518,13 +3576,21 @@
                 case APP_STANDBY_PAROLE_CHANGED:
                     synchronized (mLock) {
                         mAppStandbyParole = (Boolean) msg.obj;
-                        rebatchAllAlarmsLocked(false);
+                        if (reorderAlarmsBasedOnStandbyBuckets(null)) {
+                            rescheduleKernelAlarmsLocked();
+                            updateNextAlarmClockLocked();
+                        }
                     }
                     break;
 
                 case APP_STANDBY_BUCKET_CHANGED:
                     synchronized (mLock) {
-                        rebatchAllAlarmsLocked(false);
+                        final ArraySet<Pair<String, Integer>> filterPackages = new ArraySet<>();
+                        filterPackages.add(Pair.create((String) msg.obj, msg.arg1));
+                        if (reorderAlarmsBasedOnStandbyBuckets(filterPackages)) {
+                            rescheduleKernelAlarmsLocked();
+                            updateNextAlarmClockLocked();
+                        }
                     }
                     break;
 
@@ -3751,7 +3817,8 @@
                         bucket);
             }
             mHandler.removeMessages(AlarmHandler.APP_STANDBY_BUCKET_CHANGED);
-            mHandler.sendEmptyMessage(AlarmHandler.APP_STANDBY_BUCKET_CHANGED);
+            mHandler.obtainMessage(AlarmHandler.APP_STANDBY_BUCKET_CHANGED, userId, -1, packageName)
+                    .sendToTarget();
         }
 
         @Override
diff --git a/services/core/java/com/android/server/InputMethodManagerService.java b/services/core/java/com/android/server/InputMethodManagerService.java
index 2f42585..93f7f1d 100644
--- a/services/core/java/com/android/server/InputMethodManagerService.java
+++ b/services/core/java/com/android/server/InputMethodManagerService.java
@@ -2970,12 +2970,19 @@
                         break;
                 }
 
-                if (!didStart && attribute != null) {
-                    if (!DebugFlags.FLAG_OPTIMIZE_START_INPUT.value()
-                            || (controlFlags
-                                    & InputMethodManager.CONTROL_WINDOW_IS_TEXT_EDITOR) != 0) {
-                        res = startInputUncheckedLocked(cs, inputContext, missingMethods, attribute,
-                                controlFlags, startInputReason);
+                if (!didStart) {
+                    if (attribute != null) {
+                        if (!DebugFlags.FLAG_OPTIMIZE_START_INPUT.value()
+                                || (controlFlags
+                                & InputMethodManager.CONTROL_WINDOW_IS_TEXT_EDITOR) != 0) {
+                            res = startInputUncheckedLocked(cs, inputContext, missingMethods,
+                                    attribute,
+                                    controlFlags, startInputReason);
+                        } else {
+                            res = InputBindResult.NO_EDITOR;
+                        }
+                    } else {
+                        res = InputBindResult.NULL_EDITOR_INFO;
                     }
                 }
             }
diff --git a/services/core/java/com/android/server/am/ActiveServices.java b/services/core/java/com/android/server/am/ActiveServices.java
index 2f7d4c1..14404f5 100644
--- a/services/core/java/com/android/server/am/ActiveServices.java
+++ b/services/core/java/com/android/server/am/ActiveServices.java
@@ -84,6 +84,7 @@
 import android.util.EventLog;
 import android.util.PrintWriterPrinter;
 import android.util.Slog;
+import android.util.StatsLog;
 import android.util.SparseArray;
 import android.util.TimeUtils;
 import android.util.proto.ProtoOutputStream;
@@ -1094,6 +1095,8 @@
                     active.mNumActive++;
                 }
                 r.isForeground = true;
+                StatsLog.write(StatsLog.FOREGROUND_SERVICE_STATE_CHANGED, r.userId, r.shortName,
+                        StatsLog.FOREGROUND_SERVICE_STATE_CHANGED__STATE__ENTER);
             }
             r.postNotification();
             if (r.app != null) {
@@ -1109,6 +1112,8 @@
                     decActiveForegroundAppLocked(smap, r);
                 }
                 r.isForeground = false;
+                StatsLog.write(StatsLog.FOREGROUND_SERVICE_STATE_CHANGED, r.userId, r.shortName,
+                        StatsLog.FOREGROUND_SERVICE_STATE_CHANGED__STATE__EXIT);
                 if (r.app != null) {
                     mAm.updateLruProcessLocked(r.app, false, null);
                     updateServiceForegroundLocked(r.app, true);
@@ -2533,7 +2538,10 @@
         cancelForegroundNotificationLocked(r);
         if (r.isForeground) {
             decActiveForegroundAppLocked(smap, r);
+            StatsLog.write(StatsLog.FOREGROUND_SERVICE_STATE_CHANGED, r.userId, r.shortName,
+                    StatsLog.FOREGROUND_SERVICE_STATE_CHANGED__STATE__EXIT);
         }
+
         r.isForeground = false;
         r.foregroundId = 0;
         r.foregroundNoti = null;
diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java
index a3a854d..99f36d0 100644
--- a/services/core/java/com/android/server/am/ActivityManagerService.java
+++ b/services/core/java/com/android/server/am/ActivityManagerService.java
@@ -778,8 +778,11 @@
     long mWaitForNetworkTimeoutMs;
 
     /**
-     * Helper class which parses out priority arguments and dumps sections according to their
-     * priority. If priority arguments are omitted, function calls the legacy dump command.
+     * Helper class which strips out priority and proto arguments then calls the dump function with
+     * the appropriate arguments. If priority arguments are omitted, function calls the legacy
+     * dump command.
+     * If priority arguments are omitted all sections are dumped, otherwise sections are dumped
+     * according to their priority.
      */
     private final PriorityDump.PriorityDumper mPriorityDumper = new PriorityDump.PriorityDumper() {
         @Override
@@ -791,24 +794,7 @@
 
         @Override
         public void dumpNormal(FileDescriptor fd, PrintWriter pw, String[] args, boolean asProto) {
-            if (asProto) {
-                doDump(fd, pw, new String[0], asProto);
-            } else {
-                doDump(fd, pw, new String[]{"settings"}, asProto);
-                doDump(fd, pw, new String[]{"intents"}, asProto);
-                doDump(fd, pw, new String[]{"broadcasts"}, asProto);
-                doDump(fd, pw, new String[]{"providers"}, asProto);
-                doDump(fd, pw, new String[]{"permissions"}, asProto);
-                doDump(fd, pw, new String[]{"services"}, asProto);
-                doDump(fd, pw, new String[]{"recents"}, asProto);
-                doDump(fd, pw, new String[]{"lastanr"}, asProto);
-                doDump(fd, pw, new String[]{"starter"}, asProto);
-                doDump(fd, pw, new String[]{"containers"}, asProto);
-                if (mAssociations.size() > 0) {
-                    doDump(fd, pw, new String[]{"associations"}, asProto);
-                }
-                doDump(fd, pw, new String[]{"processes"}, asProto);
-            }
+            doDump(fd, pw, new String[]{"-a", "--normal-priority"}, asProto);
         }
 
         @Override
@@ -15504,6 +15490,7 @@
         boolean dumpClient = false;
         boolean dumpCheckin = false;
         boolean dumpCheckinFormat = false;
+        boolean dumpNormalPriority = false;
         boolean dumpVisibleStacksOnly = false;
         boolean dumpFocusedStackOnly = false;
         String dumpPackage = null;
@@ -15536,6 +15523,8 @@
                 dumpCheckin = dumpCheckinFormat = true;
             } else if ("-C".equals(opt)) {
                 dumpCheckinFormat = true;
+            } else if ("--normal-priority".equals(opt)) {
+                dumpNormalPriority = true;
             } else if ("-h".equals(opt)) {
                 ActivityManagerShellCommand.dumpHelp(pw, true);
                 return;
@@ -15937,11 +15926,15 @@
                     pw.println("-------------------------------------------------------------------------------");
                 }
                 dumpActivityContainersLocked(pw);
-                pw.println();
-                if (dumpAll) {
-                    pw.println("-------------------------------------------------------------------------------");
+                // Activities section is dumped as part of the Critical priority dump. Exclude the
+                // section if priority is Normal.
+                if (!dumpNormalPriority){
+                    pw.println();
+                    if (dumpAll) {
+                        pw.println("-------------------------------------------------------------------------------");
+                    }
+                    dumpActivitiesLocked(fd, pw, args, opti, dumpAll, dumpClient, dumpPackage);
                 }
-                dumpActivitiesLocked(fd, pw, args, opti, dumpAll, dumpClient, dumpPackage);
                 if (mAssociations.size() > 0) {
                     pw.println();
                     if (dumpAll) {
diff --git a/services/core/java/com/android/server/am/ActivityStackSupervisor.java b/services/core/java/com/android/server/am/ActivityStackSupervisor.java
index 77f6d44..a0f31cd 100644
--- a/services/core/java/com/android/server/am/ActivityStackSupervisor.java
+++ b/services/core/java/com/android/server/am/ActivityStackSupervisor.java
@@ -1266,7 +1266,7 @@
                 Trace.traceBegin(TRACE_TAG_ACTIVITY_MANAGER, "resolveIntent");
                 int modifiedFlags = flags
                         | PackageManager.MATCH_DEFAULT_ONLY | ActivityManagerService.STOCK_PM_FLAGS;
-                if (intent.isBrowsableWebIntent()
+                if (intent.isWebIntent()
                             || (intent.getFlags() & Intent.FLAG_ACTIVITY_MATCH_EXTERNAL) != 0) {
                     modifiedFlags |= PackageManager.MATCH_INSTANT;
                 }
diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java
index 1825db8..56d66de 100644
--- a/services/core/java/com/android/server/audio/AudioService.java
+++ b/services/core/java/com/android/server/audio/AudioService.java
@@ -7296,6 +7296,12 @@
     //======================
     /**  */
     public int dispatchFocusChange(AudioFocusInfo afi, int focusChange, IAudioPolicyCallback pcb) {
+        if (afi == null) {
+            throw new IllegalArgumentException("Illegal null AudioFocusInfo");
+        }
+        if (pcb == null) {
+            throw new IllegalArgumentException("Illegal null AudioPolicy callback");
+        }
         synchronized (mAudioPolicies) {
             if (!mAudioPolicies.containsKey(pcb.asBinder())) {
                 throw new IllegalStateException("Unregistered AudioPolicy for focus dispatch");
@@ -7304,6 +7310,23 @@
         }
     }
 
+    public void setFocusRequestResultFromExtPolicy(AudioFocusInfo afi, int requestResult,
+            IAudioPolicyCallback pcb) {
+        if (afi == null) {
+            throw new IllegalArgumentException("Illegal null AudioFocusInfo");
+        }
+        if (pcb == null) {
+            throw new IllegalArgumentException("Illegal null AudioPolicy callback");
+        }
+        synchronized (mAudioPolicies) {
+            if (!mAudioPolicies.containsKey(pcb.asBinder())) {
+                throw new IllegalStateException("Unregistered AudioPolicy for external focus");
+            }
+            mMediaFocusControl.setFocusRequestResultFromExtPolicy(afi, requestResult);
+        }
+    }
+
+
     //======================
     // misc
     //======================
diff --git a/services/core/java/com/android/server/audio/FocusRequester.java b/services/core/java/com/android/server/audio/FocusRequester.java
index f2ef02f..99f0840 100644
--- a/services/core/java/com/android/server/audio/FocusRequester.java
+++ b/services/core/java/com/android/server/audio/FocusRequester.java
@@ -241,15 +241,15 @@
 
 
     void release() {
+        final IBinder srcRef = mSourceRef;
+        final AudioFocusDeathHandler deathHdlr = mDeathHandler;
         try {
-            if (mSourceRef != null && mDeathHandler != null) {
-                mSourceRef.unlinkToDeath(mDeathHandler, 0);
-                mDeathHandler = null;
-                mFocusDispatcher = null;
+            if (srcRef != null && deathHdlr != null) {
+                srcRef.unlinkToDeath(deathHdlr, 0);
             }
-        } catch (java.util.NoSuchElementException e) {
-            Log.e(TAG, "FocusRequester.release() hit ", e);
-        }
+        } catch (java.util.NoSuchElementException e) { }
+        mDeathHandler = null;
+        mFocusDispatcher = null;
     }
 
     @Override
@@ -424,7 +424,7 @@
 
     int dispatchFocusChange(int focusChange) {
         if (mFocusDispatcher == null) {
-            if (MediaFocusControl.DEBUG) { Log.v(TAG, "dispatchFocusChange: no focus dispatcher"); }
+            if (MediaFocusControl.DEBUG) { Log.e(TAG, "dispatchFocusChange: no focus dispatcher"); }
             return AudioManager.AUDIOFOCUS_REQUEST_FAILED;
         }
         if (focusChange == AudioManager.AUDIOFOCUS_NONE) {
@@ -445,12 +445,29 @@
         try {
             mFocusDispatcher.dispatchAudioFocusChange(focusChange, mClientId);
         } catch (android.os.RemoteException e) {
-            Log.v(TAG, "dispatchFocusChange: error talking to focus listener", e);
+            Log.e(TAG, "dispatchFocusChange: error talking to focus listener " + mClientId, e);
             return AudioManager.AUDIOFOCUS_REQUEST_FAILED;
         }
         return AudioManager.AUDIOFOCUS_REQUEST_GRANTED;
     }
 
+    void dispatchFocusResultFromExtPolicy(int requestResult) {
+        if (mFocusDispatcher == null) {
+            if (MediaFocusControl.DEBUG) {
+                Log.e(TAG, "dispatchFocusResultFromExtPolicy: no focus dispatcher");
+            }
+        }
+        if (DEBUG) {
+            Log.v(TAG, "dispatching result" + requestResult + " to " + mClientId);
+        }
+        try {
+            mFocusDispatcher.dispatchFocusResultFromExtPolicy(requestResult, mClientId);
+        } catch (android.os.RemoteException e) {
+            Log.e(TAG, "dispatchFocusResultFromExtPolicy: error talking to focus listener"
+                    + mClientId, e);
+        }
+    }
+
     AudioFocusInfo toAudioFocusInfo() {
         return new AudioFocusInfo(mAttributes, mCallingUid, mClientId, mPackageName,
                 mFocusGainRequest, mFocusLossReceived, mGrantFlags, mSdkTarget);
diff --git a/services/core/java/com/android/server/audio/MediaFocusControl.java b/services/core/java/com/android/server/audio/MediaFocusControl.java
index 9ddc52a..d023bd7 100644
--- a/services/core/java/com/android/server/audio/MediaFocusControl.java
+++ b/services/core/java/com/android/server/audio/MediaFocusControl.java
@@ -83,6 +83,10 @@
 
     private boolean mRingOrCallActive = false;
 
+    private final Object mExtFocusChangeLock = new Object();
+    @GuardedBy("mExtFocusChangeLock")
+    private long mExtFocusChangeCounter;
+
     protected MediaFocusControl(Context cntxt, PlayerFocusEnforcer pfe) {
         mContext = cntxt;
         mAppOps = (AppOpsManager)mContext.getSystemService(Context.APP_OPS_SERVICE);
@@ -521,7 +525,7 @@
      * @param requestResult
      * @return true if the external audio focus policy (if any) is handling the focus request
      */
-    boolean notifyExtFocusPolicyFocusRequest_syncAf(AudioFocusInfo afi, int requestResult,
+    boolean notifyExtFocusPolicyFocusRequest_syncAf(AudioFocusInfo afi,
             IAudioFocusDispatcher fd, IBinder cb) {
         if (mFocusPolicy == null) {
             return false;
@@ -530,6 +534,9 @@
             Log.v(TAG, "notifyExtFocusPolicyFocusRequest client="+afi.getClientId()
             + " dispatcher=" + fd);
         }
+        synchronized (mExtFocusChangeLock) {
+            afi.setGen(mExtFocusChangeCounter++);
+        }
         final FocusRequester existingFr = mFocusOwnersForFocusPolicy.get(afi.getClientId());
         if (existingFr != null) {
             if (!existingFr.hasSameDispatcher(fd)) {
@@ -538,8 +545,7 @@
                 mFocusOwnersForFocusPolicy.put(afi.getClientId(),
                         new FocusRequester(afi, fd, cb, hdlr, this));
             }
-        } else if (requestResult == AudioManager.AUDIOFOCUS_REQUEST_GRANTED
-                 || requestResult == AudioManager.AUDIOFOCUS_REQUEST_DELAYED) {
+        } else {
             // new focus (future) focus owner to keep track of
             final AudioFocusDeathHandler hdlr = new AudioFocusDeathHandler(cb);
             mFocusOwnersForFocusPolicy.put(afi.getClientId(),
@@ -547,12 +553,25 @@
         }
         try {
             //oneway
-            mFocusPolicy.notifyAudioFocusRequest(afi, requestResult);
+            mFocusPolicy.notifyAudioFocusRequest(afi, AudioManager.AUDIOFOCUS_REQUEST_GRANTED);
+            return true;
         } catch (RemoteException e) {
             Log.e(TAG, "Can't call notifyAudioFocusRequest() on IAudioPolicyCallback "
                     + mFocusPolicy.asBinder(), e);
         }
-        return true;
+        return false;
+    }
+
+    void setFocusRequestResultFromExtPolicy(AudioFocusInfo afi, int requestResult) {
+        synchronized (mExtFocusChangeLock) {
+            if (afi.getGen() > mExtFocusChangeCounter) {
+                return;
+            }
+        }
+        final FocusRequester fr = mFocusOwnersForFocusPolicy.get(afi.getClientId());
+        if (fr != null) {
+            fr.dispatchFocusResultFromExtPolicy(requestResult);
+        }
     }
 
     /**
@@ -590,7 +609,12 @@
                 if (DEBUG) { Log.v(TAG, "> failed: no focus policy" ); }
                 return AudioManager.AUDIOFOCUS_REQUEST_FAILED;
             }
-            final FocusRequester fr = mFocusOwnersForFocusPolicy.get(afi.getClientId());
+            final FocusRequester fr;
+            if (focusChange == AudioManager.AUDIOFOCUS_LOSS) {
+                fr = mFocusOwnersForFocusPolicy.remove(afi.getClientId());
+            } else {
+                fr = mFocusOwnersForFocusPolicy.get(afi.getClientId());
+            }
             if (fr == null) {
                 if (DEBUG) { Log.v(TAG, "> failed: no such focus requester known" ); }
                 return AudioManager.AUDIOFOCUS_REQUEST_FAILED;
@@ -710,9 +734,7 @@
             boolean focusGrantDelayed = false;
             if (!canReassignAudioFocus()) {
                 if ((flags & AudioManager.AUDIOFOCUS_FLAG_DELAY_OK) == 0) {
-                    final int result = AudioManager.AUDIOFOCUS_REQUEST_FAILED;
-                    notifyExtFocusPolicyFocusRequest_syncAf(afiForExtPolicy, result, fd, cb);
-                    return result;
+                    return AudioManager.AUDIOFOCUS_REQUEST_FAILED;
                 } else {
                     // request has AUDIOFOCUS_FLAG_DELAY_OK: focus can't be
                     // granted right now, so the requester will be inserted in the focus stack
@@ -721,12 +743,11 @@
                 }
             }
 
-            // external focus policy: delay request for focus gain?
-            final int resultWithExtPolicy = AudioManager.AUDIOFOCUS_REQUEST_DELAYED;
+            // external focus policy?
             if (notifyExtFocusPolicyFocusRequest_syncAf(
-                    afiForExtPolicy, resultWithExtPolicy, fd, cb)) {
+                    afiForExtPolicy, fd, cb)) {
                 // stop handling focus request here as it is handled by external audio focus policy
-                return resultWithExtPolicy;
+                return AudioManager.AUDIOFOCUS_REQUEST_WAITING_FOR_EXT_POLICY;
             }
 
             // handle the potential premature death of the new holder of the focus
diff --git a/services/core/java/com/android/server/job/controllers/ConnectivityController.java b/services/core/java/com/android/server/job/controllers/ConnectivityController.java
index 373d87d..13873e4 100644
--- a/services/core/java/com/android/server/job/controllers/ConnectivityController.java
+++ b/services/core/java/com/android/server/job/controllers/ConnectivityController.java
@@ -328,10 +328,12 @@
     @Override
     public void dumpControllerStateLocked(PrintWriter pw, int filterUid) {
         pw.print("Connectivity: connected=");
-        pw.print(mConnected);
+        pw.println(mConnected);
+
         pw.print("Tracking ");
         pw.print(mTrackedJobs.size());
-        pw.println(":");
+        pw.println(" jobs");
+
         for (int i = 0; i < mTrackedJobs.size(); i++) {
             final JobStatus js = mTrackedJobs.valueAt(i);
             if (js.shouldDump(filterUid)) {
@@ -339,7 +341,9 @@
                 js.printUniqueId(pw);
                 pw.print(" from ");
                 UserHandle.formatUid(pw, js.getSourceUid());
-                pw.print(": "); pw.print(js.getJob().getRequiredNetwork());
+                pw.print(": ");
+                pw.print(js.getJob().getRequiredNetwork());
+                pw.println();
             }
         }
     }
diff --git a/services/core/java/com/android/server/locksettings/recoverablekeystore/certificate/CertParsingException.java b/services/core/java/com/android/server/locksettings/recoverablekeystore/certificate/CertParsingException.java
new file mode 100644
index 0000000..57a3d99
--- /dev/null
+++ b/services/core/java/com/android/server/locksettings/recoverablekeystore/certificate/CertParsingException.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.locksettings.recoverablekeystore.certificate;
+
+/** Exception thrown when parsing errors occur. */
+public class CertParsingException extends Exception {
+
+    public CertParsingException(String message) {
+        super(message);
+    }
+
+    public CertParsingException(Exception cause) {
+        super(cause);
+    }
+
+    public CertParsingException(String message, Exception cause) {
+        super(message, cause);
+    }
+}
diff --git a/services/core/java/com/android/server/locksettings/recoverablekeystore/certificate/CertUtils.java b/services/core/java/com/android/server/locksettings/recoverablekeystore/certificate/CertUtils.java
new file mode 100644
index 0000000..fea6733
--- /dev/null
+++ b/services/core/java/com/android/server/locksettings/recoverablekeystore/certificate/CertUtils.java
@@ -0,0 +1,355 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.locksettings.recoverablekeystore.certificate;
+
+import static javax.xml.xpath.XPathConstants.NODESET;
+
+import android.annotation.IntDef;
+import android.annotation.Nullable;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.security.InvalidAlgorithmParameterException;
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+import java.security.PublicKey;
+import java.security.Signature;
+import java.security.SignatureException;
+import java.security.cert.CertPath;
+import java.security.cert.CertPathBuilder;
+import java.security.cert.CertPathBuilderException;
+import java.security.cert.CertPathValidator;
+import java.security.cert.CertPathValidatorException;
+import java.security.cert.CertStore;
+import java.security.cert.CertificateException;
+import java.security.cert.CertificateFactory;
+import java.security.cert.CollectionCertStoreParameters;
+import java.security.cert.PKIXBuilderParameters;
+import java.security.cert.PKIXParameters;
+import java.security.cert.TrustAnchor;
+import java.security.cert.X509CertSelector;
+import java.security.cert.X509Certificate;
+import java.util.ArrayList;
+import java.util.Base64;
+import java.util.Date;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+import javax.xml.parsers.DocumentBuilderFactory;
+import javax.xml.parsers.ParserConfigurationException;
+import javax.xml.xpath.XPath;
+import javax.xml.xpath.XPathExpressionException;
+import javax.xml.xpath.XPathFactory;
+
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
+import org.xml.sax.SAXException;
+
+/** Utility functions related to parsing and validating public-key certificates. */
+final class CertUtils {
+
+    private static final String CERT_FORMAT = "X.509";
+    private static final String CERT_PATH_ALG = "PKIX";
+    private static final String CERT_STORE_ALG = "Collection";
+    private static final String SIGNATURE_ALG = "SHA256withRSA";
+
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef({MUST_EXIST_UNENFORCED, MUST_EXIST_EXACTLY_ONE, MUST_EXIST_AT_LEAST_ONE})
+    @interface MustExist {}
+    static final int MUST_EXIST_UNENFORCED = 0;
+    static final int MUST_EXIST_EXACTLY_ONE = 1;
+    static final int MUST_EXIST_AT_LEAST_ONE = 2;
+
+    private CertUtils() {}
+
+    /**
+     * Decodes a byte array containing an encoded X509 certificate.
+     *
+     * @param certBytes the byte array containing the encoded X509 certificate
+     * @return the decoded X509 certificate
+     * @throws CertParsingException if any parsing error occurs
+     */
+    static X509Certificate decodeCert(byte[] certBytes) throws CertParsingException {
+        return decodeCert(new ByteArrayInputStream(certBytes));
+    }
+
+    /**
+     * Decodes an X509 certificate from an {@code InputStream}.
+     *
+     * @param inStream the input stream containing the encoded X509 certificate
+     * @return the decoded X509 certificate
+     * @throws CertParsingException if any parsing error occurs
+     */
+    static X509Certificate decodeCert(InputStream inStream) throws CertParsingException {
+        CertificateFactory certFactory;
+        try {
+            certFactory = CertificateFactory.getInstance(CERT_FORMAT);
+        } catch (CertificateException e) {
+            // Should not happen, as X.509 is mandatory for all providers.
+            throw new RuntimeException(e);
+        }
+        try {
+            return (X509Certificate) certFactory.generateCertificate(inStream);
+        } catch (CertificateException e) {
+            throw new CertParsingException(e);
+        }
+    }
+
+    /**
+     * Parses a byte array as the content of an XML file, and returns the root node of the XML file.
+     *
+     * @param xmlBytes the byte array that is the XML file content
+     * @return the root node of the XML file
+     * @throws CertParsingException if any parsing error occurs
+     */
+    static Element getXmlRootNode(byte[] xmlBytes) throws CertParsingException {
+        try {
+            Document document =
+                    DocumentBuilderFactory.newInstance()
+                            .newDocumentBuilder()
+                            .parse(new ByteArrayInputStream(xmlBytes));
+            document.getDocumentElement().normalize();
+            return document.getDocumentElement();
+        } catch (SAXException | ParserConfigurationException | IOException e) {
+            throw new CertParsingException(e);
+        }
+    }
+
+    /**
+     * Gets the text contents of certain XML child nodes, given a XML root node and a list of tags
+     * representing the path to locate the child nodes. The whitespaces and newlines in the text
+     * contents are stripped away.
+     *
+     * <p>For example, the list of tags [tag1, tag2, tag3] represents the XML tree like the
+     * following:
+     *
+     * <pre>
+     *   <root>
+     *     <tag1>
+     *       <tag2>
+     *         <tag3>abc</tag3>
+     *         <tag3>def</tag3>
+     *       </tag2>
+     *     </tag1>
+     *   <root>
+     * </pre>
+     *
+     * @param mustExist whether and how many nodes must exist. If the number of child nodes does not
+     *                  satisfy the requirement, CertParsingException will be thrown.
+     * @param rootNode  the root node that serves as the starting point to locate the child nodes
+     * @param nodeTags  the list of tags representing the relative path from the root node
+     * @return a list of strings that are the text contents of the child nodes
+     * @throws CertParsingException if any parsing error occurs
+     */
+    static List<String> getXmlNodeContents(@MustExist int mustExist, Element rootNode,
+            String... nodeTags)
+            throws CertParsingException {
+        String expression = String.join("/", nodeTags);
+
+        XPath xPath = XPathFactory.newInstance().newXPath();
+        NodeList nodeList;
+        try {
+            nodeList = (NodeList) xPath.compile(expression).evaluate(rootNode, NODESET);
+        } catch (XPathExpressionException e) {
+            throw new CertParsingException(e);
+        }
+
+        switch (mustExist) {
+            case MUST_EXIST_UNENFORCED:
+                break;
+
+            case MUST_EXIST_EXACTLY_ONE:
+                if (nodeList.getLength() != 1) {
+                    throw new CertParsingException(
+                            "The XML file must contain exactly one node with the path "
+                                    + expression);
+                }
+                break;
+
+            case MUST_EXIST_AT_LEAST_ONE:
+                if (nodeList.getLength() == 0) {
+                    throw new CertParsingException(
+                            "The XML file must contain at least one node with the path "
+                                    + expression);
+                }
+                break;
+
+            default:
+                throw new UnsupportedOperationException(
+                        "This value of MustExist is not supported: " + mustExist);
+        }
+
+        List<String> result = new ArrayList<>();
+        for (int i = 0; i < nodeList.getLength(); i++) {
+            Node node = nodeList.item(i);
+            // Remove whitespaces and newlines.
+            result.add(node.getTextContent().replaceAll("\\s", ""));
+        }
+        return result;
+    }
+
+    /**
+     * Decodes a base64-encoded string.
+     *
+     * @param str the base64-encoded string
+     * @return the decoding decoding result
+     * @throws CertParsingException if the input string is not a properly base64-encoded string
+     */
+    static byte[] decodeBase64(String str) throws CertParsingException {
+        try {
+            return Base64.getDecoder().decode(str);
+        } catch (IllegalArgumentException e) {
+            throw new CertParsingException(e);
+        }
+    }
+
+    /**
+     * Verifies a public-key signature that is computed by RSA with SHA256.
+     *
+     * @param signerPublicKey the public key of the original signer
+     * @param signature       the public-key signature
+     * @param signedBytes     the bytes that have been signed
+     * @throws CertValidationException if the signature verification fails
+     */
+    static void verifyRsaSha256Signature(
+            PublicKey signerPublicKey, byte[] signature, byte[] signedBytes)
+            throws CertValidationException {
+        Signature verifier;
+        try {
+            verifier = Signature.getInstance(SIGNATURE_ALG);
+        } catch (NoSuchAlgorithmException e) {
+            // Should not happen, as SHA256withRSA is mandatory for all providers.
+            throw new RuntimeException(e);
+        }
+        try {
+            verifier.initVerify(signerPublicKey);
+            verifier.update(signedBytes);
+            if (!verifier.verify(signature)) {
+                throw new CertValidationException("The signature is invalid");
+            }
+        } catch (InvalidKeyException | SignatureException e) {
+            throw new CertValidationException(e);
+        }
+    }
+
+    /**
+     * Validates a leaf certificate, and returns the certificate path if the certificate is valid.
+     * If the given validation date is null, the current date will be used.
+     *
+     * @param validationDate    the date for which the validity of the certificate should be
+     *                          determined
+     * @param trustedRoot       the certificate of the trusted root CA
+     * @param intermediateCerts the list of certificates of possible intermediate CAs
+     * @param leafCert          the leaf certificate that is to be validated
+     * @return the certificate path if the leaf cert is valid
+     * @throws CertValidationException if {@code leafCert} is invalid (e.g., is expired, or has
+     *                                 invalid signature)
+     */
+    static CertPath validateCert(
+            @Nullable Date validationDate,
+            X509Certificate trustedRoot,
+            List<X509Certificate> intermediateCerts,
+            X509Certificate leafCert)
+            throws CertValidationException {
+        PKIXParameters pkixParams =
+                buildPkixParams(validationDate, trustedRoot, intermediateCerts, leafCert);
+        CertPath certPath = buildCertPath(pkixParams);
+
+        CertPathValidator certPathValidator;
+        try {
+            certPathValidator = CertPathValidator.getInstance(CERT_PATH_ALG);
+        } catch (NoSuchAlgorithmException e) {
+            // Should not happen, as PKIX is mandatory for all providers.
+            throw new RuntimeException(e);
+        }
+        try {
+            certPathValidator.validate(certPath, pkixParams);
+        } catch (CertPathValidatorException | InvalidAlgorithmParameterException e) {
+            throw new CertValidationException(e);
+        }
+        return certPath;
+    }
+
+    @VisibleForTesting
+    static CertPath buildCertPath(PKIXParameters pkixParams) throws CertValidationException {
+        CertPathBuilder certPathBuilder;
+        try {
+            certPathBuilder = CertPathBuilder.getInstance(CERT_PATH_ALG);
+        } catch (NoSuchAlgorithmException e) {
+            // Should not happen, as PKIX is mandatory for all providers.
+            throw new RuntimeException(e);
+        }
+        try {
+            return certPathBuilder.build(pkixParams).getCertPath();
+        } catch (CertPathBuilderException | InvalidAlgorithmParameterException e) {
+            throw new CertValidationException(e);
+        }
+    }
+
+    @VisibleForTesting
+    static PKIXParameters buildPkixParams(
+            @Nullable Date validationDate,
+            X509Certificate trustedRoot,
+            List<X509Certificate> intermediateCerts,
+            X509Certificate leafCert)
+            throws CertValidationException {
+        // Create a TrustAnchor from the trusted root certificate.
+        Set<TrustAnchor> trustedAnchors = new HashSet<>();
+        trustedAnchors.add(new TrustAnchor(trustedRoot, null));
+
+        // Create a CertStore from the list of intermediate certificates.
+        List<X509Certificate> certs = new ArrayList<>(intermediateCerts);
+        certs.add(leafCert);
+        CertStore certStore;
+        try {
+            certStore =
+                    CertStore.getInstance(CERT_STORE_ALG, new CollectionCertStoreParameters(certs));
+        } catch (NoSuchAlgorithmException e) {
+            // Should not happen, as Collection is mandatory for all providers.
+            throw new RuntimeException(e);
+        } catch (InvalidAlgorithmParameterException e) {
+            throw new CertValidationException(e);
+        }
+
+        // Create a CertSelector from the leaf certificate.
+        X509CertSelector certSelector = new X509CertSelector();
+        certSelector.setCertificate(leafCert);
+
+        // Build a PKIXParameters from TrustAnchor, CertStore, and CertSelector.
+        PKIXBuilderParameters pkixParams;
+        try {
+            pkixParams = new PKIXBuilderParameters(trustedAnchors, certSelector);
+        } catch (InvalidAlgorithmParameterException e) {
+            throw new CertValidationException(e);
+        }
+        pkixParams.addCertStore(certStore);
+
+        // If validationDate is null, the current time will be used.
+        pkixParams.setDate(validationDate);
+        pkixParams.setRevocationEnabled(false);
+
+        return pkixParams;
+    }
+}
diff --git a/services/core/java/com/android/server/locksettings/recoverablekeystore/certificate/CertValidationException.java b/services/core/java/com/android/server/locksettings/recoverablekeystore/certificate/CertValidationException.java
new file mode 100644
index 0000000..99a9ff7
--- /dev/null
+++ b/services/core/java/com/android/server/locksettings/recoverablekeystore/certificate/CertValidationException.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.locksettings.recoverablekeystore.certificate;
+
+/** Exception thrown when validation or verification fails. */
+public class CertValidationException extends Exception {
+
+    public CertValidationException(String message) {
+        super(message);
+    }
+
+    public CertValidationException(Exception cause) {
+        super(cause);
+    }
+
+    public CertValidationException(String message, Exception cause) {
+        super(message, cause);
+    }
+}
diff --git a/services/core/java/com/android/server/locksettings/recoverablekeystore/certificate/CertXml.java b/services/core/java/com/android/server/locksettings/recoverablekeystore/certificate/CertXml.java
new file mode 100644
index 0000000..c62a31e
--- /dev/null
+++ b/services/core/java/com/android/server/locksettings/recoverablekeystore/certificate/CertXml.java
@@ -0,0 +1,178 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.locksettings.recoverablekeystore.certificate;
+
+import android.annotation.Nullable;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.security.SecureRandom;
+import java.security.cert.CertPath;
+import java.security.cert.X509Certificate;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Date;
+import java.util.List;
+
+import org.w3c.dom.Element;
+
+/**
+ * Parses and holds the XML file containing the list of THM public-key certificates and related
+ * metadata.
+ */
+public final class CertXml {
+
+    private static final String METADATA_NODE_TAG = "metadata";
+    private static final String METADATA_SERIAL_NODE_TAG = "serial";
+    private static final String METADATA_REFRESH_INTERVAL_NODE_TAG = "refresh-interval";
+    private static final String ENDPOINT_CERT_LIST_TAG = "endpoints";
+    private static final String ENDPOINT_CERT_ITEM_TAG = "cert";
+    private static final String INTERMEDIATE_CERT_LIST_TAG = "intermediates";
+    private static final String INTERMEDIATE_CERT_ITEM_TAG = "cert";
+
+    private final long serial;
+    private final long refreshInterval;
+    private final List<X509Certificate> intermediateCerts;
+    private final List<X509Certificate> endpointCerts;
+
+    private CertXml(
+            long serial,
+            long refreshInterval,
+            List<X509Certificate> intermediateCerts,
+            List<X509Certificate> endpointCerts) {
+        this.serial = serial;
+        this.refreshInterval = refreshInterval;
+        this.intermediateCerts = intermediateCerts;
+        this.endpointCerts = endpointCerts;
+    }
+
+    /** Gets the serial number of the XML file containing public-key certificates. */
+    public long getSerial() {
+        return serial;
+    }
+
+    /**
+     * Gets the refresh interval in the XML file containing public-key certificates. The refresh
+     * interval denotes the number of seconds that the client should follow to contact the server to
+     * refresh the XML file.
+     */
+    public long getRefreshInterval() {
+        return refreshInterval;
+    }
+
+    @VisibleForTesting
+    List<X509Certificate> getAllIntermediateCerts() {
+        return intermediateCerts;
+    }
+
+    @VisibleForTesting
+    List<X509Certificate> getAllEndpointCerts() {
+        return endpointCerts;
+    }
+
+    /**
+     * Chooses a random endpoint certificate from the XML file, validates the chosen certificate,
+     * and returns the certificate path including the chosen certificate if it is valid.
+     *
+     * @param trustedRoot the trusted root certificate
+     * @return the certificate path including the chosen certificate if the certificate is valid
+     * @throws CertValidationException if the chosen certificate cannot be validated based on the
+     *                                 trusted root certificate
+     */
+    public CertPath getRandomEndpointCert(X509Certificate trustedRoot)
+            throws CertValidationException {
+        return getEndpointCert(
+                new SecureRandom().nextInt(this.endpointCerts.size()),
+                /*validationDate=*/ null,
+                trustedRoot);
+    }
+
+    @VisibleForTesting
+    CertPath getEndpointCert(
+            int index, @Nullable Date validationDate, X509Certificate trustedRoot)
+            throws CertValidationException {
+        X509Certificate chosenCert = endpointCerts.get(index);
+        return CertUtils.validateCert(validationDate, trustedRoot, intermediateCerts, chosenCert);
+    }
+
+    /**
+     * Parses a byte array as the content of the XML file containing a list of endpoint
+     * certificates.
+     *
+     * @param bytes the bytes of the XML file
+     * @return a {@code CertXml} instance that contains the parsing result
+     * @throws CertParsingException if any parsing error occurs
+     */
+    public static CertXml parse(byte[] bytes) throws CertParsingException {
+        Element rootNode = CertUtils.getXmlRootNode(bytes);
+        return new CertXml(
+                parseSerial(rootNode),
+                parseRefreshInterval(rootNode),
+                parseIntermediateCerts(rootNode),
+                parseEndpointCerts(rootNode));
+    }
+
+    private static long parseSerial(Element rootNode) throws CertParsingException {
+        List<String> contents =
+                CertUtils.getXmlNodeContents(
+                        CertUtils.MUST_EXIST_EXACTLY_ONE,
+                        rootNode,
+                        METADATA_NODE_TAG,
+                        METADATA_SERIAL_NODE_TAG);
+        return Long.parseLong(contents.get(0));
+    }
+
+    private static long parseRefreshInterval(Element rootNode) throws CertParsingException {
+        List<String> contents =
+                CertUtils.getXmlNodeContents(
+                        CertUtils.MUST_EXIST_EXACTLY_ONE,
+                        rootNode,
+                        METADATA_NODE_TAG,
+                        METADATA_REFRESH_INTERVAL_NODE_TAG);
+        return Long.parseLong(contents.get(0));
+    }
+
+    private static List<X509Certificate> parseIntermediateCerts(Element rootNode)
+            throws CertParsingException {
+        List<String> contents =
+                CertUtils.getXmlNodeContents(
+                        CertUtils.MUST_EXIST_UNENFORCED,
+                        rootNode,
+                        INTERMEDIATE_CERT_LIST_TAG,
+                        INTERMEDIATE_CERT_ITEM_TAG);
+        List<X509Certificate> res = new ArrayList<>();
+        for (String content : contents) {
+            res.add(CertUtils.decodeCert(CertUtils.decodeBase64(content)));
+        }
+        return Collections.unmodifiableList(res);
+    }
+
+    private static List<X509Certificate> parseEndpointCerts(Element rootNode)
+            throws CertParsingException {
+        List<String> contents =
+                CertUtils.getXmlNodeContents(
+                        CertUtils.MUST_EXIST_AT_LEAST_ONE,
+                        rootNode,
+                        ENDPOINT_CERT_LIST_TAG,
+                        ENDPOINT_CERT_ITEM_TAG);
+        List<X509Certificate> res = new ArrayList<>();
+        for (String content : contents) {
+            res.add(CertUtils.decodeCert(CertUtils.decodeBase64(content)));
+        }
+        return Collections.unmodifiableList(res);
+    }
+}
diff --git a/services/core/java/com/android/server/locksettings/recoverablekeystore/certificate/SigXml.java b/services/core/java/com/android/server/locksettings/recoverablekeystore/certificate/SigXml.java
new file mode 100644
index 0000000..e75be85
--- /dev/null
+++ b/services/core/java/com/android/server/locksettings/recoverablekeystore/certificate/SigXml.java
@@ -0,0 +1,121 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.locksettings.recoverablekeystore.certificate;
+
+import android.annotation.Nullable;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.security.cert.X509Certificate;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Date;
+import java.util.List;
+
+import org.w3c.dom.Element;
+
+/**
+ * Parses and holds the XML file containing the signature of the XML file containing the list of THM
+ * public-key certificates.
+ */
+public final class SigXml {
+
+    private static final String INTERMEDIATE_CERT_LIST_TAG = "intermediates";
+    private static final String INTERMEDIATE_CERT_ITEM_TAG = "cert";
+    private static final String SIGNER_CERT_NODE_TAG = "certificate";
+    private static final String SIGNATURE_NODE_TAG = "value";
+
+    private final List<X509Certificate> intermediateCerts;
+    private final X509Certificate signerCert;
+    private final byte[] signature;
+
+    private SigXml(
+            List<X509Certificate> intermediateCerts, X509Certificate signerCert, byte[] signature) {
+        this.intermediateCerts = intermediateCerts;
+        this.signerCert = signerCert;
+        this.signature = signature;
+    }
+
+    /**
+     * Verifies the signature contained in this XML file against a trusted root certificate and the
+     * binary content of another file. The signer's public-key certificate and possible intermediate
+     * CA certificates are included in this XML file, and will be validated against the trusted root
+     * certificate.
+     *
+     * @param trustedRoot     the trusted root certificate
+     * @param signedFileBytes the original file content that has been signed
+     * @throws CertValidationException if the signature verification fails, or the signer's
+     *                                 certificate contained in this XML file cannot be validated
+     *                                 based on the trusted root certificate
+     */
+    public void verifyFileSignature(X509Certificate trustedRoot, byte[] signedFileBytes)
+            throws CertValidationException {
+        verifyFileSignature(trustedRoot, signedFileBytes, /*validationDate=*/ null);
+    }
+
+    @VisibleForTesting
+    void verifyFileSignature(
+            X509Certificate trustedRoot, byte[] signedFileBytes, @Nullable Date validationDate)
+            throws CertValidationException {
+        CertUtils.validateCert(validationDate, trustedRoot, intermediateCerts, signerCert);
+        CertUtils.verifyRsaSha256Signature(signerCert.getPublicKey(), signature, signedFileBytes);
+    }
+
+    /**
+     * Parses a byte array as the content of the XML file containing the signature and related
+     * certificates.
+     *
+     * @param bytes the bytes of the XML file
+     * @return a {@code SigXml} instance that contains the parsing result
+     * @throws CertParsingException if any parsing error occurs
+     */
+    public static SigXml parse(byte[] bytes) throws CertParsingException {
+        Element rootNode = CertUtils.getXmlRootNode(bytes);
+        return new SigXml(
+                parseIntermediateCerts(rootNode), parseSignerCert(rootNode),
+                parseFileSignature(rootNode));
+    }
+
+    private static List<X509Certificate> parseIntermediateCerts(Element rootNode)
+            throws CertParsingException {
+        List<String> contents =
+                CertUtils.getXmlNodeContents(
+                        CertUtils.MUST_EXIST_UNENFORCED,
+                        rootNode,
+                        INTERMEDIATE_CERT_LIST_TAG,
+                        INTERMEDIATE_CERT_ITEM_TAG);
+        List<X509Certificate> res = new ArrayList<>();
+        for (String content : contents) {
+            res.add(CertUtils.decodeCert(CertUtils.decodeBase64(content)));
+        }
+        return Collections.unmodifiableList(res);
+    }
+
+    private static X509Certificate parseSignerCert(Element rootNode) throws CertParsingException {
+        List<String> contents =
+                CertUtils.getXmlNodeContents(
+                        CertUtils.MUST_EXIST_EXACTLY_ONE, rootNode, SIGNER_CERT_NODE_TAG);
+        return CertUtils.decodeCert(CertUtils.decodeBase64(contents.get(0)));
+    }
+
+    private static byte[] parseFileSignature(Element rootNode) throws CertParsingException {
+        List<String> contents =
+                CertUtils.getXmlNodeContents(
+                        CertUtils.MUST_EXIST_EXACTLY_ONE, rootNode, SIGNATURE_NODE_TAG);
+        return CertUtils.decodeBase64(contents.get(0));
+    }
+}
diff --git a/services/core/java/com/android/server/notification/ManagedServices.java b/services/core/java/com/android/server/notification/ManagedServices.java
index fd435f9..b7842d5 100644
--- a/services/core/java/com/android/server/notification/ManagedServices.java
+++ b/services/core/java/com/android/server/notification/ManagedServices.java
@@ -376,9 +376,7 @@
     protected void upgradeXml(final int xmlVersion, final int userId) {}
 
     private void loadAllowedComponentsFromSettings() {
-
-        UserManager userManager = (UserManager) mContext.getSystemService(Context.USER_SERVICE);
-        for (UserInfo user : userManager.getUsers()) {
+        for (UserInfo user : mUm.getUsers()) {
             final ContentResolver cr = mContext.getContentResolver();
             addApprovedList(Settings.Secure.getStringForUser(
                     cr,
@@ -482,7 +480,9 @@
         for (int i = 0; i < allowedByType.size(); i++) {
             final ArraySet<String> allowed = allowedByType.valueAt(i);
             allowedPackages.addAll(
-                    allowed.stream().map(this::getPackageName).collect(Collectors.toList()));
+                    allowed.stream().map(this::getPackageName).
+                            filter(value -> !TextUtils.isEmpty(value))
+                            .collect(Collectors.toList()));
         }
         return allowedPackages;
     }
diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java
index e7eed03..ada002c 100644
--- a/services/core/java/com/android/server/notification/NotificationManagerService.java
+++ b/services/core/java/com/android/server/notification/NotificationManagerService.java
@@ -25,7 +25,9 @@
 import static android.content.pm.PackageManager.FEATURE_LEANBACK;
 import static android.content.pm.PackageManager.FEATURE_TELEVISION;
 import static android.content.pm.PackageManager.PERMISSION_GRANTED;
+import static android.os.UserHandle.USER_ALL;
 import static android.os.UserHandle.USER_NULL;
+import static android.os.UserHandle.USER_SYSTEM;
 import static android.service.notification.NotificationListenerService
         .HINT_HOST_DISABLE_CALL_EFFECTS;
 import static android.service.notification.NotificationListenerService.HINT_HOST_DISABLE_EFFECTS;
@@ -88,6 +90,7 @@
 import android.companion.ICompanionDeviceManager;
 import android.content.BroadcastReceiver;
 import android.content.ComponentName;
+import android.content.ContentProvider;
 import android.content.ContentResolver;
 import android.content.Context;
 import android.content.Intent;
@@ -115,6 +118,7 @@
 import android.os.IInterface;
 import android.os.Looper;
 import android.os.Message;
+import android.os.Parcelable;
 import android.os.Process;
 import android.os.RemoteException;
 import android.os.ResultReceiver;
@@ -206,6 +210,7 @@
 import java.util.ArrayDeque;
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.HashSet;
 import java.util.Iterator;
 import java.util.List;
 import java.util.Map.Entry;
@@ -288,6 +293,7 @@
     private ICompanionDeviceManager mCompanionManager;
     private AccessibilityManager mAccessibilityManager;
     private IDeviceIdleController mDeviceIdleController;
+    private IBinder mPermissionOwner;
 
     final IBinder mForegroundToken = new Binder();
     private WorkerHandler mHandler;
@@ -524,7 +530,7 @@
             } catch (FileNotFoundException e) {
                 // No data yet
                 // Load default managed services approvals
-                readDefaultApprovedServices(UserHandle.USER_SYSTEM);
+                readDefaultApprovedServices(USER_SYSTEM);
             } catch (IOException e) {
                 Log.wtf(TAG, "Unable to read notification policy", e);
             } catch (NumberFormatException e) {
@@ -974,7 +980,7 @@
                             final int enabled = mPackageManager.getApplicationEnabledSetting(
                                     pkgName,
                                     changeUserId != UserHandle.USER_ALL ? changeUserId :
-                                            UserHandle.USER_SYSTEM);
+                                            USER_SYSTEM);
                             if (enabled == PackageManager.COMPONENT_ENABLED_STATE_ENABLED
                                     || enabled == PackageManager.COMPONENT_ENABLED_STATE_DEFAULT) {
                                 cancelNotifications = false;
@@ -1061,6 +1067,7 @@
                 }
             } else if (action.equals(Intent.ACTION_USER_REMOVED)) {
                 final int user = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, USER_NULL);
+                mUserProfiles.updateCache(context);
                 mZenModeHelper.onUserRemoved(user);
                 mRankingHelper.onUserRemoved(user);
                 mListeners.onUserRemoved(user);
@@ -1268,7 +1275,7 @@
             NotificationAssistants notificationAssistants, ConditionProviders conditionProviders,
             ICompanionDeviceManager companionManager, SnoozeHelper snoozeHelper,
             NotificationUsageStats usageStats, AtomicFile policyFile,
-            ActivityManager activityManager, GroupHelper groupHelper) {
+            ActivityManager activityManager, GroupHelper groupHelper, IActivityManager am) {
         Resources resources = getContext().getResources();
         mMaxPackageEnqueueRate = Settings.Global.getFloat(getContext().getContentResolver(),
                 Settings.Global.MAX_NOTIFICATION_ENQUEUE_RATE,
@@ -1276,7 +1283,7 @@
 
         mAccessibilityManager =
                 (AccessibilityManager) getContext().getSystemService(Context.ACCESSIBILITY_SERVICE);
-        mAm = ActivityManager.getService();
+        mAm = am;
         mPackageManager = packageManager;
         mPackageManagerClient = packageManagerClient;
         mAppOps = (AppOpsManager) getContext().getSystemService(Context.APP_OPS_SERVICE);
@@ -1287,6 +1294,11 @@
         mActivityManager = activityManager;
         mDeviceIdleController = IDeviceIdleController.Stub.asInterface(
                 ServiceManager.getService(Context.DEVICE_IDLE_CONTROLLER));
+        try {
+            mPermissionOwner = mAm.newUriPermissionOwner("notification");
+        } catch (RemoteException e) {
+            Slog.w(TAG, "AM dead", e);
+        }
 
         mHandler = new WorkerHandler(looper);
         mRankingThread.start();
@@ -1415,7 +1427,7 @@
                 null, snoozeHelper, new NotificationUsageStats(getContext()),
                 new AtomicFile(new File(systemDir, "notification_policy.xml"), "notification-policy"),
                 (ActivityManager) getContext().getSystemService(Context.ACTIVITY_SERVICE),
-                getGroupHelper());
+                getGroupHelper(), ActivityManager.getService());
 
         // register for various Intents
         IntentFilter filter = new IntentFilter();
@@ -1749,7 +1761,7 @@
     protected void reportSeen(NotificationRecord r) {
         final int userId = r.sbn.getUserId();
         mAppUsageStats.reportEvent(r.sbn.getPackageName(),
-                userId == UserHandle.USER_ALL ? UserHandle.USER_SYSTEM
+                userId == UserHandle.USER_ALL ? USER_SYSTEM
                         : userId,
                 UsageEvents.Event.NOTIFICATION_SEEN);
     }
@@ -2894,7 +2906,7 @@
             checkCallerIsSystem();
             if (DBG) Slog.d(TAG, "getBackupPayload u=" + user);
             //TODO: http://b/22388012
-            if (user != UserHandle.USER_SYSTEM) {
+            if (user != USER_SYSTEM) {
                 Slog.w(TAG, "getBackupPayload: cannot backup policy for user " + user);
                 return null;
             }
@@ -2920,7 +2932,7 @@
                 return;
             }
             //TODO: http://b/22388012
-            if (user != UserHandle.USER_SYSTEM) {
+            if (user != USER_SYSTEM) {
                 Slog.w(TAG, "applyRestore: cannot restore policy for user " + user);
                 return;
             }
@@ -3678,7 +3690,7 @@
             sbn.getNotification().flags =
                     (r.mOriginalFlags & ~Notification.FLAG_FOREGROUND_SERVICE);
             mRankingHelper.sort(mNotificationList);
-            mListeners.notifyPostedLocked(sbn, sbn /* oldSbn */);
+            mListeners.notifyPostedLocked(r, sbn /* oldSbn */);
         }
     };
 
@@ -3707,7 +3719,7 @@
         try {
             final ApplicationInfo ai = mPackageManagerClient.getApplicationInfoAsUser(
                     pkg, PackageManager.MATCH_DEBUG_TRIAGED_MISSING,
-                    (userId == UserHandle.USER_ALL) ? UserHandle.USER_SYSTEM : userId);
+                    (userId == UserHandle.USER_ALL) ? USER_SYSTEM : userId);
             Notification.addFieldsFromContext(ai, notification);
 
             int canColorize = mPackageManagerClient.checkPermission(
@@ -4126,6 +4138,8 @@
                         // Make sure we don't lose the foreground service state.
                         notification.flags |=
                                 old.getNotification().flags & Notification.FLAG_FOREGROUND_SERVICE;
+                        // revoke uri permissions for changed uris
+                        revokeUriPermissions(r, old);
                         r.isUpdate = true;
                     }
 
@@ -4147,7 +4161,7 @@
 
                     if (notification.getSmallIcon() != null) {
                         StatusBarNotification oldSbn = (old != null) ? old.sbn : null;
-                        mListeners.notifyPostedLocked(n, oldSbn);
+                        mListeners.notifyPostedLocked(r, oldSbn);
                         if (oldSbn == null || !Objects.equals(oldSbn.getGroup(), n.getGroup())) {
                             mHandler.post(new Runnable() {
                                 @Override
@@ -4912,6 +4926,9 @@
             r.recordDismissalSurface(NotificationStats.DISMISSAL_OTHER);
         }
 
+        // Revoke permissions
+        revokeUriPermissions(null, r);
+
         // tell the app
         if (sendDelete) {
             if (r.getNotification().deleteIntent != null) {
@@ -5009,6 +5026,30 @@
                 r.getLifespanMs(now), r.getFreshnessMs(now), r.getExposureMs(now), listenerName);
     }
 
+    void revokeUriPermissions(NotificationRecord newRecord, NotificationRecord oldRecord) {
+        Set<Uri> oldUris = oldRecord.getNotificationUris();
+        Set<Uri> newUris = newRecord == null ? new HashSet<>() : newRecord.getNotificationUris();
+        oldUris.removeAll(newUris);
+
+        long ident = Binder.clearCallingIdentity();
+        try {
+            for (Uri uri : oldUris) {
+                if (uri != null) {
+                    int notiUserId = oldRecord.getUserId();
+                    int sourceUserId = notiUserId == USER_ALL ? USER_SYSTEM
+                            : ContentProvider.getUserIdFromUri(uri, notiUserId);
+                    uri = ContentProvider.getUriWithoutUserId(uri);
+                    mAm.revokeUriPermissionFromOwner(mPermissionOwner,
+                            uri, Intent.FLAG_GRANT_READ_URI_PERMISSION, sourceUserId);
+                }
+            }
+        } catch (RemoteException e) {
+            Log.e(TAG, "Count not revoke uri permissions", e);
+        } finally {
+            Binder.restoreCallingIdentity(ident);
+        }
+    }
+
     /**
      * Cancels a notification ONLY if it has all of the {@code mustHaveFlags}
      * and none of the {@code mustNotHaveFlags}.
@@ -5851,10 +5892,13 @@
          * but isn't anymore.
          */
         @GuardedBy("mNotificationLock")
-        public void notifyPostedLocked(StatusBarNotification sbn, StatusBarNotification oldSbn) {
+        public void notifyPostedLocked(NotificationRecord r, StatusBarNotification oldSbn) {
             // Lazily initialized snapshots of the notification.
+            StatusBarNotification sbn = r.sbn;
             TrimCache trimCache = new TrimCache(sbn);
 
+            Set<Uri> uris = r.getNotificationUris();
+
             for (final ManagedServiceInfo info : getServices()) {
                 boolean sbnVisible = isVisibleToListener(sbn, info);
                 boolean oldSbnVisible = oldSbn != null ? isVisibleToListener(oldSbn, info) : false;
@@ -5877,6 +5921,9 @@
                     continue;
                 }
 
+                grantUriPermissions(uris, sbn.getUserId(), info.component.getPackageName(),
+                        info.userid);
+
                 final StatusBarNotification sbnToPost =  trimCache.ForListener(info);
                 mHandler.post(new Runnable() {
                     @Override
@@ -5887,6 +5934,28 @@
             }
         }
 
+        private void grantUriPermissions(Set<Uri> uris, int notiUserId, String listenerPkg,
+                int listenerUserId) {
+            long ident = Binder.clearCallingIdentity();
+            try {
+                for (Uri uri : uris) {
+                    if (uri != null) {
+                        int sourceUserId = notiUserId == USER_ALL ? USER_SYSTEM
+                                : ContentProvider.getUserIdFromUri(uri, notiUserId);
+                        uri = ContentProvider.getUriWithoutUserId(uri);
+                        mAm.grantUriPermissionFromOwner(mPermissionOwner, Process.myUid(),
+                                listenerPkg,
+                                uri, Intent.FLAG_GRANT_READ_URI_PERMISSION, sourceUserId,
+                                listenerUserId == USER_ALL ? USER_SYSTEM : listenerUserId);
+                    }
+                }
+            } catch (RemoteException e) {
+                Log.e(TAG, "Count not grant uri permission to " + listenerPkg, e);
+            } finally {
+                Binder.restoreCallingIdentity(ident);
+            }
+        }
+
         /**
          * asynchronously notify all listeners about a removed notification
          */
diff --git a/services/core/java/com/android/server/notification/NotificationRecord.java b/services/core/java/com/android/server/notification/NotificationRecord.java
index 23b9743..1ad8c74 100644
--- a/services/core/java/com/android/server/notification/NotificationRecord.java
+++ b/services/core/java/com/android/server/notification/NotificationRecord.java
@@ -38,6 +38,7 @@
 import android.net.Uri;
 import android.os.Build;
 import android.os.Bundle;
+import android.os.Parcelable;
 import android.os.UserHandle;
 import android.provider.Settings;
 import android.service.notification.Adjustment;
@@ -47,6 +48,7 @@
 import android.service.notification.SnoozeCriterion;
 import android.service.notification.StatusBarNotification;
 import android.text.TextUtils;
+import android.util.ArraySet;
 import android.util.Log;
 import android.util.Slog;
 import android.util.TimeUtils;
@@ -64,6 +66,7 @@
 import java.util.Arrays;
 import java.util.List;
 import java.util.Objects;
+import java.util.Set;
 
 /**
  * Holds data about notifications that should not be shared with the
@@ -929,6 +932,42 @@
         mStats.setViewedSettings();
     }
 
+    public Set<Uri> getNotificationUris() {
+        Notification notification = getNotification();
+        Set<Uri> uris = new ArraySet<>();
+
+        if (notification.sound != null) {
+            uris.add(notification.sound);
+        }
+        if (notification.getChannelId() != null) {
+            NotificationChannel channel = getChannel();
+            if (channel != null && channel.getSound() != null) {
+                uris.add(channel.getSound());
+            }
+        }
+        if (notification.extras.containsKey(Notification.EXTRA_AUDIO_CONTENTS_URI)) {
+            uris.add(notification.extras.getParcelable(Notification.EXTRA_AUDIO_CONTENTS_URI));
+        }
+        if (notification.extras.containsKey(Notification.EXTRA_BACKGROUND_IMAGE_URI)) {
+            uris.add(notification.extras.getParcelable(Notification.EXTRA_BACKGROUND_IMAGE_URI));
+        }
+        if (Notification.MessagingStyle.class.equals(notification.getNotificationStyle())) {
+            Parcelable[] newMessages =
+                    notification.extras.getParcelableArray(Notification.EXTRA_MESSAGES);
+            List<Notification.MessagingStyle.Message> messages
+                    = Notification.MessagingStyle.Message.getMessagesFromBundleArray(newMessages);
+            Parcelable[] histMessages =
+                    notification.extras.getParcelableArray(Notification.EXTRA_HISTORIC_MESSAGES);
+            messages.addAll(
+                    Notification.MessagingStyle.Message.getMessagesFromBundleArray(histMessages));
+            for (Notification.MessagingStyle.Message message : messages) {
+                uris.add(message.getDataUri());
+            }
+        }
+
+        return uris;
+    }
+
     public LogMaker getLogMaker(long now) {
         if (mLogMaker == null) {
             // initialize fields that only change on update (so a new record)
diff --git a/services/core/java/com/android/server/om/OverlayManagerServiceImpl.java b/services/core/java/com/android/server/om/OverlayManagerServiceImpl.java
index 7600e81..6e02db7 100644
--- a/services/core/java/com/android/server/om/OverlayManagerServiceImpl.java
+++ b/services/core/java/com/android/server/om/OverlayManagerServiceImpl.java
@@ -20,6 +20,8 @@
 import static android.content.om.OverlayInfo.STATE_ENABLED;
 import static android.content.om.OverlayInfo.STATE_MISSING_TARGET;
 import static android.content.om.OverlayInfo.STATE_NO_IDMAP;
+import static android.content.om.OverlayInfo.STATE_OVERLAY_UPGRADING;
+import static android.content.om.OverlayInfo.STATE_TARGET_UPGRADING;
 
 import static com.android.server.om.OverlayManagerService.DEBUG;
 import static com.android.server.om.OverlayManagerService.TAG;
@@ -50,6 +52,10 @@
  * @see OverlayManagerService
  */
 final class OverlayManagerServiceImpl {
+    // Flags to use in conjunction with updateState.
+    private static final int FLAG_TARGET_IS_UPGRADING = 1<<0;
+    private static final int FLAG_OVERLAY_IS_UPGRADING = 1<<1;
+
     private final PackageManagerHelper mPackageManager;
     private final IdmapManager mIdmapManager;
     private final OverlayManagerSettings mSettings;
@@ -123,9 +129,7 @@
             }
 
             try {
-                final PackageInfo targetPackage =
-                        mPackageManager.getPackageInfo(overlayPackage.overlayTarget, newUserId);
-                updateState(targetPackage, overlayPackage, newUserId);
+                updateState(overlayPackage.overlayTarget, overlayPackage.packageName, newUserId, 0);
             } catch (OverlayManagerSettings.BadKeyException e) {
                 Slog.e(TAG, "failed to update settings", e);
                 mSettings.remove(overlayPackage.packageName, newUserId);
@@ -168,8 +172,7 @@
             Slog.d(TAG, "onTargetPackageAdded packageName=" + packageName + " userId=" + userId);
         }
 
-        final PackageInfo targetPackage = mPackageManager.getPackageInfo(packageName, userId);
-        if (updateAllOverlaysForTarget(packageName, userId, targetPackage)) {
+        if (updateAllOverlaysForTarget(packageName, userId, 0)) {
             mListener.onOverlaysChanged(packageName, userId);
         }
     }
@@ -179,18 +182,18 @@
             Slog.d(TAG, "onTargetPackageChanged packageName=" + packageName + " userId=" + userId);
         }
 
-        final PackageInfo targetPackage = mPackageManager.getPackageInfo(packageName, userId);
-        if (updateAllOverlaysForTarget(packageName, userId, targetPackage)) {
+        if (updateAllOverlaysForTarget(packageName, userId, 0)) {
             mListener.onOverlaysChanged(packageName, userId);
         }
     }
 
     void onTargetPackageUpgrading(@NonNull final String packageName, final int userId) {
         if (DEBUG) {
-            Slog.d(TAG, "onTargetPackageUpgrading packageName=" + packageName + " userId=" + userId);
+            Slog.d(TAG, "onTargetPackageUpgrading packageName=" + packageName + " userId="
+                    + userId);
         }
 
-        if (updateAllOverlaysForTarget(packageName, userId, null)) {
+        if (updateAllOverlaysForTarget(packageName, userId, FLAG_TARGET_IS_UPGRADING)) {
             mListener.onOverlaysChanged(packageName, userId);
         }
     }
@@ -200,8 +203,7 @@
             Slog.d(TAG, "onTargetPackageUpgraded packageName=" + packageName + " userId=" + userId);
         }
 
-        final PackageInfo targetPackage = mPackageManager.getPackageInfo(packageName, userId);
-        if (updateAllOverlaysForTarget(packageName, userId, targetPackage)) {
+        if (updateAllOverlaysForTarget(packageName, userId, 0)) {
             mListener.onOverlaysChanged(packageName, userId);
         }
     }
@@ -211,7 +213,7 @@
             Slog.d(TAG, "onTargetPackageRemoved packageName=" + packageName + " userId=" + userId);
         }
 
-        if (updateAllOverlaysForTarget(packageName, userId, null)) {
+        if (updateAllOverlaysForTarget(packageName, userId, 0)) {
             mListener.onOverlaysChanged(packageName, userId);
         }
     }
@@ -219,20 +221,21 @@
     /**
      * Returns true if the settings were modified for this target.
      */
-    private boolean updateAllOverlaysForTarget(@NonNull final String packageName, final int userId,
-            @Nullable final PackageInfo targetPackage) {
+    private boolean updateAllOverlaysForTarget(@NonNull final String targetPackageName,
+            final int userId, final int flags) {
         boolean modified = false;
-        final List<OverlayInfo> ois = mSettings.getOverlaysForTarget(packageName, userId);
+        final List<OverlayInfo> ois = mSettings.getOverlaysForTarget(targetPackageName, userId);
         final int N = ois.size();
         for (int i = 0; i < N; i++) {
             final OverlayInfo oi = ois.get(i);
-            final PackageInfo overlayPackage = mPackageManager.getPackageInfo(oi.packageName, userId);
+            final PackageInfo overlayPackage = mPackageManager.getPackageInfo(oi.packageName,
+                    userId);
             if (overlayPackage == null) {
                 modified |= mSettings.remove(oi.packageName, oi.userId);
                 removeIdmapIfPossible(oi);
             } else {
                 try {
-                    modified |= updateState(targetPackage, overlayPackage, userId);
+                    modified |= updateState(targetPackageName, oi.packageName, userId, flags);
                 } catch (OverlayManagerSettings.BadKeyException e) {
                     Slog.e(TAG, "failed to update settings", e);
                     modified |= mSettings.remove(oi.packageName, userId);
@@ -254,14 +257,11 @@
             return;
         }
 
-        final PackageInfo targetPackage =
-                mPackageManager.getPackageInfo(overlayPackage.overlayTarget, userId);
-
         mSettings.init(packageName, userId, overlayPackage.overlayTarget,
                 overlayPackage.applicationInfo.getBaseCodePath(),
                 overlayPackage.isStaticOverlayPackage(), overlayPackage.overlayPriority);
         try {
-            if (updateState(targetPackage, overlayPackage, userId)) {
+            if (updateState(overlayPackage.overlayTarget, packageName, userId, 0)) {
                 mListener.onOverlaysChanged(overlayPackage.overlayTarget, userId);
             }
         } catch (OverlayManagerSettings.BadKeyException e) {
@@ -271,15 +271,64 @@
     }
 
     void onOverlayPackageChanged(@NonNull final String packageName, final int userId) {
-        Slog.wtf(TAG, "onOverlayPackageChanged called, but only pre-installed overlays supported");
+        if (DEBUG) {
+            Slog.d(TAG, "onOverlayPackageChanged packageName=" + packageName + " userId=" + userId);
+        }
+
+        try {
+            final OverlayInfo oi = mSettings.getOverlayInfo(packageName, userId);
+            if (updateState(oi.targetPackageName, packageName, userId, 0)) {
+                mListener.onOverlaysChanged(oi.targetPackageName, userId);
+            }
+        } catch (OverlayManagerSettings.BadKeyException e) {
+            Slog.e(TAG, "failed to update settings", e);
+        }
     }
 
     void onOverlayPackageUpgrading(@NonNull final String packageName, final int userId) {
-        Slog.wtf(TAG, "onOverlayPackageUpgrading called, but only pre-installed overlays supported");
+        if (DEBUG) {
+            Slog.d(TAG, "onOverlayPackageUpgrading packageName=" + packageName + " userId="
+                    + userId);
+        }
+
+        try {
+            final OverlayInfo oi = mSettings.getOverlayInfo(packageName, userId);
+            if (updateState(oi.targetPackageName, packageName, userId, FLAG_OVERLAY_IS_UPGRADING)) {
+                removeIdmapIfPossible(oi);
+                mListener.onOverlaysChanged(oi.targetPackageName, userId);
+            }
+        } catch (OverlayManagerSettings.BadKeyException e) {
+            Slog.e(TAG, "failed to update settings", e);
+        }
     }
 
     void onOverlayPackageUpgraded(@NonNull final String packageName, final int userId) {
-        Slog.wtf(TAG, "onOverlayPackageUpgraded called, but only pre-installed overlays supported");
+        if (DEBUG) {
+            Slog.d(TAG, "onOverlayPackageUpgraded packageName=" + packageName + " userId="
+                    + userId);
+        }
+
+        final PackageInfo pkg = mPackageManager.getPackageInfo(packageName, userId);
+        if (pkg == null) {
+            Slog.w(TAG, "overlay package " + packageName + " was upgraded, but couldn't be found");
+            onOverlayPackageRemoved(packageName, userId);
+            return;
+        }
+
+        try {
+            final OverlayInfo oldOi = mSettings.getOverlayInfo(packageName, userId);
+            if (!oldOi.targetPackageName.equals(pkg.overlayTarget)) {
+                mSettings.init(packageName, userId, pkg.overlayTarget,
+                        pkg.applicationInfo.getBaseCodePath(), pkg.isStaticOverlayPackage(),
+                        pkg.overlayPriority);
+            }
+
+            if (updateState(pkg.overlayTarget, packageName, userId, 0)) {
+                mListener.onOverlaysChanged(pkg.overlayTarget, userId);
+            }
+        } catch (OverlayManagerSettings.BadKeyException e) {
+            Slog.e(TAG, "failed to update settings", e);
+        }
     }
 
     void onOverlayPackageRemoved(@NonNull final String packageName, final int userId) {
@@ -333,10 +382,8 @@
 
         try {
             final OverlayInfo oi = mSettings.getOverlayInfo(packageName, userId);
-            final PackageInfo targetPackage =
-                    mPackageManager.getPackageInfo(oi.targetPackageName, userId);
             boolean modified = mSettings.setEnabled(packageName, userId, enable);
-            modified |= updateState(targetPackage, overlayPackage, userId);
+            modified |= updateState(oi.targetPackageName, oi.packageName, userId, 0);
 
             if (modified) {
                 mListener.onOverlaysChanged(oi.targetPackageName, userId);
@@ -349,7 +396,8 @@
 
     boolean setEnabledExclusive(@NonNull final String packageName, final int userId) {
         if (DEBUG) {
-            Slog.d(TAG, String.format("setEnabledExclusive packageName=%s userId=%d", packageName, userId));
+            Slog.d(TAG, String.format("setEnabledExclusive packageName=%s userId=%d", packageName,
+                    userId));
         }
 
         final PackageInfo overlayPackage = mPackageManager.getPackageInfo(packageName, userId);
@@ -359,10 +407,9 @@
 
         try {
             final OverlayInfo oi = mSettings.getOverlayInfo(packageName, userId);
-            final PackageInfo targetPackage =
-                    mPackageManager.getPackageInfo(oi.targetPackageName, userId);
+            final String targetPackageName = oi.targetPackageName;
 
-            List<OverlayInfo> allOverlays = getOverlayInfosForTarget(oi.targetPackageName, userId);
+            List<OverlayInfo> allOverlays = getOverlayInfosForTarget(targetPackageName, userId);
 
             boolean modified = false;
 
@@ -384,15 +431,15 @@
 
                 // Disable the overlay.
                 modified |= mSettings.setEnabled(disabledOverlayPackageName, userId, false);
-                modified |= updateState(targetPackage, disabledOverlayPackageInfo, userId);
+                modified |= updateState(targetPackageName, disabledOverlayPackageName, userId, 0);
             }
 
             // Enable the selected overlay.
             modified |= mSettings.setEnabled(packageName, userId, true);
-            modified |= updateState(targetPackage, overlayPackage, userId);
+            modified |= updateState(targetPackageName, packageName, userId, 0);
 
             if (modified) {
-                mListener.onOverlaysChanged(oi.targetPackageName, userId);
+                mListener.onOverlaysChanged(targetPackageName, userId);
             }
             return true;
         } catch (OverlayManagerSettings.BadKeyException e) {
@@ -477,7 +524,8 @@
 
     List<String> getEnabledOverlayPackageNames(@NonNull final String targetPackageName,
             final int userId) {
-        final List<OverlayInfo> overlays = mSettings.getOverlaysForTarget(targetPackageName, userId);
+        final List<OverlayInfo> overlays = mSettings.getOverlaysForTarget(targetPackageName,
+                userId);
         final List<String> paths = new ArrayList<>(overlays.size());
         final int N = overlays.size();
         for (int i = 0; i < N; i++) {
@@ -492,36 +540,59 @@
     /**
      * Returns true if the settings/state was modified, false otherwise.
      */
-    private boolean updateState(@Nullable final PackageInfo targetPackage,
-            @NonNull final PackageInfo overlayPackage, final int userId)
+    private boolean updateState(@NonNull final String targetPackageName,
+            @NonNull final String overlayPackageName, final int userId, final int flags)
             throws OverlayManagerSettings.BadKeyException {
+
+        final PackageInfo targetPackage = mPackageManager.getPackageInfo(targetPackageName, userId);
+        final PackageInfo overlayPackage = mPackageManager.getPackageInfo(overlayPackageName,
+                userId);
+
         // Static RROs targeting to "android", ie framework-res.apk, are handled by native layers.
-        if (targetPackage != null &&
-                !("android".equals(targetPackage.packageName)
+        if (targetPackage != null && overlayPackage != null &&
+                !("android".equals(targetPackageName)
                         && overlayPackage.isStaticOverlayPackage())) {
             mIdmapManager.createIdmap(targetPackage, overlayPackage, userId);
         }
 
-        boolean modified = mSettings.setBaseCodePath(overlayPackage.packageName, userId,
-                overlayPackage.applicationInfo.getBaseCodePath());
+        boolean modified = false;
+        if (overlayPackage != null) {
+            modified |= mSettings.setBaseCodePath(overlayPackageName, userId,
+                    overlayPackage.applicationInfo.getBaseCodePath());
+        }
 
-        final int currentState = mSettings.getState(overlayPackage.packageName, userId);
-        final int newState = calculateNewState(targetPackage, overlayPackage, userId);
+        final @OverlayInfo.State int currentState = mSettings.getState(overlayPackageName, userId);
+        final @OverlayInfo.State int newState = calculateNewState(targetPackage, overlayPackage,
+                userId, flags);
         if (currentState != newState) {
             if (DEBUG) {
                 Slog.d(TAG, String.format("%s:%d: %s -> %s",
-                            overlayPackage.packageName, userId,
+                            overlayPackageName, userId,
                             OverlayInfo.stateToString(currentState),
                             OverlayInfo.stateToString(newState)));
             }
-            modified |= mSettings.setState(overlayPackage.packageName, userId, newState);
+            modified |= mSettings.setState(overlayPackageName, userId, newState);
         }
         return modified;
     }
 
-    private int calculateNewState(@Nullable final PackageInfo targetPackage,
-            @NonNull final PackageInfo overlayPackage, final int userId)
+    private @OverlayInfo.State int calculateNewState(@Nullable final PackageInfo targetPackage,
+            @Nullable final PackageInfo overlayPackage, final int userId, final int flags)
         throws OverlayManagerSettings.BadKeyException {
+
+        if ((flags & FLAG_TARGET_IS_UPGRADING) != 0) {
+            return STATE_TARGET_UPGRADING;
+        }
+
+        if ((flags & FLAG_OVERLAY_IS_UPGRADING) != 0) {
+            return STATE_OVERLAY_UPGRADING;
+        }
+
+        // assert expectation on overlay package: can only be null if the flags are used
+        if (DEBUG && overlayPackage == null) {
+            throw new IllegalArgumentException("null overlay package not compatible with no flags");
+        }
+
         if (targetPackage == null) {
             return STATE_MISSING_TARGET;
         }
diff --git a/services/core/java/com/android/server/om/OverlayManagerSettings.java b/services/core/java/com/android/server/om/OverlayManagerSettings.java
index 17b38de..a80cae4 100644
--- a/services/core/java/com/android/server/om/OverlayManagerSettings.java
+++ b/services/core/java/com/android/server/om/OverlayManagerSettings.java
@@ -145,7 +145,8 @@
         return mItems.get(idx).setEnabled(enable);
     }
 
-    int getState(@NonNull final String packageName, final int userId) throws BadKeyException {
+    @OverlayInfo.State int getState(@NonNull final String packageName, final int userId)
+            throws BadKeyException {
         final int idx = select(packageName, userId);
         if (idx < 0) {
             throw new BadKeyException(packageName, userId);
@@ -156,8 +157,8 @@
     /**
      * Returns true if the settings were modified, false if they remain the same.
      */
-    boolean setState(@NonNull final String packageName, final int userId, final int state)
-            throws BadKeyException {
+    boolean setState(@NonNull final String packageName, final int userId,
+            final @OverlayInfo.State int state) throws BadKeyException {
         final int idx = select(packageName, userId);
         if (idx < 0) {
             throw new BadKeyException(packageName, userId);
@@ -413,7 +414,7 @@
         private final String mPackageName;
         private final String mTargetPackageName;
         private String mBaseCodePath;
-        private int mState;
+        private @OverlayInfo.State int mState;
         private boolean mIsEnabled;
         private OverlayInfo mCache;
         private boolean mIsStatic;
@@ -421,7 +422,7 @@
 
         SettingsItem(@NonNull final String packageName, final int userId,
                 @NonNull final String targetPackageName, @NonNull final String baseCodePath,
-                final int state, final boolean isEnabled, final boolean isStatic,
+                final @OverlayInfo.State int state, final boolean isEnabled, final boolean isStatic,
                 final int priority) {
             mPackageName = packageName;
             mUserId = userId;
@@ -462,11 +463,11 @@
             return false;
         }
 
-        private int getState() {
+        private @OverlayInfo.State int getState() {
             return mState;
         }
 
-        private boolean setState(final int state) {
+        private boolean setState(final @OverlayInfo.State int state) {
             if (mState != state) {
                 mState = state;
                 invalidateCache();
diff --git a/services/core/java/com/android/server/pm/InstantAppResolver.java b/services/core/java/com/android/server/pm/InstantAppResolver.java
index af446ba..6e898bb 100644
--- a/services/core/java/com/android/server/pm/InstantAppResolver.java
+++ b/services/core/java/com/android/server/pm/InstantAppResolver.java
@@ -377,7 +377,7 @@
         failureIntent.setFlags(failureIntent.getFlags() | Intent.FLAG_IGNORE_EPHEMERAL);
         failureIntent.setLaunchToken(token);
         ArrayList<AuxiliaryResolveInfo.AuxiliaryFilter> filters = null;
-        boolean isWebIntent = origIntent.isBrowsableWebIntent();
+        boolean isWebIntent = origIntent.isWebIntent();
         for (InstantAppResolveInfo instantAppResolveInfo : instantAppResolveInfoList) {
             if (shaPrefix.length > 0 && instantAppResolveInfo.shouldLetInstallerDecide()) {
                 Slog.e(TAG, "InstantAppResolveInfo with mShouldLetInstallerDecide=true when digest"
@@ -448,7 +448,7 @@
                 instantAppInfo.getIntentFilters();
         if (instantAppFilters == null || instantAppFilters.isEmpty()) {
             // No filters on web intent; no matches, 2nd phase unnecessary.
-            if (origIntent.isBrowsableWebIntent()) {
+            if (origIntent.isWebIntent()) {
                 return null;
             }
             // No filters; we need to start phase two
diff --git a/services/core/java/com/android/server/pm/PackageManagerService.java b/services/core/java/com/android/server/pm/PackageManagerService.java
index 940d19f..2816bbd 100644
--- a/services/core/java/com/android/server/pm/PackageManagerService.java
+++ b/services/core/java/com/android/server/pm/PackageManagerService.java
@@ -2746,12 +2746,6 @@
                                 mSettings.getDisabledSystemPkgLPr(ps.name);
                         if (disabledPs.codePath == null || !disabledPs.codePath.exists()
                                 || disabledPs.pkg == null) {
-if (REFACTOR_DEBUG) {
-Slog.e("TODD",
-        "Possibly deleted app: " + ps.dumpState_temp()
-        + "; path: " + (disabledPs.codePath == null ? "<<NULL>>":disabledPs.codePath)
-        + "; pkg: " + (disabledPs.pkg==null?"<<NULL>>":disabledPs.pkg.toString()));
-}
                             possiblyDeletedUpdatedSystemApps.add(ps.name);
                         }
                     }
@@ -2803,10 +2797,6 @@
                 for (String deletedAppName : possiblyDeletedUpdatedSystemApps) {
                     PackageParser.Package deletedPkg = mPackages.get(deletedAppName);
                     mSettings.removeDisabledSystemPackageLPw(deletedAppName);
-if (REFACTOR_DEBUG) {
-Slog.e("TODD",
-        "remove update; name: " + deletedAppName + ", exists? " + (deletedPkg != null));
-}
                     final String msg;
                     if (deletedPkg == null) {
                         // should have found an update, but, we didn't; remove everything
@@ -5982,7 +5972,7 @@
         if (!skipPackageCheck && intent.getPackage() != null) {
             return false;
         }
-        if (!intent.isBrowsableWebIntent()) {
+        if (!intent.isWebIntent()) {
             // for non web intents, we should not resolve externally if an app already exists to
             // handle it or if the caller didn't explicitly request it.
             if ((resolvedActivities != null && resolvedActivities.size() != 0)
@@ -6693,7 +6683,7 @@
                                         ai.packageName, ai.versionCode, null /* splitName */);
             }
         }
-        if (intent.isBrowsableWebIntent() && auxiliaryResponse == null) {
+        if (intent.isWebIntent() && auxiliaryResponse == null) {
             return result;
         }
         final PackageSetting ps = mSettings.mPackages.get(mInstantAppInstallerActivity.packageName);
@@ -8534,8 +8524,6 @@
         return false;
     }
 
-    // Temporary to catch potential issues with refactoring
-    private static boolean REFACTOR_DEBUG = true;
     /**
      * Adds a new package to the internal data structures during platform initialization.
      * <p>After adding, the package is known to the system and available for querying.
@@ -8576,10 +8564,6 @@
         synchronized (mPackages) {
             renamedPkgName = mSettings.getRenamedPackageLPr(pkg.mRealPackage);
             final String realPkgName = getRealPackageName(pkg, renamedPkgName);
-if (REFACTOR_DEBUG) {
-Slog.e("TODD",
-        "Add pkg: " + pkg.packageName + (realPkgName==null?"":", realName: " + realPkgName));
-}
             if (realPkgName != null) {
                 ensurePackageRenamed(pkg, renamedPkgName);
             }
@@ -8594,12 +8578,6 @@
             if (DEBUG_INSTALL && isSystemPkgUpdated) {
                 Slog.d(TAG, "updatedPkg = " + disabledPkgSetting);
             }
-if (REFACTOR_DEBUG) {
-Slog.e("TODD",
-        "SSP? " + scanSystemPartition
-        + ", exists? " + pkgAlreadyExists + (pkgAlreadyExists?" "+pkgSetting.toString():"")
-        + ", upgraded? " + isSystemPkgUpdated + (isSystemPkgUpdated?" "+disabledPkgSetting.toString():""));
-}
 
             final SharedUserSetting sharedUserSetting = (pkg.mSharedUserId != null)
                     ? mSettings.getSharedUserLPw(pkg.mSharedUserId,
@@ -8611,12 +8589,6 @@
                 Log.d(TAG, "Shared UserID " + pkg.mSharedUserId
                         + " (uid=" + sharedUserSetting.userId + "):"
                         + " packages=" + sharedUserSetting.packages);
-if (REFACTOR_DEBUG) {
-Slog.e("TODD",
-        "Shared UserID " + pkg.mSharedUserId
-        + " (uid=" + sharedUserSetting.userId + "):"
-        + " packages=" + sharedUserSetting.packages);
-}
             }
 
             if (scanSystemPartition) {
@@ -8625,10 +8597,6 @@
                 // version on /data, cycle through all of its children packages and
                 // remove children that are no longer defined.
                 if (isSystemPkgUpdated) {
-if (REFACTOR_DEBUG) {
-Slog.e("TODD",
-        "Disable child packages");
-}
                     final int scannedChildCount = (pkg.childPackages != null)
                             ? pkg.childPackages.size() : 0;
                     final int disabledChildCount = disabledPkgSetting.childPackageNames != null
@@ -8640,19 +8608,11 @@
                         for (int j = 0; j < scannedChildCount; j++) {
                             PackageParser.Package childPkg = pkg.childPackages.get(j);
                             if (childPkg.packageName.equals(disabledChildPackageName)) {
-if (REFACTOR_DEBUG) {
-Slog.e("TODD",
-        "Ignore " + disabledChildPackageName);
-}
                                 disabledPackageAvailable = true;
                                 break;
                             }
                         }
                         if (!disabledPackageAvailable) {
-if (REFACTOR_DEBUG) {
-Slog.e("TODD",
-        "Disable " + disabledChildPackageName);
-}
                             mSettings.removeDisabledSystemPackageLPw(disabledChildPackageName);
                         }
                     }
@@ -8661,44 +8621,17 @@
                             disabledPkgSetting /* pkgSetting */, null /* disabledPkgSetting */,
                             null /* originalPkgSetting */, null, parseFlags, scanFlags,
                             (pkg == mPlatformPackage), user);
-if (REFACTOR_DEBUG) {
-Slog.e("TODD",
-        "Scan disabled system package");
-Slog.e("TODD",
-        "Pre: " + request.pkgSetting.dumpState_temp());
-}
-final ScanResult result =
                     scanPackageOnlyLI(request, mFactoryTest, -1L);
-if (REFACTOR_DEBUG) {
-Slog.e("TODD",
-        "Post: " + (result.success?result.pkgSetting.dumpState_temp():"FAILED scan"));
-}
                 }
             }
         }
 
         final boolean newPkgChangedPaths =
                 pkgAlreadyExists && !pkgSetting.codePathString.equals(pkg.codePath);
-if (REFACTOR_DEBUG) {
-Slog.e("TODD",
-        "paths changed? " + newPkgChangedPaths
-        + "; old: " + pkg.codePath
-        + ", new: " + (pkgSetting==null?"<<NULL>>":pkgSetting.codePathString));
-}
         final boolean newPkgVersionGreater =
                 pkgAlreadyExists && pkg.getLongVersionCode() > pkgSetting.versionCode;
-if (REFACTOR_DEBUG) {
-Slog.e("TODD",
-        "version greater? " + newPkgVersionGreater
-        + "; old: " + pkg.getLongVersionCode()
-        + ", new: " + (pkgSetting==null?"<<NULL>>":pkgSetting.versionCode));
-}
         final boolean isSystemPkgBetter = scanSystemPartition && isSystemPkgUpdated
                 && newPkgChangedPaths && newPkgVersionGreater;
-if (REFACTOR_DEBUG) {
-    Slog.e("TODD",
-            "system better? " + isSystemPkgBetter);
-}
         if (isSystemPkgBetter) {
             // The version of the application on /system is greater than the version on
             // /data. Switch back to the application on /system.
@@ -8714,13 +8647,6 @@
                     + " name: " + pkgSetting.name
                     + "; " + pkgSetting.versionCode + " --> " + pkg.getLongVersionCode()
                     + "; " + pkgSetting.codePathString + " --> " + pkg.codePath);
-if (REFACTOR_DEBUG) {
-Slog.e("TODD",
-        "System package changed;"
-        + " name: " + pkgSetting.name
-        + "; " + pkgSetting.versionCode + " --> " + pkg.getLongVersionCode()
-        + "; " + pkgSetting.codePathString + " --> " + pkg.codePath);
-}
 
             final InstallArgs args = createInstallArgsForExisting(
                     packageFlagsToInstallFlags(pkgSetting), pkgSetting.codePathString,
@@ -8732,10 +8658,6 @@
         }
 
         if (scanSystemPartition && isSystemPkgUpdated && !isSystemPkgBetter) {
-if (REFACTOR_DEBUG) {
-Slog.e("TODD",
-        "THROW exception; system pkg version not good enough");
-}
             // The version of the application on the /system partition is less than or
             // equal to the version on the /data partition. Throw an exception and use
             // the application already installed on the /data partition.
@@ -8766,11 +8688,6 @@
                 logCriticalInfo(Log.WARN,
                         "System package signature mismatch;"
                         + " name: " + pkgSetting.name);
-if (REFACTOR_DEBUG) {
-Slog.e("TODD",
-        "System package signature mismatch;"
-        + " name: " + pkgSetting.name);
-}
                 try (PackageFreezer freezer = freezePackage(pkg.packageName,
                         "scanPackageInternalLI")) {
                     deletePackageLIF(pkg.packageName, null, true, null, 0, null, false, null);
@@ -8785,13 +8702,6 @@
                         + " name: " + pkgSetting.name
                         + "; " + pkgSetting.versionCode + " --> " + pkg.getLongVersionCode()
                         + "; " + pkgSetting.codePathString + " --> " + pkg.codePath);
-if (REFACTOR_DEBUG) {
-Slog.e("TODD",
-        "System package enabled;"
-        + " name: " + pkgSetting.name
-        + "; " + pkgSetting.versionCode + " --> " + pkg.getLongVersionCode()
-        + "; " + pkgSetting.codePathString + " --> " + pkg.codePath);
-}
                 InstallArgs args = createInstallArgsForExisting(
                         packageFlagsToInstallFlags(pkgSetting), pkgSetting.codePathString,
                         pkgSetting.resourcePathString, getAppDexInstructionSets(pkgSetting));
@@ -8808,35 +8718,13 @@
                         + " name: " + pkgSetting.name
                         + "; old: " + pkgSetting.codePathString + " @ " + pkgSetting.versionCode
                         + "; new: " + pkg.codePath + " @ " + pkg.codePath);
-if (REFACTOR_DEBUG) {
-Slog.e("TODD",
-        "System package disabled;"
-        + " name: " + pkgSetting.name
-        + "; old: " + pkgSetting.codePathString + " @ " + pkgSetting.versionCode
-        + "; new: " + pkg.codePath + " @ " + pkg.codePath);
-}
             }
         }
 
-if (REFACTOR_DEBUG) {
-Slog.e("TODD",
-        "Scan package");
-Slog.e("TODD",
-        "Pre: " + (pkgSetting==null?"<<NONE>>":pkgSetting.dumpState_temp()));
-}
         final PackageParser.Package scannedPkg = scanPackageNewLI(pkg, parseFlags, scanFlags
                 | SCAN_UPDATE_SIGNATURE, currentTime, user);
-if (REFACTOR_DEBUG) {
-pkgSetting = mSettings.getPackageLPr(pkg.packageName);
-Slog.e("TODD",
-        "Post: " + (pkgSetting==null?"<<NONE>>":pkgSetting.dumpState_temp()));
-}
 
         if (shouldHideSystemApp) {
-if (REFACTOR_DEBUG) {
-Slog.e("TODD",
-        "Disable package: " + pkg.packageName);
-}
             synchronized (mPackages) {
                 mSettings.disableSystemPackageLPw(pkg.packageName, true);
             }
diff --git a/services/core/java/com/android/server/pm/PackageSetting.java b/services/core/java/com/android/server/pm/PackageSetting.java
index 3e2bd4a..ea05b74 100644
--- a/services/core/java/com/android/server/pm/PackageSetting.java
+++ b/services/core/java/com/android/server/pm/PackageSetting.java
@@ -97,35 +97,6 @@
             + " " + name + "/" + appId + "}";
     }
 
-    // Temporary to catch potential issues with refactoring
-    public String dumpState_temp() {
-        String flags = "";
-        flags += ((pkgFlags & ApplicationInfo.FLAG_UPDATED_SYSTEM_APP) != 0 ? "U" : "");
-        flags += ((pkgFlags & ApplicationInfo.FLAG_SYSTEM) != 0 ? "S" : "");
-        if ("".equals(flags)) {
-            flags = "-";
-        }
-        String privFlags = "";
-        privFlags += ((pkgPrivateFlags & ApplicationInfo.PRIVATE_FLAG_PRIVILEGED) != 0 ? "P" : "");
-        privFlags += ((pkgPrivateFlags & ApplicationInfo.PRIVATE_FLAG_OEM) != 0 ? "O" : "");
-        privFlags += ((pkgPrivateFlags & ApplicationInfo.PRIVATE_FLAG_VENDOR) != 0 ? "V" : "");
-        if ("".equals(privFlags)) {
-            privFlags = "-";
-        }
-        return "PackageSetting{"
-                + Integer.toHexString(System.identityHashCode(this))
-                + " " + name + (realName == null ? "" : "("+realName+")") + "/" + appId + (sharedUser==null?"":" u:" + sharedUser.name+"("+sharedUserId+")")
-                + ", ver:" + versionCode
-                + ", path: " + codePath
-                + ", pABI: " + primaryCpuAbiString
-                + ", sABI: " + secondaryCpuAbiString
-                + ", oABI: " + cpuAbiOverrideString
-                + ", flags: " + flags
-                + ", privFlags: " + privFlags
-                + ", pkg: " + (pkg == null ? "<<NULL>>" : pkg.dumpState_temp())
-                + "}";
-    }
-
     public void copyFrom(PackageSetting orig) {
         super.copyFrom(orig);
         doCopy(orig);
diff --git a/services/core/java/com/android/server/stats/StatsCompanionService.java b/services/core/java/com/android/server/stats/StatsCompanionService.java
index f498cdd..c280739 100644
--- a/services/core/java/com/android/server/stats/StatsCompanionService.java
+++ b/services/core/java/com/android/server/stats/StatsCompanionService.java
@@ -113,7 +113,6 @@
         new StatFs(Environment.getRootDirectory().getAbsolutePath());
     private final StatFs mStatFsTemp =
         new StatFs(Environment.getDownloadCacheDirectory().getAbsolutePath());
-    private final BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
 
     public StatsCompanionService(Context context) {
         super();
@@ -677,12 +676,13 @@
     }
 
     private synchronized BluetoothActivityEnergyInfo pullBluetoothData() {
+        final BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
       if (adapter != null) {
-        SynchronousResultReceiver bluetoothReceiver = null;
-        bluetoothReceiver = new SynchronousResultReceiver("bluetooth");
+        SynchronousResultReceiver bluetoothReceiver = new SynchronousResultReceiver("bluetooth");
         adapter.requestControllerActivityEnergyInfo(bluetoothReceiver);
         return awaitControllerInfo(bluetoothReceiver);
       } else {
+          Slog.e(TAG, "Failed to get bluetooth adapter!");
         return null;
       }
     }
diff --git a/services/core/java/com/android/server/wm/Dimmer.java b/services/core/java/com/android/server/wm/Dimmer.java
index b435605..4394a99 100644
--- a/services/core/java/com/android/server/wm/Dimmer.java
+++ b/services/core/java/com/android/server/wm/Dimmer.java
@@ -16,7 +16,6 @@
 
 package com.android.server.wm;
 
-import android.util.ArrayMap;
 import android.view.SurfaceControl;
 import android.graphics.Rect;
 
@@ -124,17 +123,30 @@
         }
     }
 
-    @VisibleForTesting
-    ArrayMap<WindowContainer, DimState> mDimLayerUsers = new ArrayMap<>();
-
     /**
      * The {@link WindowContainer} that our Dim's are bounded to. We may be dimming on behalf of the
      * host, some controller of it, or one of the hosts children.
      */
     private WindowContainer mHost;
+    private WindowContainer mLastRequestedDimContainer;
+    @VisibleForTesting
+    DimState mDimState;
+
+    private final SurfaceAnimatorStarter mSurfaceAnimatorStarter;
+
+    @VisibleForTesting
+    interface SurfaceAnimatorStarter {
+        void startAnimation(SurfaceAnimator surfaceAnimator, SurfaceControl.Transaction t,
+                AnimationAdapter anim, boolean hidden);
+    }
 
     Dimmer(WindowContainer host) {
+        this(host, SurfaceAnimator::startAnimation);
+    }
+
+    Dimmer(WindowContainer host, SurfaceAnimatorStarter surfaceAnimatorStarter) {
         mHost = host;
+        mSurfaceAnimatorStarter = surfaceAnimatorStarter;
     }
 
     private SurfaceControl makeDimLayer() {
@@ -146,29 +158,32 @@
     }
 
     /**
-     * Retreive the DimState for a given child of the host.
+     * Retrieve the DimState, creating one if it doesn't exist.
      */
     private DimState getDimState(WindowContainer container) {
-        DimState state = mDimLayerUsers.get(container);
-        if (state == null) {
+        if (mDimState == null) {
             final SurfaceControl ctl = makeDimLayer();
-            state = new DimState(ctl);
+            mDimState = new DimState(ctl);
             /**
              * See documentation on {@link #dimAbove} to understand lifecycle management of Dim's
              * via state resetting for Dim's with containers.
              */
             if (container == null) {
-                state.mDontReset = true;
+                mDimState.mDontReset = true;
             }
-            mDimLayerUsers.put(container, state);
         }
-        return state;
+
+        mLastRequestedDimContainer = container;
+        return mDimState;
     }
 
     private void dim(SurfaceControl.Transaction t, WindowContainer container, int relativeLayer,
             float alpha) {
         final DimState d = getDimState(container);
         if (container != null) {
+            // The dim method is called from WindowState.prepareSurfaces(), which is always called
+            // in the correct Z from lowest Z to highest. This ensures that the dim layer is always
+            // relative to the highest Z layer with a dim.
             t.setRelativeLayer(d.mDimLayer, container.getSurfaceControl(), relativeLayer);
         } else {
             t.setLayer(d.mDimLayer, Integer.MAX_VALUE);
@@ -237,11 +252,8 @@
      * a chance to request dims to continue.
      */
     void resetDimStates() {
-        for (int i = mDimLayerUsers.size() - 1; i >= 0; i--) {
-            final DimState state = mDimLayerUsers.valueAt(i);
-            if (!state.mDontReset) {
-                state.mDimming = false;
-            }
+        if (mDimState != null && !mDimState.mDontReset) {
+            mDimState.mDimming = false;
         }
     }
 
@@ -254,30 +266,25 @@
      * @return true if any Dims were updated.
      */
     boolean updateDims(SurfaceControl.Transaction t, Rect bounds) {
-        boolean didSomething = false;
-        for (int i = mDimLayerUsers.size() - 1; i >= 0; i--) {
-            DimState state = mDimLayerUsers.valueAt(i);
-            WindowContainer container = mDimLayerUsers.keyAt(i);
-
-            // TODO: We want to animate the addition and removal of Dim's instead of immediately
-            // acting. When we do this we need to take care to account for the "Replacing Windows"
-            // case (and seamless dim transfer).
-            if (!state.mDimming) {
-                mDimLayerUsers.removeAt(i);
-                startDimExit(container, state.mSurfaceAnimator, t);
-            } else {
-                didSomething = true;
-                // TODO: Once we use geometry from hierarchy this falls away.
-                t.setSize(state.mDimLayer, bounds.width(), bounds.height());
-                t.setPosition(state.mDimLayer, bounds.left, bounds.top);
-                if (!state.isVisible) {
-                    state.isVisible = true;
-                    t.show(state.mDimLayer);
-                    startDimEnter(container, state.mSurfaceAnimator, t);
-                }
-            }
+        if (mDimState == null) {
+            return false;
         }
-        return didSomething;
+
+        if (!mDimState.mDimming) {
+            startDimExit(mLastRequestedDimContainer, mDimState.mSurfaceAnimator, t);
+            mDimState = null;
+            return false;
+        } else {
+            // TODO: Once we use geometry from hierarchy this falls away.
+            t.setSize(mDimState.mDimLayer, bounds.width(), bounds.height());
+            t.setPosition(mDimState.mDimLayer, bounds.left, bounds.top);
+            if (!mDimState.isVisible) {
+                mDimState.isVisible = true;
+                t.show(mDimState.mDimLayer);
+                startDimEnter(mLastRequestedDimContainer, mDimState.mSurfaceAnimator, t);
+            }
+            return true;
+        }
     }
 
     private void startDimEnter(WindowContainer container, SurfaceAnimator animator,
@@ -292,7 +299,7 @@
 
     private void startAnim(WindowContainer container, SurfaceAnimator animator,
             SurfaceControl.Transaction t, float startAlpha, float endAlpha) {
-        animator.startAnimation(t, new LocalAnimationAdapter(
+        mSurfaceAnimatorStarter.startAnimation(animator, t, new LocalAnimationAdapter(
                 new AlphaAnimationSpec(startAlpha, endAlpha, getDimDuration(container)),
                 mHost.mService.mSurfaceAnimationRunner), false /* hidden */);
     }
diff --git a/services/core/java/com/android/server/wm/SurfaceAnimator.java b/services/core/java/com/android/server/wm/SurfaceAnimator.java
index 0512a08..83baee1 100644
--- a/services/core/java/com/android/server/wm/SurfaceAnimator.java
+++ b/services/core/java/com/android/server/wm/SurfaceAnimator.java
@@ -53,7 +53,8 @@
     SurfaceControl mLeash;
     private final Animatable mAnimatable;
     private final OnAnimationFinishedCallback mInnerAnimationFinishedCallback;
-    private final Runnable mAnimationFinishedCallback;
+    @VisibleForTesting
+    final Runnable mAnimationFinishedCallback;
     private boolean mAnimationStartDelayed;
 
     /**
diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java
index d565a6a..676fb9f 100644
--- a/services/core/java/com/android/server/wm/WindowManagerService.java
+++ b/services/core/java/com/android/server/wm/WindowManagerService.java
@@ -904,16 +904,9 @@
     public static WindowManagerService main(final Context context, final InputManagerService im,
             final boolean haveInputMethods, final boolean showBootMsgs, final boolean onlyCore,
             WindowManagerPolicy policy) {
-        return main(context, im, haveInputMethods, showBootMsgs, onlyCore, policy,
-                new SurfaceAnimationRunner());
-    }
-
-    public static WindowManagerService main(final Context context, final InputManagerService im,
-            final boolean haveInputMethods, final boolean showBootMsgs, final boolean onlyCore,
-            WindowManagerPolicy policy, SurfaceAnimationRunner surfaceAnimationRunner) {
         DisplayThread.getHandler().runWithScissors(() ->
                 sInstance = new WindowManagerService(context, im, haveInputMethods, showBootMsgs,
-                        onlyCore, policy, surfaceAnimationRunner), 0);
+                        onlyCore, policy), 0);
         return sInstance;
     }
 
@@ -936,7 +929,7 @@
 
     private WindowManagerService(Context context, InputManagerService inputManager,
             boolean haveInputMethods, boolean showBootMsgs, boolean onlyCore,
-            WindowManagerPolicy policy, SurfaceAnimationRunner surfaceAnimationRunner) {
+            WindowManagerPolicy policy) {
         installLock(this, INDEX_WINDOW);
         mContext = context;
         mHaveInputMethods = haveInputMethods;
@@ -1063,7 +1056,7 @@
                 PowerManager.SCREEN_BRIGHT_WAKE_LOCK | PowerManager.ON_AFTER_RELEASE, TAG_WM);
         mHoldingScreenWakeLock.setReferenceCounted(false);
 
-        mSurfaceAnimationRunner = surfaceAnimationRunner;
+        mSurfaceAnimationRunner = new SurfaceAnimationRunner();
 
         mAllowTheaterModeWakeFromLayout = context.getResources().getBoolean(
                 com.android.internal.R.bool.config_allowTheaterModeWakeFromWindowLayout);
diff --git a/services/core/java/com/android/server/wm/WindowState.java b/services/core/java/com/android/server/wm/WindowState.java
index 3bee1e8..48d29e3 100644
--- a/services/core/java/com/android/server/wm/WindowState.java
+++ b/services/core/java/com/android/server/wm/WindowState.java
@@ -4480,8 +4480,13 @@
         if (!mAnimatingExit && mAppDied) {
             mIsDimming = true;
             dimmer.dimAbove(getPendingTransaction(), this, DEFAULT_DIM_AMOUNT_DEAD_WINDOW);
-        } else if ((mAttrs.flags & FLAG_DIM_BEHIND) != 0
-                && !mAnimatingExit && isVisible() && !mWinAnimator.mLastHidden) {
+        } else if ((mAttrs.flags & FLAG_DIM_BEHIND) != 0 && isVisibleNow()
+                && !mWinAnimator.mLastHidden) {
+            // Only show a dim behind when the following is satisfied:
+            // 1. The window has the flag FLAG_DIM_BEHIND
+            // 2. The WindowToken is not hidden so dims aren't shown when the window is exiting.
+            // 3. The WS is considered visible according to the isVisible() method
+            // 4. The WSA is not hidden.
             mIsDimming = true;
             dimmer.dimBelow(getPendingTransaction(), this, mAttrs.dimAmount);
         }
diff --git a/services/tests/servicestests/assets/KeyStoreRecoveryControllerTest/pem/invalid-cert-1-no-begin-end.pem b/services/tests/servicestests/assets/KeyStoreRecoveryControllerTest/pem/invalid-cert-1-no-begin-end.pem
new file mode 100644
index 0000000..b5d513c
--- /dev/null
+++ b/services/tests/servicestests/assets/KeyStoreRecoveryControllerTest/pem/invalid-cert-1-no-begin-end.pem
@@ -0,0 +1,49 @@
+MIIJJzCCBQ6gAwIBAgIJAM7fBGeQ1wBkMA0GCSqGSIb3DQEBDQUAMCAxHjAcBgNV
+BAMMFUdvb2dsZSBDcnlwdEF1dGhWYXVsdDAeFw0xODAxMTEwNjQ5MzNaFw0zODAx
+MDYwNjQ5MzNaMCAxHjAcBgNVBAMMFUdvb2dsZSBDcnlwdEF1dGhWYXVsdDCCBCIw
+DQYJKoZIhvcNAQEBBQADggQPADCCBAoCggQBCcFv05M1seLPYIW7oFivh7u5otCt
+Mm7ryq0UjpbTQPcQxJQbYzcUQF7CbPYTdWDid4EuO8Zec03ownsduFhKud6H3yIQ
+4ueGiqCBoG1D6+N8fF9R8awTmAsbNg63VInx6IwcBZnjFlsIIftOFxqIJBYhiKhN
+JhPtF1i9dn+N5HjEKmkJO3pXhYhRXMp7OwL/epxzhBXFYT7aDg9pnaM6C+4hmhQ/
+0q2oyzYtAqFmrEenbtI6G47SzMc+shNTuYLtq21j/Z3uA3RwB9Szfu99F66tlgTX
+v7K7YS573hN3TQY/+nkLfFy/oF2LQRYvKHF+Nv0BHzQLzqDEYBaILcMf3i2Ce/b7
+wZjitLqFAI1swqGzgH/QpB3OrX51M/B7UCF2nB7Pa8knu4kBDGkz2Q41jAL0W/qt
+j43VwJDW0Y98OuqQiCqJrTrGdv7b/phnVVBvFrtIjYMfyK34jy5VLXctV5CSkWj5
+3ul3mvGFHJD+6nneDR4PUkmYN0khT4t/RqnQlwYE0a6Erq1+Rof6/DoWSzeBLBYV
+JaHhRy9mrudR/VcQynLKty6Zst4Lyh6aPMHcpTwGZbG+4mXnWeTaLEnGvivldksT
+XOxipcO/fXJfDss4b0glGzP3GD0+H5EZB9coYzNT47QZd9drxHdrLxtPoi+MeqkG
+gCdyFyBZO8G2k/JuyziT6hy+50VXJnl6Ujxj7MVUYAsISHsHgqETDsukQbbKvTKg
+3gxPVNN/vKWwyh7KLcFIaOEoPOgStkmVsqrXm7YLE6Bvzm8nu4rwJeAF9Yseg9BE
+Y86TRRmAI7fW4eDEPnxgCUUvcYSAh5mcayIyIr0KTuXkevwYbVRHMVmy9DaqzsP8
+YFXIqFvDXRCFSy/gMkoNb9ZoqdkmjZ+VBsjAKI+u/Haf6pgdpGZbVGKEFmaVHCkr
+tPp/gy4kE4qmd/SIaccG8o6Eb9X9fbqTTDZv34kcGgxOvBJVIaNHprTjgvYEnRaD
+KTlmZoCUmBlHzvbf68YWBmIz0K8vYPdx9r98LiUgpbTHtKZIYrJnbgPnbC9icP24
+2ksB4yaTx1QWc14vTNv1lUtv4zJEmaaoynNlETJFf/Tz0QKJxtT+l/BIAz8kEJMA
+cKsfoTx9OTtfuL85pXbCgxbKKmKn6RzxUCzSzgMboC0z6W8Zxy2gLIhqMm8AXAF7
+salwrRirV4lWsM9MOhVEgfjcv/qmQSYr1ARrwwegHRqxPA3qh11kfq5YSFU7W7+f
+JrWH6VuLZ0B1fj2+lsoMNekFA1ULD8DK7aAFIh9Y1y4Jt//xMuOPcD5PWNGFmUk7
+oPewiIUMLjXSWcgrQVYbZEDW/vooMJoo47Vg1fQPehejbONE1nBIaeRVhJcCAwEA
+AaNjMGEwHQYDVR0OBBYEFNd7oYeSi7hSGimRpTZaHLQy6+zRMB8GA1UdIwQYMBaA
+FNd7oYeSi7hSGimRpTZaHLQy6+zRMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/
+BAQDAgGGMA0GCSqGSIb3DQEBDQUAA4IEAgABny011veuPplTcNeyLeKhWRn/y9VM
+QEyhaLlsC1Di35WN8owjj4+gugLlDdtEc/pvzgk+ZkAuplQkrsU907dvwkDPb4rW
+ZB5hjbr9yVyEHK1bHh7RSUkJrB3NRwqH1lyWz/LfaVfbV4CtaaERhThzZCp/MweO
+Tivg2XpSluy5s8lEEbMC/dGuIsBMitX3XLlbYa2ND3aHZLo6X9yQMFfTCjgwYG2n
+eDYupnvcLINlrlJYcrYSrIvoQn9XfsnjU3AXz+jc6xLrO3EtXDhi9e+QTfcnvRsg
+l/Hj9SZr1w1L1PPJo+KjsRavVvzaHXlBAvvUtEojJrkR3j+b5zvQB6dgVrM0zUhM
+Q9zRp5R/xqHeZ/0TTQe9kEa8QuRzuRIkK5Wbh76Eix3S+2uTsbj462nk4E0oPR8p
+iYopS4ZEFEfrKW14HOph9ZscI4l/HfDmTNfgpyFl62UrvzVBnoz+sbhTgbPHPcCX
+OUrhmpz9I5oBkyEAZYunSvzY/9SXUsz6psXHJmVzLQcne/YQTtpWzV/wGD7cjjDl
+bfzsmGCfZ8jqPBoicl5IUVdyZsJgufEZHXxKZQ7wL7R6jKrj/GtCDey1Wr2QT8VX
+5JTk9cJQFjgjDWaAyCBpGEaQvYJcaOxk2D+Wap5ax8nUfW/99vVFA0EJKsSVVzw7
+daRty0UpfZsx2Sfzpg0mymmgB8+NY6t68dL5C/xxAv5mEQ8wGJmP45iQpo5T6LVV
+MrktLf5eIzxlALQIW/AgpSH9JKCqpItdxfisAIIs9e8XHbVJJA0Jde7rtAj+TUY0
+h00xSqyfSSbpcDJ9lIoSZOJvFQdWOxB8c3vZZGGhMuRFm06sUHvcHjo8KwnbqyOx
+DGjeqt6YWty6WcNin0WciR33vGHIzwVNxNnmuY308bNsMvY9jsmd37hdmmwnmQge
+7AIa7TMPjaKm0vV/1ztFSODWCI2K7klmL2MtOJMGfqUeOfjPANbS3lMJBAH9qxLM
+7Kng+nfqVtt+NG9MxcTbP80FkBa/6JxGgjjsiwDmhr2MTCYOK/eD+WZikMOieyvH
+m2vgxYCdWrhaGfc3t6oQ2YO+mXI7e6d3F3a90UUYkBIgje9zu0RLxnBBhuoRyGwv
+uQAlqgMDBZIzTO0Vnwew7KRLdzLhWbiikhi81q6Lg62aWjbdF6Ue6AVXch+dqmr+
+9aVt0Y6ETTS77nrQyglyLKIeNx6cEHDjETXlPYGbCAlrdKAdTA4ngnBZnzGQ/8zg
+tP9zvIJVA6cuOAn8GFEsrb7GN20QSDwyJWrYi6f+m64D9rOK4Jz4t+lEfjcfJeM/
+UcNlhmATcMHXWPCoKkOfll4PBc/Wigv1xYw70RZ4pai07LzJxNHYhvpE3Q==
diff --git a/services/tests/servicestests/assets/KeyStoreRecoveryControllerTest/pem/invalid-cert-2-empty-block.pem b/services/tests/servicestests/assets/KeyStoreRecoveryControllerTest/pem/invalid-cert-2-empty-block.pem
new file mode 100644
index 0000000..c412709
--- /dev/null
+++ b/services/tests/servicestests/assets/KeyStoreRecoveryControllerTest/pem/invalid-cert-2-empty-block.pem
@@ -0,0 +1,2 @@
+-----BEGIN CERTIFICATE-----
+-----END CERTIFICATE-----
diff --git a/services/tests/servicestests/assets/KeyStoreRecoveryControllerTest/pem/invalid-cert-3-invalid-key.pem b/services/tests/servicestests/assets/KeyStoreRecoveryControllerTest/pem/invalid-cert-3-invalid-key.pem
new file mode 100644
index 0000000..9137b16
--- /dev/null
+++ b/services/tests/servicestests/assets/KeyStoreRecoveryControllerTest/pem/invalid-cert-3-invalid-key.pem
@@ -0,0 +1,50 @@
+-----BEGIN CERTIFICATE-----
+BAMMFUdvb2dsZSBDcnlwdEF1dGhWYXVsdDAeFw0xODAxMTEwNjQ5MzNaFw0zODAx
+MDYwNjQ5MzNaMCAxHjAcBgNVBAMMFUdvb2dsZSBDcnlwdEF1dGhWYXVsdDCCBCIw
+DQYJKoZIhvcNAQEBBQADggQPADCCBAoCggQBCcFv05M1seLPYIW7oFivh7u5otCt
+Mm7ryq0UjpbTQPcQxJQbYzcUQF7CbPYTdWDid4EuO8Zec03ownsduFhKud6H3yIQ
+4ueGiqCBoG1D6+N8fF9R8awTmAsbNg63VInx6IwcBZnjFlsIIftOFxqIJBYhiKhN
+JhPtF1i9dn+N5HjEKmkJO3pXhYhRXMp7OwL/epxzhBXFYT7aDg9pnaM6C+4hmhQ/
+0q2oyzYtAqFmrEenbtI6G47SzMc+shNTuYLtq21j/Z3uA3RwB9Szfu99F66tlgTX
+v7K7YS573hN3TQY/+nkLfFy/oF2LQRYvKHF+Nv0BHzQLzqDEYBaILcMf3i2Ce/b7
+wZjitLqFAI1swqGzgH/QpB3OrX51M/B7UCF2nB7Pa8knu4kBDGkz2Q41jAL0W/qt
+j43VwJDW0Y98OuqQiCqJrTrGdv7b/phnVVBvFrtIjYMfyK34jy5VLXctV5CSkWj5
+3ul3mvGFHJD+6nneDR4PUkmYN0khT4t/RqnQlwYE0a6Erq1+Rof6/DoWSzeBLBYV
+JaHhRy9mrudR/VcQynLKty6Zst4Lyh6aPMHcpTwGZbG+4mXnWeTaLEnGvivldksT
+XOxipcO/fXJfDss4b0glGzP3GD0+H5EZB9coYzNT47QZd9drxHdrLxtPoi+MeqkG
+gCdyFyBZO8G2k/JuyziT6hy+50VXJnl6Ujxj7MVUYAsISHsHgqETDsukQbbKvTKg
+3gxPVNN/vKWwyh7KLcFIaOEoPOgStkmVsqrXm7YLE6Bvzm8nu4rwJeAF9Yseg9BE
+Y86TRRmAI7fW4eDEPnxgCUUvcYSAh5mcayIyIr0KTuXkevwYbVRHMVmy9DaqzsP8
+YFXIqFvDXRCFSy/gMkoNb9ZoqdkmjZ+VBsjAKI+u/Haf6pgdpGZbVGKEFmaVHCkr
+tPp/gy4kE4qmd/SIaccG8o6Eb9X9fbqTTDZv34kcGgxOvBJVIaNHprTjgvYEnRaD
+KTlmZoCUmBlHzvbf68YWBmIz0K8vYPdx9r98LiUgpbTHtKZIYrJnbgPnbC9icP24
+2ksB4yaTx1QWc14vTNv1lUtv4zJEmaaoynNlETJFf/Tz0QKJxtT+l/BIAz8kEJMA
+cKsfoTx9OTtfuL85pXbCgxbKKmKn6RzxUCzSzgMboC0z6W8Zxy2gLIhqMm8AXAF7
+salwrRirV4lWsM9MOhVEgfjcv/qmQSYr1ARrwwegHRqxPA3qh11kfq5YSFU7W7+f
+JrWH6VuLZ0B1fj2+lsoMNekFA1ULD8DK7aAFIh9Y1y4Jt//xMuOPcD5PWNGFmUk7
+oPewiIUMLjXSWcgrQVYbZEDW/vooMJoo47Vg1fQPehejbONE1nBIaeRVhJcCAwEA
+AaNjMGEwHQYDVR0OBBYEFNd7oYeSi7hSGimRpTZaHLQy6+zRMB8GA1UdIwQYMBaA
+FNd7oYeSi7hSGimRpTZaHLQy6+zRMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/
+BAQDAgGGMA0GCSqGSIb3DQEBDQUAA4IEAgABny011veuPplTcNeyLeKhWRn/y9VM
+QEyhaLlsC1Di35WN8owjj4+gugLlDdtEc/pvzgk+ZkAuplQkrsU907dvwkDPb4rW
+ZB5hjbr9yVyEHK1bHh7RSUkJrB3NRwqH1lyWz/LfaVfbV4CtaaERhThzZCp/MweO
+Tivg2XpSluy5s8lEEbMC/dGuIsBMitX3XLlbYa2ND3aHZLo6X9yQMFfTCjgwYG2n
+eDYupnvcLINlrlJYcrYSrIvoQn9XfsnjU3AXz+jc6xLrO3EtXDhi9e+QTfcnvRsg
+l/Hj9SZr1w1L1PPJo+KjsRavVvzaHXlBAvvUtEojJrkR3j+b5zvQB6dgVrM0zUhM
+Q9zRp5R/xqHeZ/0TTQe9kEa8QuRzuRIkK5Wbh76Eix3S+2uTsbj462nk4E0oPR8p
+iYopS4ZEFEfrKW14HOph9ZscI4l/HfDmTNfgpyFl62UrvzVBnoz+sbhTgbPHPcCX
+OUrhmpz9I5oBkyEAZYunSvzY/9SXUsz6psXHJmVzLQcne/YQTtpWzV/wGD7cjjDl
+bfzsmGCfZ8jqPBoicl5IUVdyZsJgufEZHXxKZQ7wL7R6jKrj/GtCDey1Wr2QT8VX
+5JTk9cJQFjgjDWaAyCBpGEaQvYJcaOxk2D+Wap5ax8nUfW/99vVFA0EJKsSVVzw7
+daRty0UpfZsx2Sfzpg0mymmgB8+NY6t68dL5C/xxAv5mEQ8wGJmP45iQpo5T6LVV
+MrktLf5eIzxlALQIW/AgpSH9JKCqpItdxfisAIIs9e8XHbVJJA0Jde7rtAj+TUY0
+h00xSqyfSSbpcDJ9lIoSZOJvFQdWOxB8c3vZZGGhMuRFm06sUHvcHjo8KwnbqyOx
+DGjeqt6YWty6WcNin0WciR33vGHIzwVNxNnmuY308bNsMvY9jsmd37hdmmwnmQge
+7AIa7TMPjaKm0vV/1ztFSODWCI2K7klmL2MtOJMGfqUeOfjPANbS3lMJBAH9qxLM
+7Kng+nfqVtt+NG9MxcTbP80FkBa/6JxGgjjsiwDmhr2MTCYOK/eD+WZikMOieyvH
+m2vgxYCdWrhaGfc3t6oQ2YO+mXI7e6d3F3a90UUYkBIgje9zu0RLxnBBhuoRyGwv
+uQAlqgMDBZIzTO0Vnwew7KRLdzLhWbiikhi81q6Lg62aWjbdF6Ue6AVXch+dqmr+
+9aVt0Y6ETTS77nrQyglyLKIeNx6cEHDjETXlPYGbCAlrdKAdTA4ngnBZnzGQ/8zg
+tP9zvIJVA6cuOAn8GFEsrb7GN20QSDwyJWrYi6f+m64D9rOK4Jz4t+lEfjcfJeM/
+UcNlhmATcMHXWPCoKkOfll4PBc/Wigv1xYw70RZ4pai07LzJxNHYhvpE3Q==
+-----END CERTIFICATE-----
diff --git a/services/tests/servicestests/assets/KeyStoreRecoveryControllerTest/pem/valid-cert-multiple-blocks.pem b/services/tests/servicestests/assets/KeyStoreRecoveryControllerTest/pem/valid-cert-multiple-blocks.pem
new file mode 100644
index 0000000..e3abeaa
--- /dev/null
+++ b/services/tests/servicestests/assets/KeyStoreRecoveryControllerTest/pem/valid-cert-multiple-blocks.pem
@@ -0,0 +1,70 @@
+-----BEGIN CERTIFICATE-----
+MIIJJzCCBQ6gAwIBAgIJAM7fBGeQ1wBkMA0GCSqGSIb3DQEBDQUAMCAxHjAcBgNV
+BAMMFUdvb2dsZSBDcnlwdEF1dGhWYXVsdDAeFw0xODAxMTEwNjQ5MzNaFw0zODAx
+MDYwNjQ5MzNaMCAxHjAcBgNVBAMMFUdvb2dsZSBDcnlwdEF1dGhWYXVsdDCCBCIw
+DQYJKoZIhvcNAQEBBQADggQPADCCBAoCggQBCcFv05M1seLPYIW7oFivh7u5otCt
+Mm7ryq0UjpbTQPcQxJQbYzcUQF7CbPYTdWDid4EuO8Zec03ownsduFhKud6H3yIQ
+4ueGiqCBoG1D6+N8fF9R8awTmAsbNg63VInx6IwcBZnjFlsIIftOFxqIJBYhiKhN
+JhPtF1i9dn+N5HjEKmkJO3pXhYhRXMp7OwL/epxzhBXFYT7aDg9pnaM6C+4hmhQ/
+0q2oyzYtAqFmrEenbtI6G47SzMc+shNTuYLtq21j/Z3uA3RwB9Szfu99F66tlgTX
+v7K7YS573hN3TQY/+nkLfFy/oF2LQRYvKHF+Nv0BHzQLzqDEYBaILcMf3i2Ce/b7
+wZjitLqFAI1swqGzgH/QpB3OrX51M/B7UCF2nB7Pa8knu4kBDGkz2Q41jAL0W/qt
+j43VwJDW0Y98OuqQiCqJrTrGdv7b/phnVVBvFrtIjYMfyK34jy5VLXctV5CSkWj5
+3ul3mvGFHJD+6nneDR4PUkmYN0khT4t/RqnQlwYE0a6Erq1+Rof6/DoWSzeBLBYV
+JaHhRy9mrudR/VcQynLKty6Zst4Lyh6aPMHcpTwGZbG+4mXnWeTaLEnGvivldksT
+XOxipcO/fXJfDss4b0glGzP3GD0+H5EZB9coYzNT47QZd9drxHdrLxtPoi+MeqkG
+gCdyFyBZO8G2k/JuyziT6hy+50VXJnl6Ujxj7MVUYAsISHsHgqETDsukQbbKvTKg
+3gxPVNN/vKWwyh7KLcFIaOEoPOgStkmVsqrXm7YLE6Bvzm8nu4rwJeAF9Yseg9BE
+Y86TRRmAI7fW4eDEPnxgCUUvcYSAh5mcayIyIr0KTuXkevwYbVRHMVmy9DaqzsP8
+YFXIqFvDXRCFSy/gMkoNb9ZoqdkmjZ+VBsjAKI+u/Haf6pgdpGZbVGKEFmaVHCkr
+tPp/gy4kE4qmd/SIaccG8o6Eb9X9fbqTTDZv34kcGgxOvBJVIaNHprTjgvYEnRaD
+KTlmZoCUmBlHzvbf68YWBmIz0K8vYPdx9r98LiUgpbTHtKZIYrJnbgPnbC9icP24
+2ksB4yaTx1QWc14vTNv1lUtv4zJEmaaoynNlETJFf/Tz0QKJxtT+l/BIAz8kEJMA
+cKsfoTx9OTtfuL85pXbCgxbKKmKn6RzxUCzSzgMboC0z6W8Zxy2gLIhqMm8AXAF7
+salwrRirV4lWsM9MOhVEgfjcv/qmQSYr1ARrwwegHRqxPA3qh11kfq5YSFU7W7+f
+JrWH6VuLZ0B1fj2+lsoMNekFA1ULD8DK7aAFIh9Y1y4Jt//xMuOPcD5PWNGFmUk7
+oPewiIUMLjXSWcgrQVYbZEDW/vooMJoo47Vg1fQPehejbONE1nBIaeRVhJcCAwEA
+AaNjMGEwHQYDVR0OBBYEFNd7oYeSi7hSGimRpTZaHLQy6+zRMB8GA1UdIwQYMBaA
+FNd7oYeSi7hSGimRpTZaHLQy6+zRMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/
+BAQDAgGGMA0GCSqGSIb3DQEBDQUAA4IEAgABny011veuPplTcNeyLeKhWRn/y9VM
+QEyhaLlsC1Di35WN8owjj4+gugLlDdtEc/pvzgk+ZkAuplQkrsU907dvwkDPb4rW
+ZB5hjbr9yVyEHK1bHh7RSUkJrB3NRwqH1lyWz/LfaVfbV4CtaaERhThzZCp/MweO
+Tivg2XpSluy5s8lEEbMC/dGuIsBMitX3XLlbYa2ND3aHZLo6X9yQMFfTCjgwYG2n
+eDYupnvcLINlrlJYcrYSrIvoQn9XfsnjU3AXz+jc6xLrO3EtXDhi9e+QTfcnvRsg
+l/Hj9SZr1w1L1PPJo+KjsRavVvzaHXlBAvvUtEojJrkR3j+b5zvQB6dgVrM0zUhM
+Q9zRp5R/xqHeZ/0TTQe9kEa8QuRzuRIkK5Wbh76Eix3S+2uTsbj462nk4E0oPR8p
+iYopS4ZEFEfrKW14HOph9ZscI4l/HfDmTNfgpyFl62UrvzVBnoz+sbhTgbPHPcCX
+OUrhmpz9I5oBkyEAZYunSvzY/9SXUsz6psXHJmVzLQcne/YQTtpWzV/wGD7cjjDl
+bfzsmGCfZ8jqPBoicl5IUVdyZsJgufEZHXxKZQ7wL7R6jKrj/GtCDey1Wr2QT8VX
+5JTk9cJQFjgjDWaAyCBpGEaQvYJcaOxk2D+Wap5ax8nUfW/99vVFA0EJKsSVVzw7
+daRty0UpfZsx2Sfzpg0mymmgB8+NY6t68dL5C/xxAv5mEQ8wGJmP45iQpo5T6LVV
+MrktLf5eIzxlALQIW/AgpSH9JKCqpItdxfisAIIs9e8XHbVJJA0Jde7rtAj+TUY0
+h00xSqyfSSbpcDJ9lIoSZOJvFQdWOxB8c3vZZGGhMuRFm06sUHvcHjo8KwnbqyOx
+DGjeqt6YWty6WcNin0WciR33vGHIzwVNxNnmuY308bNsMvY9jsmd37hdmmwnmQge
+7AIa7TMPjaKm0vV/1ztFSODWCI2K7klmL2MtOJMGfqUeOfjPANbS3lMJBAH9qxLM
+7Kng+nfqVtt+NG9MxcTbP80FkBa/6JxGgjjsiwDmhr2MTCYOK/eD+WZikMOieyvH
+m2vgxYCdWrhaGfc3t6oQ2YO+mXI7e6d3F3a90UUYkBIgje9zu0RLxnBBhuoRyGwv
+uQAlqgMDBZIzTO0Vnwew7KRLdzLhWbiikhi81q6Lg62aWjbdF6Ue6AVXch+dqmr+
+9aVt0Y6ETTS77nrQyglyLKIeNx6cEHDjETXlPYGbCAlrdKAdTA4ngnBZnzGQ/8zg
+tP9zvIJVA6cuOAn8GFEsrb7GN20QSDwyJWrYi6f+m64D9rOK4Jz4t+lEfjcfJeM/
+UcNlhmATcMHXWPCoKkOfll4PBc/Wigv1xYw70RZ4pai07LzJxNHYhvpE3Q==
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIDCDCB8aADAgECAgYBYOlweDswDQYJKoZIhvcNAQELBQAwLTErMCkGA1UEAwwi
+R29vZ2xlIENyeXB0QXV0aFZhdWx0IEludGVybWVkaWF0ZTAeFw0xODAxMTEwODE1
+NTBaFw0yMDAxMTIwODE1NTBaMCkxJzAlBgNVBAMTHkdvb2dsZSBDcnlwdEF1dGhW
+YXVsdCBJbnN0YW5jZTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABLgAERiYHfBu
+tJT+htocB40BtDr2jdxh0EZJlQ8QhpMkZuA/0t/zeSAdkVWw5b16izJ9JVOi/KVl
+4b0hRH54UvowDQYJKoZIhvcNAQELBQADggIBABZALhC9j3hpZ0AgN0tsqAP2Ix21
+tNOcvo/aFJuSFanOM4DZbycZEYAo5rorvuFu7eXETBKDGnI5xreNAoQsaj/dyCHu
+HKIn5P7yCmKvG2sV2TQ5go+0xV2x8BhTrtUWLeHvUbM3fXipa3NrordbA8MgzXwr
+GR1Y1FuMOn5n4kiuHJ2sQTbDdzSQSK5VpH+6rjARlfOCyLUX0u8UKRRH81qhIQWb
+UFMp9q1CVfiLP2O3CdDdpZXCysdflIb62TWnma+I8jqMryyxrMVs9kpfa8zkX9qe
+33Vxp+QaQTqQ07/7KYVw869MeFn+bXeHnjUhqGY6S8M71vrTMG3M5p8Sq9LmV8Y5
+7YB5uqKap2Inf0FOuJS7h7nVVzU/kOFkepaQVHyScwTPuuXNgpQg8XZnN/AWfRwJ
+hf5zE6vXXTHMzQA1mY2eEhxGfpryv7LH8pvfcyTakdBlw8aMJjKdre8xLLGZeVCa
+79plkfYD0rMrxtRHCGyTKGzUcx/B9kYJK5qBgJiDJLKF3XwGbAs/F8CyEPihjvj4
+M2EoeyhmHWKLYsps6+uTksJ+PxZU14M7672K2y8BdulyfkZIhili118XnRykKkMf
+JLQJKMqZx5O0B9bF8yQdcGKEGEwMQt5ENdH8HeiwLm4QS3VzFXYetgUPCM5lPDIp
+BuwwuQxvQDF4pmQd
+-----END CERTIFICATE-----
diff --git a/services/tests/servicestests/assets/KeyStoreRecoveryControllerTest/pem/valid-cert.pem b/services/tests/servicestests/assets/KeyStoreRecoveryControllerTest/pem/valid-cert.pem
new file mode 100644
index 0000000..5c16a1f
--- /dev/null
+++ b/services/tests/servicestests/assets/KeyStoreRecoveryControllerTest/pem/valid-cert.pem
@@ -0,0 +1,204 @@
+-----BEGIN CERTIFICATE-----
+MIIJJzCCBQ6gAwIBAgIJAM7fBGeQ1wBkMA0GCSqGSIb3DQEBDQUAMCAxHjAcBgNV
+BAMMFUdvb2dsZSBDcnlwdEF1dGhWYXVsdDAeFw0xODAxMTEwNjQ5MzNaFw0zODAx
+MDYwNjQ5MzNaMCAxHjAcBgNVBAMMFUdvb2dsZSBDcnlwdEF1dGhWYXVsdDCCBCIw
+DQYJKoZIhvcNAQEBBQADggQPADCCBAoCggQBCcFv05M1seLPYIW7oFivh7u5otCt
+Mm7ryq0UjpbTQPcQxJQbYzcUQF7CbPYTdWDid4EuO8Zec03ownsduFhKud6H3yIQ
+4ueGiqCBoG1D6+N8fF9R8awTmAsbNg63VInx6IwcBZnjFlsIIftOFxqIJBYhiKhN
+JhPtF1i9dn+N5HjEKmkJO3pXhYhRXMp7OwL/epxzhBXFYT7aDg9pnaM6C+4hmhQ/
+0q2oyzYtAqFmrEenbtI6G47SzMc+shNTuYLtq21j/Z3uA3RwB9Szfu99F66tlgTX
+v7K7YS573hN3TQY/+nkLfFy/oF2LQRYvKHF+Nv0BHzQLzqDEYBaILcMf3i2Ce/b7
+wZjitLqFAI1swqGzgH/QpB3OrX51M/B7UCF2nB7Pa8knu4kBDGkz2Q41jAL0W/qt
+j43VwJDW0Y98OuqQiCqJrTrGdv7b/phnVVBvFrtIjYMfyK34jy5VLXctV5CSkWj5
+3ul3mvGFHJD+6nneDR4PUkmYN0khT4t/RqnQlwYE0a6Erq1+Rof6/DoWSzeBLBYV
+JaHhRy9mrudR/VcQynLKty6Zst4Lyh6aPMHcpTwGZbG+4mXnWeTaLEnGvivldksT
+XOxipcO/fXJfDss4b0glGzP3GD0+H5EZB9coYzNT47QZd9drxHdrLxtPoi+MeqkG
+gCdyFyBZO8G2k/JuyziT6hy+50VXJnl6Ujxj7MVUYAsISHsHgqETDsukQbbKvTKg
+3gxPVNN/vKWwyh7KLcFIaOEoPOgStkmVsqrXm7YLE6Bvzm8nu4rwJeAF9Yseg9BE
+Y86TRRmAI7fW4eDEPnxgCUUvcYSAh5mcayIyIr0KTuXkevwYbVRHMVmy9DaqzsP8
+YFXIqFvDXRCFSy/gMkoNb9ZoqdkmjZ+VBsjAKI+u/Haf6pgdpGZbVGKEFmaVHCkr
+tPp/gy4kE4qmd/SIaccG8o6Eb9X9fbqTTDZv34kcGgxOvBJVIaNHprTjgvYEnRaD
+KTlmZoCUmBlHzvbf68YWBmIz0K8vYPdx9r98LiUgpbTHtKZIYrJnbgPnbC9icP24
+2ksB4yaTx1QWc14vTNv1lUtv4zJEmaaoynNlETJFf/Tz0QKJxtT+l/BIAz8kEJMA
+cKsfoTx9OTtfuL85pXbCgxbKKmKn6RzxUCzSzgMboC0z6W8Zxy2gLIhqMm8AXAF7
+salwrRirV4lWsM9MOhVEgfjcv/qmQSYr1ARrwwegHRqxPA3qh11kfq5YSFU7W7+f
+JrWH6VuLZ0B1fj2+lsoMNekFA1ULD8DK7aAFIh9Y1y4Jt//xMuOPcD5PWNGFmUk7
+oPewiIUMLjXSWcgrQVYbZEDW/vooMJoo47Vg1fQPehejbONE1nBIaeRVhJcCAwEA
+AaNjMGEwHQYDVR0OBBYEFNd7oYeSi7hSGimRpTZaHLQy6+zRMB8GA1UdIwQYMBaA
+FNd7oYeSi7hSGimRpTZaHLQy6+zRMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/
+BAQDAgGGMA0GCSqGSIb3DQEBDQUAA4IEAgABny011veuPplTcNeyLeKhWRn/y9VM
+QEyhaLlsC1Di35WN8owjj4+gugLlDdtEc/pvzgk+ZkAuplQkrsU907dvwkDPb4rW
+ZB5hjbr9yVyEHK1bHh7RSUkJrB3NRwqH1lyWz/LfaVfbV4CtaaERhThzZCp/MweO
+Tivg2XpSluy5s8lEEbMC/dGuIsBMitX3XLlbYa2ND3aHZLo6X9yQMFfTCjgwYG2n
+eDYupnvcLINlrlJYcrYSrIvoQn9XfsnjU3AXz+jc6xLrO3EtXDhi9e+QTfcnvRsg
+l/Hj9SZr1w1L1PPJo+KjsRavVvzaHXlBAvvUtEojJrkR3j+b5zvQB6dgVrM0zUhM
+Q9zRp5R/xqHeZ/0TTQe9kEa8QuRzuRIkK5Wbh76Eix3S+2uTsbj462nk4E0oPR8p
+iYopS4ZEFEfrKW14HOph9ZscI4l/HfDmTNfgpyFl62UrvzVBnoz+sbhTgbPHPcCX
+OUrhmpz9I5oBkyEAZYunSvzY/9SXUsz6psXHJmVzLQcne/YQTtpWzV/wGD7cjjDl
+bfzsmGCfZ8jqPBoicl5IUVdyZsJgufEZHXxKZQ7wL7R6jKrj/GtCDey1Wr2QT8VX
+5JTk9cJQFjgjDWaAyCBpGEaQvYJcaOxk2D+Wap5ax8nUfW/99vVFA0EJKsSVVzw7
+daRty0UpfZsx2Sfzpg0mymmgB8+NY6t68dL5C/xxAv5mEQ8wGJmP45iQpo5T6LVV
+MrktLf5eIzxlALQIW/AgpSH9JKCqpItdxfisAIIs9e8XHbVJJA0Jde7rtAj+TUY0
+h00xSqyfSSbpcDJ9lIoSZOJvFQdWOxB8c3vZZGGhMuRFm06sUHvcHjo8KwnbqyOx
+DGjeqt6YWty6WcNin0WciR33vGHIzwVNxNnmuY308bNsMvY9jsmd37hdmmwnmQge
+7AIa7TMPjaKm0vV/1ztFSODWCI2K7klmL2MtOJMGfqUeOfjPANbS3lMJBAH9qxLM
+7Kng+nfqVtt+NG9MxcTbP80FkBa/6JxGgjjsiwDmhr2MTCYOK/eD+WZikMOieyvH
+m2vgxYCdWrhaGfc3t6oQ2YO+mXI7e6d3F3a90UUYkBIgje9zu0RLxnBBhuoRyGwv
+uQAlqgMDBZIzTO0Vnwew7KRLdzLhWbiikhi81q6Lg62aWjbdF6Ue6AVXch+dqmr+
+9aVt0Y6ETTS77nrQyglyLKIeNx6cEHDjETXlPYGbCAlrdKAdTA4ngnBZnzGQ/8zg
+tP9zvIJVA6cuOAn8GFEsrb7GN20QSDwyJWrYi6f+m64D9rOK4Jz4t+lEfjcfJeM/
+UcNlhmATcMHXWPCoKkOfll4PBc/Wigv1xYw70RZ4pai07LzJxNHYhvpE3Q==
+-----END CERTIFICATE-----
+Certificate:
+    Data:
+        Version: 3 (0x2)
+        Serial Number:
+            ce:df:04:67:90:d7:00:64
+    Signature Algorithm: sha512WithRSAEncryption
+        Issuer: CN = Google CryptAuthVault
+        Validity
+            Not Before: Jan 11 06:49:33 2018 GMT
+            Not After : Jan  6 06:49:33 2038 GMT
+        Subject: CN = Google CryptAuthVault
+        Subject Public Key Info:
+            Public Key Algorithm: rsaEncryption
+                Public-Key: (8196 bit)
+                Modulus:
+                    09:c1:6f:d3:93:35:b1:e2:cf:60:85:bb:a0:58:af:
+                    87:bb:b9:a2:d0:ad:32:6e:eb:ca:ad:14:8e:96:d3:
+                    40:f7:10:c4:94:1b:63:37:14:40:5e:c2:6c:f6:13:
+                    75:60:e2:77:81:2e:3b:c6:5e:73:4d:e8:c2:7b:1d:
+                    b8:58:4a:b9:de:87:df:22:10:e2:e7:86:8a:a0:81:
+                    a0:6d:43:eb:e3:7c:7c:5f:51:f1:ac:13:98:0b:1b:
+                    36:0e:b7:54:89:f1:e8:8c:1c:05:99:e3:16:5b:08:
+                    21:fb:4e:17:1a:88:24:16:21:88:a8:4d:26:13:ed:
+                    17:58:bd:76:7f:8d:e4:78:c4:2a:69:09:3b:7a:57:
+                    85:88:51:5c:ca:7b:3b:02:ff:7a:9c:73:84:15:c5:
+                    61:3e:da:0e:0f:69:9d:a3:3a:0b:ee:21:9a:14:3f:
+                    d2:ad:a8:cb:36:2d:02:a1:66:ac:47:a7:6e:d2:3a:
+                    1b:8e:d2:cc:c7:3e:b2:13:53:b9:82:ed:ab:6d:63:
+                    fd:9d:ee:03:74:70:07:d4:b3:7e:ef:7d:17:ae:ad:
+                    96:04:d7:bf:b2:bb:61:2e:7b:de:13:77:4d:06:3f:
+                    fa:79:0b:7c:5c:bf:a0:5d:8b:41:16:2f:28:71:7e:
+                    36:fd:01:1f:34:0b:ce:a0:c4:60:16:88:2d:c3:1f:
+                    de:2d:82:7b:f6:fb:c1:98:e2:b4:ba:85:00:8d:6c:
+                    c2:a1:b3:80:7f:d0:a4:1d:ce:ad:7e:75:33:f0:7b:
+                    50:21:76:9c:1e:cf:6b:c9:27:bb:89:01:0c:69:33:
+                    d9:0e:35:8c:02:f4:5b:fa:ad:8f:8d:d5:c0:90:d6:
+                    d1:8f:7c:3a:ea:90:88:2a:89:ad:3a:c6:76:fe:db:
+                    fe:98:67:55:50:6f:16:bb:48:8d:83:1f:c8:ad:f8:
+                    8f:2e:55:2d:77:2d:57:90:92:91:68:f9:de:e9:77:
+                    9a:f1:85:1c:90:fe:ea:79:de:0d:1e:0f:52:49:98:
+                    37:49:21:4f:8b:7f:46:a9:d0:97:06:04:d1:ae:84:
+                    ae:ad:7e:46:87:fa:fc:3a:16:4b:37:81:2c:16:15:
+                    25:a1:e1:47:2f:66:ae:e7:51:fd:57:10:ca:72:ca:
+                    b7:2e:99:b2:de:0b:ca:1e:9a:3c:c1:dc:a5:3c:06:
+                    65:b1:be:e2:65:e7:59:e4:da:2c:49:c6:be:2b:e5:
+                    76:4b:13:5c:ec:62:a5:c3:bf:7d:72:5f:0e:cb:38:
+                    6f:48:25:1b:33:f7:18:3d:3e:1f:91:19:07:d7:28:
+                    63:33:53:e3:b4:19:77:d7:6b:c4:77:6b:2f:1b:4f:
+                    a2:2f:8c:7a:a9:06:80:27:72:17:20:59:3b:c1:b6:
+                    93:f2:6e:cb:38:93:ea:1c:be:e7:45:57:26:79:7a:
+                    52:3c:63:ec:c5:54:60:0b:08:48:7b:07:82:a1:13:
+                    0e:cb:a4:41:b6:ca:bd:32:a0:de:0c:4f:54:d3:7f:
+                    bc:a5:b0:ca:1e:ca:2d:c1:48:68:e1:28:3c:e8:12:
+                    b6:49:95:b2:aa:d7:9b:b6:0b:13:a0:6f:ce:6f:27:
+                    bb:8a:f0:25:e0:05:f5:8b:1e:83:d0:44:63:ce:93:
+                    45:19:80:23:b7:d6:e1:e0:c4:3e:7c:60:09:45:2f:
+                    71:84:80:87:99:9c:6b:22:32:22:bd:0a:4e:e5:e4:
+                    7a:fc:18:6d:54:47:31:59:b2:f4:36:aa:ce:c3:fc:
+                    60:55:c8:a8:5b:c3:5d:10:85:4b:2f:e0:32:4a:0d:
+                    6f:d6:68:a9:d9:26:8d:9f:95:06:c8:c0:28:8f:ae:
+                    fc:76:9f:ea:98:1d:a4:66:5b:54:62:84:16:66:95:
+                    1c:29:2b:b4:fa:7f:83:2e:24:13:8a:a6:77:f4:88:
+                    69:c7:06:f2:8e:84:6f:d5:fd:7d:ba:93:4c:36:6f:
+                    df:89:1c:1a:0c:4e:bc:12:55:21:a3:47:a6:b4:e3:
+                    82:f6:04:9d:16:83:29:39:66:66:80:94:98:19:47:
+                    ce:f6:df:eb:c6:16:06:62:33:d0:af:2f:60:f7:71:
+                    f6:bf:7c:2e:25:20:a5:b4:c7:b4:a6:48:62:b2:67:
+                    6e:03:e7:6c:2f:62:70:fd:b8:da:4b:01:e3:26:93:
+                    c7:54:16:73:5e:2f:4c:db:f5:95:4b:6f:e3:32:44:
+                    99:a6:a8:ca:73:65:11:32:45:7f:f4:f3:d1:02:89:
+                    c6:d4:fe:97:f0:48:03:3f:24:10:93:00:70:ab:1f:
+                    a1:3c:7d:39:3b:5f:b8:bf:39:a5:76:c2:83:16:ca:
+                    2a:62:a7:e9:1c:f1:50:2c:d2:ce:03:1b:a0:2d:33:
+                    e9:6f:19:c7:2d:a0:2c:88:6a:32:6f:00:5c:01:7b:
+                    b1:a9:70:ad:18:ab:57:89:56:b0:cf:4c:3a:15:44:
+                    81:f8:dc:bf:fa:a6:41:26:2b:d4:04:6b:c3:07:a0:
+                    1d:1a:b1:3c:0d:ea:87:5d:64:7e:ae:58:48:55:3b:
+                    5b:bf:9f:26:b5:87:e9:5b:8b:67:40:75:7e:3d:be:
+                    96:ca:0c:35:e9:05:03:55:0b:0f:c0:ca:ed:a0:05:
+                    22:1f:58:d7:2e:09:b7:ff:f1:32:e3:8f:70:3e:4f:
+                    58:d1:85:99:49:3b:a0:f7:b0:88:85:0c:2e:35:d2:
+                    59:c8:2b:41:56:1b:64:40:d6:fe:fa:28:30:9a:28:
+                    e3:b5:60:d5:f4:0f:7a:17:a3:6c:e3:44:d6:70:48:
+                    69:e4:55:84:97
+                Exponent: 65537 (0x10001)
+        X509v3 extensions:
+            X509v3 Subject Key Identifier:
+                D7:7B:A1:87:92:8B:B8:52:1A:29:91:A5:36:5A:1C:B4:32:EB:EC:D1
+            X509v3 Authority Key Identifier:
+                keyid:D7:7B:A1:87:92:8B:B8:52:1A:29:91:A5:36:5A:1C:B4:32:EB:EC:D1
+
+            X509v3 Basic Constraints: critical
+                CA:TRUE
+            X509v3 Key Usage: critical
+                Digital Signature, Certificate Sign, CRL Sign
+    Signature Algorithm: sha512WithRSAEncryption
+         01:9f:2d:35:d6:f7:ae:3e:99:53:70:d7:b2:2d:e2:a1:59:19:
+         ff:cb:d5:4c:40:4c:a1:68:b9:6c:0b:50:e2:df:95:8d:f2:8c:
+         23:8f:8f:a0:ba:02:e5:0d:db:44:73:fa:6f:ce:09:3e:66:40:
+         2e:a6:54:24:ae:c5:3d:d3:b7:6f:c2:40:cf:6f:8a:d6:64:1e:
+         61:8d:ba:fd:c9:5c:84:1c:ad:5b:1e:1e:d1:49:49:09:ac:1d:
+         cd:47:0a:87:d6:5c:96:cf:f2:df:69:57:db:57:80:ad:69:a1:
+         11:85:38:73:64:2a:7f:33:07:8e:4e:2b:e0:d9:7a:52:96:ec:
+         b9:b3:c9:44:11:b3:02:fd:d1:ae:22:c0:4c:8a:d5:f7:5c:b9:
+         5b:61:ad:8d:0f:76:87:64:ba:3a:5f:dc:90:30:57:d3:0a:38:
+         30:60:6d:a7:78:36:2e:a6:7b:dc:2c:83:65:ae:52:58:72:b6:
+         12:ac:8b:e8:42:7f:57:7e:c9:e3:53:70:17:cf:e8:dc:eb:12:
+         eb:3b:71:2d:5c:38:62:f5:ef:90:4d:f7:27:bd:1b:20:97:f1:
+         e3:f5:26:6b:d7:0d:4b:d4:f3:c9:a3:e2:a3:b1:16:af:56:fc:
+         da:1d:79:41:02:fb:d4:b4:4a:23:26:b9:11:de:3f:9b:e7:3b:
+         d0:07:a7:60:56:b3:34:cd:48:4c:43:dc:d1:a7:94:7f:c6:a1:
+         de:67:fd:13:4d:07:bd:90:46:bc:42:e4:73:b9:12:24:2b:95:
+         9b:87:be:84:8b:1d:d2:fb:6b:93:b1:b8:f8:eb:69:e4:e0:4d:
+         28:3d:1f:29:89:8a:29:4b:86:44:14:47:eb:29:6d:78:1c:ea:
+         61:f5:9b:1c:23:89:7f:1d:f0:e6:4c:d7:e0:a7:21:65:eb:65:
+         2b:bf:35:41:9e:8c:fe:b1:b8:53:81:b3:c7:3d:c0:97:39:4a:
+         e1:9a:9c:fd:23:9a:01:93:21:00:65:8b:a7:4a:fc:d8:ff:d4:
+         97:52:cc:fa:a6:c5:c7:26:65:73:2d:07:27:7b:f6:10:4e:da:
+         56:cd:5f:f0:18:3e:dc:8e:30:e5:6d:fc:ec:98:60:9f:67:c8:
+         ea:3c:1a:22:72:5e:48:51:57:72:66:c2:60:b9:f1:19:1d:7c:
+         4a:65:0e:f0:2f:b4:7a:8c:aa:e3:fc:6b:42:0d:ec:b5:5a:bd:
+         90:4f:c5:57:e4:94:e4:f5:c2:50:16:38:23:0d:66:80:c8:20:
+         69:18:46:90:bd:82:5c:68:ec:64:d8:3f:96:6a:9e:5a:c7:c9:
+         d4:7d:6f:fd:f6:f5:45:03:41:09:2a:c4:95:57:3c:3b:75:a4:
+         6d:cb:45:29:7d:9b:31:d9:27:f3:a6:0d:26:ca:69:a0:07:cf:
+         8d:63:ab:7a:f1:d2:f9:0b:fc:71:02:fe:66:11:0f:30:18:99:
+         8f:e3:98:90:a6:8e:53:e8:b5:55:32:b9:2d:2d:fe:5e:23:3c:
+         65:00:b4:08:5b:f0:20:a5:21:fd:24:a0:aa:a4:8b:5d:c5:f8:
+         ac:00:82:2c:f5:ef:17:1d:b5:49:24:0d:09:75:ee:eb:b4:08:
+         fe:4d:46:34:87:4d:31:4a:ac:9f:49:26:e9:70:32:7d:94:8a:
+         12:64:e2:6f:15:07:56:3b:10:7c:73:7b:d9:64:61:a1:32:e4:
+         45:9b:4e:ac:50:7b:dc:1e:3a:3c:2b:09:db:ab:23:b1:0c:68:
+         de:aa:de:98:5a:dc:ba:59:c3:62:9f:45:9c:89:1d:f7:bc:61:
+         c8:cf:05:4d:c4:d9:e6:b9:8d:f4:f1:b3:6c:32:f6:3d:8e:c9:
+         9d:df:b8:5d:9a:6c:27:99:08:1e:ec:02:1a:ed:33:0f:8d:a2:
+         a6:d2:f5:7f:d7:3b:45:48:e0:d6:08:8d:8a:ee:49:66:2f:63:
+         2d:38:93:06:7e:a5:1e:39:f8:cf:00:d6:d2:de:53:09:04:01:
+         fd:ab:12:cc:ec:a9:e0:fa:77:ea:56:db:7e:34:6f:4c:c5:c4:
+         db:3f:cd:05:90:16:bf:e8:9c:46:82:38:ec:8b:00:e6:86:bd:
+         8c:4c:26:0e:2b:f7:83:f9:66:62:90:c3:a2:7b:2b:c7:9b:6b:
+         e0:c5:80:9d:5a:b8:5a:19:f7:37:b7:aa:10:d9:83:be:99:72:
+         3b:7b:a7:77:17:76:bd:d1:45:18:90:12:20:8d:ef:73:bb:44:
+         4b:c6:70:41:86:ea:11:c8:6c:2f:b9:00:25:aa:03:03:05:92:
+         33:4c:ed:15:9f:07:b0:ec:a4:4b:77:32:e1:59:b8:a2:92:18:
+         bc:d6:ae:8b:83:ad:9a:5a:36:dd:17:a5:1e:e8:05:57:72:1f:
+         9d:aa:6a:fe:f5:a5:6d:d1:8e:84:4d:34:bb:ee:7a:d0:ca:09:
+         72:2c:a2:1e:37:1e:9c:10:70:e3:11:35:e5:3d:81:9b:08:09:
+         6b:74:a0:1d:4c:0e:27:82:70:59:9f:31:90:ff:cc:e0:b4:ff:
+         73:bc:82:55:03:a7:2e:38:09:fc:18:51:2c:ad:be:c6:37:6d:
+         10:48:3c:32:25:6a:d8:8b:a7:fe:9b:ae:03:f6:b3:8a:e0:9c:
+         f8:b7:e9:44:7e:37:1f:25:e3:3f:51:c3:65:86:60:13:70:c1:
+         d7:58:f0:a8:2a:43:9f:96:5e:0f:05:cf:d6:8a:0b:f5:c5:8c:
+         3b:d1:16:78:a5:a8:b4:ec:bc:c9:c4:d1:d8:86:fa:44:dd
\ No newline at end of file
diff --git a/services/tests/servicestests/assets/KeyStoreRecoveryControllerTest/xml/invalid-cert-file-no-endpoint-cert.xml b/services/tests/servicestests/assets/KeyStoreRecoveryControllerTest/xml/invalid-cert-file-no-endpoint-cert.xml
new file mode 100644
index 0000000..6519f49
--- /dev/null
+++ b/services/tests/servicestests/assets/KeyStoreRecoveryControllerTest/xml/invalid-cert-file-no-endpoint-cert.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<certificates>
+  <metadata>
+    <serial>
+      1000
+    </serial>
+    <creation-time>
+      1515697631
+    </creation-time>
+    <refresh-interval>
+      2592000
+    </refresh-interval>
+    <previous>
+      <serial>
+        0
+      </serial>
+      <hash>
+        47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=
+      </hash>
+    </previous>
+  </metadata>
+  <endpoints>
+  </endpoints>
+</certificates>
diff --git a/services/tests/servicestests/assets/KeyStoreRecoveryControllerTest/xml/invalid-cert-file-no-refresh-interval.xml b/services/tests/servicestests/assets/KeyStoreRecoveryControllerTest/xml/invalid-cert-file-no-refresh-interval.xml
new file mode 100644
index 0000000..3da0122
--- /dev/null
+++ b/services/tests/servicestests/assets/KeyStoreRecoveryControllerTest/xml/invalid-cert-file-no-refresh-interval.xml
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<certificates>
+  <metadata>
+    <serial>
+      1000
+    </serial>
+    <creation-time>
+      1515697631
+    </creation-time>
+    <previous>
+      <serial>
+        0
+      </serial>
+      <hash>
+        47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=
+      </hash>
+    </previous>
+  </metadata>
+  <endpoints>
+    <cert>
+      MIIDCDCB8aADAgECAgYBYOlweDswDQYJKoZIhvcNAQELBQAwLTErMCkGA1UEAwwi
+      R29vZ2xlIENyeXB0QXV0aFZhdWx0IEludGVybWVkaWF0ZTAeFw0xODAxMTEwODE1
+      NTBaFw0yMDAxMTIwODE1NTBaMCkxJzAlBgNVBAMTHkdvb2dsZSBDcnlwdEF1dGhW
+      YXVsdCBJbnN0YW5jZTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABLgAERiYHfBu
+      tJT+htocB40BtDr2jdxh0EZJlQ8QhpMkZuA/0t/zeSAdkVWw5b16izJ9JVOi/KVl
+      4b0hRH54UvowDQYJKoZIhvcNAQELBQADggIBABZALhC9j3hpZ0AgN0tsqAP2Ix21
+      tNOcvo/aFJuSFanOM4DZbycZEYAo5rorvuFu7eXETBKDGnI5xreNAoQsaj/dyCHu
+      HKIn5P7yCmKvG2sV2TQ5go+0xV2x8BhTrtUWLeHvUbM3fXipa3NrordbA8MgzXwr
+      GR1Y1FuMOn5n4kiuHJ2sQTbDdzSQSK5VpH+6rjARlfOCyLUX0u8UKRRH81qhIQWb
+      UFMp9q1CVfiLP2O3CdDdpZXCysdflIb62TWnma+I8jqMryyxrMVs9kpfa8zkX9qe
+      33Vxp+QaQTqQ07/7KYVw869MeFn+bXeHnjUhqGY6S8M71vrTMG3M5p8Sq9LmV8Y5
+      7YB5uqKap2Inf0FOuJS7h7nVVzU/kOFkepaQVHyScwTPuuXNgpQg8XZnN/AWfRwJ
+      hf5zE6vXXTHMzQA1mY2eEhxGfpryv7LH8pvfcyTakdBlw8aMJjKdre8xLLGZeVCa
+      79plkfYD0rMrxtRHCGyTKGzUcx/B9kYJK5qBgJiDJLKF3XwGbAs/F8CyEPihjvj4
+      M2EoeyhmHWKLYsps6+uTksJ+PxZU14M7672K2y8BdulyfkZIhili118XnRykKkMf
+      JLQJKMqZx5O0B9bF8yQdcGKEGEwMQt5ENdH8HeiwLm4QS3VzFXYetgUPCM5lPDIp
+      BuwwuQxvQDF4pmQd
+    </cert>
+  </endpoints>
+</certificates>
diff --git a/services/tests/servicestests/assets/KeyStoreRecoveryControllerTest/xml/invalid-cert-file-no-serial.xml b/services/tests/servicestests/assets/KeyStoreRecoveryControllerTest/xml/invalid-cert-file-no-serial.xml
new file mode 100644
index 0000000..4370ff0
--- /dev/null
+++ b/services/tests/servicestests/assets/KeyStoreRecoveryControllerTest/xml/invalid-cert-file-no-serial.xml
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<certificates>
+  <metadata>
+    <creation-time>
+      1515697631
+    </creation-time>
+    <refresh-interval>
+      2592000
+    </refresh-interval>
+    <previous>
+      <serial>
+        0
+      </serial>
+      <hash>
+        47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=
+      </hash>
+    </previous>
+  </metadata>
+  <endpoints>
+    <cert>
+      MIIDCDCB8aADAgECAgYBYOlweDswDQYJKoZIhvcNAQELBQAwLTErMCkGA1UEAwwi
+      R29vZ2xlIENyeXB0QXV0aFZhdWx0IEludGVybWVkaWF0ZTAeFw0xODAxMTEwODE1
+      NTBaFw0yMDAxMTIwODE1NTBaMCkxJzAlBgNVBAMTHkdvb2dsZSBDcnlwdEF1dGhW
+      YXVsdCBJbnN0YW5jZTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABLgAERiYHfBu
+      tJT+htocB40BtDr2jdxh0EZJlQ8QhpMkZuA/0t/zeSAdkVWw5b16izJ9JVOi/KVl
+      4b0hRH54UvowDQYJKoZIhvcNAQELBQADggIBABZALhC9j3hpZ0AgN0tsqAP2Ix21
+      tNOcvo/aFJuSFanOM4DZbycZEYAo5rorvuFu7eXETBKDGnI5xreNAoQsaj/dyCHu
+      HKIn5P7yCmKvG2sV2TQ5go+0xV2x8BhTrtUWLeHvUbM3fXipa3NrordbA8MgzXwr
+      GR1Y1FuMOn5n4kiuHJ2sQTbDdzSQSK5VpH+6rjARlfOCyLUX0u8UKRRH81qhIQWb
+      UFMp9q1CVfiLP2O3CdDdpZXCysdflIb62TWnma+I8jqMryyxrMVs9kpfa8zkX9qe
+      33Vxp+QaQTqQ07/7KYVw869MeFn+bXeHnjUhqGY6S8M71vrTMG3M5p8Sq9LmV8Y5
+      7YB5uqKap2Inf0FOuJS7h7nVVzU/kOFkepaQVHyScwTPuuXNgpQg8XZnN/AWfRwJ
+      hf5zE6vXXTHMzQA1mY2eEhxGfpryv7LH8pvfcyTakdBlw8aMJjKdre8xLLGZeVCa
+      79plkfYD0rMrxtRHCGyTKGzUcx/B9kYJK5qBgJiDJLKF3XwGbAs/F8CyEPihjvj4
+      M2EoeyhmHWKLYsps6+uTksJ+PxZU14M7672K2y8BdulyfkZIhili118XnRykKkMf
+      JLQJKMqZx5O0B9bF8yQdcGKEGEwMQt5ENdH8HeiwLm4QS3VzFXYetgUPCM5lPDIp
+      BuwwuQxvQDF4pmQd
+    </cert>
+  </endpoints>
+</certificates>
diff --git a/services/tests/servicestests/assets/KeyStoreRecoveryControllerTest/xml/invalid-cert-file-two-refresh-intervals.xml b/services/tests/servicestests/assets/KeyStoreRecoveryControllerTest/xml/invalid-cert-file-two-refresh-intervals.xml
new file mode 100644
index 0000000..0f4e8a3
--- /dev/null
+++ b/services/tests/servicestests/assets/KeyStoreRecoveryControllerTest/xml/invalid-cert-file-two-refresh-intervals.xml
@@ -0,0 +1,46 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<certificates>
+  <metadata>
+    <serial>
+      1000
+    </serial>
+    <creation-time>
+      1515697631
+    </creation-time>
+    <refresh-interval>
+      2592000
+    </refresh-interval>
+    <refresh-interval>
+      2592000
+    </refresh-interval>
+    <previous>
+      <serial>
+        0
+      </serial>
+      <hash>
+        47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=
+      </hash>
+    </previous>
+  </metadata>
+  <endpoints>
+    <cert>
+      MIIDCDCB8aADAgECAgYBYOlweDswDQYJKoZIhvcNAQELBQAwLTErMCkGA1UEAwwi
+      R29vZ2xlIENyeXB0QXV0aFZhdWx0IEludGVybWVkaWF0ZTAeFw0xODAxMTEwODE1
+      NTBaFw0yMDAxMTIwODE1NTBaMCkxJzAlBgNVBAMTHkdvb2dsZSBDcnlwdEF1dGhW
+      YXVsdCBJbnN0YW5jZTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABLgAERiYHfBu
+      tJT+htocB40BtDr2jdxh0EZJlQ8QhpMkZuA/0t/zeSAdkVWw5b16izJ9JVOi/KVl
+      4b0hRH54UvowDQYJKoZIhvcNAQELBQADggIBABZALhC9j3hpZ0AgN0tsqAP2Ix21
+      tNOcvo/aFJuSFanOM4DZbycZEYAo5rorvuFu7eXETBKDGnI5xreNAoQsaj/dyCHu
+      HKIn5P7yCmKvG2sV2TQ5go+0xV2x8BhTrtUWLeHvUbM3fXipa3NrordbA8MgzXwr
+      GR1Y1FuMOn5n4kiuHJ2sQTbDdzSQSK5VpH+6rjARlfOCyLUX0u8UKRRH81qhIQWb
+      UFMp9q1CVfiLP2O3CdDdpZXCysdflIb62TWnma+I8jqMryyxrMVs9kpfa8zkX9qe
+      33Vxp+QaQTqQ07/7KYVw869MeFn+bXeHnjUhqGY6S8M71vrTMG3M5p8Sq9LmV8Y5
+      7YB5uqKap2Inf0FOuJS7h7nVVzU/kOFkepaQVHyScwTPuuXNgpQg8XZnN/AWfRwJ
+      hf5zE6vXXTHMzQA1mY2eEhxGfpryv7LH8pvfcyTakdBlw8aMJjKdre8xLLGZeVCa
+      79plkfYD0rMrxtRHCGyTKGzUcx/B9kYJK5qBgJiDJLKF3XwGbAs/F8CyEPihjvj4
+      M2EoeyhmHWKLYsps6+uTksJ+PxZU14M7672K2y8BdulyfkZIhili118XnRykKkMf
+      JLQJKMqZx5O0B9bF8yQdcGKEGEwMQt5ENdH8HeiwLm4QS3VzFXYetgUPCM5lPDIp
+      BuwwuQxvQDF4pmQd
+    </cert>
+  </endpoints>
+</certificates>
diff --git a/services/tests/servicestests/assets/KeyStoreRecoveryControllerTest/xml/invalid-cert-file-two-serials.xml b/services/tests/servicestests/assets/KeyStoreRecoveryControllerTest/xml/invalid-cert-file-two-serials.xml
new file mode 100644
index 0000000..a2685aa
--- /dev/null
+++ b/services/tests/servicestests/assets/KeyStoreRecoveryControllerTest/xml/invalid-cert-file-two-serials.xml
@@ -0,0 +1,46 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<certificates>
+  <metadata>
+    <serial>
+      1000
+    </serial>
+    <serial>
+      1000
+    </serial>
+    <creation-time>
+      1515697631
+    </creation-time>
+    <refresh-interval>
+      2592000
+    </refresh-interval>
+    <previous>
+      <serial>
+        0
+      </serial>
+      <hash>
+        47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=
+      </hash>
+    </previous>
+  </metadata>
+  <endpoints>
+    <cert>
+      MIIDCDCB8aADAgECAgYBYOlweDswDQYJKoZIhvcNAQELBQAwLTErMCkGA1UEAwwi
+      R29vZ2xlIENyeXB0QXV0aFZhdWx0IEludGVybWVkaWF0ZTAeFw0xODAxMTEwODE1
+      NTBaFw0yMDAxMTIwODE1NTBaMCkxJzAlBgNVBAMTHkdvb2dsZSBDcnlwdEF1dGhW
+      YXVsdCBJbnN0YW5jZTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABLgAERiYHfBu
+      tJT+htocB40BtDr2jdxh0EZJlQ8QhpMkZuA/0t/zeSAdkVWw5b16izJ9JVOi/KVl
+      4b0hRH54UvowDQYJKoZIhvcNAQELBQADggIBABZALhC9j3hpZ0AgN0tsqAP2Ix21
+      tNOcvo/aFJuSFanOM4DZbycZEYAo5rorvuFu7eXETBKDGnI5xreNAoQsaj/dyCHu
+      HKIn5P7yCmKvG2sV2TQ5go+0xV2x8BhTrtUWLeHvUbM3fXipa3NrordbA8MgzXwr
+      GR1Y1FuMOn5n4kiuHJ2sQTbDdzSQSK5VpH+6rjARlfOCyLUX0u8UKRRH81qhIQWb
+      UFMp9q1CVfiLP2O3CdDdpZXCysdflIb62TWnma+I8jqMryyxrMVs9kpfa8zkX9qe
+      33Vxp+QaQTqQ07/7KYVw869MeFn+bXeHnjUhqGY6S8M71vrTMG3M5p8Sq9LmV8Y5
+      7YB5uqKap2Inf0FOuJS7h7nVVzU/kOFkepaQVHyScwTPuuXNgpQg8XZnN/AWfRwJ
+      hf5zE6vXXTHMzQA1mY2eEhxGfpryv7LH8pvfcyTakdBlw8aMJjKdre8xLLGZeVCa
+      79plkfYD0rMrxtRHCGyTKGzUcx/B9kYJK5qBgJiDJLKF3XwGbAs/F8CyEPihjvj4
+      M2EoeyhmHWKLYsps6+uTksJ+PxZU14M7672K2y8BdulyfkZIhili118XnRykKkMf
+      JLQJKMqZx5O0B9bF8yQdcGKEGEwMQt5ENdH8HeiwLm4QS3VzFXYetgUPCM5lPDIp
+      BuwwuQxvQDF4pmQd
+    </cert>
+  </endpoints>
+</certificates>
diff --git a/services/tests/servicestests/assets/KeyStoreRecoveryControllerTest/xml/invalid-sig-file-no-signature.sig.xml b/services/tests/servicestests/assets/KeyStoreRecoveryControllerTest/xml/invalid-sig-file-no-signature.sig.xml
new file mode 100644
index 0000000..5dc8ffa
--- /dev/null
+++ b/services/tests/servicestests/assets/KeyStoreRecoveryControllerTest/xml/invalid-sig-file-no-signature.sig.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<signature>
+  <certificate>
+    MIIDCDCB8aADAgECAgYBYOlweDswDQYJKoZIhvcNAQELBQAwLTErMCkGA1UEAwwi
+    R29vZ2xlIENyeXB0QXV0aFZhdWx0IEludGVybWVkaWF0ZTAeFw0xODAxMTEwODE1
+    NTBaFw0yMDAxMTIwODE1NTBaMCkxJzAlBgNVBAMTHkdvb2dsZSBDcnlwdEF1dGhW
+    YXVsdCBJbnN0YW5jZTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABLgAERiYHfBu
+    tJT+htocB40BtDr2jdxh0EZJlQ8QhpMkZuA/0t/zeSAdkVWw5b16izJ9JVOi/KVl
+    4b0hRH54UvowDQYJKoZIhvcNAQELBQADggIBABZALhC9j3hpZ0AgN0tsqAP2Ix21
+    tNOcvo/aFJuSFanOM4DZbycZEYAo5rorvuFu7eXETBKDGnI5xreNAoQsaj/dyCHu
+    HKIn5P7yCmKvG2sV2TQ5go+0xV2x8BhTrtUWLeHvUbM3fXipa3NrordbA8MgzXwr
+    GR1Y1FuMOn5n4kiuHJ2sQTbDdzSQSK5VpH+6rjARlfOCyLUX0u8UKRRH81qhIQWb
+    UFMp9q1CVfiLP2O3CdDdpZXCysdflIb62TWnma+I8jqMryyxrMVs9kpfa8zkX9qe
+    33Vxp+QaQTqQ07/7KYVw869MeFn+bXeHnjUhqGY6S8M71vrTMG3M5p8Sq9LmV8Y5
+    7YB5uqKap2Inf0FOuJS7h7nVVzU/kOFkepaQVHyScwTPuuXNgpQg8XZnN/AWfRwJ
+    hf5zE6vXXTHMzQA1mY2eEhxGfpryv7LH8pvfcyTakdBlw8aMJjKdre8xLLGZeVCa
+    79plkfYD0rMrxtRHCGyTKGzUcx/B9kYJK5qBgJiDJLKF3XwGbAs/F8CyEPihjvj4
+    M2EoeyhmHWKLYsps6+uTksJ+PxZU14M7672K2y8BdulyfkZIhili118XnRykKkMf
+    JLQJKMqZx5O0B9bF8yQdcGKEGEwMQt5ENdH8HeiwLm4QS3VzFXYetgUPCM5lPDIp
+    BuwwuQxvQDF4pmQd
+  </certificate>
+</signature>
diff --git a/services/tests/servicestests/assets/KeyStoreRecoveryControllerTest/xml/invalid-sig-file-no-signer-cert.sig.xml b/services/tests/servicestests/assets/KeyStoreRecoveryControllerTest/xml/invalid-sig-file-no-signer-cert.sig.xml
new file mode 100644
index 0000000..aa81295
--- /dev/null
+++ b/services/tests/servicestests/assets/KeyStoreRecoveryControllerTest/xml/invalid-sig-file-no-signer-cert.sig.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<signature>
+  <value>
+    VEVTVA==
+  </value>
+</signature>
diff --git a/services/tests/servicestests/assets/KeyStoreRecoveryControllerTest/xml/invalid-sig-file-two-signatures.sig.xml b/services/tests/servicestests/assets/KeyStoreRecoveryControllerTest/xml/invalid-sig-file-two-signatures.sig.xml
new file mode 100644
index 0000000..09d0f44
--- /dev/null
+++ b/services/tests/servicestests/assets/KeyStoreRecoveryControllerTest/xml/invalid-sig-file-two-signatures.sig.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<signature>
+  <certificate>
+    MIIDCDCB8aADAgECAgYBYOlweDswDQYJKoZIhvcNAQELBQAwLTErMCkGA1UEAwwi
+    R29vZ2xlIENyeXB0QXV0aFZhdWx0IEludGVybWVkaWF0ZTAeFw0xODAxMTEwODE1
+    NTBaFw0yMDAxMTIwODE1NTBaMCkxJzAlBgNVBAMTHkdvb2dsZSBDcnlwdEF1dGhW
+    YXVsdCBJbnN0YW5jZTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABLgAERiYHfBu
+    tJT+htocB40BtDr2jdxh0EZJlQ8QhpMkZuA/0t/zeSAdkVWw5b16izJ9JVOi/KVl
+    4b0hRH54UvowDQYJKoZIhvcNAQELBQADggIBABZALhC9j3hpZ0AgN0tsqAP2Ix21
+    tNOcvo/aFJuSFanOM4DZbycZEYAo5rorvuFu7eXETBKDGnI5xreNAoQsaj/dyCHu
+    HKIn5P7yCmKvG2sV2TQ5go+0xV2x8BhTrtUWLeHvUbM3fXipa3NrordbA8MgzXwr
+    GR1Y1FuMOn5n4kiuHJ2sQTbDdzSQSK5VpH+6rjARlfOCyLUX0u8UKRRH81qhIQWb
+    UFMp9q1CVfiLP2O3CdDdpZXCysdflIb62TWnma+I8jqMryyxrMVs9kpfa8zkX9qe
+    33Vxp+QaQTqQ07/7KYVw869MeFn+bXeHnjUhqGY6S8M71vrTMG3M5p8Sq9LmV8Y5
+    7YB5uqKap2Inf0FOuJS7h7nVVzU/kOFkepaQVHyScwTPuuXNgpQg8XZnN/AWfRwJ
+    hf5zE6vXXTHMzQA1mY2eEhxGfpryv7LH8pvfcyTakdBlw8aMJjKdre8xLLGZeVCa
+    79plkfYD0rMrxtRHCGyTKGzUcx/B9kYJK5qBgJiDJLKF3XwGbAs/F8CyEPihjvj4
+    M2EoeyhmHWKLYsps6+uTksJ+PxZU14M7672K2y8BdulyfkZIhili118XnRykKkMf
+    JLQJKMqZx5O0B9bF8yQdcGKEGEwMQt5ENdH8HeiwLm4QS3VzFXYetgUPCM5lPDIp
+    BuwwuQxvQDF4pmQd
+  </certificate>
+  <value>
+    VEVTVA==
+  </value>
+  <value>
+    VEVTVA==
+  </value>
+</signature>
diff --git a/services/tests/servicestests/assets/KeyStoreRecoveryControllerTest/xml/invalid-sig-file-two-signer-certs.sig.xml b/services/tests/servicestests/assets/KeyStoreRecoveryControllerTest/xml/invalid-sig-file-two-signer-certs.sig.xml
new file mode 100644
index 0000000..44e8993
--- /dev/null
+++ b/services/tests/servicestests/assets/KeyStoreRecoveryControllerTest/xml/invalid-sig-file-two-signer-certs.sig.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<signature>
+  <certificate>
+    signer certificate 1
+  </certificate>
+  <certificate>
+    signer certificate 2
+  </certificate>
+  <value>
+    VEVTVA==
+  </value>
+</signature>
diff --git a/services/tests/servicestests/assets/KeyStoreRecoveryControllerTest/xml/valid-cert-file-no-intermediates.xml b/services/tests/servicestests/assets/KeyStoreRecoveryControllerTest/xml/valid-cert-file-no-intermediates.xml
new file mode 100644
index 0000000..e59bf36
--- /dev/null
+++ b/services/tests/servicestests/assets/KeyStoreRecoveryControllerTest/xml/valid-cert-file-no-intermediates.xml
@@ -0,0 +1,43 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<certificates>
+  <metadata>
+    <serial>
+      1000
+    </serial>
+    <creation-time>
+      1515697631
+    </creation-time>
+    <refresh-interval>
+      2592000
+    </refresh-interval>
+    <previous>
+      <serial>
+        0
+      </serial>
+      <hash>
+        47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=
+      </hash>
+    </previous>
+  </metadata>
+  <endpoints>
+    <cert>
+      MIIDCDCB8aADAgECAgYBYOlweDswDQYJKoZIhvcNAQELBQAwLTErMCkGA1UEAwwi
+      R29vZ2xlIENyeXB0QXV0aFZhdWx0IEludGVybWVkaWF0ZTAeFw0xODAxMTEwODE1
+      NTBaFw0yMDAxMTIwODE1NTBaMCkxJzAlBgNVBAMTHkdvb2dsZSBDcnlwdEF1dGhW
+      YXVsdCBJbnN0YW5jZTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABLgAERiYHfBu
+      tJT+htocB40BtDr2jdxh0EZJlQ8QhpMkZuA/0t/zeSAdkVWw5b16izJ9JVOi/KVl
+      4b0hRH54UvowDQYJKoZIhvcNAQELBQADggIBABZALhC9j3hpZ0AgN0tsqAP2Ix21
+      tNOcvo/aFJuSFanOM4DZbycZEYAo5rorvuFu7eXETBKDGnI5xreNAoQsaj/dyCHu
+      HKIn5P7yCmKvG2sV2TQ5go+0xV2x8BhTrtUWLeHvUbM3fXipa3NrordbA8MgzXwr
+      GR1Y1FuMOn5n4kiuHJ2sQTbDdzSQSK5VpH+6rjARlfOCyLUX0u8UKRRH81qhIQWb
+      UFMp9q1CVfiLP2O3CdDdpZXCysdflIb62TWnma+I8jqMryyxrMVs9kpfa8zkX9qe
+      33Vxp+QaQTqQ07/7KYVw869MeFn+bXeHnjUhqGY6S8M71vrTMG3M5p8Sq9LmV8Y5
+      7YB5uqKap2Inf0FOuJS7h7nVVzU/kOFkepaQVHyScwTPuuXNgpQg8XZnN/AWfRwJ
+      hf5zE6vXXTHMzQA1mY2eEhxGfpryv7LH8pvfcyTakdBlw8aMJjKdre8xLLGZeVCa
+      79plkfYD0rMrxtRHCGyTKGzUcx/B9kYJK5qBgJiDJLKF3XwGbAs/F8CyEPihjvj4
+      M2EoeyhmHWKLYsps6+uTksJ+PxZU14M7672K2y8BdulyfkZIhili118XnRykKkMf
+      JLQJKMqZx5O0B9bF8yQdcGKEGEwMQt5ENdH8HeiwLm4QS3VzFXYetgUPCM5lPDIp
+      BuwwuQxvQDF4pmQd
+    </cert>
+  </endpoints>
+</certificates>
diff --git a/services/tests/servicestests/assets/KeyStoreRecoveryControllerTest/xml/valid-cert-file.xml b/services/tests/servicestests/assets/KeyStoreRecoveryControllerTest/xml/valid-cert-file.xml
new file mode 100644
index 0000000..be4893f
--- /dev/null
+++ b/services/tests/servicestests/assets/KeyStoreRecoveryControllerTest/xml/valid-cert-file.xml
@@ -0,0 +1,150 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<certificates>
+  <metadata>
+    <serial>
+      1000
+    </serial>
+    <creation-time>
+      1515697631
+    </creation-time>
+    <refresh-interval>
+      2592000
+    </refresh-interval>
+    <previous>
+      <serial>
+        0
+      </serial>
+      <hash>
+        47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=
+      </hash>
+    </previous>
+  </metadata>
+  <intermediates>
+    <cert>
+      MIIHMDCCAxegAwIBAgICEAEwDQYJKoZIhvcNAQELBQAwIDEeMBwGA1UEAwwVR29v
+      Z2xlIENyeXB0QXV0aFZhdWx0MB4XDTE4MDExMjAwMjM1N1oXDTI4MDExMDAwMjM1
+      N1owLTErMCkGA1UEAwwiR29vZ2xlIENyeXB0QXV0aFZhdWx0IEludGVybWVkaWF0
+      ZTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBALlhoKYuwxaatY9ERDFp
+      iEygSnjy0xzaiF4uCyiTfAuboSi5QwGon3ohf0ufJF02L9lnTMoeBAg+88m8AMgW
+      KFcEupabqlZfA3F/50mMCmnJvBSLXJ+chUdcAVpwcZAsq6ko22ARBxao1wu2qxNe
+      D8eXiiK8DRpTtKy3wOldZJ222v35v9JGOTjORZRrcv7Z8f6I5/cSsTS+WoVk/aBt
+      QqyFkcdw1zqulnlFE2rxNAVgyLlW71WJikYTDtUDeo79LvkDGjVsLo2MfpNxuK+5
+      MuMqzyN5LmzXJmNCEW1O5IIIUAPhgy5s08G+3G644wCEWsAnv2FBWLBn/HmJu6Uq
+      nSM2AaJN56V0tJG/yL2VgTnPJrJypNTKZW3OTCLCaYcTEbKfarxLwVWxvIWSIgkn
+      0q57GYhf7O+x9vvcOUmZwVxZECorIiK4n5AWG/KD3dWI3UGGGpYsDazRngA/bQPu
+      DSzBP9FBVcQt3/DMBG1s6f2Eko5f6aTFcVW9iV7aWLeIq+pQYlbmG42decj+aHLQ
+      COp5KV+Q77y4kFhZQFAQ1mN4crnhuEc1K5SmjAK24zIqWbwM3ly0KSQFc9jAmONg
+      0xu7kAObP3PZk85En12yLLscNmHCWYfOOEvTHf4KX7tjBl4HHp/ur+2Qwgpt9MFB
+      MGqR2cni5OV6gZcRdHaEerjrAgMBAAGjZjBkMB0GA1UdDgQWBBRE9RxHT7U3EP1v
+      djRzNYMrU7EseDAfBgNVHSMEGDAWgBTXe6GHkou4UhopkaU2Why0Muvs0TASBgNV
+      HRMBAf8ECDAGAQH/AgEBMA4GA1UdDwEB/wQEAwIBhjANBgkqhkiG9w0BAQsFAAOC
+      BAIAAfa7FKkfh4t+G8Sv4n3gUcuEAtmpCsTtHJU67r5eSvR24fDX7sqqSIib9sQ8
+      adA1FtnE3wnC4bEKnIwQLerWTN4i4V9oEKRIT5o5Rr61LyO9R+Prpo6uJVoPujFH
+      GZPdIj/Qs4Xax4a11QD+70+ZmmdL6yfigVDWA4yduFdMg6heHRf3EFCbVBv5qbBS
+      n8+eFKRnBZ/kQdFlYt+G+92Qqyn2uAcER6kZjIfPdnZh44SazLP37W8AkDX30Pmk
+      V0PHVGCDrap44q3maI1m8NAE1jGwwmRzJL953x5XgbVGt0K/3cNoWtKLenwX/G3I
+      akrgvOY5Zl0v3FRDZwGFt9UIBfZDDOGRMXIgIGs/1cvkwWpOT6dyReqDXempiQ1q
+      Yy6J5VsK5WK6gEelUyoACbzgby25V6a79Q1MI7dXmFQfCcX0nAD/AZmM1HkeYgrC
+      uq6fWoPOVMKML2mN90rCzkWxGaLcl5dPfad0O1LrcP48SRE5MXMWyxZZBon+wDIk
+      ascyM/r4fmk4kq64YKdm2wxCDMNArAIcyBkwOaWWfabtSagxJ3qtMtxK0qBUsbLC
+      yMyYpgU1h9c8rEdc4JgeE2LXJzxTKDc3SBOqbuNMlKWjYA+X+SUvVYALrQKAC+5v
+      wdUhLYdAPAksqk/ZoiBjkW35FfvqQMJBY29VnDT1h7/Nxk5gu+goTA9oFIYNrNte
+      +s0my+IUgYhKJBsgh7Mupv+B92GN5b3b440BMHB5QR959Jdq6BAXNUyZLM5fhZQE
+      Jj/rxZFXaqq757kgUhwWBz5TDbYF7GkqTyM4k430xwJKY0AYYEHmv1UYNo5X4G3x
+      SC2LhWC1b9VAykdkHbLs+IA8klxURmLmRiRj1UryhQjjT8h/FvNyPnbT1AKoElix
+      QLnLi8thkJ+tQggO0hISFsIrKNfnn0V6O0VKw9UZsMigsbYG5EbzIXcAyy8Avr9n
+      um7gBBZDt7fWso0+pG1UenJ+PybeuW/azQDLRw1Syz8OwU+ABRLq0JyyAtV7VPY5
+      C9pkKS+bU8nECxr6dMhAbpLBHlKsyb1qtkOt1p7WagEQZFIIc6svc73+L/ET/lWn
+      GBmkVVsCN7Aqyo5aXQWueXP4FUL+6O5+JALqw3qPeQgfnLkh0cUuccNND05QeEiv
+      Zswc/23KJXy1XbdVKT3UP0RAF7DxstbRGQbAT3z+n931e3KhtU28OKjsFtoeq2Dj
+      6STPEXh4rYFWMM8+DrJetAtBqk/i+vBwRA8f7jqIPPep/vEjPqqMOpdSVcoFQ1df
+      JuOZtGfEUjFHnlDr3eGP7KUIEZvhan1zm544dDgPVTXxrY4moi2BhKEY69zRSX6B
+      +a0fa5B3pxc8BN0LsHA0stT/Y2o=
+    </cert>
+    <cert>
+      MIIESTCCAjGgAwIBAgICEAAwDQYJKoZIhvcNAQELBQAwLTErMCkGA1UEAwwiR29v
+      Z2xlIENyeXB0QXV0aFZhdWx0IEludGVybWVkaWF0ZTAeFw0xODAxMTIwMDM4MDNa
+      Fw0yMzAxMTEwMDM4MDNaMDoxODA2BgNVBAMML0dvb2dsZSBDcnlwdEF1dGhWYXVs
+      dCBJbnRlcm1lZGlhdGUgSW50ZXJtZWRpYXRlMIIBIjANBgkqhkiG9w0BAQEFAAOC
+      AQ8AMIIBCgKCAQEA0v3bN3MwKifDAkF1524XzuaxYtk1sQKUlAlNngh+Qv4RjCkX
+      TECi7po8LeNsY+hWxmW3XZ22RBphe/yP4YcOdbqlIjZYNx3b75hCSJCadOkdW+Z9
+      f6+tKsHgeUja6r9r2TThzileImAvjXShe7GZYW8csPv6HaEVRXQlu8fGAZf8skmJ
+      EMfJx84//WeULdVz94beDhi9YAf4gLfmOayQcdWhDcMYI39knJcRny1ffRGgb1Hf
+      lE+3/a3aGFeODaxfkPaGRxEhzhZ/JDBiNgUAH/u7C5nxqa2WOu5e0wq3S0TndIOE
+      hmnwCE2GvxADFQst+rSsOn4EHit70hv4CfrMRQIDAQABo2YwZDAdBgNVHQ4EFgQU
+      0dKv4xTaEmdwHyox3tY8H8XVDGIwHwYDVR0jBBgwFoAURPUcR0+1NxD9b3Y0czWD
+      K1OxLHgwEgYDVR0TAQH/BAgwBgEB/wIBADAOBgNVHQ8BAf8EBAMCAYYwDQYJKoZI
+      hvcNAQELBQADggIBAJaArqLlJ2SLQ8JRwanD6LPlqQxucQ+x/LztTQpzPrsFfJds
+      E/sDZr2rXhDpz/bifIdj/DCbQ6/3w+t5JdFjT8GjXLgz1kCa/4W409FgSTgy1ENn
+      AMUU6pFIbOq/Qy/71uOTrrFWYTN5Vk+RBGxx5iDfHjDYraudi4VlcNkal4OyM98n
+      N3qp9cZD0RtWxMhvq6ahgmf9cTbEw6+l8yf/bogGLBYXXYeOoO5Q134AxrrgfthE
+      tvyKwJkT/l3OFKRcaHrebs+V1z5gPs7zWOyO5n2Z1SAmcOGfTfKMZWwp3Hi3OTr2
+      gB3LUYKyQVhC70dka3X+IbnFg5YfzJtX6YGnHlnI1SufOkEpGQDfcc0UQAWg/lgb
+      RkfMFD9tuJomBhyqv1YaxLN8yL4ZTRU0KCvvC5I5+X/zt9kBjnHlBOdYtknZT5jz
+      7+mjqWdpmWoAjeV5+CgIzG2k7JAm6rQuE1ZQNF0wAYxPret4NHPJFqfD5gGhdrYw
+      pEUxkcwHERA/E1CkpyqUy/Hd3kqHvnEDqzFcxBdUdmOgnbpI2nAZdEpfxmA5+M1n
+      UoxQ8ZWAZH+Mdlkw/Hx5hVjGjz8snD4QN25pj/wT+V6AR5OmYb8yfsQb2S8a8yDp
+      HzcIHW+dEWpX2boirOsrdI16kNtxYqtG7c5qWBPJy5Zjkvh9qbnfT/RQx10g
+    </cert>
+  </intermediates>
+  <endpoints>
+    <cert>
+      MIIDCDCB8aADAgECAgYBYOlweDswDQYJKoZIhvcNAQELBQAwLTErMCkGA1UEAwwi
+      R29vZ2xlIENyeXB0QXV0aFZhdWx0IEludGVybWVkaWF0ZTAeFw0xODAxMTEwODE1
+      NTBaFw0yMDAxMTIwODE1NTBaMCkxJzAlBgNVBAMTHkdvb2dsZSBDcnlwdEF1dGhW
+      YXVsdCBJbnN0YW5jZTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABLgAERiYHfBu
+      tJT+htocB40BtDr2jdxh0EZJlQ8QhpMkZuA/0t/zeSAdkVWw5b16izJ9JVOi/KVl
+      4b0hRH54UvowDQYJKoZIhvcNAQELBQADggIBABZALhC9j3hpZ0AgN0tsqAP2Ix21
+      tNOcvo/aFJuSFanOM4DZbycZEYAo5rorvuFu7eXETBKDGnI5xreNAoQsaj/dyCHu
+      HKIn5P7yCmKvG2sV2TQ5go+0xV2x8BhTrtUWLeHvUbM3fXipa3NrordbA8MgzXwr
+      GR1Y1FuMOn5n4kiuHJ2sQTbDdzSQSK5VpH+6rjARlfOCyLUX0u8UKRRH81qhIQWb
+      UFMp9q1CVfiLP2O3CdDdpZXCysdflIb62TWnma+I8jqMryyxrMVs9kpfa8zkX9qe
+      33Vxp+QaQTqQ07/7KYVw869MeFn+bXeHnjUhqGY6S8M71vrTMG3M5p8Sq9LmV8Y5
+      7YB5uqKap2Inf0FOuJS7h7nVVzU/kOFkepaQVHyScwTPuuXNgpQg8XZnN/AWfRwJ
+      hf5zE6vXXTHMzQA1mY2eEhxGfpryv7LH8pvfcyTakdBlw8aMJjKdre8xLLGZeVCa
+      79plkfYD0rMrxtRHCGyTKGzUcx/B9kYJK5qBgJiDJLKF3XwGbAs/F8CyEPihjvj4
+      M2EoeyhmHWKLYsps6+uTksJ+PxZU14M7672K2y8BdulyfkZIhili118XnRykKkMf
+      JLQJKMqZx5O0B9bF8yQdcGKEGEwMQt5ENdH8HeiwLm4QS3VzFXYetgUPCM5lPDIp
+      BuwwuQxvQDF4pmQd
+    </cert>
+    <cert>
+      MIICrDCCAZSgAwIBAgICEAAwDQYJKoZIhvcNAQELBQAwOjE4MDYGA1UEAwwvR29v
+      Z2xlIENyeXB0QXV0aFZhdWx0IEludGVybWVkaWF0ZSBJbnRlcm1lZGlhdGUwHhcN
+      MTgwMTEyMDEwMzA5WhcNMTkwMTEyMDEwMzA5WjArMSkwJwYDVQQDDCBHb29nbGUg
+      Q3J5cHRBdXRoVmF1bHQgSW5zdGFuY2UgMjBZMBMGByqGSM49AgEGCCqGSM49AwEH
+      A0IABGhmBQyWdjsXKJRbkW4iIrvt6iqhX5t2XGt/vZS9CoOl0fs+EvJXo4kgrnx8
+      /8SGxz3pwRkFhY943QYy6a1gv/2jgZUwgZIwCQYDVR0TBAIwADAdBgNVHQ4EFgQU
+      xFmLyxUS2JHKURBtewBKRP6kQBgwVgYDVR0jBE8wTYAU0dKv4xTaEmdwHyox3tY8
+      H8XVDGKhMaQvMC0xKzApBgNVBAMMIkdvb2dsZSBDcnlwdEF1dGhWYXVsdCBJbnRl
+      cm1lZGlhdGWCAhAAMA4GA1UdDwEB/wQEAwIDCDANBgkqhkiG9w0BAQsFAAOCAQEA
+      EJWpl7HU6LxukLqhw2tVZr7IRrKIucRk+RKaaiMx1Hx2jsTTskiJRiZas/xoPSqX
+      z1K5DVgI486i7HyqnWkGH5xVzCsv+rya5FOSTS3jVtgtoA4HFEqeeAcDowPDqVw3
+      yFTA55ukZnzVaPLpDfPqkhzWiuLQ/4fI6YCmOnWB8KtHTMdyGsDSAkpoxpok++NJ
+      Lu79BoBLe2ucjN383lTlieLxmrmHjF9ryYSQczcm0v6irMOMxEovw5iT4LHiEhbm
+      DfOPW909fe/s+K3TGZ3Q6U77x8g5k9dVovMgA4pFwtREtknFjeK1wXR3/eXGcP3W
+      0bMX1yTWYJQFWCG3DFoC5w==
+    </cert>
+    <cert>
+      MIIDkjCCAXqgAwIBAgICEAEwDQYJKoZIhvcNAQELBQAwLTErMCkGA1UEAwwiR29v
+      Z2xlIENyeXB0QXV0aFZhdWx0IEludGVybWVkaWF0ZTAeFw0xODAxMTIwMDU2MjNa
+      Fw0yMDA3MTMwMDU2MjNaMCsxKTAnBgNVBAMMIEdvb2dsZSBDcnlwdEF1dGhWYXVs
+      dCBJbnN0YW5jZSAxMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEimGC4KEks4JL
+      83DNAAGCA5sKqqxqUPgV8gxgsijlMPL0TOtyJhJ2vSFIULEa0coVFbu+fAdxt3CV
+      DbzD0vWAmaOBiDCBhTAJBgNVHRMEAjAAMB0GA1UdDgQWBBTVeDueShnq1LCqkCFL
+      MAYtpxkCuDBJBgNVHSMEQjBAgBRE9RxHT7U3EP1vdjRzNYMrU7EseKEkpCIwIDEe
+      MBwGA1UEAwwVR29vZ2xlIENyeXB0QXV0aFZhdWx0ggIQATAOBgNVHQ8BAf8EBAMC
+      AwgwDQYJKoZIhvcNAQELBQADggIBADUZCjfBwBL9SSZDkMwE3n08schTBAgZCCrv
+      XOQVPGrfbFUcMv1mT4e8a5zxE98HsCS6K4HB40RtTXkmt6nuN+NyBrAmZJrBCqvG
+      IYtGsBLLEnojgWuSpQIBQeQy9it3RFdSR/1FIPssrWUB5KrtRvd+07+Mo7tI91jE
+      EunOrocu46g6p/OKSIZ7UmwZzczn2CJsrxuNPgqdlza3ytb+TTm536ZHnqaefSZD
+      rrruNneTXoqjC2OFn/OVLHQ1ee/vrHiX70P8p1V09cccDiwMCIZskNACYgWRdLYU
+      F5aYGueoFajrb4zmMoy8DK/4lh1EsfWMsrsQK6whmPidzgz37nb3rPpiHTdxu1Fc
+      2XM9QV3Jfj2U5FMOZTcDha7W7kb++gSnQEPTM0+Zu6lTJmcZgK4RJ7lmIfK/eVbJ
+      6V/wplOzXSxO3jBb2LNhLbhkUzcg68EIEPxaBpXYVOU2tSo2FMgi7/YLwQLorc6Q
+      h1pUZep8T8SLpvI02GvsB8TroFr2tCvCe5A1VxBQDx9IE7nEd2N30XxqReFk8Y82
+      xZMUOA4DL33NI45KWjhcawm0tzAPFfKjta/zYvnf7rwwE4r2PVuOXdet5eN8zBje
+      yJbEpjCemADujcwtYQ8hScyj/eCT2KNbZ9ibY2yrZEEuRQanq8CLAbpvSZYIHVhg
+      Clar7+38
+    </cert>
+  </endpoints>
+</certificates>
diff --git a/services/tests/servicestests/assets/KeyStoreRecoveryControllerTest/xml/valid-sig-file-no-intermediates.sig.xml b/services/tests/servicestests/assets/KeyStoreRecoveryControllerTest/xml/valid-sig-file-no-intermediates.sig.xml
new file mode 100644
index 0000000..9c0adcd
--- /dev/null
+++ b/services/tests/servicestests/assets/KeyStoreRecoveryControllerTest/xml/valid-sig-file-no-intermediates.sig.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<signature>
+  <certificate>
+    MIIDCDCB8aADAgECAgYBYOlweDswDQYJKoZIhvcNAQELBQAwLTErMCkGA1UEAwwi
+    R29vZ2xlIENyeXB0QXV0aFZhdWx0IEludGVybWVkaWF0ZTAeFw0xODAxMTEwODE1
+    NTBaFw0yMDAxMTIwODE1NTBaMCkxJzAlBgNVBAMTHkdvb2dsZSBDcnlwdEF1dGhW
+    YXVsdCBJbnN0YW5jZTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABLgAERiYHfBu
+    tJT+htocB40BtDr2jdxh0EZJlQ8QhpMkZuA/0t/zeSAdkVWw5b16izJ9JVOi/KVl
+    4b0hRH54UvowDQYJKoZIhvcNAQELBQADggIBABZALhC9j3hpZ0AgN0tsqAP2Ix21
+    tNOcvo/aFJuSFanOM4DZbycZEYAo5rorvuFu7eXETBKDGnI5xreNAoQsaj/dyCHu
+    HKIn5P7yCmKvG2sV2TQ5go+0xV2x8BhTrtUWLeHvUbM3fXipa3NrordbA8MgzXwr
+    GR1Y1FuMOn5n4kiuHJ2sQTbDdzSQSK5VpH+6rjARlfOCyLUX0u8UKRRH81qhIQWb
+    UFMp9q1CVfiLP2O3CdDdpZXCysdflIb62TWnma+I8jqMryyxrMVs9kpfa8zkX9qe
+    33Vxp+QaQTqQ07/7KYVw869MeFn+bXeHnjUhqGY6S8M71vrTMG3M5p8Sq9LmV8Y5
+    7YB5uqKap2Inf0FOuJS7h7nVVzU/kOFkepaQVHyScwTPuuXNgpQg8XZnN/AWfRwJ
+    hf5zE6vXXTHMzQA1mY2eEhxGfpryv7LH8pvfcyTakdBlw8aMJjKdre8xLLGZeVCa
+    79plkfYD0rMrxtRHCGyTKGzUcx/B9kYJK5qBgJiDJLKF3XwGbAs/F8CyEPihjvj4
+    M2EoeyhmHWKLYsps6+uTksJ+PxZU14M7672K2y8BdulyfkZIhili118XnRykKkMf
+    JLQJKMqZx5O0B9bF8yQdcGKEGEwMQt5ENdH8HeiwLm4QS3VzFXYetgUPCM5lPDIp
+    BuwwuQxvQDF4pmQd
+  </certificate>
+  <value>
+    VEVTVA==
+  </value>
+</signature>
diff --git a/services/tests/servicestests/assets/KeyStoreRecoveryControllerTest/xml/valid-sig-file.sig.xml b/services/tests/servicestests/assets/KeyStoreRecoveryControllerTest/xml/valid-sig-file.sig.xml
new file mode 100644
index 0000000..f94b109
--- /dev/null
+++ b/services/tests/servicestests/assets/KeyStoreRecoveryControllerTest/xml/valid-sig-file.sig.xml
@@ -0,0 +1,78 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<signature>
+  <intermediates>
+    <cert>
+      MIIHMDCCAxegAwIBAgICEAEwDQYJKoZIhvcNAQELBQAwIDEeMBwGA1UEAwwVR29v
+      Z2xlIENyeXB0QXV0aFZhdWx0MB4XDTE4MDExMjAwMjM1N1oXDTI4MDExMDAwMjM1
+      N1owLTErMCkGA1UEAwwiR29vZ2xlIENyeXB0QXV0aFZhdWx0IEludGVybWVkaWF0
+      ZTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBALlhoKYuwxaatY9ERDFp
+      iEygSnjy0xzaiF4uCyiTfAuboSi5QwGon3ohf0ufJF02L9lnTMoeBAg+88m8AMgW
+      KFcEupabqlZfA3F/50mMCmnJvBSLXJ+chUdcAVpwcZAsq6ko22ARBxao1wu2qxNe
+      D8eXiiK8DRpTtKy3wOldZJ222v35v9JGOTjORZRrcv7Z8f6I5/cSsTS+WoVk/aBt
+      QqyFkcdw1zqulnlFE2rxNAVgyLlW71WJikYTDtUDeo79LvkDGjVsLo2MfpNxuK+5
+      MuMqzyN5LmzXJmNCEW1O5IIIUAPhgy5s08G+3G644wCEWsAnv2FBWLBn/HmJu6Uq
+      nSM2AaJN56V0tJG/yL2VgTnPJrJypNTKZW3OTCLCaYcTEbKfarxLwVWxvIWSIgkn
+      0q57GYhf7O+x9vvcOUmZwVxZECorIiK4n5AWG/KD3dWI3UGGGpYsDazRngA/bQPu
+      DSzBP9FBVcQt3/DMBG1s6f2Eko5f6aTFcVW9iV7aWLeIq+pQYlbmG42decj+aHLQ
+      COp5KV+Q77y4kFhZQFAQ1mN4crnhuEc1K5SmjAK24zIqWbwM3ly0KSQFc9jAmONg
+      0xu7kAObP3PZk85En12yLLscNmHCWYfOOEvTHf4KX7tjBl4HHp/ur+2Qwgpt9MFB
+      MGqR2cni5OV6gZcRdHaEerjrAgMBAAGjZjBkMB0GA1UdDgQWBBRE9RxHT7U3EP1v
+      djRzNYMrU7EseDAfBgNVHSMEGDAWgBTXe6GHkou4UhopkaU2Why0Muvs0TASBgNV
+      HRMBAf8ECDAGAQH/AgEBMA4GA1UdDwEB/wQEAwIBhjANBgkqhkiG9w0BAQsFAAOC
+      BAIAAfa7FKkfh4t+G8Sv4n3gUcuEAtmpCsTtHJU67r5eSvR24fDX7sqqSIib9sQ8
+      adA1FtnE3wnC4bEKnIwQLerWTN4i4V9oEKRIT5o5Rr61LyO9R+Prpo6uJVoPujFH
+      GZPdIj/Qs4Xax4a11QD+70+ZmmdL6yfigVDWA4yduFdMg6heHRf3EFCbVBv5qbBS
+      n8+eFKRnBZ/kQdFlYt+G+92Qqyn2uAcER6kZjIfPdnZh44SazLP37W8AkDX30Pmk
+      V0PHVGCDrap44q3maI1m8NAE1jGwwmRzJL953x5XgbVGt0K/3cNoWtKLenwX/G3I
+      akrgvOY5Zl0v3FRDZwGFt9UIBfZDDOGRMXIgIGs/1cvkwWpOT6dyReqDXempiQ1q
+      Yy6J5VsK5WK6gEelUyoACbzgby25V6a79Q1MI7dXmFQfCcX0nAD/AZmM1HkeYgrC
+      uq6fWoPOVMKML2mN90rCzkWxGaLcl5dPfad0O1LrcP48SRE5MXMWyxZZBon+wDIk
+      ascyM/r4fmk4kq64YKdm2wxCDMNArAIcyBkwOaWWfabtSagxJ3qtMtxK0qBUsbLC
+      yMyYpgU1h9c8rEdc4JgeE2LXJzxTKDc3SBOqbuNMlKWjYA+X+SUvVYALrQKAC+5v
+      wdUhLYdAPAksqk/ZoiBjkW35FfvqQMJBY29VnDT1h7/Nxk5gu+goTA9oFIYNrNte
+      +s0my+IUgYhKJBsgh7Mupv+B92GN5b3b440BMHB5QR959Jdq6BAXNUyZLM5fhZQE
+      Jj/rxZFXaqq757kgUhwWBz5TDbYF7GkqTyM4k430xwJKY0AYYEHmv1UYNo5X4G3x
+      SC2LhWC1b9VAykdkHbLs+IA8klxURmLmRiRj1UryhQjjT8h/FvNyPnbT1AKoElix
+      QLnLi8thkJ+tQggO0hISFsIrKNfnn0V6O0VKw9UZsMigsbYG5EbzIXcAyy8Avr9n
+      um7gBBZDt7fWso0+pG1UenJ+PybeuW/azQDLRw1Syz8OwU+ABRLq0JyyAtV7VPY5
+      C9pkKS+bU8nECxr6dMhAbpLBHlKsyb1qtkOt1p7WagEQZFIIc6svc73+L/ET/lWn
+      GBmkVVsCN7Aqyo5aXQWueXP4FUL+6O5+JALqw3qPeQgfnLkh0cUuccNND05QeEiv
+      Zswc/23KJXy1XbdVKT3UP0RAF7DxstbRGQbAT3z+n931e3KhtU28OKjsFtoeq2Dj
+      6STPEXh4rYFWMM8+DrJetAtBqk/i+vBwRA8f7jqIPPep/vEjPqqMOpdSVcoFQ1df
+      JuOZtGfEUjFHnlDr3eGP7KUIEZvhan1zm544dDgPVTXxrY4moi2BhKEY69zRSX6B
+      +a0fa5B3pxc8BN0LsHA0stT/Y2o=
+    </cert>
+  </intermediates>
+  <certificate>
+    MIIESTCCAjGgAwIBAgICEAAwDQYJKoZIhvcNAQELBQAwLTErMCkGA1UEAwwiR29v
+    Z2xlIENyeXB0QXV0aFZhdWx0IEludGVybWVkaWF0ZTAeFw0xODAxMTIwMDM4MDNa
+    Fw0yMzAxMTEwMDM4MDNaMDoxODA2BgNVBAMML0dvb2dsZSBDcnlwdEF1dGhWYXVs
+    dCBJbnRlcm1lZGlhdGUgSW50ZXJtZWRpYXRlMIIBIjANBgkqhkiG9w0BAQEFAAOC
+    AQ8AMIIBCgKCAQEA0v3bN3MwKifDAkF1524XzuaxYtk1sQKUlAlNngh+Qv4RjCkX
+    TECi7po8LeNsY+hWxmW3XZ22RBphe/yP4YcOdbqlIjZYNx3b75hCSJCadOkdW+Z9
+    f6+tKsHgeUja6r9r2TThzileImAvjXShe7GZYW8csPv6HaEVRXQlu8fGAZf8skmJ
+    EMfJx84//WeULdVz94beDhi9YAf4gLfmOayQcdWhDcMYI39knJcRny1ffRGgb1Hf
+    lE+3/a3aGFeODaxfkPaGRxEhzhZ/JDBiNgUAH/u7C5nxqa2WOu5e0wq3S0TndIOE
+    hmnwCE2GvxADFQst+rSsOn4EHit70hv4CfrMRQIDAQABo2YwZDAdBgNVHQ4EFgQU
+    0dKv4xTaEmdwHyox3tY8H8XVDGIwHwYDVR0jBBgwFoAURPUcR0+1NxD9b3Y0czWD
+    K1OxLHgwEgYDVR0TAQH/BAgwBgEB/wIBADAOBgNVHQ8BAf8EBAMCAYYwDQYJKoZI
+    hvcNAQELBQADggIBAJaArqLlJ2SLQ8JRwanD6LPlqQxucQ+x/LztTQpzPrsFfJds
+    E/sDZr2rXhDpz/bifIdj/DCbQ6/3w+t5JdFjT8GjXLgz1kCa/4W409FgSTgy1ENn
+    AMUU6pFIbOq/Qy/71uOTrrFWYTN5Vk+RBGxx5iDfHjDYraudi4VlcNkal4OyM98n
+    N3qp9cZD0RtWxMhvq6ahgmf9cTbEw6+l8yf/bogGLBYXXYeOoO5Q134AxrrgfthE
+    tvyKwJkT/l3OFKRcaHrebs+V1z5gPs7zWOyO5n2Z1SAmcOGfTfKMZWwp3Hi3OTr2
+    gB3LUYKyQVhC70dka3X+IbnFg5YfzJtX6YGnHlnI1SufOkEpGQDfcc0UQAWg/lgb
+    RkfMFD9tuJomBhyqv1YaxLN8yL4ZTRU0KCvvC5I5+X/zt9kBjnHlBOdYtknZT5jz
+    7+mjqWdpmWoAjeV5+CgIzG2k7JAm6rQuE1ZQNF0wAYxPret4NHPJFqfD5gGhdrYw
+    pEUxkcwHERA/E1CkpyqUy/Hd3kqHvnEDqzFcxBdUdmOgnbpI2nAZdEpfxmA5+M1n
+    UoxQ8ZWAZH+Mdlkw/Hx5hVjGjz8snD4QN25pj/wT+V6AR5OmYb8yfsQb2S8a8yDp
+    HzcIHW+dEWpX2boirOsrdI16kNtxYqtG7c5qWBPJy5Zjkvh9qbnfT/RQx10g
+  </certificate>
+  <value>
+    zELcMKbEb82mjWdhaV62Po4Tn/fEnHg7TMQzlz3cpDP3uzGKXg4fvCa+yDYrEYqm17uywOfFQpJs
+    pVjoUMINdnYogO44mul+E+m/klzSQN3GbmvYDtKpFsSGqsymyFSg8Bv2LeDLx2Pisc3sLUhxwKN4
+    8B6MwfZ3qUnRa5/ySk0bzEYYiRWsYR5oY7vK0kAI5c4Oi77E0W440FaEhnT7WxucFUnAhZbOSwXA
+    apE39BXu6ZbAPpTSc4f+uMErF7cRGbIODcAQJko6yjliBmvnCxj0ct7qzwUpgJojy/G5DLaVx3MF
+    Xnee9peYCrKFWpB+Z/7io0/Fqs/fDF7U25BsXA==
+  </value>
+</signature>
diff --git a/services/tests/servicestests/src/com/android/server/locksettings/recoverablekeystore/certificate/CertUtilsTest.java b/services/tests/servicestests/src/com/android/server/locksettings/recoverablekeystore/certificate/CertUtilsTest.java
new file mode 100644
index 0000000..d08dab4
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/locksettings/recoverablekeystore/certificate/CertUtilsTest.java
@@ -0,0 +1,356 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.locksettings.recoverablekeystore.certificate;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import static org.testng.Assert.assertThrows;
+import static org.testng.Assert.expectThrows;
+
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import java.io.InputStream;
+import java.security.KeyPairGenerator;
+import java.security.PublicKey;
+import java.security.cert.CertPath;
+import java.security.cert.X509Certificate;
+import java.security.spec.ECGenParameterSpec;
+import java.util.Arrays;
+import java.util.Base64;
+import java.util.Collections;
+import java.util.List;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.w3c.dom.Element;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public final class CertUtilsTest {
+
+    private static final String XML_STR = ""
+            + "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
+            + "<!-- comment 1 -->"
+            + "<root>\n\n\n\r\r\r"
+            + "  <node1>\r\n\r\n"
+            + "    node1-1</node1>"
+            + "  <!-- comment 2 -->"
+            + "  <node1>node1-2"
+            + "    \n\r\n\r</node1>"
+            + "  <node2>"
+            + "    <node1>   node2-node1-1</node1>"
+            + "    <node1>node2-node1-2   </node1>"
+            + "    <!-- comment 3 -->"
+            + "    <node1>   node2-node1-3   </node1>"
+            + "  </node2>"
+            + "</root>";
+
+    private static final String SIGNED_STR = "abcdefg\n";
+    private static final String SIGNATURE_BASE64 = ""
+            + "KxBt9B3pwL3/59SrjTJTpuhc9JRxLOUNwNr3J4EEdXj5BqkYOUeXIOjyBGp8XaOnmuW8WmBxhko3"
+            + "yTR3/M9x0/pJuKDgqQSSFG+I56O/IAri7DmMBfY8QqcgiF8RaR86G7mWXUIdu8ixEtpKa//T4bN7"
+            + "c8Txvt96ApAcW0wJDihfCqDEXyi56pFCp+qEZuL4fS8iZtZTUkvxim1tb2/IsZ9OyDd9BWxp+JTs"
+            + "zihzH6xqnUCa1aELSUZnU8OzWGeuKpVDQDbDMtQpcxJ9o+6L6wO5vmQutZAulgw5gRPGhYWVs8+0"
+            + "ATdNEbv8TSomkXkZ3/lMYnmPXKmaHxcP4330DA==";
+    private static final PublicKey SIGNER_PUBLIC_KEY = TestData.INTERMEDIATE_CA_2.getPublicKey();
+
+    @Test
+    public void decodeCert_readPemFile_succeeds_singleBlock() throws Exception {
+        InputStream f = TestData.openTestFile("pem/valid-cert.pem");
+        X509Certificate cert = CertUtils.decodeCert(f);
+        assertThat(cert).isEqualTo(TestData.ROOT_CA_TRUSTED);
+    }
+
+    @Test
+    public void decodeCert_readPemFile_succeeds_multipleBlocks() throws Exception {
+        InputStream in = TestData.openTestFile("pem/valid-cert-multiple-blocks.pem");
+        X509Certificate cert = CertUtils.decodeCert(in);
+        assertThat(cert).isEqualTo(TestData.ROOT_CA_TRUSTED);
+    }
+
+    @Test
+    public void decodeCert_readPemFile_throwsIfNoBeginEndLines() throws Exception {
+        InputStream in = TestData.openTestFile("pem/invalid-cert-1-no-begin-end.pem");
+        assertThrows(CertParsingException.class, () -> CertUtils.decodeCert(in));
+    }
+
+    @Test
+    public void decodeCert_readPemFile_throwsIfEmptyBlock() throws Exception {
+        InputStream in = TestData.openTestFile("pem/invalid-cert-2-empty-block.pem");
+        assertThrows(CertParsingException.class, () -> CertUtils.decodeCert(in));
+    }
+
+    @Test
+    public void decodeCert_readPemFile_throwsIfInvalidCert() throws Exception {
+        InputStream in = TestData.openTestFile("pem/invalid-cert-3-invalid-key.pem");
+        assertThrows(CertParsingException.class, () -> CertUtils.decodeCert(in));
+    }
+
+    @Test
+    public void decodeCert_readBytes_succeeds() throws Exception {
+        X509Certificate cert = CertUtils.decodeCert(TestData.INTERMEDIATE_CA_2.getEncoded());
+        assertThat(cert.getIssuerX500Principal().getName())
+                .isEqualTo("CN=Google CryptAuthVault Intermediate");
+    }
+
+    @Test
+    public void decodeCert_readBytes_throwsIfInvalidCert() throws Exception {
+        byte[] modifiedCertBytes = TestData.INTERMEDIATE_CA_1.getEncoded();
+        modifiedCertBytes[0] ^= (byte) 1;
+        assertThrows(CertParsingException.class, () -> CertUtils.decodeCert(modifiedCertBytes));
+    }
+
+    @Test
+    public void decodeBase64_succeeds() throws Exception {
+        assertThat(CertUtils.decodeBase64("VEVTVA==")).isEqualTo("TEST".getBytes(UTF_8));
+    }
+
+    @Test
+    public void decodeBase64_succeedsIfEmptyInput() throws Exception {
+        assertThat(CertUtils.decodeBase64("")).hasLength(0);
+    }
+
+    @Test
+    public void decodeBase64_throwsIfInvalidInput() throws Exception {
+        assertThrows(CertParsingException.class, () -> CertUtils.decodeBase64("EVTVA=="));
+    }
+
+    @Test
+    public void getXmlRootNode_succeeds() throws Exception {
+        Element root = CertUtils.getXmlRootNode(XML_STR.getBytes(UTF_8));
+        assertThat(root.getTagName()).isEqualTo("root");
+    }
+
+    @Test
+    public void getXmlRootNode_throwsIfEmptyInput() throws Exception {
+        assertThrows(CertParsingException.class, () -> CertUtils.getXmlRootNode(new byte[0]));
+    }
+
+    @Test
+    public void getXmlNodeContents_singleLevel_succeeds() throws Exception {
+        Element root = CertUtils.getXmlRootNode(XML_STR.getBytes(UTF_8));
+        assertThat(CertUtils.getXmlNodeContents(CertUtils.MUST_EXIST_UNENFORCED, root, "node1"))
+                .containsExactly("node1-1", "node1-2");
+    }
+
+    @Test
+    public void getXmlNodeContents_multipleLevels_succeeds() throws Exception {
+        Element root = CertUtils.getXmlRootNode(XML_STR.getBytes(UTF_8));
+        assertThat(CertUtils.getXmlNodeContents(CertUtils.MUST_EXIST_UNENFORCED, root, "node2", "node1"))
+                .containsExactly("node2-node1-1", "node2-node1-2", "node2-node1-3");
+    }
+
+    @Test
+    public void getXmlNodeContents_mustExistFalse_succeedsIfNotExist() throws Exception {
+        Element root = CertUtils.getXmlRootNode(XML_STR.getBytes(UTF_8));
+        assertThat(
+                CertUtils.getXmlNodeContents(
+                        CertUtils.MUST_EXIST_UNENFORCED, root, "node2", "node-not-exist"))
+                .isEmpty();
+    }
+
+    @Test
+    public void getXmlNodeContents_mustExistAtLeastOne_throwsIfNotExist() throws Exception {
+        Element root = CertUtils.getXmlRootNode(XML_STR.getBytes(UTF_8));
+        CertParsingException expected =
+                expectThrows(
+                        CertParsingException.class,
+                        () ->
+                                CertUtils.getXmlNodeContents(
+                                        CertUtils.MUST_EXIST_AT_LEAST_ONE, root, "node2",
+                                        "node-not-exist"));
+        assertThat(expected.getMessage()).contains("must contain at least one");
+    }
+
+    @Test
+    public void getXmlNodeContents_mustExistExactlyOne_throwsIfNotExist() throws Exception {
+        Element root = CertUtils.getXmlRootNode(XML_STR.getBytes(UTF_8));
+        CertParsingException expected =
+                expectThrows(
+                        CertParsingException.class,
+                        () ->
+                                CertUtils.getXmlNodeContents(
+                                        CertUtils.MUST_EXIST_EXACTLY_ONE, root, "node-not-exist",
+                                        "node1"));
+        assertThat(expected.getMessage()).contains("must contain exactly one");
+    }
+
+    @Test
+    public void getXmlNodeContents_mustExistExactlyOne_throwsIfMultipleExist() throws Exception {
+        Element root = CertUtils.getXmlRootNode(XML_STR.getBytes(UTF_8));
+        CertParsingException expected =
+                expectThrows(
+                        CertParsingException.class,
+                        () ->
+                                CertUtils.getXmlNodeContents(
+                                        CertUtils.MUST_EXIST_EXACTLY_ONE, root, "node2", "node1"));
+        assertThat(expected.getMessage()).contains("must contain exactly one");
+    }
+
+    @Test
+    public void verifyRsaSha256Signature_succeeds() throws Exception {
+        CertUtils.verifyRsaSha256Signature(
+                SIGNER_PUBLIC_KEY,
+                Base64.getDecoder().decode(SIGNATURE_BASE64),
+                SIGNED_STR.getBytes(UTF_8));
+    }
+
+    @Test
+    public void verifyRsaSha256Signature_throwsIfMismatchSignature() throws Exception {
+        byte[] modifiedBytes = SIGNED_STR.getBytes(UTF_8);
+        modifiedBytes[0] ^= (byte) 1;
+        assertThrows(
+                CertValidationException.class,
+                () ->
+                        CertUtils.verifyRsaSha256Signature(
+                                SIGNER_PUBLIC_KEY, Base64.getDecoder().decode(SIGNATURE_BASE64),
+                                modifiedBytes));
+    }
+
+    @Test
+    public void verifyRsaSha256Signature_throwsIfWrongKeyType() throws Exception {
+        KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("EC");
+        keyPairGenerator.initialize(new ECGenParameterSpec("secp256r1"));
+        PublicKey publicKey = keyPairGenerator.generateKeyPair().getPublic();
+        assertThrows(
+                CertValidationException.class,
+                () ->
+                        CertUtils.verifyRsaSha256Signature(
+                                publicKey,
+                                Base64.getDecoder().decode(SIGNATURE_BASE64),
+                                SIGNED_STR.getBytes(UTF_8)));
+    }
+
+    @Test
+    public void buildCertPath_succeedsWithoutIntermediates() throws Exception {
+        X509Certificate rootCert = TestData.ROOT_CA_TRUSTED;
+        X509Certificate leafCert = TestData.INTERMEDIATE_CA_1;
+        CertPath certPath = CertUtils.buildCertPath(
+                CertUtils.buildPkixParams(
+                        TestData.DATE_ALL_CERTS_VALID, rootCert, Collections.emptyList(),
+                        leafCert));
+        assertThat(certPath.getCertificates()).containsExactly(
+                TestData.INTERMEDIATE_CA_1).inOrder();
+    }
+
+    @Test
+    public void buildCertPath_succeedsWithIntermediates() throws Exception {
+        X509Certificate rootCert = TestData.ROOT_CA_TRUSTED;
+        List<X509Certificate> intermediateCerts =
+                Arrays.asList(TestData.INTERMEDIATE_CA_1, TestData.INTERMEDIATE_CA_2);
+        X509Certificate leafCert = TestData.LEAF_CERT_2;
+        CertPath certPath =
+                CertUtils.buildCertPath(
+                        CertUtils.buildPkixParams(
+                                TestData.DATE_ALL_CERTS_VALID, rootCert, intermediateCerts,
+                                leafCert));
+        assertThat(certPath.getCertificates())
+                .containsExactly(
+                        TestData.LEAF_CERT_2, TestData.INTERMEDIATE_CA_2,
+                        TestData.INTERMEDIATE_CA_1)
+                .inOrder();
+    }
+
+    @Test
+    public void buildCertPath_succeedsWithIntermediates_ignoreUnrelatedIntermedateCert()
+            throws Exception {
+        X509Certificate rootCert = TestData.ROOT_CA_TRUSTED;
+        List<X509Certificate> intermediateCerts =
+                Arrays.asList(TestData.INTERMEDIATE_CA_1, TestData.INTERMEDIATE_CA_2);
+        X509Certificate leafCert = TestData.LEAF_CERT_1;
+        CertPath certPath =
+                CertUtils.buildCertPath(
+                        CertUtils.buildPkixParams(
+                                TestData.DATE_ALL_CERTS_VALID, rootCert, intermediateCerts,
+                                leafCert));
+        assertThat(certPath.getCertificates())
+                .containsExactly(TestData.LEAF_CERT_1, TestData.INTERMEDIATE_CA_1)
+                .inOrder();
+    }
+
+    @Test
+    public void buildCertPath_throwsIfWrongRootCommonName() throws Exception {
+        X509Certificate rootCert = TestData.ROOT_CA_DIFFERENT_COMMON_NAME;
+        List<X509Certificate> intermediateCerts =
+                Arrays.asList(TestData.INTERMEDIATE_CA_1, TestData.INTERMEDIATE_CA_2);
+        X509Certificate leafCert = TestData.LEAF_CERT_1;
+
+        assertThrows(
+                CertValidationException.class,
+                () ->
+                        CertUtils.buildCertPath(
+                                CertUtils.buildPkixParams(
+                                        TestData.DATE_ALL_CERTS_VALID, rootCert, intermediateCerts,
+                                        leafCert)));
+    }
+
+    @Test
+    public void buildCertPath_throwsIfMissingIntermediateCert() throws Exception {
+        X509Certificate rootCert = TestData.ROOT_CA_DIFFERENT_COMMON_NAME;
+        List<X509Certificate> intermediateCerts = Collections.emptyList();
+        X509Certificate leafCert = TestData.LEAF_CERT_1;
+
+        assertThrows(
+                CertValidationException.class,
+                () ->
+                        CertUtils.buildCertPath(
+                                CertUtils.buildPkixParams(
+                                        TestData.DATE_ALL_CERTS_VALID, rootCert, intermediateCerts,
+                                        leafCert)));
+    }
+
+    @Test
+    public void validateCert_succeeds() throws Exception {
+        X509Certificate rootCert = TestData.ROOT_CA_TRUSTED;
+        List<X509Certificate> intermediateCerts =
+                Arrays.asList(TestData.INTERMEDIATE_CA_1, TestData.INTERMEDIATE_CA_2);
+        X509Certificate leafCert = TestData.LEAF_CERT_2;
+        CertUtils.validateCert(TestData.DATE_ALL_CERTS_VALID, rootCert, intermediateCerts,
+                leafCert);
+    }
+
+    @Test
+    public void validateCert_throwsIfExpired() throws Exception {
+        X509Certificate rootCert = TestData.ROOT_CA_TRUSTED;
+        List<X509Certificate> intermediateCerts =
+                Arrays.asList(TestData.INTERMEDIATE_CA_1, TestData.INTERMEDIATE_CA_2);
+        X509Certificate leafCert = TestData.LEAF_CERT_2;
+        assertThrows(
+                CertValidationException.class,
+                () ->
+                        CertUtils.validateCert(
+                                TestData.DATE_LEAF_CERT_2_EXPIRED, rootCert, intermediateCerts,
+                                leafCert));
+    }
+
+    @Test
+    public void validateCert_throwsIfWrongRootWithTheSameCommonName() throws Exception {
+        X509Certificate rootCert = TestData.ROOT_CA_DIFFERENT_KEY;
+        List<X509Certificate> intermediateCerts =
+                Arrays.asList(TestData.INTERMEDIATE_CA_1, TestData.INTERMEDIATE_CA_2);
+        X509Certificate leafCert = TestData.LEAF_CERT_2;
+        assertThrows(
+                CertValidationException.class,
+                () ->
+                        CertUtils.validateCert(
+                                TestData.DATE_ALL_CERTS_VALID, rootCert, intermediateCerts,
+                                leafCert));
+    }
+}
diff --git a/services/tests/servicestests/src/com/android/server/locksettings/recoverablekeystore/certificate/CertXmlTest.java b/services/tests/servicestests/src/com/android/server/locksettings/recoverablekeystore/certificate/CertXmlTest.java
new file mode 100644
index 0000000..52269d9
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/locksettings/recoverablekeystore/certificate/CertXmlTest.java
@@ -0,0 +1,199 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.locksettings.recoverablekeystore.certificate;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.testng.Assert.assertThrows;
+import static org.testng.Assert.expectThrows;
+
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import java.security.cert.CertPath;
+import java.security.cert.X509Certificate;
+import java.util.List;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public final class CertXmlTest {
+
+    private byte[] certXmlBytes;
+
+    @Before
+    public void setUp() throws Exception {
+        certXmlBytes = TestData.readTestFile("xml/valid-cert-file.xml");
+    }
+
+    @Test
+    public void parse_succeeds() throws Exception {
+        CertXml certXml = CertXml.parse(certXmlBytes);
+        assertThat(certXml.getSerial()).isEqualTo(1000L);
+        assertThat(certXml.getRefreshInterval()).isEqualTo(2592000L);
+    }
+
+    @Test
+    public void parse_succeedsIfNoIntermediateCerts() throws Exception {
+        CertXml certXml =
+                CertXml.parse(TestData.readTestFile("xml/valid-cert-file-no-intermediates.xml"));
+        assertThat(certXml.getAllIntermediateCerts()).isEmpty();
+    }
+
+    @Test
+    public void parse_checkIntermediateCerts() throws Exception {
+        CertXml certXml = CertXml.parse(certXmlBytes);
+        List<X509Certificate> intermediateCerts = certXml.getAllIntermediateCerts();
+        assertThat(intermediateCerts)
+                .containsExactly(TestData.INTERMEDIATE_CA_1, TestData.INTERMEDIATE_CA_2)
+                .inOrder();
+    }
+
+    @Test
+    public void parse_checkEndpointCerts() throws Exception {
+        CertXml certXml = CertXml.parse(certXmlBytes);
+        List<X509Certificate> endpointCerts = certXml.getAllEndpointCerts();
+        assertThat(endpointCerts).hasSize(3);
+        assertThat(endpointCerts).containsAllOf(TestData.LEAF_CERT_1, TestData.LEAF_CERT_2);
+    }
+
+    @Test
+    public void parse_throwsIfNoEndpointCert() throws Exception {
+        CertParsingException expected =
+                expectThrows(
+                        CertParsingException.class,
+                        () ->
+                                CertXml.parse(
+                                        TestData.readTestFile(
+                                                "xml/invalid-cert-file-no-endpoint-cert.xml")));
+        assertThat(expected.getMessage()).contains("at least one");
+    }
+
+    @Test
+    public void parse_throwsIfNoRefreshInterval() throws Exception {
+        CertParsingException expected =
+                expectThrows(
+                        CertParsingException.class,
+                        () ->
+                                CertXml.parse(
+                                        TestData.readTestFile(
+                                                "xml/invalid-cert-file-no-refresh-interval.xml")));
+        assertThat(expected.getMessage()).contains("exactly one");
+    }
+
+    @Test
+    public void parse_throwsIfNoSerial() throws Exception {
+        CertParsingException expected =
+                expectThrows(
+                        CertParsingException.class,
+                        () ->
+                                CertXml.parse(
+                                        TestData.readTestFile(
+                                                "xml/invalid-cert-file-no-serial.xml")));
+        assertThat(expected.getMessage()).contains("exactly one");
+    }
+
+    @Test
+    public void parse_throwsIfTwoRefreshIntervals() throws Exception {
+        CertParsingException expected =
+                expectThrows(
+                        CertParsingException.class,
+                        () ->
+                                CertXml.parse(
+                                        TestData.readTestFile(
+                                                "xml/invalid-cert-file-two-refresh-intervals"
+                                                        + ".xml")));
+        assertThat(expected.getMessage()).contains("exactly one");
+    }
+
+    @Test
+    public void parse_throwsIfTwoSerials() throws Exception {
+        CertParsingException expected =
+                expectThrows(
+                        CertParsingException.class,
+                        () ->
+                                CertXml.parse(
+                                        TestData.readTestFile(
+                                                "xml/invalid-cert-file-two-serials.xml")));
+        assertThat(expected.getMessage()).contains("exactly one node");
+    }
+
+    @Test
+    public void parseAndValidateAllCerts_succeeds() throws Exception {
+        CertXml certXml = CertXml.parse(certXmlBytes);
+        for (int index = 0; index < certXml.getAllEndpointCerts().size(); index++) {
+            assertThat(
+                    certXml.getEndpointCert(
+                            index, TestData.DATE_ALL_CERTS_VALID, TestData.ROOT_CA_TRUSTED))
+                    .isNotNull();
+        }
+    }
+
+    @Test
+    public void parseAndValidate_returnsExpectedCertPath() throws Exception {
+        CertXml certXml = CertXml.parse(certXmlBytes);
+        CertPath certPath =
+                certXml.getEndpointCert(
+                        /*index=*/ 1, // TestData.LEAF_CERT_2
+                        TestData.DATE_ALL_CERTS_VALID,
+                        TestData.ROOT_CA_TRUSTED);
+        assertThat(certPath.getCertificates())
+                .containsExactly(
+                        TestData.LEAF_CERT_2, TestData.INTERMEDIATE_CA_2,
+                        TestData.INTERMEDIATE_CA_1)
+                .inOrder();
+    }
+
+    @Test
+    public void validateCert_throwsIfRootCertWithDifferentCommonName() throws Exception {
+        CertXml certXml = CertXml.parse(certXmlBytes);
+        assertThrows(
+                CertValidationException.class,
+                () ->
+                        certXml.getEndpointCert(
+                                /*index=*/ 0, // TestData.LEAF_CERT_1
+                                TestData.DATE_ALL_CERTS_VALID,
+                                TestData.ROOT_CA_DIFFERENT_COMMON_NAME));
+    }
+
+    @Test
+    public void validateCert_throwsIfRootCertWithDifferentPublicKey() throws Exception {
+        CertXml certXml = CertXml.parse(certXmlBytes);
+        assertThrows(
+                CertValidationException.class,
+                () ->
+                        certXml.getEndpointCert(
+                                /*index=*/ 0, // TestData.LEAF_CERT_1
+                                TestData.DATE_ALL_CERTS_VALID,
+                                TestData.ROOT_CA_DIFFERENT_KEY));
+    }
+
+    @Test
+    public void validateCert_throwsIfExpired() throws Exception {
+        CertXml certXml = CertXml.parse(certXmlBytes);
+        assertThrows(
+                CertValidationException.class,
+                () ->
+                        certXml.getEndpointCert(
+                                /*index=*/ 1, // TestData.LEAF_CERT_2
+                                TestData.DATE_LEAF_CERT_2_EXPIRED,
+                                TestData.ROOT_CA_TRUSTED));
+    }
+}
diff --git a/services/tests/servicestests/src/com/android/server/locksettings/recoverablekeystore/certificate/SigXmlTest.java b/services/tests/servicestests/src/com/android/server/locksettings/recoverablekeystore/certificate/SigXmlTest.java
new file mode 100644
index 0000000..4d87006
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/locksettings/recoverablekeystore/certificate/SigXmlTest.java
@@ -0,0 +1,141 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.locksettings.recoverablekeystore.certificate;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.testng.Assert.assertThrows;
+import static org.testng.Assert.expectThrows;
+
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public final class SigXmlTest {
+
+    private byte[] certXmlBytes;
+    private byte[] sigXmlBytes;
+
+    @Before
+    public void setUp() throws Exception {
+        certXmlBytes = TestData.readTestFile("xml/valid-cert-file.xml");
+        sigXmlBytes = TestData.readTestFile("xml/valid-sig-file.sig.xml");
+    }
+
+    @Test
+    public void parseAndVerifyFileSignature_succeeds() throws Exception {
+        SigXml sigXml = SigXml.parse(sigXmlBytes);
+        sigXml.verifyFileSignature(
+                TestData.ROOT_CA_TRUSTED, certXmlBytes, TestData.DATE_ALL_CERTS_VALID);
+    }
+
+    @Test
+    public void parseAndVerifyFileSignature_throwsIfExpiredCert() throws Exception {
+        SigXml sigXml = SigXml.parse(sigXmlBytes);
+        assertThrows(
+                CertValidationException.class,
+                () ->
+                        sigXml.verifyFileSignature(
+                                TestData.ROOT_CA_TRUSTED, certXmlBytes,
+                                TestData.DATE_INTERMEDIATE_CA_2_EXPIRED));
+    }
+
+    @Test
+    public void parseAndVerifyFileSignature_throwsIfInvalidSignature() throws Exception {
+        SigXml sigXml = SigXml.parse(sigXmlBytes);
+        byte[] modifiedBytes = sigXmlBytes.clone();
+        modifiedBytes[0] ^= (byte) 1; // Flip one bit
+        CertValidationException expected =
+                expectThrows(
+                        CertValidationException.class,
+                        () ->
+                                sigXml.verifyFileSignature(
+                                        TestData.ROOT_CA_TRUSTED, modifiedBytes,
+                                        TestData.DATE_ALL_CERTS_VALID));
+        assertThat(expected.getMessage()).contains("signature is invalid");
+    }
+
+    @Test
+    public void parseAndVerifyFileSignature_throwsIfRootCertWithWrongCommonName() throws Exception {
+        SigXml sigXml = SigXml.parse(sigXmlBytes);
+        assertThrows(
+                CertValidationException.class,
+                () ->
+                        sigXml.verifyFileSignature(
+                                TestData.ROOT_CA_DIFFERENT_COMMON_NAME,
+                                certXmlBytes,
+                                TestData.DATE_ALL_CERTS_VALID));
+    }
+
+    @Test
+    public void parse_succeedsWithoutIntermediateCerts() throws Exception {
+        SigXml.parse(TestData.readTestFile("xml/valid-sig-file-no-intermediates.sig.xml"));
+    }
+
+    @Test
+    public void parse_throwsIfNoSignerCert() throws Exception {
+        CertParsingException expected =
+                expectThrows(
+                        CertParsingException.class,
+                        () ->
+                                SigXml.parse(
+                                        TestData.readTestFile(
+                                                "xml/invalid-sig-file-no-signer-cert.sig.xml")));
+        assertThat(expected.getMessage()).contains("exactly one");
+    }
+
+    @Test
+    public void parse_throwsIfTwoSignerCerts() throws Exception {
+        CertParsingException expected =
+                expectThrows(
+                        CertParsingException.class,
+                        () ->
+                                SigXml.parse(
+                                        TestData.readTestFile(
+                                                "xml/invalid-sig-file-two-signer-certs.sig.xml")));
+        assertThat(expected.getMessage()).contains("exactly one");
+    }
+
+    @Test
+    public void parse_throwsIfNoSignatureValue() throws Exception {
+        CertParsingException expected =
+                expectThrows(
+                        CertParsingException.class,
+                        () ->
+                                SigXml.parse(
+                                        TestData.readTestFile(
+                                                "xml/invalid-sig-file-no-signature.sig.xml")));
+        assertThat(expected.getMessage()).contains("exactly one");
+    }
+
+    @Test
+    public void parse_throwsIfTwoSignatureValues() throws Exception {
+        CertParsingException expected =
+                expectThrows(
+                        CertParsingException.class,
+                        () ->
+                                SigXml.parse(
+                                        TestData.readTestFile(
+                                                "xml/invalid-sig-file-two-signatures.sig.xml")));
+        assertThat(expected.getMessage()).contains("exactly one");
+    }
+}
diff --git a/services/tests/servicestests/src/com/android/server/locksettings/recoverablekeystore/certificate/TestData.java b/services/tests/servicestests/src/com/android/server/locksettings/recoverablekeystore/certificate/TestData.java
new file mode 100644
index 0000000..5eb4166
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/locksettings/recoverablekeystore/certificate/TestData.java
@@ -0,0 +1,307 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.locksettings.recoverablekeystore.certificate;
+
+import android.content.Context;
+import android.support.test.InstrumentationRegistry;
+
+import com.google.common.io.ByteStreams;
+
+import java.io.ByteArrayInputStream;
+import java.io.InputStream;
+import java.security.cert.CertificateFactory;
+import java.security.cert.X509Certificate;
+import java.util.Base64;
+import java.util.Date;
+
+/** Constants used by the tests in the folder. */
+public final class TestData {
+
+    private static final String TEST_FILE_FOLDER_NAME = "KeyStoreRecoveryControllerTest";
+
+    private TestData() {}
+
+    // Some test data that is generated by using OpenSSL command line tools.
+    private static final String ROOT_CA_TRUSTED_BASE64 = ""
+            + "MIIJJzCCBQ6gAwIBAgIJAM7fBGeQ1wBkMA0GCSqGSIb3DQEBDQUAMCAxHjAcBgNV"
+            + "BAMMFUdvb2dsZSBDcnlwdEF1dGhWYXVsdDAeFw0xODAxMTEwNjQ5MzNaFw0zODAx"
+            + "MDYwNjQ5MzNaMCAxHjAcBgNVBAMMFUdvb2dsZSBDcnlwdEF1dGhWYXVsdDCCBCIw"
+            + "DQYJKoZIhvcNAQEBBQADggQPADCCBAoCggQBCcFv05M1seLPYIW7oFivh7u5otCt"
+            + "Mm7ryq0UjpbTQPcQxJQbYzcUQF7CbPYTdWDid4EuO8Zec03ownsduFhKud6H3yIQ"
+            + "4ueGiqCBoG1D6+N8fF9R8awTmAsbNg63VInx6IwcBZnjFlsIIftOFxqIJBYhiKhN"
+            + "JhPtF1i9dn+N5HjEKmkJO3pXhYhRXMp7OwL/epxzhBXFYT7aDg9pnaM6C+4hmhQ/"
+            + "0q2oyzYtAqFmrEenbtI6G47SzMc+shNTuYLtq21j/Z3uA3RwB9Szfu99F66tlgTX"
+            + "v7K7YS573hN3TQY/+nkLfFy/oF2LQRYvKHF+Nv0BHzQLzqDEYBaILcMf3i2Ce/b7"
+            + "wZjitLqFAI1swqGzgH/QpB3OrX51M/B7UCF2nB7Pa8knu4kBDGkz2Q41jAL0W/qt"
+            + "j43VwJDW0Y98OuqQiCqJrTrGdv7b/phnVVBvFrtIjYMfyK34jy5VLXctV5CSkWj5"
+            + "3ul3mvGFHJD+6nneDR4PUkmYN0khT4t/RqnQlwYE0a6Erq1+Rof6/DoWSzeBLBYV"
+            + "JaHhRy9mrudR/VcQynLKty6Zst4Lyh6aPMHcpTwGZbG+4mXnWeTaLEnGvivldksT"
+            + "XOxipcO/fXJfDss4b0glGzP3GD0+H5EZB9coYzNT47QZd9drxHdrLxtPoi+MeqkG"
+            + "gCdyFyBZO8G2k/JuyziT6hy+50VXJnl6Ujxj7MVUYAsISHsHgqETDsukQbbKvTKg"
+            + "3gxPVNN/vKWwyh7KLcFIaOEoPOgStkmVsqrXm7YLE6Bvzm8nu4rwJeAF9Yseg9BE"
+            + "Y86TRRmAI7fW4eDEPnxgCUUvcYSAh5mcayIyIr0KTuXkevwYbVRHMVmy9DaqzsP8"
+            + "YFXIqFvDXRCFSy/gMkoNb9ZoqdkmjZ+VBsjAKI+u/Haf6pgdpGZbVGKEFmaVHCkr"
+            + "tPp/gy4kE4qmd/SIaccG8o6Eb9X9fbqTTDZv34kcGgxOvBJVIaNHprTjgvYEnRaD"
+            + "KTlmZoCUmBlHzvbf68YWBmIz0K8vYPdx9r98LiUgpbTHtKZIYrJnbgPnbC9icP24"
+            + "2ksB4yaTx1QWc14vTNv1lUtv4zJEmaaoynNlETJFf/Tz0QKJxtT+l/BIAz8kEJMA"
+            + "cKsfoTx9OTtfuL85pXbCgxbKKmKn6RzxUCzSzgMboC0z6W8Zxy2gLIhqMm8AXAF7"
+            + "salwrRirV4lWsM9MOhVEgfjcv/qmQSYr1ARrwwegHRqxPA3qh11kfq5YSFU7W7+f"
+            + "JrWH6VuLZ0B1fj2+lsoMNekFA1ULD8DK7aAFIh9Y1y4Jt//xMuOPcD5PWNGFmUk7"
+            + "oPewiIUMLjXSWcgrQVYbZEDW/vooMJoo47Vg1fQPehejbONE1nBIaeRVhJcCAwEA"
+            + "AaNjMGEwHQYDVR0OBBYEFNd7oYeSi7hSGimRpTZaHLQy6+zRMB8GA1UdIwQYMBaA"
+            + "FNd7oYeSi7hSGimRpTZaHLQy6+zRMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/"
+            + "BAQDAgGGMA0GCSqGSIb3DQEBDQUAA4IEAgABny011veuPplTcNeyLeKhWRn/y9VM"
+            + "QEyhaLlsC1Di35WN8owjj4+gugLlDdtEc/pvzgk+ZkAuplQkrsU907dvwkDPb4rW"
+            + "ZB5hjbr9yVyEHK1bHh7RSUkJrB3NRwqH1lyWz/LfaVfbV4CtaaERhThzZCp/MweO"
+            + "Tivg2XpSluy5s8lEEbMC/dGuIsBMitX3XLlbYa2ND3aHZLo6X9yQMFfTCjgwYG2n"
+            + "eDYupnvcLINlrlJYcrYSrIvoQn9XfsnjU3AXz+jc6xLrO3EtXDhi9e+QTfcnvRsg"
+            + "l/Hj9SZr1w1L1PPJo+KjsRavVvzaHXlBAvvUtEojJrkR3j+b5zvQB6dgVrM0zUhM"
+            + "Q9zRp5R/xqHeZ/0TTQe9kEa8QuRzuRIkK5Wbh76Eix3S+2uTsbj462nk4E0oPR8p"
+            + "iYopS4ZEFEfrKW14HOph9ZscI4l/HfDmTNfgpyFl62UrvzVBnoz+sbhTgbPHPcCX"
+            + "OUrhmpz9I5oBkyEAZYunSvzY/9SXUsz6psXHJmVzLQcne/YQTtpWzV/wGD7cjjDl"
+            + "bfzsmGCfZ8jqPBoicl5IUVdyZsJgufEZHXxKZQ7wL7R6jKrj/GtCDey1Wr2QT8VX"
+            + "5JTk9cJQFjgjDWaAyCBpGEaQvYJcaOxk2D+Wap5ax8nUfW/99vVFA0EJKsSVVzw7"
+            + "daRty0UpfZsx2Sfzpg0mymmgB8+NY6t68dL5C/xxAv5mEQ8wGJmP45iQpo5T6LVV"
+            + "MrktLf5eIzxlALQIW/AgpSH9JKCqpItdxfisAIIs9e8XHbVJJA0Jde7rtAj+TUY0"
+            + "h00xSqyfSSbpcDJ9lIoSZOJvFQdWOxB8c3vZZGGhMuRFm06sUHvcHjo8KwnbqyOx"
+            + "DGjeqt6YWty6WcNin0WciR33vGHIzwVNxNnmuY308bNsMvY9jsmd37hdmmwnmQge"
+            + "7AIa7TMPjaKm0vV/1ztFSODWCI2K7klmL2MtOJMGfqUeOfjPANbS3lMJBAH9qxLM"
+            + "7Kng+nfqVtt+NG9MxcTbP80FkBa/6JxGgjjsiwDmhr2MTCYOK/eD+WZikMOieyvH"
+            + "m2vgxYCdWrhaGfc3t6oQ2YO+mXI7e6d3F3a90UUYkBIgje9zu0RLxnBBhuoRyGwv"
+            + "uQAlqgMDBZIzTO0Vnwew7KRLdzLhWbiikhi81q6Lg62aWjbdF6Ue6AVXch+dqmr+"
+            + "9aVt0Y6ETTS77nrQyglyLKIeNx6cEHDjETXlPYGbCAlrdKAdTA4ngnBZnzGQ/8zg"
+            + "tP9zvIJVA6cuOAn8GFEsrb7GN20QSDwyJWrYi6f+m64D9rOK4Jz4t+lEfjcfJeM/"
+            + "UcNlhmATcMHXWPCoKkOfll4PBc/Wigv1xYw70RZ4pai07LzJxNHYhvpE3Q==";
+    private static final String ROOT_CA_DIFFERENT_KEY_BASE64 = ""
+            + "MIIFJjCCAw6gAwIBAgIJANpazyIWdcb/MA0GCSqGSIb3DQEBCwUAMCAxHjAcBgNV"
+            + "BAMMFUdvb2dsZSBDcnlwdEF1dGhWYXVsdDAeFw0xODAxMjEwMzI0MjVaFw0zODAx"
+            + "MTYwMzI0MjVaMCAxHjAcBgNVBAMMFUdvb2dsZSBDcnlwdEF1dGhWYXVsdDCCAiIw"
+            + "DQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAKkAUqkRxdVy9UpI9BjQnylGVPRW"
+            + "aQsCwT2iWJ7fuCnQQon1U9nOyw2R5GYKcA8Zy+4Co++6nzRblYYXJG3Fzsj+kxei"
+            + "pZGmU11djRJDOhHPPe5jSW37Y0czWaj8jx4xvMim18dGYR7fg6SsOOYXA2y5tlvZ"
+            + "xLjvw5qpL62J5bVoAAxjng/Oc2Osu+vpv6M50pUZr0OEiFi59WlwrCCZpf1/80bT"
+            + "j2ebCKKAtTYa6+Q+oMGGxb3imSRpmQPtFcvhUPmAaocUjYM/FeIGNRv14oED/aXz"
+            + "khuPb+QNkXgk9yiokE10IeAk6oNUNDyuiMNIFy67lUrwc45lv9y0s/8fHj9pvKse"
+            + "n3+UOKAuV9atUXLdFKQwnQPt4SOmHPkXoj+5tv32RSvVeYhb0ZOpQPkRhxv4wgs7"
+            + "NldNbKhzVDM9K4M5Q2TrPK1yJKrc5/z0bDzmPOcH4AAXPvSt5PZOs0NlXUJ99BcA"
+            + "KE1sWArUhipz5mx0hxPTNEM9/8bMb//HkbZtx2log1/fc207W/AFd2FICOpRY+Sb"
+            + "CJs9WjstpisppotONvgXxZbZiypGKxpeZOb4s6y3iXtZ0FWXUZrc65b8S06fDVCa"
+            + "ZomNFDWhspHFKyueBgU6cR9K97cDo85Juy6RhnouXxi+XpXPdGFwPqVm1glFYos8"
+            + "5+Scbwwx69RihN1nAgMBAAGjYzBhMB0GA1UdDgQWBBRoosUAVtfHHeMl8/5x1m2m"
+            + "arEOoTAfBgNVHSMEGDAWgBRoosUAVtfHHeMl8/5x1m2marEOoTAPBgNVHRMBAf8E"
+            + "BTADAQH/MA4GA1UdDwEB/wQEAwIBhjANBgkqhkiG9w0BAQsFAAOCAgEAYbLVQlsi"
+            + "KdzCPsaybSrUtMrmkad8Gy+/QR3J16adxy/2WpxsZ1Su0YA4tFzOSWqt74C0mnUi"
+            + "i+3prW7nyEOwezs+NH0SreF4B7tO1FSj157LoHxR2WmCCwul8FfsMi0x6MvEf40v"
+            + "bMeLrRAA4ysRITua1INb05fzJpczoaW/Q1lBPXConvolnIMvYsLbCZX4/OhQQCLa"
+            + "mJx8mGrt1wcNp7Kvh+3JfuOXw+WGeCuB5sSWBsOUvhfN+8sgyk1Dtq5c7rVKKtqz"
+            + "gqHfCNZ+lYa6Vkii7plIcWYJXXa7DMmozX07mrDqdJZEocv6XCoZAKDlJorB8pQT"
+            + "im47RF60X/FCHSCK2cHbG5M+kF84xwj5fTLztLM1+RlJSJiGk6jhxJRQ+hl0Vkhp"
+            + "+u7UbUDwkUF/CJB3d1Gtfm+QtzFVKe27ClU5YFSKCXRV/K4KnkZqpyG8Py+PUtUf"
+            + "WRahp4hkWFkIoLeTnJwgAFRvp/KCtSW0/trI/vfInzqBk/qWIVhxYB8Qv9DXtKBX"
+            + "3AZ36HM2dxmjef/rpYRphuEN0ZwYdynsGy9dF0SihbR8curSg67sbtYfyw0xURhU"
+            + "Nk99YMy7T0EUYnaki/PIPK/gnJjZTX1FLCyHUR38fDJIWkGB4xr8pSrwmRoPSvNF"
+            + "dFr3YlFWwHWd1gXlwJtzeMSuVgoKVtZGmmk=";
+    private static final String ROOT_CA_DIFFERENT_COMMON_NAME_BASE64 = ""
+            + "MIIJFzCCBP6gAwIBAgIJAMobGgw5LXwqMA0GCSqGSIb3DQEBCwUAMBgxFjAUBgNV"
+            + "BAMMDVdyb25nIFJvb3QgQ0EwHhcNMTgwMTE2MTgzNDA5WhcNMzgwMTExMTgzNDA5"
+            + "WjAYMRYwFAYDVQQDDA1Xcm9uZyBSb290IENBMIIEIjANBgkqhkiG9w0BAQEFAAOC"
+            + "BA8AMIIECgKCBAEJwW/TkzWx4s9ghbugWK+Hu7mi0K0ybuvKrRSOltNA9xDElBtj"
+            + "NxRAXsJs9hN1YOJ3gS47xl5zTejCex24WEq53offIhDi54aKoIGgbUPr43x8X1Hx"
+            + "rBOYCxs2DrdUifHojBwFmeMWWwgh+04XGogkFiGIqE0mE+0XWL12f43keMQqaQk7"
+            + "eleFiFFcyns7Av96nHOEFcVhPtoOD2mdozoL7iGaFD/SrajLNi0CoWasR6du0job"
+            + "jtLMxz6yE1O5gu2rbWP9ne4DdHAH1LN+730Xrq2WBNe/srthLnveE3dNBj/6eQt8"
+            + "XL+gXYtBFi8ocX42/QEfNAvOoMRgFogtwx/eLYJ79vvBmOK0uoUAjWzCobOAf9Ck"
+            + "Hc6tfnUz8HtQIXacHs9rySe7iQEMaTPZDjWMAvRb+q2PjdXAkNbRj3w66pCIKomt"
+            + "OsZ2/tv+mGdVUG8Wu0iNgx/IrfiPLlUtdy1XkJKRaPne6Xea8YUckP7qed4NHg9S"
+            + "SZg3SSFPi39GqdCXBgTRroSurX5Gh/r8OhZLN4EsFhUloeFHL2au51H9VxDKcsq3"
+            + "Lpmy3gvKHpo8wdylPAZlsb7iZedZ5NosSca+K+V2SxNc7GKlw799cl8OyzhvSCUb"
+            + "M/cYPT4fkRkH1yhjM1PjtBl312vEd2svG0+iL4x6qQaAJ3IXIFk7wbaT8m7LOJPq"
+            + "HL7nRVcmeXpSPGPsxVRgCwhIeweCoRMOy6RBtsq9MqDeDE9U03+8pbDKHsotwUho"
+            + "4Sg86BK2SZWyqtebtgsToG/Obye7ivAl4AX1ix6D0ERjzpNFGYAjt9bh4MQ+fGAJ"
+            + "RS9xhICHmZxrIjIivQpO5eR6/BhtVEcxWbL0NqrOw/xgVcioW8NdEIVLL+AySg1v"
+            + "1mip2SaNn5UGyMAoj678dp/qmB2kZltUYoQWZpUcKSu0+n+DLiQTiqZ39Ihpxwby"
+            + "joRv1f19upNMNm/fiRwaDE68ElUho0emtOOC9gSdFoMpOWZmgJSYGUfO9t/rxhYG"
+            + "YjPQry9g93H2v3wuJSCltMe0pkhismduA+dsL2Jw/bjaSwHjJpPHVBZzXi9M2/WV"
+            + "S2/jMkSZpqjKc2URMkV/9PPRAonG1P6X8EgDPyQQkwBwqx+hPH05O1+4vzmldsKD"
+            + "FsoqYqfpHPFQLNLOAxugLTPpbxnHLaAsiGoybwBcAXuxqXCtGKtXiVawz0w6FUSB"
+            + "+Ny/+qZBJivUBGvDB6AdGrE8DeqHXWR+rlhIVTtbv58mtYfpW4tnQHV+Pb6Wygw1"
+            + "6QUDVQsPwMrtoAUiH1jXLgm3//Ey449wPk9Y0YWZSTug97CIhQwuNdJZyCtBVhtk"
+            + "QNb++igwmijjtWDV9A96F6Ns40TWcEhp5FWElwIDAQABo2MwYTAdBgNVHQ4EFgQU"
+            + "13uhh5KLuFIaKZGlNloctDLr7NEwHwYDVR0jBBgwFoAU13uhh5KLuFIaKZGlNloc"
+            + "tDLr7NEwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAYYwDQYJKoZIhvcN"
+            + "AQELBQADggQCAAWZS0RIH4FdQhlUkdJW06cUYYIJ7p8rRI+6XgMkfj7vupjVnfDN"
+            + "3Z3s0yf+dGaGGJbv68W952j33z94FuubsacCixNSaS4bIwLXvcL9kQtI9Qf09Pd6"
+            + "GB102Q6nz/daLVF/5urQPbp4UFkyhcP/d2mg9CmxwBL3Djrf/dKf4EoiV5xmQpUB"
+            + "a6BNBMntnK1VZ3y8YxDF3XnhbjUZAOTLLYe4+isqvXPb9osdmdaZQU3iHQQwGYJN"
+            + "6rTYvHmsdfU5eLYLdWxOOm/5Sz5rWXxa1XqSfQgOIaFYQ1w69Z+3BfNbaBnYck3l"
+            + "xtxGHromigt+2iimXFFML7EHiSznVhHl3SOX0RBLeUvP8oNwwSsaHXuXbceYWvb+"
+            + "ic7FyTN4f3+wRGjN01U3be93dj+qZlvTmCzpOrJeUcym3F94d0tWQvk3kkBp/Egi"
+            + "Dd85vYWCdEeCfODW6sReVdj/IuT5xv1T8kKaNaEjJjAjeX6xjPskw3I2LuPeEyz+"
+            + "26LiOs1hPRHC4CL3JS6LNmRmIYKuy7K0DHwxS6wplDYXXH+a0VuLvQrbsw5zTh3f"
+            + "Xwq0CLGwPPRyMnFk13+PYBa0bmJ1dNu5hUc9biziCJlvcg0c+FzzUYG4poN0R73R"
+            + "XPyFHmpULAHit05dw3QaPZqX1GCeiVxrCl6N6G4/9PsVOvi/WEUasHDkk+R4/r9b"
+            + "RwvQw0PVdDvGndouRcSzHvPEdW9y2TxSDhltvtC2xvp3mGaT0j2+cCRDINLFB6rK"
+            + "v18oLzpzu3HaZ2ptmm4OpeRnCTLa4qheV1rlSWi3mvPoh77glgHEyTHvhiFrlq+6"
+            + "f6oMpkcbp4KtOT/npvB3yY4RZWn+J1cDcOW34ssSN2PDVSx1IsianxtSCXKReYrv"
+            + "kjS4sTQWw0LYG6146g2rz1OKzuT+6YSIPlpw/4/DK0Sz/Q1AY0cuOHhgMSW0cSaE"
+            + "6PQ275hFjJ4zYYEYNOp56nhsvgbLAu1V5rwQpwi2RNo4teFzP/AKyZzNbApfl8Q5"
+            + "PKHHE8+Uk3/oLZ8h12JzceKL5ivXU4i8r9sw+o7b/UReVsbrFDXTuO3sRFyA5kI0"
+            + "aBpYrAyb8xVubbi7gCWhDfJsSKc7CR2N+EUtBrT08TD0AtuxpRLr6Sz2RrTabZNV"
+            + "lQlgWmKalWT/i7gyA/MNxbBK0hyr7Pvl5f7Ud2+muKOfhEXiCzp8yzXU9I8cTz2p"
+            + "K85LYJfRNIO2kPrNLdomPL+J2S298GvX2j08CZR8qBXLOs6XJOvZ1KC8BrJ9M7kG"
+            + "2MHGXeL+9/11khM09dbC+Q0NUKTKOpSU7M7RaON1Dp4RyzdIcdwZ/dFvvxcYbtOP"
+            + "6OAzRvlk6CZWG3obkt3yaB1NhBEw8hiYvX9F";
+    private static final String INTERMEDIATE_CA_1_BASE64 = ""
+            + "MIIHMDCCAxegAwIBAgICEAEwDQYJKoZIhvcNAQELBQAwIDEeMBwGA1UEAwwVR29v"
+            + "Z2xlIENyeXB0QXV0aFZhdWx0MB4XDTE4MDExMjAwMjM1N1oXDTI4MDExMDAwMjM1"
+            + "N1owLTErMCkGA1UEAwwiR29vZ2xlIENyeXB0QXV0aFZhdWx0IEludGVybWVkaWF0"
+            + "ZTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBALlhoKYuwxaatY9ERDFp"
+            + "iEygSnjy0xzaiF4uCyiTfAuboSi5QwGon3ohf0ufJF02L9lnTMoeBAg+88m8AMgW"
+            + "KFcEupabqlZfA3F/50mMCmnJvBSLXJ+chUdcAVpwcZAsq6ko22ARBxao1wu2qxNe"
+            + "D8eXiiK8DRpTtKy3wOldZJ222v35v9JGOTjORZRrcv7Z8f6I5/cSsTS+WoVk/aBt"
+            + "QqyFkcdw1zqulnlFE2rxNAVgyLlW71WJikYTDtUDeo79LvkDGjVsLo2MfpNxuK+5"
+            + "MuMqzyN5LmzXJmNCEW1O5IIIUAPhgy5s08G+3G644wCEWsAnv2FBWLBn/HmJu6Uq"
+            + "nSM2AaJN56V0tJG/yL2VgTnPJrJypNTKZW3OTCLCaYcTEbKfarxLwVWxvIWSIgkn"
+            + "0q57GYhf7O+x9vvcOUmZwVxZECorIiK4n5AWG/KD3dWI3UGGGpYsDazRngA/bQPu"
+            + "DSzBP9FBVcQt3/DMBG1s6f2Eko5f6aTFcVW9iV7aWLeIq+pQYlbmG42decj+aHLQ"
+            + "COp5KV+Q77y4kFhZQFAQ1mN4crnhuEc1K5SmjAK24zIqWbwM3ly0KSQFc9jAmONg"
+            + "0xu7kAObP3PZk85En12yLLscNmHCWYfOOEvTHf4KX7tjBl4HHp/ur+2Qwgpt9MFB"
+            + "MGqR2cni5OV6gZcRdHaEerjrAgMBAAGjZjBkMB0GA1UdDgQWBBRE9RxHT7U3EP1v"
+            + "djRzNYMrU7EseDAfBgNVHSMEGDAWgBTXe6GHkou4UhopkaU2Why0Muvs0TASBgNV"
+            + "HRMBAf8ECDAGAQH/AgEBMA4GA1UdDwEB/wQEAwIBhjANBgkqhkiG9w0BAQsFAAOC"
+            + "BAIAAfa7FKkfh4t+G8Sv4n3gUcuEAtmpCsTtHJU67r5eSvR24fDX7sqqSIib9sQ8"
+            + "adA1FtnE3wnC4bEKnIwQLerWTN4i4V9oEKRIT5o5Rr61LyO9R+Prpo6uJVoPujFH"
+            + "GZPdIj/Qs4Xax4a11QD+70+ZmmdL6yfigVDWA4yduFdMg6heHRf3EFCbVBv5qbBS"
+            + "n8+eFKRnBZ/kQdFlYt+G+92Qqyn2uAcER6kZjIfPdnZh44SazLP37W8AkDX30Pmk"
+            + "V0PHVGCDrap44q3maI1m8NAE1jGwwmRzJL953x5XgbVGt0K/3cNoWtKLenwX/G3I"
+            + "akrgvOY5Zl0v3FRDZwGFt9UIBfZDDOGRMXIgIGs/1cvkwWpOT6dyReqDXempiQ1q"
+            + "Yy6J5VsK5WK6gEelUyoACbzgby25V6a79Q1MI7dXmFQfCcX0nAD/AZmM1HkeYgrC"
+            + "uq6fWoPOVMKML2mN90rCzkWxGaLcl5dPfad0O1LrcP48SRE5MXMWyxZZBon+wDIk"
+            + "ascyM/r4fmk4kq64YKdm2wxCDMNArAIcyBkwOaWWfabtSagxJ3qtMtxK0qBUsbLC"
+            + "yMyYpgU1h9c8rEdc4JgeE2LXJzxTKDc3SBOqbuNMlKWjYA+X+SUvVYALrQKAC+5v"
+            + "wdUhLYdAPAksqk/ZoiBjkW35FfvqQMJBY29VnDT1h7/Nxk5gu+goTA9oFIYNrNte"
+            + "+s0my+IUgYhKJBsgh7Mupv+B92GN5b3b440BMHB5QR959Jdq6BAXNUyZLM5fhZQE"
+            + "Jj/rxZFXaqq757kgUhwWBz5TDbYF7GkqTyM4k430xwJKY0AYYEHmv1UYNo5X4G3x"
+            + "SC2LhWC1b9VAykdkHbLs+IA8klxURmLmRiRj1UryhQjjT8h/FvNyPnbT1AKoElix"
+            + "QLnLi8thkJ+tQggO0hISFsIrKNfnn0V6O0VKw9UZsMigsbYG5EbzIXcAyy8Avr9n"
+            + "um7gBBZDt7fWso0+pG1UenJ+PybeuW/azQDLRw1Syz8OwU+ABRLq0JyyAtV7VPY5"
+            + "C9pkKS+bU8nECxr6dMhAbpLBHlKsyb1qtkOt1p7WagEQZFIIc6svc73+L/ET/lWn"
+            + "GBmkVVsCN7Aqyo5aXQWueXP4FUL+6O5+JALqw3qPeQgfnLkh0cUuccNND05QeEiv"
+            + "Zswc/23KJXy1XbdVKT3UP0RAF7DxstbRGQbAT3z+n931e3KhtU28OKjsFtoeq2Dj"
+            + "6STPEXh4rYFWMM8+DrJetAtBqk/i+vBwRA8f7jqIPPep/vEjPqqMOpdSVcoFQ1df"
+            + "JuOZtGfEUjFHnlDr3eGP7KUIEZvhan1zm544dDgPVTXxrY4moi2BhKEY69zRSX6B"
+            + "+a0fa5B3pxc8BN0LsHA0stT/Y2o=";
+    private static final String INTERMEDIATE_CA_2_BASE64 = ""
+            + "MIIESTCCAjGgAwIBAgICEAAwDQYJKoZIhvcNAQELBQAwLTErMCkGA1UEAwwiR29v"
+            + "Z2xlIENyeXB0QXV0aFZhdWx0IEludGVybWVkaWF0ZTAeFw0xODAxMTIwMDM4MDNa"
+            + "Fw0yMzAxMTEwMDM4MDNaMDoxODA2BgNVBAMML0dvb2dsZSBDcnlwdEF1dGhWYXVs"
+            + "dCBJbnRlcm1lZGlhdGUgSW50ZXJtZWRpYXRlMIIBIjANBgkqhkiG9w0BAQEFAAOC"
+            + "AQ8AMIIBCgKCAQEA0v3bN3MwKifDAkF1524XzuaxYtk1sQKUlAlNngh+Qv4RjCkX"
+            + "TECi7po8LeNsY+hWxmW3XZ22RBphe/yP4YcOdbqlIjZYNx3b75hCSJCadOkdW+Z9"
+            + "f6+tKsHgeUja6r9r2TThzileImAvjXShe7GZYW8csPv6HaEVRXQlu8fGAZf8skmJ"
+            + "EMfJx84//WeULdVz94beDhi9YAf4gLfmOayQcdWhDcMYI39knJcRny1ffRGgb1Hf"
+            + "lE+3/a3aGFeODaxfkPaGRxEhzhZ/JDBiNgUAH/u7C5nxqa2WOu5e0wq3S0TndIOE"
+            + "hmnwCE2GvxADFQst+rSsOn4EHit70hv4CfrMRQIDAQABo2YwZDAdBgNVHQ4EFgQU"
+            + "0dKv4xTaEmdwHyox3tY8H8XVDGIwHwYDVR0jBBgwFoAURPUcR0+1NxD9b3Y0czWD"
+            + "K1OxLHgwEgYDVR0TAQH/BAgwBgEB/wIBADAOBgNVHQ8BAf8EBAMCAYYwDQYJKoZI"
+            + "hvcNAQELBQADggIBAJaArqLlJ2SLQ8JRwanD6LPlqQxucQ+x/LztTQpzPrsFfJds"
+            + "E/sDZr2rXhDpz/bifIdj/DCbQ6/3w+t5JdFjT8GjXLgz1kCa/4W409FgSTgy1ENn"
+            + "AMUU6pFIbOq/Qy/71uOTrrFWYTN5Vk+RBGxx5iDfHjDYraudi4VlcNkal4OyM98n"
+            + "N3qp9cZD0RtWxMhvq6ahgmf9cTbEw6+l8yf/bogGLBYXXYeOoO5Q134AxrrgfthE"
+            + "tvyKwJkT/l3OFKRcaHrebs+V1z5gPs7zWOyO5n2Z1SAmcOGfTfKMZWwp3Hi3OTr2"
+            + "gB3LUYKyQVhC70dka3X+IbnFg5YfzJtX6YGnHlnI1SufOkEpGQDfcc0UQAWg/lgb"
+            + "RkfMFD9tuJomBhyqv1YaxLN8yL4ZTRU0KCvvC5I5+X/zt9kBjnHlBOdYtknZT5jz"
+            + "7+mjqWdpmWoAjeV5+CgIzG2k7JAm6rQuE1ZQNF0wAYxPret4NHPJFqfD5gGhdrYw"
+            + "pEUxkcwHERA/E1CkpyqUy/Hd3kqHvnEDqzFcxBdUdmOgnbpI2nAZdEpfxmA5+M1n"
+            + "UoxQ8ZWAZH+Mdlkw/Hx5hVjGjz8snD4QN25pj/wT+V6AR5OmYb8yfsQb2S8a8yDp"
+            + "HzcIHW+dEWpX2boirOsrdI16kNtxYqtG7c5qWBPJy5Zjkvh9qbnfT/RQx10g";
+    private static final String LEAF_CERT_1_BASE64 = ""
+            + "MIIDCDCB8aADAgECAgYBYOlweDswDQYJKoZIhvcNAQELBQAwLTErMCkGA1UEAwwi"
+            + "R29vZ2xlIENyeXB0QXV0aFZhdWx0IEludGVybWVkaWF0ZTAeFw0xODAxMTEwODE1"
+            + "NTBaFw0yMDAxMTIwODE1NTBaMCkxJzAlBgNVBAMTHkdvb2dsZSBDcnlwdEF1dGhW"
+            + "YXVsdCBJbnN0YW5jZTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABLgAERiYHfBu"
+            + "tJT+htocB40BtDr2jdxh0EZJlQ8QhpMkZuA/0t/zeSAdkVWw5b16izJ9JVOi/KVl"
+            + "4b0hRH54UvowDQYJKoZIhvcNAQELBQADggIBABZALhC9j3hpZ0AgN0tsqAP2Ix21"
+            + "tNOcvo/aFJuSFanOM4DZbycZEYAo5rorvuFu7eXETBKDGnI5xreNAoQsaj/dyCHu"
+            + "HKIn5P7yCmKvG2sV2TQ5go+0xV2x8BhTrtUWLeHvUbM3fXipa3NrordbA8MgzXwr"
+            + "GR1Y1FuMOn5n4kiuHJ2sQTbDdzSQSK5VpH+6rjARlfOCyLUX0u8UKRRH81qhIQWb"
+            + "UFMp9q1CVfiLP2O3CdDdpZXCysdflIb62TWnma+I8jqMryyxrMVs9kpfa8zkX9qe"
+            + "33Vxp+QaQTqQ07/7KYVw869MeFn+bXeHnjUhqGY6S8M71vrTMG3M5p8Sq9LmV8Y5"
+            + "7YB5uqKap2Inf0FOuJS7h7nVVzU/kOFkepaQVHyScwTPuuXNgpQg8XZnN/AWfRwJ"
+            + "hf5zE6vXXTHMzQA1mY2eEhxGfpryv7LH8pvfcyTakdBlw8aMJjKdre8xLLGZeVCa"
+            + "79plkfYD0rMrxtRHCGyTKGzUcx/B9kYJK5qBgJiDJLKF3XwGbAs/F8CyEPihjvj4"
+            + "M2EoeyhmHWKLYsps6+uTksJ+PxZU14M7672K2y8BdulyfkZIhili118XnRykKkMf"
+            + "JLQJKMqZx5O0B9bF8yQdcGKEGEwMQt5ENdH8HeiwLm4QS3VzFXYetgUPCM5lPDIp"
+            + "BuwwuQxvQDF4pmQd";
+    private static final String LEAF_CERT_2_BASE64 = ""
+            + "MIICrDCCAZSgAwIBAgICEAAwDQYJKoZIhvcNAQELBQAwOjE4MDYGA1UEAwwvR29v"
+            + "Z2xlIENyeXB0QXV0aFZhdWx0IEludGVybWVkaWF0ZSBJbnRlcm1lZGlhdGUwHhcN"
+            + "MTgwMTEyMDEwMzA5WhcNMTkwMTEyMDEwMzA5WjArMSkwJwYDVQQDDCBHb29nbGUg"
+            + "Q3J5cHRBdXRoVmF1bHQgSW5zdGFuY2UgMjBZMBMGByqGSM49AgEGCCqGSM49AwEH"
+            + "A0IABGhmBQyWdjsXKJRbkW4iIrvt6iqhX5t2XGt/vZS9CoOl0fs+EvJXo4kgrnx8"
+            + "/8SGxz3pwRkFhY943QYy6a1gv/2jgZUwgZIwCQYDVR0TBAIwADAdBgNVHQ4EFgQU"
+            + "xFmLyxUS2JHKURBtewBKRP6kQBgwVgYDVR0jBE8wTYAU0dKv4xTaEmdwHyox3tY8"
+            + "H8XVDGKhMaQvMC0xKzApBgNVBAMMIkdvb2dsZSBDcnlwdEF1dGhWYXVsdCBJbnRl"
+            + "cm1lZGlhdGWCAhAAMA4GA1UdDwEB/wQEAwIDCDANBgkqhkiG9w0BAQsFAAOCAQEA"
+            + "EJWpl7HU6LxukLqhw2tVZr7IRrKIucRk+RKaaiMx1Hx2jsTTskiJRiZas/xoPSqX"
+            + "z1K5DVgI486i7HyqnWkGH5xVzCsv+rya5FOSTS3jVtgtoA4HFEqeeAcDowPDqVw3"
+            + "yFTA55ukZnzVaPLpDfPqkhzWiuLQ/4fI6YCmOnWB8KtHTMdyGsDSAkpoxpok++NJ"
+            + "Lu79BoBLe2ucjN383lTlieLxmrmHjF9ryYSQczcm0v6irMOMxEovw5iT4LHiEhbm"
+            + "DfOPW909fe/s+K3TGZ3Q6U77x8g5k9dVovMgA4pFwtREtknFjeK1wXR3/eXGcP3W"
+            + "0bMX1yTWYJQFWCG3DFoC5w==";
+
+    /** The cert of the root CA. */
+    static final X509Certificate ROOT_CA_TRUSTED = decodeBase64Cert(ROOT_CA_TRUSTED_BASE64);
+    /** This root CA cert has a different Common Name than ROOT_CA_TRUSTED. */
+    static final X509Certificate ROOT_CA_DIFFERENT_COMMON_NAME =
+            decodeBase64Cert(ROOT_CA_DIFFERENT_COMMON_NAME_BASE64);
+    /** This root CA cert has the same CN as ROOT_CA_TRUSTED, but a different public key. */
+    static final X509Certificate ROOT_CA_DIFFERENT_KEY =
+            decodeBase64Cert(ROOT_CA_DIFFERENT_KEY_BASE64);
+    /** This intermediate CA cert is signed by the corresponding private key of ROOT_CA_TRUSTED. */
+    static final X509Certificate INTERMEDIATE_CA_1 = decodeBase64Cert(INTERMEDIATE_CA_1_BASE64);
+    /** This intermediate CA cert is signed by the private key of INTERMEDIATE_CA_1. */
+    static final X509Certificate INTERMEDIATE_CA_2 = decodeBase64Cert(INTERMEDIATE_CA_2_BASE64);
+    /** This leaf cert is signed by the corresponding private key of INTERMEDIATE_CA_1. */
+    static final X509Certificate LEAF_CERT_1 = decodeBase64Cert(LEAF_CERT_1_BASE64);
+    /** This leaf cert is signed by the corresponding private key of INTERMEDIATE_CA_2. */
+    static final X509Certificate LEAF_CERT_2 = decodeBase64Cert(LEAF_CERT_2_BASE64);
+
+    private static X509Certificate decodeBase64Cert(String str) {
+        try {
+            byte[] bytes = Base64.getDecoder().decode(str);
+            CertificateFactory cf = CertificateFactory.getInstance("X.509");
+            return (X509Certificate) cf.generateCertificate(new ByteArrayInputStream(bytes));
+        } catch (Exception e) {
+            throw new RuntimeException(e.getMessage());
+        }
+    }
+
+    static final Date DATE_ALL_CERTS_VALID = new Date(1516406400000L); // Jan 20, 2018
+    static final Date DATE_LEAF_CERT_2_EXPIRED = new Date(1547254989001L); // Jan 12, 2019
+    static final Date DATE_INTERMEDIATE_CA_2_EXPIRED = new Date(1673397483001L); // Jan 11, 2023
+
+    static InputStream openTestFile(String relativePath) throws Exception {
+        Context context = InstrumentationRegistry.getContext();
+        return context.getResources().getAssets().open(TEST_FILE_FOLDER_NAME + "/" + relativePath);
+    }
+
+    static byte[] readTestFile(String relativePath) throws Exception {
+        InputStream in = openTestFile(relativePath);
+        return ByteStreams.toByteArray(in);
+    }
+}
diff --git a/services/tests/servicestests/src/com/android/server/wm/DimmerTests.java b/services/tests/servicestests/src/com/android/server/wm/DimmerTests.java
index 396fef4..57dd808 100644
--- a/services/tests/servicestests/src/com/android/server/wm/DimmerTests.java
+++ b/services/tests/servicestests/src/com/android/server/wm/DimmerTests.java
@@ -18,11 +18,9 @@
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotNull;
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.Mockito.doAnswer;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
-import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 
 import android.graphics.Rect;
@@ -32,24 +30,17 @@
 import android.view.SurfaceSession;
 
 import org.junit.Before;
-import org.junit.Ignore;
 import org.junit.Test;
 import org.junit.runner.RunWith;
-import org.mockito.stubbing.Answer;
 
 /**
  * Build/Install/Run:
  * atest FrameworksServicesTests:com.android.server.wm.DimmerTests;
  */
 @Presubmit
-@Ignore("b/72450130")
 @RunWith(AndroidJUnit4.class)
 public class DimmerTests extends WindowTestsBase {
 
-    public DimmerTests() {
-        super(spy(new SurfaceAnimationRunner()));
-    }
-
     private class TestWindowContainer extends WindowContainer<TestWindowContainer> {
         final SurfaceControl mControl = mock(SurfaceControl.class);
         final SurfaceControl.Transaction mTransaction = mock(SurfaceControl.Transaction.class);
@@ -71,14 +62,11 @@
 
     private class MockSurfaceBuildingContainer extends WindowContainer<TestWindowContainer> {
         final SurfaceSession mSession = new SurfaceSession();
+        final SurfaceControl mHostControl = mock(SurfaceControl.class);
         final SurfaceControl.Transaction mHostTransaction = mock(SurfaceControl.Transaction.class);
 
         MockSurfaceBuildingContainer() {
             super(sWm);
-            mSurfaceControl = sWm.makeSurfaceBuilder(mSession)
-                    .setName("test surface")
-                    .setSize(1, 1)
-                    .build();
         }
 
         class MockSurfaceBuilder extends SurfaceControl.Builder {
@@ -88,34 +76,29 @@
 
             @Override
             public SurfaceControl build() {
-                return spy(sWm.makeSurfaceBuilder(mSession)
-                        .setName("test surface")
-                        .setSize(1, 1)
-                        .build());
+                return mock(SurfaceControl.class);
             }
         }
 
         @Override
-        SurfaceControl.Builder makeSurface() {
-            return sWm.makeSurfaceBuilder(mSession)
-                    .setName("test surface")
-                    .setSize(1, 1);
-        }
-
-        @Override
         SurfaceControl.Builder makeChildSurface(WindowContainer child) {
             return new MockSurfaceBuilder(mSession);
         }
 
         @Override
+        public SurfaceControl getSurfaceControl() {
+            return mHostControl;
+        }
+
+        @Override
         public SurfaceControl.Transaction getPendingTransaction() {
             return mHostTransaction;
         }
     }
 
-    MockSurfaceBuildingContainer mHost;
-    Dimmer mDimmer;
-    SurfaceControl.Transaction mTransaction;
+    private MockSurfaceBuildingContainer mHost;
+    private Dimmer mDimmer;
+    private SurfaceControl.Transaction mTransaction;
 
     @Before
     public void setUp() throws Exception {
@@ -123,13 +106,9 @@
         mHost = new MockSurfaceBuildingContainer();
 
         mTransaction = mock(SurfaceControl.Transaction.class);
-        mDimmer = new Dimmer(mHost);
-
-        doAnswer((Answer<Void>) invocation -> {
-            Runnable runnable = invocation.getArgument(3);
-            runnable.run();
-            return null;
-        }).when(sWm.mSurfaceAnimationRunner).startAnimation(any(), any(), any(), any());
+        mDimmer = new Dimmer(mHost,
+                (surfaceAnimator, t, anim, hidden) -> surfaceAnimator.mAnimationFinishedCallback
+                        .run());
     }
 
     @Test
@@ -137,7 +116,7 @@
         final float alpha = 0.8f;
         mDimmer.dimAbove(mTransaction, alpha);
 
-        SurfaceControl dimLayer = getDimLayer(null);
+        SurfaceControl dimLayer = getDimLayer();
 
         assertNotNull("Dimmer should have created a surface", dimLayer);
 
@@ -149,12 +128,12 @@
     public void testDimAboveNoChildRedundantlyUpdatesAlphaOnExistingSurface() throws Exception {
         float alpha = 0.8f;
         mDimmer.dimAbove(mTransaction, alpha);
-        final SurfaceControl firstSurface = getDimLayer(null);
+        final SurfaceControl firstSurface = getDimLayer();
 
         alpha = 0.9f;
         mDimmer.dimAbove(mTransaction, alpha);
 
-        assertEquals(firstSurface, getDimLayer(null));
+        assertEquals(firstSurface, getDimLayer());
         verify(mTransaction).setAlpha(firstSurface, 0.9f);
     }
 
@@ -167,18 +146,18 @@
         Rect bounds = new Rect(0, 0, width, height);
         mDimmer.updateDims(mTransaction, bounds);
 
-        verify(mTransaction).setSize(getDimLayer(null), width, height);
-        verify(mTransaction).show(getDimLayer(null));
+        verify(mTransaction).setSize(getDimLayer(), width, height);
+        verify(mTransaction).show(getDimLayer());
     }
 
     @Test
     public void testDimAboveNoChildNotReset() throws Exception {
         mDimmer.dimAbove(mTransaction, 0.8f);
-        SurfaceControl dimLayer = getDimLayer(null);
+        SurfaceControl dimLayer = getDimLayer();
         mDimmer.resetDimStates();
 
         mDimmer.updateDims(mTransaction, new Rect());
-        verify(mTransaction).show(getDimLayer(null));
+        verify(mTransaction).show(getDimLayer());
         verify(dimLayer, never()).destroy();
     }
 
@@ -189,12 +168,12 @@
 
         final float alpha = 0.8f;
         mDimmer.dimAbove(mTransaction, child, alpha);
-        SurfaceControl mDimLayer = getDimLayer(child);
+        SurfaceControl dimLayer = getDimLayer();
 
-        assertNotNull("Dimmer should have created a surface", mDimLayer);
+        assertNotNull("Dimmer should have created a surface", dimLayer);
 
-        verify(mTransaction).setAlpha(mDimLayer, alpha);
-        verify(mTransaction).setRelativeLayer(mDimLayer, child.mControl, 1);
+        verify(mTransaction).setAlpha(dimLayer, alpha);
+        verify(mTransaction).setRelativeLayer(dimLayer, child.mControl, 1);
     }
 
     @Test
@@ -204,12 +183,12 @@
 
         final float alpha = 0.8f;
         mDimmer.dimBelow(mTransaction, child, alpha);
-        SurfaceControl mDimLayer = getDimLayer(child);
+        SurfaceControl dimLayer = getDimLayer();
 
-        assertNotNull("Dimmer should have created a surface", mDimLayer);
+        assertNotNull("Dimmer should have created a surface", dimLayer);
 
-        verify(mTransaction).setAlpha(mDimLayer, alpha);
-        verify(mTransaction).setRelativeLayer(mDimLayer, child.mControl, -1);
+        verify(mTransaction).setAlpha(dimLayer, alpha);
+        verify(mTransaction).setRelativeLayer(dimLayer, child.mControl, -1);
     }
 
     @Test
@@ -219,7 +198,7 @@
 
         final float alpha = 0.8f;
         mDimmer.dimAbove(mTransaction, child, alpha);
-        SurfaceControl dimLayer = getDimLayer(child);
+        SurfaceControl dimLayer = getDimLayer();
         mDimmer.resetDimStates();
 
         mDimmer.updateDims(mTransaction, new Rect());
@@ -233,7 +212,7 @@
 
         final float alpha = 0.8f;
         mDimmer.dimAbove(mTransaction, child, alpha);
-        SurfaceControl dimLayer = getDimLayer(child);
+        SurfaceControl dimLayer = getDimLayer();
         mDimmer.resetDimStates();
         mDimmer.dimAbove(mTransaction, child, alpha);
 
@@ -242,7 +221,29 @@
         verify(dimLayer, never()).destroy();
     }
 
-    private SurfaceControl getDimLayer(WindowContainer windowContainer) {
-        return mDimmer.mDimLayerUsers.get(windowContainer).mDimLayer;
+    @Test
+    public void testDimUpdateWhileDimming() throws Exception {
+        Rect bounds = new Rect();
+        TestWindowContainer child = new TestWindowContainer();
+        mHost.addChild(child, 0);
+
+        final float alpha = 0.8f;
+        mDimmer.dimAbove(mTransaction, child, alpha);
+
+        SurfaceControl dimLayer = getDimLayer();
+        bounds.set(0, 0, 10, 10);
+        mDimmer.updateDims(mTransaction, bounds);
+        verify(mTransaction, times(1)).show(dimLayer);
+        verify(mTransaction).setSize(dimLayer, bounds.width(), bounds.height());
+        verify(mTransaction).setPosition(dimLayer, 0, 0);
+
+        bounds.set(10, 10, 30, 30);
+        mDimmer.updateDims(mTransaction, bounds);
+        verify(mTransaction).setSize(dimLayer, bounds.width(), bounds.height());
+        verify(mTransaction).setPosition(dimLayer, 10, 10);
+    }
+
+    private SurfaceControl getDimLayer() {
+        return mDimmer.mDimState.mDimLayer;
     }
 }
diff --git a/services/tests/servicestests/src/com/android/server/wm/TestWindowManagerPolicy.java b/services/tests/servicestests/src/com/android/server/wm/TestWindowManagerPolicy.java
index 81fd889..000cf38 100644
--- a/services/tests/servicestests/src/com/android/server/wm/TestWindowManagerPolicy.java
+++ b/services/tests/servicestests/src/com/android/server/wm/TestWindowManagerPolicy.java
@@ -69,11 +69,6 @@
     private Runnable mRunnableWhenAddingSplashScreen;
 
     static synchronized WindowManagerService getWindowManagerService(Context context) {
-        return getWindowManagerService(context, new SurfaceAnimationRunner());
-    }
-
-    static synchronized WindowManagerService getWindowManagerService(Context context,
-            SurfaceAnimationRunner surfaceAnimationRunner) {
         if (sWm == null) {
             // We only want to do this once for the test process as we don't want WM to try to
             // register a bunch of local services again.
@@ -111,7 +106,7 @@
             }
 
             sWm = WindowManagerService.main(context, ims, true, false,
-                    false, new TestWindowManagerPolicy(), surfaceAnimationRunner);
+                    false, new TestWindowManagerPolicy());
         }
         return sWm;
     }
diff --git a/services/tests/servicestests/src/com/android/server/wm/WindowTestsBase.java b/services/tests/servicestests/src/com/android/server/wm/WindowTestsBase.java
index 7918901..69b1378 100644
--- a/services/tests/servicestests/src/com/android/server/wm/WindowTestsBase.java
+++ b/services/tests/servicestests/src/com/android/server/wm/WindowTestsBase.java
@@ -84,16 +84,6 @@
     WindowState mChildAppWindowBelow;
     HashSet<WindowState> mCommonWindows;
 
-    private final SurfaceAnimationRunner mSurfaceAnimationRunner;
-
-    public WindowTestsBase() {
-        this(new SurfaceAnimationRunner());
-    }
-
-    public WindowTestsBase(SurfaceAnimationRunner surfaceAnimationRunner) {
-        mSurfaceAnimationRunner = surfaceAnimationRunner;
-    }
-
     @Before
     public void setUp() throws Exception {
         if (!sOneTimeSetupDone) {
@@ -108,7 +98,7 @@
         final Context context = InstrumentationRegistry.getTargetContext();
         AttributeCache.init(context);
 
-        sWm = TestWindowManagerPolicy.getWindowManagerService(context, mSurfaceAnimationRunner);
+        sWm = TestWindowManagerPolicy.getWindowManagerService(context);
         beforeCreateDisplay();
 
         context.getDisplay().getDisplayInfo(mDisplayInfo);
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
index cd32425..d6f61cd 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
@@ -38,6 +38,7 @@
 import static org.mockito.Matchers.eq;
 import static org.mockito.Mockito.any;
 import static org.mockito.Mockito.anyInt;
+import static org.mockito.Mockito.atLeastOnce;
 import static org.mockito.Mockito.doAnswer;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
@@ -49,8 +50,10 @@
 import static org.mockito.Mockito.when;
 
 import android.app.ActivityManager;
+import android.app.IActivityManager;
 import android.app.INotificationManager;
 import android.app.Notification;
+import android.app.Notification.MessagingStyle.Message;
 import android.app.NotificationChannel;
 import android.app.NotificationChannelGroup;
 import android.app.NotificationManager;
@@ -64,9 +67,11 @@
 import android.content.pm.ParceledListSlice;
 import android.graphics.Color;
 import android.media.AudioManager;
+import android.net.Uri;
 import android.os.Binder;
 import android.os.Build;
 import android.os.Bundle;
+import android.os.IBinder;
 import android.os.Process;
 import android.os.UserHandle;
 import android.provider.Settings.Secure;
@@ -149,6 +154,10 @@
     @Mock private ICompanionDeviceManager mCompanionMgr;
     @Mock SnoozeHelper mSnoozeHelper;
     @Mock GroupHelper mGroupHelper;
+    @Mock
+    IBinder mPermOwner;
+    @Mock
+    IActivityManager mAm;
 
     // Use a Testable subclass so we can simulate calls from the system without failing.
     private static class TestableNotificationManagerService extends NotificationManagerService {
@@ -208,6 +217,7 @@
         when(mockLightsManager.getLight(anyInt())).thenReturn(mock(Light.class));
         when(mAudioManager.getRingerModeInternal()).thenReturn(AudioManager.RINGER_MODE_NORMAL);
         when(mPackageManagerClient.hasSystemFeature(FEATURE_WATCH)).thenReturn(false);
+        when(mAm.newUriPermissionOwner(anyString())).thenReturn(mPermOwner);
 
         // write to a test file; the system file isn't readable from tests
         mFile = new File(mContext.getCacheDir(), "test.xml");
@@ -238,7 +248,7 @@
                     mPackageManager, mPackageManagerClient, mockLightsManager,
                     mListeners, mAssistants, mConditionProviders,
                     mCompanionMgr, mSnoozeHelper, mUsageStats, mPolicyFile, mActivityManager,
-                    mGroupHelper);
+                    mGroupHelper, mAm);
         } catch (SecurityException e) {
             if (!e.getMessage().contains("Permission Denial: not allowed to send broadcast")) {
                 throw e;
@@ -592,6 +602,8 @@
                 mBinderService.getActiveNotifications(PKG);
         assertEquals(0, notifs.length);
         assertEquals(0, mService.getNotificationRecordCount());
+        verify(mAm, atLeastOnce()).revokeUriPermissionFromOwner(
+                any(), any(), anyInt(), anyInt());
     }
 
     @Test
@@ -610,6 +622,7 @@
         ArgumentCaptor<NotificationStats> captor = ArgumentCaptor.forClass(NotificationStats.class);
         verify(mListeners, times(1)).notifyRemovedLocked(any(), anyInt(), captor.capture());
         assertEquals(NotificationStats.DISMISSAL_OTHER, captor.getValue().getDismissalSurface());
+        verify(mAm, atLeastOnce()).revokeUriPermissionFromOwner(any(), any(), anyInt(), anyInt());
     }
 
     @Test
@@ -624,6 +637,7 @@
                 mBinderService.getActiveNotifications(sbn.getPackageName());
         assertEquals(0, notifs.length);
         assertEquals(0, mService.getNotificationRecordCount());
+        verify(mAm, atLeastOnce()).revokeUriPermissionFromOwner(any(), any(), anyInt(), anyInt());
     }
 
     @Test
@@ -637,6 +651,7 @@
                 mBinderService.getActiveNotifications(sbn.getPackageName());
         assertEquals(0, notifs.length);
         assertEquals(0, mService.getNotificationRecordCount());
+        verify(mAm, atLeastOnce()).revokeUriPermissionFromOwner(any(), any(), anyInt(), anyInt());
     }
 
     @Test
@@ -658,6 +673,7 @@
         ArgumentCaptor<NotificationStats> captor = ArgumentCaptor.forClass(NotificationStats.class);
         verify(mListeners, times(1)).notifyRemovedLocked(any(), anyInt(), captor.capture());
         assertEquals(NotificationStats.DISMISSAL_OTHER, captor.getValue().getDismissalSurface());
+        verify(mAm, atLeastOnce()).revokeUriPermissionFromOwner(any(), any(), anyInt(), anyInt());
     }
 
     @Test
@@ -676,6 +692,7 @@
         mBinderService.cancelAllNotifications(PKG, parent.sbn.getUserId());
         waitForIdle();
         assertEquals(0, mService.getNotificationRecordCount());
+        verify(mAm, atLeastOnce()).revokeUriPermissionFromOwner(any(), any(), anyInt(), anyInt());
     }
 
     @Test
@@ -689,6 +706,7 @@
         waitForIdle();
 
         assertEquals(0, mService.getNotificationRecordCount());
+        verify(mAm, atLeastOnce()).revokeUriPermissionFromOwner(any(), any(), anyInt(), anyInt());
     }
 
     @Test
@@ -2447,4 +2465,64 @@
         mBinderService.getBackupPayload(1);
         assertEquals(1, mService.countSystemChecks - systemChecks);
     }
+
+    @Test
+    public void revokeUriPermissions_update() throws Exception {
+        NotificationChannel c = new NotificationChannel(
+                TEST_CHANNEL_ID, TEST_CHANNEL_ID, NotificationManager.IMPORTANCE_DEFAULT);
+        c.setSound(null, Notification.AUDIO_ATTRIBUTES_DEFAULT);
+        Message message1 = new Message("", 0, "");
+        message1.setData("", Uri.fromParts("old", "", "old stuff"));
+        Message message2 = new Message("", 1, "");
+        message2.setData("", Uri.fromParts("new", "", "new stuff"));
+
+        Notification.Builder nb = new Notification.Builder(mContext, c.getId())
+                .setContentTitle("foo")
+                .setSmallIcon(android.R.drawable.sym_def_app_icon)
+                .setStyle(new Notification.MessagingStyle("")
+                        .addMessage(message1)
+                        .addMessage(message2));
+        StatusBarNotification oldSbn = new StatusBarNotification(PKG, PKG, 0, "tag", mUid, 0,
+                nb.build(), new UserHandle(mUid), null, 0);
+        NotificationRecord oldRecord =
+                new NotificationRecord(mContext, oldSbn, c);
+
+        Notification.Builder nb1 = new Notification.Builder(mContext, c.getId())
+                .setContentTitle("foo")
+                .setSmallIcon(android.R.drawable.sym_def_app_icon)
+                .setStyle(new Notification.MessagingStyle("").addMessage(message2));
+        StatusBarNotification newSbn = new StatusBarNotification(PKG, PKG, 0, "tag", mUid, 0,
+                nb1.build(), new UserHandle(mUid), null, 0);
+        NotificationRecord newRecord =
+                new NotificationRecord(mContext, newSbn, c);
+
+        mService.revokeUriPermissions(newRecord, oldRecord);
+
+        verify(mAm, times(1)).revokeUriPermissionFromOwner(any(), eq(message1.getDataUri()),
+                anyInt(), anyInt());
+    }
+
+    @Test
+    public void revokeUriPermissions_cancel() throws Exception {
+        NotificationChannel c = new NotificationChannel(
+                TEST_CHANNEL_ID, TEST_CHANNEL_ID, NotificationManager.IMPORTANCE_DEFAULT);
+        c.setSound(null, Notification.AUDIO_ATTRIBUTES_DEFAULT);
+        Message message1 = new Message("", 0, "");
+        message1.setData("", Uri.fromParts("old", "", "old stuff"));
+
+        Notification.Builder nb = new Notification.Builder(mContext, c.getId())
+                .setContentTitle("foo")
+                .setSmallIcon(android.R.drawable.sym_def_app_icon)
+                .setStyle(new Notification.MessagingStyle("")
+                        .addMessage(message1));
+        StatusBarNotification oldSbn = new StatusBarNotification(PKG, PKG, 0, "tag", mUid, 0,
+                nb.build(), new UserHandle(mUid), null, 0);
+        NotificationRecord oldRecord =
+                new NotificationRecord(mContext, oldSbn, c);
+
+        mService.revokeUriPermissions(null, oldRecord);
+
+        verify(mAm, times(1)).revokeUriPermissionFromOwner(any(), eq(message1.getDataUri()),
+                anyInt(), anyInt());
+    }
 }
diff --git a/services/usage/java/com/android/server/usage/UsageStatsService.java b/services/usage/java/com/android/server/usage/UsageStatsService.java
index 36a2a95..3b0fd1f 100644
--- a/services/usage/java/com/android/server/usage/UsageStatsService.java
+++ b/services/usage/java/com/android/server/usage/UsageStatsService.java
@@ -654,7 +654,7 @@
         public boolean isAppInactive(String packageName, int userId) {
             try {
                 userId = ActivityManager.getService().handleIncomingUser(Binder.getCallingPid(),
-                        Binder.getCallingUid(), userId, false, true, "isAppInactive", null);
+                        Binder.getCallingUid(), userId, false, false, "isAppInactive", null);
             } catch (RemoteException re) {
                 throw re.rethrowFromSystemServer();
             }
diff --git a/telecomm/java/android/telecom/Connection.java b/telecomm/java/android/telecom/Connection.java
index 63f970a..e0b0bbf 100644
--- a/telecomm/java/android/telecom/Connection.java
+++ b/telecomm/java/android/telecom/Connection.java
@@ -787,6 +787,10 @@
             builder.append(isLong ? " PROPERTY_HAS_CDMA_VOICE_PRIVACY" : " priv");
         }
 
+        if (can(properties, PROPERTY_IS_RTT)) {
+            builder.append(isLong ? " PROPERTY_IS_RTT" : " rtt");
+        }
+
         builder.append("]");
         return builder.toString();
     }
@@ -2646,6 +2650,7 @@
      * side of the coll.
      */
     public final void sendRttSessionRemotelyTerminated() {
+        unsetRttProperty();
         mListeners.forEach((l) -> l.onRttSessionRemotelyTerminated(Connection.this));
     }
 
diff --git a/telephony/java/android/telephony/TelephonyManager.java b/telephony/java/android/telephony/TelephonyManager.java
index 5c290da..03a8f33 100644
--- a/telephony/java/android/telephony/TelephonyManager.java
+++ b/telephony/java/android/telephony/TelephonyManager.java
@@ -6587,11 +6587,48 @@
      * @hide
      */
     public void setBasebandVersionForPhone(int phoneId, String version) {
+        setTelephonyProperty(phoneId, TelephonyProperties.PROPERTY_BASEBAND_VERSION, version);
+    }
+
+    /**
+     * Get baseband version for the default phone.
+     *
+     * @return baseband version.
+     * @hide
+     */
+    public String getBasebandVersion() {
+        int phoneId = getPhoneId();
+        return getBasebandVersionForPhone(phoneId);
+    }
+
+    /**
+     * Get baseband version for the default phone using the legacy approach.
+     * This change was added in P, to ensure backward compatiblity.
+     *
+     * @return baseband version.
+     * @hide
+     */
+    private String getBasebandVersionLegacy(int phoneId) {
         if (SubscriptionManager.isValidPhoneId(phoneId)) {
             String prop = TelephonyProperties.PROPERTY_BASEBAND_VERSION +
                     ((phoneId == 0) ? "" : Integer.toString(phoneId));
-            SystemProperties.set(prop, version);
+            return SystemProperties.get(prop);
         }
+        return null;
+    }
+
+    /**
+     * Get baseband version by phone id.
+     *
+     * @return baseband version.
+     * @hide
+     */
+    public String getBasebandVersionForPhone(int phoneId) {
+        String version = getBasebandVersionLegacy(phoneId);
+        if (version != null && !version.isEmpty()) {
+            setBasebandVersionForPhone(phoneId, version);
+        }
+        return getTelephonyProperty(phoneId, TelephonyProperties.PROPERTY_BASEBAND_VERSION, "");
     }
 
     /**
diff --git a/tests/ActivityManagerPerfTests/test-app/src/com/android/frameworks/perftests/amteststestapp/TestActivity.java b/tests/ActivityManagerPerfTests/test-app/src/com/android/frameworks/perftests/amteststestapp/TestActivity.java
index 7ea9ba3..1f06121 100644
--- a/tests/ActivityManagerPerfTests/test-app/src/com/android/frameworks/perftests/amteststestapp/TestActivity.java
+++ b/tests/ActivityManagerPerfTests/test-app/src/com/android/frameworks/perftests/amteststestapp/TestActivity.java
@@ -17,16 +17,19 @@
 package com.android.frameworks.perftests.amteststestapp;
 
 import android.app.Activity;
-import android.os.Bundle;
+import android.os.Looper;
+import android.os.MessageQueue;
 
 import com.android.frameworks.perftests.am.util.Constants;
 import com.android.frameworks.perftests.am.util.Utils;
 
 public class TestActivity extends Activity {
     @Override
-    protected void onCreate(Bundle savedInstanceState) {
-        super.onCreate(savedInstanceState);
-
-        Utils.sendTime(getIntent(), Constants.TYPE_ACTIVITY_CREATED);
+    protected void onResume() {
+        super.onResume();
+        Looper.myQueue().addIdleHandler(() -> {
+            Utils.sendTime(getIntent(), Constants.TYPE_TARGET_PACKAGE_START);
+            return false;
+        });
     }
 }
diff --git a/tests/ActivityManagerPerfTests/tests/src/com/android/frameworks/perftests/am/util/TargetPackageUtils.java b/tests/ActivityManagerPerfTests/tests/src/com/android/frameworks/perftests/am/util/TargetPackageUtils.java
index c867141..26a8e7b 100644
--- a/tests/ActivityManagerPerfTests/tests/src/com/android/frameworks/perftests/am/util/TargetPackageUtils.java
+++ b/tests/ActivityManagerPerfTests/tests/src/com/android/frameworks/perftests/am/util/TargetPackageUtils.java
@@ -62,7 +62,7 @@
             sleep();
         }
         // make sure Application has run
-        timeReceiver.getReceivedTimeNs(Constants.TYPE_ACTIVITY_CREATED);
+        timeReceiver.getReceivedTimeNs(Constants.TYPE_TARGET_PACKAGE_START);
         Utils.drainBroadcastQueue();
     }
 
diff --git a/tests/ActivityManagerPerfTests/utils/src/com/android/frameworks/perftests/am/util/Constants.java b/tests/ActivityManagerPerfTests/utils/src/com/android/frameworks/perftests/am/util/Constants.java
index 6528028..f35c2fd 100644
--- a/tests/ActivityManagerPerfTests/utils/src/com/android/frameworks/perftests/am/util/Constants.java
+++ b/tests/ActivityManagerPerfTests/utils/src/com/android/frameworks/perftests/am/util/Constants.java
@@ -17,7 +17,7 @@
 package com.android.frameworks.perftests.am.util;
 
 public class Constants {
-    public static final String TYPE_ACTIVITY_CREATED = "activity_create";
+    public static final String TYPE_TARGET_PACKAGE_START = "target_package_start";
     public static final String TYPE_BROADCAST_RECEIVE = "broadcast_receive";
 
     public static final String ACTION_BROADCAST_MANIFEST_RECEIVE =
diff --git a/tests/Internal/src/com/android/internal/colorextraction/types/TonalTest.java b/tests/Internal/src/com/android/internal/colorextraction/types/TonalTest.java
index d46fff4..a7d5ae8 100644
--- a/tests/Internal/src/com/android/internal/colorextraction/types/TonalTest.java
+++ b/tests/Internal/src/com/android/internal/colorextraction/types/TonalTest.java
@@ -99,6 +99,21 @@
     }
 
     @Test
+    public void tonal_rangeTest() {
+        Tonal.ConfigParser config = new Tonal.ConfigParser(InstrumentationRegistry.getContext());
+        for (Tonal.TonalPalette palette : config.getTonalPalettes()) {
+            assertTrue("minHue should be >= to 0.", palette.minHue >= 0);
+            assertTrue("maxHue should be <= to 360.", palette.maxHue <= 360);
+
+            assertTrue("S should be >= to 0.", palette.s[0] >= 0);
+            assertTrue("S should be <= to 1.", palette.s[1] <= 1);
+
+            assertTrue("L should be >= to 0.", palette.l[0] >= 0);
+            assertTrue("L should be <= to 1.", palette.l[1] <= 1);
+        }
+    }
+
+    @Test
     public void tonal_blacklistTest() {
         // Make sure that palette generation will fail.
         final Tonal tonal = new Tonal(InstrumentationRegistry.getContext());
diff --git a/tests/net/java/com/android/server/ConnectivityServiceTest.java b/tests/net/java/com/android/server/ConnectivityServiceTest.java
index 6e643a3..e7abede 100644
--- a/tests/net/java/com/android/server/ConnectivityServiceTest.java
+++ b/tests/net/java/com/android/server/ConnectivityServiceTest.java
@@ -318,6 +318,7 @@
 
     // This test has an inherent race condition in it, and cannot be enabled for continuous testing
     // or presubmit tests. It is kept for manual runs and documentation purposes.
+    @Ignore
     public void verifyThatNotWaitingForIdleCausesRaceConditions() {
         // Bring up a network that we can use to send messages to ConnectivityService.
         ConditionVariable cv = waitForConnectivityBroadcasts(1);
diff --git a/wifi/tests/src/android/net/wifi/WifiManagerTest.java b/wifi/tests/src/android/net/wifi/WifiManagerTest.java
index 71ca068..f3a78bd 100644
--- a/wifi/tests/src/android/net/wifi/WifiManagerTest.java
+++ b/wifi/tests/src/android/net/wifi/WifiManagerTest.java
@@ -611,6 +611,7 @@
     /**
      * Verify the watchLocalOnlyHotspot call goes to WifiServiceImpl.
      */
+    @Test
     public void testWatchLocalOnlyHotspot() throws Exception {
         TestLocalOnlyHotspotObserver observer = new TestLocalOnlyHotspotObserver();
 
diff --git a/wifi/tests/src/android/net/wifi/p2p/WifiP2pManagerTest.java b/wifi/tests/src/android/net/wifi/p2p/WifiP2pManagerTest.java
index 1e8382f..e8e4dc2 100644
--- a/wifi/tests/src/android/net/wifi/p2p/WifiP2pManagerTest.java
+++ b/wifi/tests/src/android/net/wifi/p2p/WifiP2pManagerTest.java
@@ -59,10 +59,10 @@
      */
     @Test
     public void testChannelFinalize() throws Exception {
-        WifiP2pManager.Channel channel = new WifiP2pManager.Channel(mContextMock,
-                mTestLooper.getLooper(), null, null, mDut);
-
-        leakageDetectorRule.assertUnreleasedResourceCount(channel, 1);
+        try (WifiP2pManager.Channel channel = new WifiP2pManager.Channel(mContextMock,
+                mTestLooper.getLooper(), null, null, mDut)) {
+            leakageDetectorRule.assertUnreleasedResourceCount(channel, 1);
+        }
     }
 
     /**