Merge "Add method to show/hide preference in base controller"
diff --git a/api/current.txt b/api/current.txt
index 0eb4715..12a3f07 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -31015,6 +31015,7 @@
   }
 
   public final class Debug {
+    method public static void attachJvmtiAgent(java.lang.String, java.lang.String) throws java.io.IOException;
     method public static deprecated void changeDebugPort(int);
     method public static void dumpHprofData(java.lang.String) throws java.io.IOException;
     method public static boolean dumpService(java.lang.String, java.io.FileDescriptor, java.lang.String[]);
@@ -44558,6 +44559,7 @@
     field public static final int STATE_DOZE_SUSPEND = 4; // 0x4
     field public static final int STATE_OFF = 1; // 0x1
     field public static final int STATE_ON = 2; // 0x2
+    field public static final int STATE_ON_SUSPEND = 6; // 0x6
     field public static final int STATE_UNKNOWN = 0; // 0x0
     field public static final int STATE_VR = 5; // 0x5
   }
diff --git a/api/system-current.txt b/api/system-current.txt
index 1747f23..0b34782 100644
--- a/api/system-current.txt
+++ b/api/system-current.txt
@@ -33756,6 +33756,7 @@
   }
 
   public final class Debug {
+    method public static void attachJvmtiAgent(java.lang.String, java.lang.String) throws java.io.IOException;
     method public static deprecated void changeDebugPort(int);
     method public static void dumpHprofData(java.lang.String) throws java.io.IOException;
     method public static boolean dumpService(java.lang.String, java.io.FileDescriptor, java.lang.String[]);
@@ -48293,6 +48294,7 @@
     field public static final int STATE_DOZE_SUSPEND = 4; // 0x4
     field public static final int STATE_OFF = 1; // 0x1
     field public static final int STATE_ON = 2; // 0x2
+    field public static final int STATE_ON_SUSPEND = 6; // 0x6
     field public static final int STATE_UNKNOWN = 0; // 0x0
     field public static final int STATE_VR = 5; // 0x5
   }
diff --git a/api/test-current.txt b/api/test-current.txt
index 5664391..4eba70c 100644
--- a/api/test-current.txt
+++ b/api/test-current.txt
@@ -31218,6 +31218,7 @@
   }
 
   public final class Debug {
+    method public static void attachJvmtiAgent(java.lang.String, java.lang.String) throws java.io.IOException;
     method public static deprecated void changeDebugPort(int);
     method public static void dumpHprofData(java.lang.String) throws java.io.IOException;
     method public static boolean dumpService(java.lang.String, java.io.FileDescriptor, java.lang.String[]);
@@ -45170,6 +45171,7 @@
     field public static final int STATE_DOZE_SUSPEND = 4; // 0x4
     field public static final int STATE_OFF = 1; // 0x1
     field public static final int STATE_ON = 2; // 0x2
+    field public static final int STATE_ON_SUSPEND = 6; // 0x6
     field public static final int STATE_UNKNOWN = 0; // 0x0
     field public static final int STATE_VR = 5; // 0x5
   }
diff --git a/cmds/appwidget/appwidget b/cmds/appwidget/appwidget
index 6105009..26ab173 100755
--- a/cmds/appwidget/appwidget
+++ b/cmds/appwidget/appwidget
@@ -1,3 +1,4 @@
+#!/system/bin/sh
 # Script to start "appwidget" on the device, which has a very rudimentary shell.
 base=/system
 export CLASSPATH=$base/framework/appwidget.jar
diff --git a/cmds/bmgr/bmgr b/cmds/bmgr/bmgr
index 6b4bbe2d..60b5833 100755
--- a/cmds/bmgr/bmgr
+++ b/cmds/bmgr/bmgr
@@ -1,3 +1,4 @@
+#!/system/bin/sh
 # Script to start "bmgr" on the device, which has a very rudimentary
 # shell.
 #
diff --git a/cmds/bu/bu b/cmds/bu/bu
index e8dbc31..e50b53d 100755
--- a/cmds/bu/bu
+++ b/cmds/bu/bu
@@ -1,3 +1,4 @@
+#!/system/bin/sh
 # Script to start "bu" on the device
 #
 base=/system
diff --git a/cmds/content/content b/cmds/content/content
index a8e056d..f1bfe17 100755
--- a/cmds/content/content
+++ b/cmds/content/content
@@ -1,3 +1,4 @@
+#!/system/bin/sh
 # Script to start "content" on the device, which has a very rudimentary shell.
 base=/system
 export CLASSPATH=$base/framework/content.jar
diff --git a/cmds/dpm/dpm b/cmds/dpm/dpm
index c2e5cbb..e0efdc1 100755
--- a/cmds/dpm/dpm
+++ b/cmds/dpm/dpm
@@ -1,3 +1,4 @@
+#!/system/bin/sh
 # Script to start "dpm" on the device
 #
 base=/system
diff --git a/cmds/ime/ime b/cmds/ime/ime
index 96c56d3..1a1fdd9 100755
--- a/cmds/ime/ime
+++ b/cmds/ime/ime
@@ -1,3 +1,4 @@
+#!/system/bin/sh
 # Script to start "pm" on the device, which has a very rudimentary
 # shell.
 #
diff --git a/cmds/input/input b/cmds/input/input
index 7f1a18e..54ab947 100755
--- a/cmds/input/input
+++ b/cmds/input/input
@@ -1,3 +1,4 @@
+#!/system/bin/sh
 # Script to start "input" on the device, which has a very rudimentary
 # shell.
 #
diff --git a/cmds/locksettings/locksettings b/cmds/locksettings/locksettings
index c963b23..0ef4fa9 100755
--- a/cmds/locksettings/locksettings
+++ b/cmds/locksettings/locksettings
@@ -1,3 +1,4 @@
+#!/system/bin/sh
 # Script to start "locksettings" on the device
 #
 base=/system
diff --git a/cmds/media/media b/cmds/media/media
index 1194442..5c0eb2f 100755
--- a/cmds/media/media
+++ b/cmds/media/media
@@ -1,3 +1,4 @@
+#!/system/bin/sh
 # Script to start "media_cmd" on the device, which has a very rudimentary
 # shell.
 #
diff --git a/cmds/pm/pm b/cmds/pm/pm
index 8183838..53f85b2 100755
--- a/cmds/pm/pm
+++ b/cmds/pm/pm
@@ -1,3 +1,4 @@
+#!/system/bin/sh
 # Script to start "pm" on the device, which has a very rudimentary
 # shell.
 #
diff --git a/cmds/requestsync/requestsync b/cmds/requestsync/requestsync
index 9315675..2d5d0e4 100755
--- a/cmds/requestsync/requestsync
+++ b/cmds/requestsync/requestsync
@@ -1,3 +1,4 @@
+#!/system/bin/sh
 # Script to start "requestsync" on the device
 #
 base=/system
diff --git a/cmds/sm/sm b/cmds/sm/sm
index 8fba007..4bc859e0 100755
--- a/cmds/sm/sm
+++ b/cmds/sm/sm
@@ -1,3 +1,4 @@
+#!/system/bin/sh
 # Script to start "sm" on the device, which has a very rudimentary
 # shell.
 #
diff --git a/cmds/sm/src/com/android/commands/sm/Sm.java b/cmds/sm/src/com/android/commands/sm/Sm.java
index a9a4118..699de94 100644
--- a/cmds/sm/src/com/android/commands/sm/Sm.java
+++ b/cmds/sm/src/com/android/commands/sm/Sm.java
@@ -20,6 +20,9 @@
 import static android.os.storage.StorageManager.PROP_HAS_ADOPTABLE;
 import static android.os.storage.StorageManager.PROP_VIRTUAL_DISK;
 
+import android.os.IBinder;
+import android.os.IVoldTaskListener;
+import android.os.PersistableBundle;
 import android.os.RemoteException;
 import android.os.ServiceManager;
 import android.os.SystemProperties;
@@ -29,6 +32,8 @@
 import android.os.storage.VolumeInfo;
 import android.util.Log;
 
+import java.util.concurrent.CompletableFuture;
+
 public final class Sm {
     private static final String TAG = "Sm";
 
@@ -221,9 +226,23 @@
         mSm.format(volId);
     }
 
-    public void runBenchmark() throws RemoteException {
+    public void runBenchmark() throws Exception {
         final String volId = nextArg();
-        mSm.benchmark(volId);
+        final CompletableFuture<PersistableBundle> result = new CompletableFuture<>();
+        mSm.benchmark(volId, new IVoldTaskListener.Stub() {
+            @Override
+            public void onStatus(int status, PersistableBundle extras) {
+                // Ignored
+            }
+
+            @Override
+            public void onFinished(int status, PersistableBundle extras) {
+                // Touch to unparcel
+                extras.size();
+                result.complete(extras);
+            }
+        });
+        System.out.println(result.get());
     }
 
     public void runForget() throws RemoteException {
@@ -235,8 +254,22 @@
         }
     }
 
-    public void runFstrim() throws RemoteException {
-        mSm.fstrim(0);
+    public void runFstrim() throws Exception {
+        final CompletableFuture<PersistableBundle> result = new CompletableFuture<>();
+        mSm.fstrim(0, new IVoldTaskListener.Stub() {
+            @Override
+            public void onStatus(int status, PersistableBundle extras) {
+                // Ignored
+            }
+
+            @Override
+            public void onFinished(int status, PersistableBundle extras) {
+                // Touch to unparcel
+                extras.size();
+                result.complete(extras);
+            }
+        });
+        System.out.println(result.get());
     }
 
     public void runSetVirtualDisk() throws RemoteException {
diff --git a/cmds/statsd/Android.mk b/cmds/statsd/Android.mk
index 86f353b..87d318b 100644
--- a/cmds/statsd/Android.mk
+++ b/cmds/statsd/Android.mk
@@ -157,8 +157,10 @@
     tests/LogReader_test.cpp \
     tests/MetricsManager_test.cpp \
     tests/UidMap_test.cpp \
-    tests/OringDurationTracker_test.cpp \
-    tests/MaxDurationTracker_test.cpp
+    tests/metrics/OringDurationTracker_test.cpp \
+    tests/metrics/MaxDurationTracker_test.cpp \
+    tests/metrics/CountMetricProducer_test.cpp \
+    tests/metrics/EventMetricProducer_test.cpp
 
 
 LOCAL_STATIC_LIBRARIES := \
diff --git a/cmds/statsd/src/StatsLogProcessor.cpp b/cmds/statsd/src/StatsLogProcessor.cpp
index c0cedb1..8910523 100644
--- a/cmds/statsd/src/StatsLogProcessor.cpp
+++ b/cmds/statsd/src/StatsLogProcessor.cpp
@@ -89,7 +89,7 @@
         *dest = m;
     }
     auto temp = mUidMap->getOutput(key);
-    report.set_allocated_uid_map(&temp);
+    report.mutable_uid_map()->Swap(&temp);
     return report;
 }
 
diff --git a/cmds/statsd/src/external/ResourcePowerManagerPuller.cpp b/cmds/statsd/src/external/ResourcePowerManagerPuller.cpp
index 38953f1..3608ee4 100644
--- a/cmds/statsd/src/external/ResourcePowerManagerPuller.cpp
+++ b/cmds/statsd/src/external/ResourcePowerManagerPuller.cpp
@@ -83,28 +83,31 @@
         return false;
     }
 
+    uint64_t timestamp = time(nullptr) * NS_PER_SEC;
+
     data->clear();
     Return<void> ret = gPowerHalV1_0->getPlatformLowPowerStats(
-            [&data](hidl_vec<PowerStatePlatformSleepState> states, Status status) {
+            [&data, timestamp](hidl_vec<PowerStatePlatformSleepState> states, Status status) {
 
                 if (status != Status::SUCCESS) return;
 
                 for (size_t i = 0; i < states.size(); i++) {
                     const PowerStatePlatformSleepState& state = states[i];
 
-                    auto statePtr = make_shared<LogEvent>(power_state_platform_sleep_state_tag);
+                    auto statePtr =
+                            make_shared<LogEvent>(power_state_platform_sleep_state_tag, timestamp);
                     auto elemList = statePtr->GetAndroidLogEventList();
                     *elemList << state.name;
                     *elemList << state.residencyInMsecSinceBoot;
                     *elemList << state.totalTransitions;
                     *elemList << state.supportedOnlyInSuspend;
+                    statePtr->init();
                     data->push_back(statePtr);
-
                     VLOG("powerstate: %s, %lld, %lld, %d", state.name.c_str(),
                          (long long)state.residencyInMsecSinceBoot,
                          (long long)state.totalTransitions, state.supportedOnlyInSuspend ? 1 : 0);
                     for (auto voter : state.voters) {
-                        auto voterPtr = make_shared<LogEvent>(power_state_voter_tag);
+                        auto voterPtr = make_shared<LogEvent>(power_state_voter_tag, timestamp);
                         auto elemList = voterPtr->GetAndroidLogEventList();
                         *elemList << state.name;
                         *elemList << voter.name;
@@ -128,7 +131,7 @@
             android::hardware::power::V1_1::IPower::castFrom(gPowerHalV1_0);
     if (gPowerHal_1_1 != nullptr) {
         ret = gPowerHal_1_1->getSubsystemLowPowerStats(
-                [&data](hidl_vec<PowerStateSubsystem> subsystems, Status status) {
+                [&data, timestamp](hidl_vec<PowerStateSubsystem> subsystems, Status status) {
 
                     if (status != Status::SUCCESS) return;
 
@@ -137,8 +140,8 @@
                             const PowerStateSubsystem& subsystem = subsystems[i];
                             for (size_t j = 0; j < subsystem.states.size(); j++) {
                                 const PowerStateSubsystemSleepState& state = subsystem.states[j];
-                                auto subsystemStatePtr =
-                                        make_shared<LogEvent>(power_state_subsystem_state_tag);
+                                auto subsystemStatePtr = make_shared<LogEvent>(
+                                        power_state_subsystem_state_tag, timestamp);
                                 auto elemList = subsystemStatePtr->GetAndroidLogEventList();
                                 *elemList << subsystem.name;
                                 *elemList << state.name;
@@ -146,6 +149,7 @@
                                 *elemList << state.totalTransitions;
                                 *elemList << state.lastEntryTimestampMs;
                                 *elemList << state.supportedOnlyInSuspend;
+                                subsystemStatePtr->init();
                                 data->push_back(subsystemStatePtr);
                                 VLOG("subsystemstate: %s, %s, %lld, %lld, %lld",
                                      subsystem.name.c_str(), state.name.c_str(),
diff --git a/cmds/statsd/src/external/StatsPullerManager.cpp b/cmds/statsd/src/external/StatsPullerManager.cpp
index 003b5c4..43543cc 100644
--- a/cmds/statsd/src/external/StatsPullerManager.cpp
+++ b/cmds/statsd/src/external/StatsPullerManager.cpp
@@ -30,11 +30,11 @@
 
 #include <iostream>
 
+using std::make_shared;
 using std::map;
+using std::shared_ptr;
 using std::string;
 using std::vector;
-using std::make_shared;
-using std::shared_ptr;
 
 namespace android {
 namespace os {
@@ -42,40 +42,35 @@
 
 StatsPullerManager::StatsPullerManager()
     : mCurrentPullingInterval(LONG_MAX), mPullStartTimeMs(get_pull_start_time_ms()) {
-    shared_ptr<StatsPuller> statsCompanionServicePuller = make_shared<StatsCompanionServicePuller>();
-    shared_ptr <StatsPuller>
-        resourcePowerManagerPuller = make_shared<ResourcePowerManagerPuller>();
+    shared_ptr<StatsPuller> statsCompanionServicePuller =
+            make_shared<StatsCompanionServicePuller>();
+    shared_ptr<StatsPuller> resourcePowerManagerPuller = make_shared<ResourcePowerManagerPuller>();
 
-    mPullers.insert({android::util::KERNEL_WAKELOCK_PULLED,
-                     statsCompanionServicePuller});
-    mPullers.insert({android::util::WIFI_BYTES_TRANSFERRED,
-                     statsCompanionServicePuller});
-    mPullers.insert({android::util::MOBILE_BYTES_TRANSFERRED,
-                     statsCompanionServicePuller});
-    mPullers.insert({android::util::WIFI_BYTES_TRANSFERRED_BY_FG_BG,
-                     statsCompanionServicePuller});
-    mPullers.insert({android::util::MOBILE_BYTES_TRANSFERRED_BY_FG_BG,
-                     statsCompanionServicePuller});
-    mPullers.insert({android::util::POWER_STATE_PLATFORM_SLEEP_STATE_PULLED,
-                     resourcePowerManagerPuller});
-    mPullers.insert({android::util::POWER_STATE_VOTER_PULLED,
-                     resourcePowerManagerPuller});
-    mPullers.insert({android::util::POWER_STATE_SUBSYSTEM_SLEEP_STATE_PULLED,
-                     resourcePowerManagerPuller});
+    mPullers.insert({android::util::KERNEL_WAKELOCK_PULLED, statsCompanionServicePuller});
+    mPullers.insert({android::util::WIFI_BYTES_TRANSFERRED, statsCompanionServicePuller});
+    mPullers.insert({android::util::MOBILE_BYTES_TRANSFERRED, statsCompanionServicePuller});
+    mPullers.insert({android::util::WIFI_BYTES_TRANSFERRED_BY_FG_BG, statsCompanionServicePuller});
+    mPullers.insert(
+            {android::util::MOBILE_BYTES_TRANSFERRED_BY_FG_BG, statsCompanionServicePuller});
+    mPullers.insert(
+            {android::util::POWER_STATE_PLATFORM_SLEEP_STATE_PULLED, resourcePowerManagerPuller});
+    mPullers.insert({android::util::POWER_STATE_VOTER_PULLED, resourcePowerManagerPuller});
+    mPullers.insert(
+            {android::util::POWER_STATE_SUBSYSTEM_SLEEP_STATE_PULLED, resourcePowerManagerPuller});
 
     mStatsCompanionService = StatsService::getStatsCompanionService();
 }
 
-        bool StatsPullerManager::Pull(int tagId, vector<shared_ptr<LogEvent>>* data) {
-            if (DEBUG) ALOGD("Initiating pulling %d", tagId);
+bool StatsPullerManager::Pull(int tagId, vector<shared_ptr<LogEvent>>* data) {
+    if (DEBUG) ALOGD("Initiating pulling %d", tagId);
 
-            if (mPullers.find(tagId) != mPullers.end()) {
-                return mPullers.find(tagId)->second->Pull(tagId, data);
-            } else {
-                ALOGD("Unknown tagId %d", tagId);
-                return false;  // Return early since we don't know what to pull.
-            }
-        }
+    if (mPullers.find(tagId) != mPullers.end()) {
+        return mPullers.find(tagId)->second->Pull(tagId, data);
+    } else {
+        ALOGD("Unknown tagId %d", tagId);
+        return false;  // Return early since we don't know what to pull.
+    }
+}
 
 StatsPullerManager& StatsPullerManager::GetInstance() {
     static StatsPullerManager instance;
@@ -91,7 +86,8 @@
     return time(nullptr) * 1000;
 }
 
-void StatsPullerManager::RegisterReceiver(int tagId, sp<PullDataReceiver> receiver, long intervalMs) {
+void StatsPullerManager::RegisterReceiver(int tagId, sp<PullDataReceiver> receiver,
+                                          long intervalMs) {
     AutoMutex _l(mReceiversLock);
     vector<ReceiverInfo>& receivers = mReceivers[tagId];
     for (auto it = receivers.begin(); it != receivers.end(); it++) {
@@ -143,8 +139,8 @@
             vector<pair<int, vector<ReceiverInfo*>>>();
     for (auto& pair : mReceivers) {
         vector<ReceiverInfo*> receivers = vector<ReceiverInfo*>();
-        if (pair.second.size() != 0){
-            for(auto& receiverInfo : pair.second) {
+        if (pair.second.size() != 0) {
+            for (auto& receiverInfo : pair.second) {
                 if (receiverInfo.timeInfo.first + receiverInfo.timeInfo.second > currentTimeMs) {
                     receivers.push_back(&receiverInfo);
                 }
diff --git a/cmds/statsd/src/logd/LogEvent.cpp b/cmds/statsd/src/logd/LogEvent.cpp
index 8220fcb..913b906 100644
--- a/cmds/statsd/src/logd/LogEvent.cpp
+++ b/cmds/statsd/src/logd/LogEvent.cpp
@@ -35,7 +35,7 @@
     init(msg.entry_v1.sec * NS_PER_SEC + msg.entry_v1.nsec, &mList);
 }
 
-LogEvent::LogEvent(int tag) : mList(tag) {
+LogEvent::LogEvent(int tag, uint64_t timestampNs) : mList(tag), mTimestampNs(timestampNs) {
 }
 
 LogEvent::~LogEvent() {
diff --git a/cmds/statsd/src/logd/LogEvent.h b/cmds/statsd/src/logd/LogEvent.h
index df75d9f..2984940 100644
--- a/cmds/statsd/src/logd/LogEvent.h
+++ b/cmds/statsd/src/logd/LogEvent.h
@@ -50,7 +50,7 @@
      * any of the values. This constructor is useful for unit-testing since we can't pass in an
      * android_log_event_list since there is no copy constructor or assignment operator available.
      */
-    explicit LogEvent(int tag);
+    explicit LogEvent(int tag, uint64_t timestampNs);
 
     ~LogEvent();
 
@@ -123,7 +123,9 @@
     vector<android_log_list_element> mElements;
     // Need a copy of the android_log_event_list so the strings are not cleared.
     android_log_event_list mList;
-    long mTimestampNs;
+
+    uint64_t mTimestampNs;
+
     int mTagId;
 };
 
diff --git a/cmds/statsd/src/metrics/CountMetricProducer.cpp b/cmds/statsd/src/metrics/CountMetricProducer.cpp
index 71cb771..100a7a4 100644
--- a/cmds/statsd/src/metrics/CountMetricProducer.cpp
+++ b/cmds/statsd/src/metrics/CountMetricProducer.cpp
@@ -58,10 +58,9 @@
 
 // TODO: add back AnomalyTracker.
 CountMetricProducer::CountMetricProducer(const CountMetric& metric, const int conditionIndex,
-                                         const sp<ConditionWizard>& wizard)
-    // TODO: Pass in the start time from MetricsManager, instead of calling time() here.
-    : MetricProducer((time(nullptr) * NANO_SECONDS_IN_A_SECOND), conditionIndex, wizard),
-      mMetric(metric) {
+                                         const sp<ConditionWizard>& wizard,
+                                         const uint64_t startTimeNs)
+    : MetricProducer(startTimeNs, conditionIndex, wizard), mMetric(metric) {
     // TODO: evaluate initial conditions. and set mConditionMet.
     if (metric.has_bucket() && metric.bucket().has_bucket_size_millis()) {
         mBucketSizeNs = metric.bucket().bucket_size_millis() * 1000 * 1000;
@@ -114,15 +113,17 @@
 }
 
 StatsLogReport CountMetricProducer::onDumpReport() {
-    long long endTime = time(nullptr) * NANO_SECONDS_IN_A_SECOND;
+    long long endTime = time(nullptr) * NS_PER_SEC;
 
     // Dump current bucket if it's stale.
     // If current bucket is still on-going, don't force dump current bucket.
     // In finish(), We can force dump current bucket.
     flushCounterIfNeeded(endTime);
+    VLOG("metric %lld dump report now...", mMetric.metric_id());
 
-    for (const auto& counter : mPastBucketProtos) {
+    for (const auto& counter : mPastBuckets) {
         const HashableDimensionKey& hashableKey = counter.first;
+        VLOG("  dimension key %s", hashableKey.c_str());
         auto it = mDimensionKeyMap.find(hashableKey);
         if (it == mDimensionKeyMap.end()) {
             ALOGE("Dimension key %s not found?!?! skip...", hashableKey.c_str());
@@ -147,20 +148,17 @@
         }
 
         // Then fill bucket_info (CountBucketInfo).
-        for (const auto& proto : counter.second) {
-            size_t bufferSize = proto->size();
-            char* buffer(new char[bufferSize]);
-            size_t pos = 0;
-            auto it = proto->data();
-            while (it.readBuffer() != NULL) {
-                size_t toRead = it.currentToRead();
-                std::memcpy(&buffer[pos], it.readBuffer(), toRead);
-                pos += toRead;
-                it.rp()->move(toRead);
-            }
-            mProto->write(FIELD_TYPE_MESSAGE | FIELD_ID_DIMENSION, buffer, bufferSize);
+        for (const auto& bucket : counter.second) {
+            long long bucketInfoToken = mProto->start(FIELD_TYPE_MESSAGE | FIELD_ID_BUCKET_INFO);
+            mProto->write(FIELD_TYPE_INT64 | FIELD_ID_START_BUCKET_NANOS,
+                          (long long)bucket.mBucketStartNs);
+            mProto->write(FIELD_TYPE_INT64 | FIELD_ID_END_BUCKET_NANOS,
+                          (long long)bucket.mBucketEndNs);
+            mProto->write(FIELD_TYPE_INT64 | FIELD_ID_COUNT, (long long)bucket.mCount);
+            mProto->end(bucketInfoToken);
+            VLOG("\t bucket [%lld - %lld] count: %lld", (long long)bucket.mBucketStartNs,
+                 (long long)bucket.mBucketEndNs, (long long)bucket.mCount);
         }
-
         mProto->end(wrapperToken);
     }
 
@@ -169,8 +167,8 @@
                   (long long)mCurrentBucketStartTimeNs);
 
     size_t bufferSize = mProto->size();
-    VLOG("metric %lld dump report now...", mMetric.metric_id());
     std::unique_ptr<uint8_t[]> buffer(new uint8_t[bufferSize]);
+
     size_t pos = 0;
     auto it = mProto->data();
     while (it.readBuffer() != NULL) {
@@ -181,7 +179,7 @@
     }
 
     startNewProtoOutputStream(endTime);
-    mPastBucketProtos.clear();
+    mPastBuckets.clear();
     mByteSize = 0;
 
     // TODO: Once we migrate all MetricProducers to use ProtoOutputStream, we should return this:
@@ -239,20 +237,16 @@
     // adjust the bucket start time
     int64_t numBucketsForward = (eventTimeNs - mCurrentBucketStartTimeNs) / mBucketSizeNs;
 
+    CountBucket info;
+    info.mBucketStartNs = mCurrentBucketStartTimeNs;
+    info.mBucketEndNs = mCurrentBucketStartTimeNs + mBucketSizeNs;
     for (const auto& counter : mCurrentSlicedCounter) {
-        unique_ptr<ProtoOutputStream> proto = make_unique<ProtoOutputStream>();
-        proto->write(FIELD_TYPE_INT64 | FIELD_ID_START_BUCKET_NANOS,
-                     (long long)mCurrentBucketStartTimeNs);
-        proto->write(FIELD_TYPE_INT64 | FIELD_ID_END_BUCKET_NANOS,
-                      (long long)mCurrentBucketStartTimeNs + mBucketSizeNs);
-        proto->write(FIELD_TYPE_INT64 | FIELD_ID_COUNT, (long long)counter.second);
-
-        auto& bucketList = mPastBucketProtos[counter.first];
-        bucketList.push_back(std::move(proto));
-        mByteSize += proto->size();
-
+        info.mCount = counter.second;
+        auto& bucketList = mPastBuckets[counter.first];
+        bucketList.push_back(info);
         VLOG("metric %lld, dump key value: %s -> %d", mMetric.metric_id(), counter.first.c_str(),
              counter.second);
+        mByteSize += sizeof(info);
     }
 
     // TODO: Re-add anomaly detection (similar to):
diff --git a/cmds/statsd/src/metrics/CountMetricProducer.h b/cmds/statsd/src/metrics/CountMetricProducer.h
index 473a4ba..3bfc724 100644
--- a/cmds/statsd/src/metrics/CountMetricProducer.h
+++ b/cmds/statsd/src/metrics/CountMetricProducer.h
@@ -20,6 +20,7 @@
 #include <unordered_map>
 
 #include <android/util/ProtoOutputStream.h>
+#include <gtest/gtest_prod.h>
 #include "../condition/ConditionTracker.h"
 #include "../matchers/matcher_util.h"
 #include "CountAnomalyTracker.h"
@@ -34,11 +35,17 @@
 namespace os {
 namespace statsd {
 
+struct CountBucket {
+    int64_t mBucketStartNs;
+    int64_t mBucketEndNs;
+    int64_t mCount;
+};
+
 class CountMetricProducer : public MetricProducer {
 public:
     // TODO: Pass in the start time from MetricsManager, it should be consistent for all metrics.
     CountMetricProducer(const CountMetric& countMetric, const int conditionIndex,
-                        const sp<ConditionWizard>& wizard);
+                        const sp<ConditionWizard>& wizard, const uint64_t startTimeNs);
 
     virtual ~CountMetricProducer();
 
@@ -66,8 +73,7 @@
 private:
     const CountMetric mMetric;
 
-    std::unordered_map<HashableDimensionKey,
-        std::vector<unique_ptr<android::util::ProtoOutputStream>>> mPastBucketProtos;
+    std::unordered_map<HashableDimensionKey, std::vector<CountBucket>> mPastBuckets;
 
     size_t mByteSize;
 
@@ -83,6 +89,10 @@
     long long mProtoToken;
 
     void startNewProtoOutputStream(long long timestamp);
+
+    FRIEND_TEST(CountMetricProducerTest, TestNonDimensionalEvents);
+    FRIEND_TEST(CountMetricProducerTest, TestEventsWithNonSlicedCondition);
+    FRIEND_TEST(CountMetricProducerTest, TestEventsWithSlicedCondition);
 };
 
 }  // namespace statsd
diff --git a/cmds/statsd/src/metrics/DurationMetricProducer.cpp b/cmds/statsd/src/metrics/DurationMetricProducer.cpp
index 340f503..09132bf 100644
--- a/cmds/statsd/src/metrics/DurationMetricProducer.cpp
+++ b/cmds/statsd/src/metrics/DurationMetricProducer.cpp
@@ -35,9 +35,9 @@
                                                const int conditionIndex, const size_t startIndex,
                                                const size_t stopIndex, const size_t stopAllIndex,
                                                const sp<ConditionWizard>& wizard,
-                                               const vector<KeyMatcher>& internalDimension)
-    // TODO: Pass in the start time from MetricsManager, instead of calling time() here.
-    : MetricProducer(time(nullptr) * NANO_SECONDS_IN_A_SECOND, conditionIndex, wizard),
+                                               const vector<KeyMatcher>& internalDimension,
+                                               const uint64_t startTimeNs)
+    : MetricProducer(startTimeNs, conditionIndex, wizard),
       mMetric(metric),
       mStartIndex(startIndex),
       mStopIndex(stopIndex),
@@ -131,7 +131,7 @@
     // Dump current bucket if it's stale.
     // If current bucket is still on-going, don't force dump current bucket.
     // In finish(), We can force dump current bucket.
-    flushIfNeeded(time(nullptr) * NANO_SECONDS_IN_A_SECOND);
+    flushIfNeeded(time(nullptr) * NS_PER_SEC);
     report.set_end_report_nanos(mCurrentBucketStartTimeNs);
 
     StatsLogReport_DurationMetricDataWrapper* wrapper = report.mutable_duration_metrics();
@@ -195,10 +195,10 @@
 }
 
 size_t DurationMetricProducer::byteSize() {
-// TODO: return actual proto size when ProtoOutputStream is ready for use for
-// DurationMetricsProducer.
-//    return mProto->size();
-  return 0;
+    // TODO: return actual proto size when ProtoOutputStream is ready for use for
+    // DurationMetricsProducer.
+    //    return mProto->size();
+    return 0;
 }
 
 }  // namespace statsd
diff --git a/cmds/statsd/src/metrics/DurationMetricProducer.h b/cmds/statsd/src/metrics/DurationMetricProducer.h
index febf25d..12ff58e 100644
--- a/cmds/statsd/src/metrics/DurationMetricProducer.h
+++ b/cmds/statsd/src/metrics/DurationMetricProducer.h
@@ -40,7 +40,7 @@
     DurationMetricProducer(const DurationMetric& durationMetric, const int conditionIndex,
                            const size_t startIndex, const size_t stopIndex,
                            const size_t stopAllIndex, const sp<ConditionWizard>& wizard,
-                           const vector<KeyMatcher>& internalDimension);
+                           const vector<KeyMatcher>& internalDimension, const uint64_t startTimeNs);
 
     virtual ~DurationMetricProducer();
 
diff --git a/cmds/statsd/src/metrics/EventMetricProducer.cpp b/cmds/statsd/src/metrics/EventMetricProducer.cpp
index cbae1d3..677ae38 100644
--- a/cmds/statsd/src/metrics/EventMetricProducer.cpp
+++ b/cmds/statsd/src/metrics/EventMetricProducer.cpp
@@ -46,10 +46,9 @@
 const int FIELD_ID_STATS_EVENTS = 2;
 
 EventMetricProducer::EventMetricProducer(const EventMetric& metric, const int conditionIndex,
-                                         const sp<ConditionWizard>& wizard)
-    // TODO: Pass in the start time from MetricsManager, instead of calling time() here.
-    : MetricProducer((time(nullptr) * NANO_SECONDS_IN_A_SECOND), conditionIndex, wizard),
-      mMetric(metric) {
+                                         const sp<ConditionWizard>& wizard,
+                                         const uint64_t startTimeNs)
+    : MetricProducer(startTimeNs, conditionIndex, wizard), mMetric(metric) {
     if (metric.links().size() > 0) {
         mConditionLinks.insert(mConditionLinks.begin(), metric.links().begin(),
                                metric.links().end());
@@ -82,7 +81,7 @@
 }
 
 StatsLogReport EventMetricProducer::onDumpReport() {
-    long long endTime = time(nullptr) * NANO_SECONDS_IN_A_SECOND;
+    long long endTime = time(nullptr) * NS_PER_SEC;
     mProto->end(mProtoToken);
     mProto->write(FIELD_TYPE_INT64 | FIELD_ID_END_REPORT_NANOS, endTime);
 
@@ -114,7 +113,6 @@
         const size_t matcherIndex, const HashableDimensionKey& eventKey,
         const std::map<std::string, HashableDimensionKey>& conditionKey, bool condition,
         const LogEvent& event, bool scheduledPull) {
-
     if (!condition) {
         return;
     }
@@ -128,7 +126,7 @@
 }
 
 size_t EventMetricProducer::byteSize() {
-  return mProto->size();
+    return mProto->size();
 }
 
 }  // namespace statsd
diff --git a/cmds/statsd/src/metrics/EventMetricProducer.h b/cmds/statsd/src/metrics/EventMetricProducer.h
index 7dd0e38..0fc2b5b 100644
--- a/cmds/statsd/src/metrics/EventMetricProducer.h
+++ b/cmds/statsd/src/metrics/EventMetricProducer.h
@@ -20,6 +20,7 @@
 #include <unordered_map>
 
 #include <android/util/ProtoOutputStream.h>
+
 #include "../condition/ConditionTracker.h"
 #include "../matchers/matcher_util.h"
 #include "MetricProducer.h"
@@ -35,13 +36,14 @@
 public:
     // TODO: Pass in the start time from MetricsManager, it should be consistent for all metrics.
     EventMetricProducer(const EventMetric& eventMetric, const int conditionIndex,
-                        const sp<ConditionWizard>& wizard);
+                        const sp<ConditionWizard>& wizard, const uint64_t startTimeNs);
 
     virtual ~EventMetricProducer();
 
     void onMatchedLogEventInternal(const size_t matcherIndex, const HashableDimensionKey& eventKey,
                                    const std::map<std::string, HashableDimensionKey>& conditionKey,
-                                   bool condition, const LogEvent& event, bool scheduledPull) override;
+                                   bool condition, const LogEvent& event,
+                                   bool scheduledPull) override;
 
     void onConditionChanged(const bool conditionMet, const uint64_t eventTime) override;
 
diff --git a/cmds/statsd/src/metrics/GaugeMetricProducer.cpp b/cmds/statsd/src/metrics/GaugeMetricProducer.cpp
index bd638b4..285c8f4 100644
--- a/cmds/statsd/src/metrics/GaugeMetricProducer.cpp
+++ b/cmds/statsd/src/metrics/GaugeMetricProducer.cpp
@@ -35,7 +35,7 @@
 
 GaugeMetricProducer::GaugeMetricProducer(const GaugeMetric& metric, const int conditionIndex,
                                          const sp<ConditionWizard>& wizard, const int pullTagId)
-    : MetricProducer((time(nullptr) * NANO_SECONDS_IN_A_SECOND), conditionIndex, wizard),
+    : MetricProducer((time(nullptr) * NS_PER_SEC), conditionIndex, wizard),
       mMetric(metric),
       mPullTagId(pullTagId) {
     if (metric.has_bucket() && metric.bucket().has_bucket_size_millis()) {
@@ -94,7 +94,7 @@
     // Dump current bucket if it's stale.
     // If current bucket is still on-going, don't force dump current bucket.
     // In finish(), We can force dump current bucket.
-    flushGaugeIfNeededLocked(time(nullptr) * NANO_SECONDS_IN_A_SECOND);
+    flushGaugeIfNeededLocked(time(nullptr) * NS_PER_SEC);
     report.set_end_report_nanos(mCurrentBucketStartTimeNs);
 
     StatsLogReport_GaugeMetricDataWrapper* wrapper = report.mutable_gauge_metrics();
diff --git a/cmds/statsd/src/metrics/MetricProducer.h b/cmds/statsd/src/metrics/MetricProducer.h
index 3b117ec..6ba726f4 100644
--- a/cmds/statsd/src/metrics/MetricProducer.h
+++ b/cmds/statsd/src/metrics/MetricProducer.h
@@ -40,6 +40,7 @@
         : mStartTimeNs(startTimeNs),
           mCurrentBucketStartTimeNs(startTimeNs),
           mCondition(conditionIndex >= 0 ? false : true),
+          mConditionSliced(false),
           mWizard(wizard),
           mConditionTrackerIndex(conditionIndex) {
         // reuse the same map for non-sliced metrics too. this way, we avoid too many if-else.
diff --git a/cmds/statsd/src/metrics/MetricsManager.cpp b/cmds/statsd/src/metrics/MetricsManager.cpp
index 9b708dd6..80b325f 100644
--- a/cmds/statsd/src/metrics/MetricsManager.cpp
+++ b/cmds/statsd/src/metrics/MetricsManager.cpp
@@ -15,17 +15,17 @@
  */
 #define DEBUG true  // STOPSHIP if true
 #include "Log.h"
-
 #include "MetricsManager.h"
-#include <log/logprint.h>
-#include "../condition/CombinationConditionTracker.h"
-#include "../condition/SimpleConditionTracker.h"
-#include "../matchers/CombinationLogMatchingTracker.h"
-#include "../matchers/SimpleLogMatchingTracker.h"
+
 #include "CountMetricProducer.h"
+#include "condition/CombinationConditionTracker.h"
+#include "condition/SimpleConditionTracker.h"
+#include "matchers/CombinationLogMatchingTracker.h"
+#include "matchers/SimpleLogMatchingTracker.h"
 #include "metrics_manager_util.h"
 #include "stats_util.h"
 
+#include <log/logprint.h>
 using std::make_unique;
 using std::set;
 using std::string;
diff --git a/cmds/statsd/src/metrics/MetricsManager.h b/cmds/statsd/src/metrics/MetricsManager.h
index 44cd637..63e2c33 100644
--- a/cmds/statsd/src/metrics/MetricsManager.h
+++ b/cmds/statsd/src/metrics/MetricsManager.h
@@ -96,4 +96,3 @@
 }  // namespace statsd
 }  // namespace os
 }  // namespace android
-
diff --git a/cmds/statsd/src/metrics/ValueMetricProducer.cpp b/cmds/statsd/src/metrics/ValueMetricProducer.cpp
index ca33371..07a078f 100644
--- a/cmds/statsd/src/metrics/ValueMetricProducer.cpp
+++ b/cmds/statsd/src/metrics/ValueMetricProducer.cpp
@@ -23,12 +23,12 @@
 #include <limits.h>
 #include <stdlib.h>
 
-using std::map;
-using std::unordered_map;
 using std::list;
 using std::make_shared;
+using std::map;
 using std::shared_ptr;
 using std::unique_ptr;
+using std::unordered_map;
 
 namespace android {
 namespace os {
@@ -36,51 +36,50 @@
 
 // ValueMetric has a minimum bucket size of 10min so that we don't pull too frequently
 ValueMetricProducer::ValueMetricProducer(const ValueMetric& metric, const int conditionIndex,
-                                         const sp<ConditionWizard>& wizard, const int pullTagId)
-    : MetricProducer((time(nullptr) / 600 * 600 * NANO_SECONDS_IN_A_SECOND), conditionIndex,
-                     wizard),
-      mMetric(metric),
-      mPullTagId(pullTagId) {
-  // TODO: valuemetric for pushed events may need unlimited bucket length
-  mBucketSizeNs = mMetric.bucket().bucket_size_millis() * 1000 * 1000;
+                                         const sp<ConditionWizard>& wizard, const int pullTagId,
+                                         const uint64_t startTimeNs)
+    : MetricProducer(startTimeNs, conditionIndex, wizard), mMetric(metric), mPullTagId(pullTagId) {
+    // TODO: valuemetric for pushed events may need unlimited bucket length
+    mBucketSizeNs = mMetric.bucket().bucket_size_millis() * 1000 * 1000;
 
-  mDimension.insert(mDimension.begin(), metric.dimension().begin(), metric.dimension().end());
+    mDimension.insert(mDimension.begin(), metric.dimension().begin(), metric.dimension().end());
 
-  if (metric.links().size() > 0) {
-    mConditionLinks.insert(mConditionLinks.begin(), metric.links().begin(),
-                           metric.links().end());
-    mConditionSliced = true;
-  }
+    if (metric.links().size() > 0) {
+        mConditionLinks.insert(mConditionLinks.begin(), metric.links().begin(),
+                               metric.links().end());
+        mConditionSliced = true;
+    }
 
-  if (!metric.has_condition() && mPullTagId != -1) {
-    mStatsPullerManager.RegisterReceiver(mPullTagId, this, metric.bucket().bucket_size_millis());
-  }
+    if (!metric.has_condition() && mPullTagId != -1) {
+        mStatsPullerManager.RegisterReceiver(mPullTagId, this,
+                                             metric.bucket().bucket_size_millis());
+    }
 
-  VLOG("value metric %lld created. bucket size %lld start_time: %lld", metric.metric_id(),
-       (long long)mBucketSizeNs, (long long)mStartTimeNs);
+    VLOG("value metric %lld created. bucket size %lld start_time: %lld", metric.metric_id(),
+         (long long)mBucketSizeNs, (long long)mStartTimeNs);
 }
 
 ValueMetricProducer::~ValueMetricProducer() {
-  VLOG("~ValueMetricProducer() called");
+    VLOG("~ValueMetricProducer() called");
 }
 
 void ValueMetricProducer::finish() {
-  // TODO: write the StatsLogReport to dropbox using
-  // DropboxWriter.
+    // TODO: write the StatsLogReport to dropbox using
+    // DropboxWriter.
 }
 
 static void addSlicedCounterToReport(StatsLogReport_ValueMetricDataWrapper& wrapper,
                                      const vector<KeyValuePair>& key,
                                      const vector<ValueBucketInfo>& buckets) {
-  ValueMetricData* data = wrapper.add_data();
-  for (const auto& kv : key) {
-    data->add_dimension()->CopyFrom(kv);
-  }
-  for (const auto& bucket : buckets) {
-    data->add_bucket_info()->CopyFrom(bucket);
-    VLOG("\t bucket [%lld - %lld] value: %lld", bucket.start_bucket_nanos(),
-         bucket.end_bucket_nanos(), bucket.value());
-  }
+    ValueMetricData* data = wrapper.add_data();
+    for (const auto& kv : key) {
+        data->add_dimension()->CopyFrom(kv);
+    }
+    for (const auto& bucket : buckets) {
+        data->add_bucket_info()->CopyFrom(bucket);
+        VLOG("\t bucket [%lld - %lld] value: %lld", bucket.start_bucket_nanos(),
+             bucket.end_bucket_nanos(), bucket.value());
+    }
 }
 
 void ValueMetricProducer::onSlicedConditionMayChange(const uint64_t eventTime) {
@@ -88,33 +87,28 @@
 }
 
 StatsLogReport ValueMetricProducer::onDumpReport() {
-  VLOG("metric %lld dump report now...", mMetric.metric_id());
+    VLOG("metric %lld dump report now...", mMetric.metric_id());
 
-  StatsLogReport report;
-  report.set_metric_id(mMetric.metric_id());
-  report.set_start_report_nanos(mStartTimeNs);
+    StatsLogReport report;
+    report.set_metric_id(mMetric.metric_id());
+    report.set_start_report_nanos(mStartTimeNs);
+    report.set_end_report_nanos(mCurrentBucketStartTimeNs);
 
-  // Dump current bucket if it's stale.
-  // If current bucket is still on-going, don't force dump current bucket.
-  // In finish(), We can force dump current bucket.
-  //    flush_if_needed(time(nullptr) * NANO_SECONDS_IN_A_SECOND);
-  report.set_end_report_nanos(mCurrentBucketStartTimeNs);
+    StatsLogReport_ValueMetricDataWrapper* wrapper = report.mutable_value_metrics();
 
-  StatsLogReport_ValueMetricDataWrapper* wrapper = report.mutable_value_metrics();
+    for (const auto& pair : mPastBuckets) {
+        const HashableDimensionKey& hashableKey = pair.first;
+        auto it = mDimensionKeyMap.find(hashableKey);
+        if (it == mDimensionKeyMap.end()) {
+            ALOGE("Dimension key %s not found?!?! skip...", hashableKey.c_str());
+            continue;
+        }
 
-  for (const auto& pair : mPastBuckets) {
-    const HashableDimensionKey& hashableKey = pair.first;
-    auto it = mDimensionKeyMap.find(hashableKey);
-    if (it == mDimensionKeyMap.end()) {
-      ALOGE("Dimension key %s not found?!?! skip...", hashableKey.c_str());
-      continue;
+        VLOG("  dimension key %s", hashableKey.c_str());
+        addSlicedCounterToReport(*wrapper, it->second, pair.second);
     }
-
-    VLOG("  dimension key %s", hashableKey.c_str());
-    addSlicedCounterToReport(*wrapper, it->second, pair.second);
-  }
-  return report;
-  // TODO: Clear mPastBuckets, mDimensionKeyMap once the report is dumped.
+    return report;
+    // TODO: Clear mPastBuckets, mDimensionKeyMap once the report is dumped.
 }
 
 void ValueMetricProducer::onConditionChanged(const bool condition, const uint64_t eventTime) {
@@ -158,50 +152,50 @@
 }
 
 void ValueMetricProducer::onMatchedLogEventInternal(
-    const size_t matcherIndex, const HashableDimensionKey& eventKey,
-    const map<string, HashableDimensionKey>& conditionKey, bool condition,
-    const LogEvent& event, bool scheduledPull) {
-  uint64_t eventTimeNs = event.GetTimestampNs();
-  if (eventTimeNs < mCurrentBucketStartTimeNs) {
-      VLOG("Skip event due to late arrival: %lld vs %lld", (long long)eventTimeNs,
-           (long long)mCurrentBucketStartTimeNs);
-      return;
-  }
-
-  Interval& interval = mCurrentSlicedBucket[eventKey];
-
-  long value = get_value(event);
-
-  if (scheduledPull) {
-    if (interval.raw.size() > 0) {
-      interval.raw.back().second = value;
-    } else {
-      interval.raw.push_back(std::make_pair(value, value));
+        const size_t matcherIndex, const HashableDimensionKey& eventKey,
+        const map<string, HashableDimensionKey>& conditionKey, bool condition,
+        const LogEvent& event, bool scheduledPull) {
+    uint64_t eventTimeNs = event.GetTimestampNs();
+    if (eventTimeNs < mCurrentBucketStartTimeNs) {
+        VLOG("Skip event due to late arrival: %lld vs %lld", (long long)eventTimeNs,
+             (long long)mCurrentBucketStartTimeNs);
+        return;
     }
-    mNextSlicedBucket[eventKey].raw[0].first = value;
-  } else {
-    if (mCondition == ConditionState::kTrue) {
-      interval.raw.push_back(std::make_pair(value, 0));
+
+    Interval& interval = mCurrentSlicedBucket[eventKey];
+
+    long value = get_value(event);
+
+    if (scheduledPull) {
+        if (interval.raw.size() > 0) {
+            interval.raw.back().second = value;
+        } else {
+            interval.raw.push_back(std::make_pair(value, value));
+        }
+        mNextSlicedBucket[eventKey].raw[0].first = value;
     } else {
-      if (interval.raw.size() != 0) {
-        interval.raw.back().second = value;
-      }
+        if (mCondition == ConditionState::kTrue) {
+            interval.raw.push_back(std::make_pair(value, 0));
+        } else {
+            if (interval.raw.size() != 0) {
+                interval.raw.back().second = value;
+            }
+        }
     }
-  }
-  if (mPullTagId == -1) {
-      flush_if_needed(eventTimeNs);
-  }
+    if (mPullTagId == -1) {
+        flush_if_needed(eventTimeNs);
+    }
 }
 
 long ValueMetricProducer::get_value(const LogEvent& event) {
-  status_t err = NO_ERROR;
-  long val = event.GetLong(mMetric.value_field(), &err);
-  if (err == NO_ERROR) {
-    return val;
-  } else {
-    VLOG("Can't find value in message.");
-    return 0;
-  }
+    status_t err = NO_ERROR;
+    long val = event.GetLong(mMetric.value_field(), &err);
+    if (err == NO_ERROR) {
+        return val;
+    } else {
+        VLOG("Can't find value in message.");
+        return 0;
+    }
 }
 
 void ValueMetricProducer::flush_if_needed(const uint64_t eventTimeNs) {
@@ -218,22 +212,22 @@
     info.set_end_bucket_nanos(mCurrentBucketStartTimeNs + mBucketSizeNs);
 
     for (const auto& slice : mCurrentSlicedBucket) {
-      long value = 0;
-      for (const auto& pair : slice.second.raw) {
-        value += pair.second - pair.first;
-      }
-      info.set_value(value);
-      VLOG(" %s, %ld", slice.first.c_str(), value);
-      // it will auto create new vector of ValuebucketInfo if the key is not found.
-      auto& bucketList = mPastBuckets[slice.first];
-      bucketList.push_back(info);
+        long value = 0;
+        for (const auto& pair : slice.second.raw) {
+            value += pair.second - pair.first;
+        }
+        info.set_value(value);
+        VLOG(" %s, %ld", slice.first.c_str(), value);
+        // it will auto create new vector of ValuebucketInfo if the key is not found.
+        auto& bucketList = mPastBuckets[slice.first];
+        bucketList.push_back(info);
     }
 
     // Reset counters
     mCurrentSlicedBucket.swap(mNextSlicedBucket);
     mNextSlicedBucket.clear();
     int64_t numBucketsForward = (eventTimeNs - mCurrentBucketStartTimeNs) / mBucketSizeNs;
-    if (numBucketsForward >1) {
+    if (numBucketsForward > 1) {
         VLOG("Skipping forward %lld buckets", (long long)numBucketsForward);
     }
     mCurrentBucketStartTimeNs = mCurrentBucketStartTimeNs + numBucketsForward * mBucketSizeNs;
@@ -243,4 +237,4 @@
 
 }  // namespace statsd
 }  // namespace os
-}  // namespace android
\ No newline at end of file
+}  // namespace android
diff --git a/cmds/statsd/src/metrics/ValueMetricProducer.h b/cmds/statsd/src/metrics/ValueMetricProducer.h
index 8653981..548cd44 100644
--- a/cmds/statsd/src/metrics/ValueMetricProducer.h
+++ b/cmds/statsd/src/metrics/ValueMetricProducer.h
@@ -33,7 +33,8 @@
 class ValueMetricProducer : public virtual MetricProducer, public virtual PullDataReceiver {
 public:
     ValueMetricProducer(const ValueMetric& valueMetric, const int conditionIndex,
-                        const sp<ConditionWizard>& wizard, const int pullTagId);
+                        const sp<ConditionWizard>& wizard, const int pullTagId,
+                        const uint64_t startTimeNs);
 
     virtual ~ValueMetricProducer();
 
@@ -47,7 +48,9 @@
 
     void onDataPulled(const std::vector<std::shared_ptr<LogEvent>>& data) override;
     // TODO: Implement this later.
-    size_t byteSize() override{return 0;};
+    size_t byteSize() override {
+        return 0;
+    };
 
     // TODO: Implement this later.
     virtual void notifyAppUpgrade(const string& apk, const int uid, const int version) override{};
diff --git a/cmds/statsd/src/metrics/metrics_manager_util.cpp b/cmds/statsd/src/metrics/metrics_manager_util.cpp
index a0fdcdc..ca9cdfb 100644
--- a/cmds/statsd/src/metrics/metrics_manager_util.cpp
+++ b/cmds/statsd/src/metrics/metrics_manager_util.cpp
@@ -192,10 +192,11 @@
                  unordered_map<int, std::vector<int>>& conditionToMetricMap,
                  unordered_map<int, std::vector<int>>& trackerToMetricMap) {
     sp<ConditionWizard> wizard = new ConditionWizard(allConditionTrackers);
-    const int allMetricsCount =
-            config.count_metric_size() + config.duration_metric_size() + config.event_metric_size() + config.value_metric_size();
+    const int allMetricsCount = config.count_metric_size() + config.duration_metric_size() +
+                                config.event_metric_size() + config.value_metric_size();
     allMetricProducers.reserve(allMetricsCount);
     StatsPullerManager& statsPullerManager = StatsPullerManager::GetInstance();
+    uint64_t startTimeNs = time(nullptr) * NS_PER_SEC;
 
     // Build MetricProducers for each metric defined in config.
     // build CountMetricProducer
@@ -221,7 +222,8 @@
                                        conditionToMetricMap);
         }
 
-        sp<MetricProducer> countProducer = new CountMetricProducer(metric, conditionIndex, wizard);
+        sp<MetricProducer> countProducer =
+                new CountMetricProducer(metric, conditionIndex, wizard, startTimeNs);
         allMetricProducers.push_back(countProducer);
     }
 
@@ -282,7 +284,7 @@
 
         sp<MetricProducer> durationMetric = new DurationMetricProducer(
                 metric, conditionIndex, trackerIndices[0], trackerIndices[1], trackerIndices[2],
-                wizard, internalDimension);
+                wizard, internalDimension, startTimeNs);
 
         allMetricProducers.push_back(durationMetric);
     }
@@ -308,7 +310,9 @@
                                        conditionToMetricMap);
         }
 
-        sp<MetricProducer> eventMetric = new EventMetricProducer(metric, conditionIndex, wizard);
+        sp<MetricProducer> eventMetric =
+                new EventMetricProducer(metric, conditionIndex, wizard, startTimeNs);
+
         allMetricProducers.push_back(eventMetric);
     }
 
@@ -348,7 +352,7 @@
         }
 
         sp<MetricProducer> valueProducer =
-                new ValueMetricProducer(metric, conditionIndex, wizard, pullTagId);
+                new ValueMetricProducer(metric, conditionIndex, wizard, pullTagId, startTimeNs);
         allMetricProducers.push_back(valueProducer);
     }
 
diff --git a/cmds/statsd/src/stats_util.h b/cmds/statsd/src/stats_util.h
index d3d7e37..a9507bf 100644
--- a/cmds/statsd/src/stats_util.h
+++ b/cmds/statsd/src/stats_util.h
@@ -28,7 +28,6 @@
 
 #define DEFAULT_DIMENSION_KEY ""
 #define MATCHER_NOT_FOUND -2
-#define NANO_SECONDS_IN_A_SECOND (1000 * 1000 * 1000)
 
 typedef std::string HashableDimensionKey;
 
diff --git a/cmds/statsd/tests/LogEntryMatcher_test.cpp b/cmds/statsd/tests/LogEntryMatcher_test.cpp
index fdfe8ef..d0898b0 100644
--- a/cmds/statsd/tests/LogEntryMatcher_test.cpp
+++ b/cmds/statsd/tests/LogEntryMatcher_test.cpp
@@ -42,7 +42,7 @@
     auto simpleMatcher = matcher.mutable_simple_log_entry_matcher();
     simpleMatcher->set_tag(TAG_ID);
 
-    LogEvent event(TAG_ID);
+    LogEvent event(TAG_ID, 0);
 
     // Convert to a LogEvent
     event.init();
@@ -62,7 +62,7 @@
     keyValue2->mutable_key_matcher()->set_key(FIELD_ID_2);
 
     // Set up the event
-    LogEvent event(TAG_ID);
+    LogEvent event(TAG_ID, 0);
     auto list = event.GetAndroidLogEventList();
     *list << true;
     *list << false;
@@ -98,7 +98,7 @@
     keyValue->set_eq_string("some value");
 
     // Set up the event
-    LogEvent event(TAG_ID);
+    LogEvent event(TAG_ID, 0);
     auto list = event.GetAndroidLogEventList();
     *list << "some value";
 
@@ -119,7 +119,7 @@
     keyValue->mutable_key_matcher()->set_key(FIELD_ID_1);
 
     // Set up the event
-    LogEvent event(TAG_ID);
+    LogEvent event(TAG_ID, 0);
     auto list = event.GetAndroidLogEventList();
     *list << 11;
 
diff --git a/cmds/statsd/tests/metrics/CountMetricProducer_test.cpp b/cmds/statsd/tests/metrics/CountMetricProducer_test.cpp
new file mode 100644
index 0000000..5a4ee73
--- /dev/null
+++ b/cmds/statsd/tests/metrics/CountMetricProducer_test.cpp
@@ -0,0 +1,183 @@
+// 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.
+
+#include "metrics_test_helper.h"
+#include "src/metrics/CountMetricProducer.h"
+
+#include <gmock/gmock.h>
+#include <gtest/gtest.h>
+#include <stdio.h>
+#include <vector>
+
+using namespace testing;
+using android::sp;
+using std::set;
+using std::unordered_map;
+using std::vector;
+
+#ifdef __ANDROID__
+
+namespace android {
+namespace os {
+namespace statsd {
+
+TEST(CountMetricProducerTest, TestNonDimensionalEvents) {
+    int64_t bucketStartTimeNs = 10000000000;
+    int64_t bucketSizeNs = 30 * 1000 * 1000 * 1000LL;
+    int64_t bucket2StartTimeNs = bucketStartTimeNs + bucketSizeNs;
+    int64_t bucket3StartTimeNs = bucketStartTimeNs + 2 * bucketSizeNs;
+    int tagId = 1;
+
+    CountMetric metric;
+    metric.set_metric_id(1);
+    metric.mutable_bucket()->set_bucket_size_millis(bucketSizeNs / 1000000);
+
+    LogEvent event1(tagId, bucketStartTimeNs + 1);
+    LogEvent event2(tagId, bucketStartTimeNs + 2);
+
+    sp<MockConditionWizard> wizard = new NaggyMock<MockConditionWizard>();
+
+    CountMetricProducer countProducer(metric, -1 /*-1 meaning no condition*/, wizard,
+                                      bucketStartTimeNs);
+
+    // 2 events in bucket 1.
+    countProducer.onMatchedLogEvent(1 /*log matcher index*/, event1, false);
+    countProducer.onMatchedLogEvent(1 /*log matcher index*/, event2, false);
+    countProducer.flushCounterIfNeeded(bucketStartTimeNs + bucketSizeNs + 1);
+    EXPECT_EQ(1UL, countProducer.mPastBuckets.size());
+    EXPECT_TRUE(countProducer.mPastBuckets.find(DEFAULT_DIMENSION_KEY) !=
+                countProducer.mPastBuckets.end());
+    const auto& buckets = countProducer.mPastBuckets[DEFAULT_DIMENSION_KEY];
+    EXPECT_EQ(1UL, buckets.size());
+    const auto& bucketInfo = buckets[0];
+    EXPECT_EQ(bucketStartTimeNs, bucketInfo.mBucketStartNs);
+    EXPECT_EQ(bucketStartTimeNs + bucketSizeNs, bucketInfo.mBucketEndNs);
+    EXPECT_EQ(2LL, bucketInfo.mCount);
+
+    // 1 matched event happens in bucket 2.
+    LogEvent event3(tagId, bucketStartTimeNs + bucketSizeNs + 2);
+    countProducer.onMatchedLogEvent(1 /*log matcher index*/, event3, false);
+    countProducer.flushCounterIfNeeded(bucketStartTimeNs + 2 * bucketSizeNs + 1);
+    EXPECT_EQ(1UL, countProducer.mPastBuckets.size());
+    EXPECT_TRUE(countProducer.mPastBuckets.find(DEFAULT_DIMENSION_KEY) !=
+                countProducer.mPastBuckets.end());
+    EXPECT_EQ(2UL, countProducer.mPastBuckets[DEFAULT_DIMENSION_KEY].size());
+    const auto& bucketInfo2 = countProducer.mPastBuckets[DEFAULT_DIMENSION_KEY][1];
+    EXPECT_EQ(bucket2StartTimeNs, bucketInfo2.mBucketStartNs);
+    EXPECT_EQ(bucket2StartTimeNs + bucketSizeNs, bucketInfo2.mBucketEndNs);
+    EXPECT_EQ(1LL, bucketInfo2.mCount);
+
+    // nothing happens in bucket 3. we should not record anything for bucket 3.
+    countProducer.flushCounterIfNeeded(bucketStartTimeNs + 3 * bucketSizeNs + 1);
+    EXPECT_EQ(1UL, countProducer.mPastBuckets.size());
+    EXPECT_TRUE(countProducer.mPastBuckets.find(DEFAULT_DIMENSION_KEY) !=
+                countProducer.mPastBuckets.end());
+    const auto& buckets3 = countProducer.mPastBuckets[DEFAULT_DIMENSION_KEY];
+    EXPECT_EQ(2UL, buckets3.size());
+}
+
+TEST(CountMetricProducerTest, TestEventsWithNonSlicedCondition) {
+    int64_t bucketStartTimeNs = 10000000000;
+    int64_t bucketSizeNs = 30 * 1000 * 1000 * 1000LL;
+
+    CountMetric metric;
+    metric.set_metric_id(1);
+    metric.mutable_bucket()->set_bucket_size_millis(bucketSizeNs / 1000000);
+    metric.set_condition("SCREEN_ON");
+
+    LogEvent event1(1, bucketStartTimeNs + 1);
+    LogEvent event2(1, bucketStartTimeNs + 10);
+
+    sp<MockConditionWizard> wizard = new NaggyMock<MockConditionWizard>();
+
+    CountMetricProducer countProducer(metric, 1, wizard, bucketStartTimeNs);
+
+    countProducer.onConditionChanged(true, bucketStartTimeNs);
+    countProducer.onMatchedLogEvent(1 /*matcher index*/, event1, false /*pulled*/);
+    EXPECT_EQ(0UL, countProducer.mPastBuckets.size());
+
+    countProducer.onConditionChanged(false /*new condition*/, bucketStartTimeNs + 2);
+    countProducer.onMatchedLogEvent(1 /*matcher index*/, event2, false /*pulled*/);
+    EXPECT_EQ(0UL, countProducer.mPastBuckets.size());
+
+    countProducer.flushCounterIfNeeded(bucketStartTimeNs + bucketSizeNs + 1);
+
+    EXPECT_EQ(1UL, countProducer.mPastBuckets.size());
+    EXPECT_TRUE(countProducer.mPastBuckets.find(DEFAULT_DIMENSION_KEY) !=
+                countProducer.mPastBuckets.end());
+    const auto& buckets = countProducer.mPastBuckets[DEFAULT_DIMENSION_KEY];
+    EXPECT_EQ(1UL, buckets.size());
+    const auto& bucketInfo = buckets[0];
+    EXPECT_EQ(bucketStartTimeNs, bucketInfo.mBucketStartNs);
+    EXPECT_EQ(bucketStartTimeNs + bucketSizeNs, bucketInfo.mBucketEndNs);
+    EXPECT_EQ(1LL, bucketInfo.mCount);
+}
+
+TEST(CountMetricProducerTest, TestEventsWithSlicedCondition) {
+    int64_t bucketStartTimeNs = 10000000000;
+    int64_t bucketSizeNs = 30 * 1000 * 1000 * 1000LL;
+
+    CountMetric metric;
+    metric.set_metric_id(1);
+    metric.mutable_bucket()->set_bucket_size_millis(bucketSizeNs / 1000000);
+    metric.set_condition("APP_IN_BACKGROUND_PER_UID_AND_SCREEN_ON");
+    EventConditionLink* link = metric.add_links();
+    link->set_condition("APP_IN_BACKGROUND_PER_UID");
+    link->add_key_in_main()->set_key(1);
+    link->add_key_in_condition()->set_key(2);
+
+    LogEvent event1(1, bucketStartTimeNs + 1);
+    auto list = event1.GetAndroidLogEventList();
+    *list << "111";  // uid
+    event1.init();
+    ConditionKey key1;
+    key1["APP_IN_BACKGROUND_PER_UID"] = "2:111|";
+
+    LogEvent event2(1, bucketStartTimeNs + 10);
+    auto list2 = event2.GetAndroidLogEventList();
+    *list2 << "222";  // uid
+    event2.init();
+    ConditionKey key2;
+    key2["APP_IN_BACKGROUND_PER_UID"] = "2:222|";
+
+    sp<MockConditionWizard> wizard = new NaggyMock<MockConditionWizard>();
+    EXPECT_CALL(*wizard, query(_, key1)).WillOnce(Return(ConditionState::kFalse));
+
+    EXPECT_CALL(*wizard, query(_, key2)).WillOnce(Return(ConditionState::kTrue));
+
+    CountMetricProducer countProducer(metric, 1 /*condition tracker index*/, wizard,
+                                      bucketStartTimeNs);
+
+    countProducer.onMatchedLogEvent(1 /*log matcher index*/, event1, false);
+    countProducer.onMatchedLogEvent(1 /*log matcher index*/, event2, false);
+
+    countProducer.flushCounterIfNeeded(bucketStartTimeNs + bucketSizeNs + 1);
+
+    EXPECT_EQ(1UL, countProducer.mPastBuckets.size());
+    EXPECT_TRUE(countProducer.mPastBuckets.find(DEFAULT_DIMENSION_KEY) !=
+                countProducer.mPastBuckets.end());
+    const auto& buckets = countProducer.mPastBuckets[DEFAULT_DIMENSION_KEY];
+    EXPECT_EQ(1UL, buckets.size());
+    const auto& bucketInfo = buckets[0];
+    EXPECT_EQ(bucketStartTimeNs, bucketInfo.mBucketStartNs);
+    EXPECT_EQ(bucketStartTimeNs + bucketSizeNs, bucketInfo.mBucketEndNs);
+    EXPECT_EQ(1LL, bucketInfo.mCount);
+}
+
+}  // namespace statsd
+}  // namespace os
+}  // namespace android
+#else
+GTEST_LOG_(INFO) << "This test does nothing.\n";
+#endif
diff --git a/cmds/statsd/tests/metrics/EventMetricProducer_test.cpp b/cmds/statsd/tests/metrics/EventMetricProducer_test.cpp
new file mode 100644
index 0000000..76dbc73
--- /dev/null
+++ b/cmds/statsd/tests/metrics/EventMetricProducer_test.cpp
@@ -0,0 +1,130 @@
+// 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.
+
+#include "metrics_test_helper.h"
+#include "src/metrics/EventMetricProducer.h"
+
+#include <gmock/gmock.h>
+#include <gtest/gtest.h>
+#include <stdio.h>
+#include <vector>
+
+using namespace testing;
+using android::sp;
+using std::set;
+using std::unordered_map;
+using std::vector;
+
+#ifdef __ANDROID__
+
+namespace android {
+namespace os {
+namespace statsd {
+
+TEST(EventMetricProducerTest, TestNoCondition) {
+    uint64_t bucketStartTimeNs = 10000000000;
+    uint64_t eventStartTimeNs = bucketStartTimeNs + 1;
+    uint64_t bucketSizeNs = 30 * 1000 * 1000 * 1000LL;
+
+    EventMetric metric;
+    metric.set_metric_id(1);
+
+    LogEvent event1(1 /*tag id*/, bucketStartTimeNs + 1);
+    LogEvent event2(1 /*tag id*/, bucketStartTimeNs + 2);
+
+    sp<MockConditionWizard> wizard = new NaggyMock<MockConditionWizard>();
+
+    EventMetricProducer eventProducer(metric, -1 /*-1 meaning no condition*/, wizard,
+                                      bucketStartTimeNs);
+
+    eventProducer.onMatchedLogEvent(1 /*matcher index*/, event1, false /*pulled*/);
+    eventProducer.onMatchedLogEvent(1 /*matcher index*/, event2, false /*pulled*/);
+
+    // TODO: get the report and check the content after the ProtoOutputStream change is done.
+    // eventProducer.onDumpReport();
+}
+
+TEST(EventMetricProducerTest, TestEventsWithNonSlicedCondition) {
+    uint64_t bucketStartTimeNs = 10000000000;
+    uint64_t eventStartTimeNs = bucketStartTimeNs + 1;
+    uint64_t bucketSizeNs = 30 * 1000 * 1000 * 1000LL;
+
+    EventMetric metric;
+    metric.set_metric_id(1);
+    metric.set_condition("SCREEN_ON");
+
+    LogEvent event1(1, bucketStartTimeNs + 1);
+    LogEvent event2(1, bucketStartTimeNs + 10);
+
+    sp<MockConditionWizard> wizard = new NaggyMock<MockConditionWizard>();
+
+    EventMetricProducer eventProducer(metric, 1, wizard, bucketStartTimeNs);
+
+    eventProducer.onConditionChanged(true /*condition*/, bucketStartTimeNs);
+    eventProducer.onMatchedLogEvent(1 /*matcher index*/, event1, false /*pulled*/);
+
+    eventProducer.onConditionChanged(false /*condition*/, bucketStartTimeNs + 2);
+
+    eventProducer.onMatchedLogEvent(1 /*matcher index*/, event2, false /*pulled*/);
+
+    // TODO: get the report and check the content after the ProtoOutputStream change is done.
+    // eventProducer.onDumpReport();
+}
+
+TEST(EventMetricProducerTest, TestEventsWithSlicedCondition) {
+    uint64_t bucketStartTimeNs = 10000000000;
+    uint64_t bucketSizeNs = 30 * 1000 * 1000 * 1000LL;
+
+    EventMetric metric;
+    metric.set_metric_id(1);
+    metric.set_condition("APP_IN_BACKGROUND_PER_UID_AND_SCREEN_ON");
+    EventConditionLink* link = metric.add_links();
+    link->set_condition("APP_IN_BACKGROUND_PER_UID");
+    link->add_key_in_main()->set_key(1);
+    link->add_key_in_condition()->set_key(2);
+
+    LogEvent event1(1, bucketStartTimeNs + 1);
+    auto list = event1.GetAndroidLogEventList();
+    *list << "111";  // uid
+    event1.init();
+    ConditionKey key1;
+    key1["APP_IN_BACKGROUND_PER_UID"] = "2:111|";
+
+    LogEvent event2(1, bucketStartTimeNs + 10);
+    auto list2 = event2.GetAndroidLogEventList();
+    *list2 << "222";  // uid
+    event2.init();
+    ConditionKey key2;
+    key2["APP_IN_BACKGROUND_PER_UID"] = "2:222|";
+
+    sp<MockConditionWizard> wizard = new NaggyMock<MockConditionWizard>();
+    EXPECT_CALL(*wizard, query(_, key1)).WillOnce(Return(ConditionState::kFalse));
+
+    EXPECT_CALL(*wizard, query(_, key2)).WillOnce(Return(ConditionState::kTrue));
+
+    EventMetricProducer eventProducer(metric, 1, wizard, bucketStartTimeNs);
+
+    eventProducer.onMatchedLogEvent(1 /*matcher index*/, event1, false /*pulled*/);
+    eventProducer.onMatchedLogEvent(1 /*matcher index*/, event2, false /*pulled*/);
+
+    // TODO: get the report and check the content after the ProtoOutputStream change is done.
+    // eventProducer.onDumpReport();
+}
+
+}  // namespace statsd
+}  // namespace os
+}  // namespace android
+#else
+GTEST_LOG_(INFO) << "This test does nothing.\n";
+#endif
diff --git a/cmds/statsd/tests/MaxDurationTracker_test.cpp b/cmds/statsd/tests/metrics/MaxDurationTracker_test.cpp
similarity index 92%
rename from cmds/statsd/tests/MaxDurationTracker_test.cpp
rename to cmds/statsd/tests/metrics/MaxDurationTracker_test.cpp
index ae8bf42..f2abe7b 100644
--- a/cmds/statsd/tests/MaxDurationTracker_test.cpp
+++ b/cmds/statsd/tests/metrics/MaxDurationTracker_test.cpp
@@ -12,12 +12,12 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-#include <gmock/gmock.h>
-#include <gtest/gtest.h>
-
+#include "metrics_test_helper.h"
 #include "src/condition/ConditionWizard.h"
 #include "src/metrics/duration_helper/MaxDurationTracker.h"
 
+#include <gmock/gmock.h>
+#include <gtest/gtest.h>
 #include <stdio.h>
 #include <set>
 #include <unordered_map>
@@ -32,13 +32,9 @@
 
 #ifdef __ANDROID__
 
-class MockConditionWizard : public ConditionWizard {
-public:
-    MOCK_METHOD2(
-            query,
-            ConditionState(const int conditionIndex,
-                           const std::map<std::string, HashableDimensionKey>& conditionParameters));
-};
+namespace android {
+namespace os {
+namespace statsd {
 
 TEST(MaxDurationTrackerTest, TestSimpleMaxDuration) {
     sp<MockConditionWizard> wizard = new NaggyMock<MockConditionWizard>();
@@ -110,6 +106,9 @@
     EXPECT_EQ(5, buckets[0].duration_nanos());
 }
 
+}  // namespace statsd
+}  // namespace os
+}  // namespace android
 #else
 GTEST_LOG_(INFO) << "This test does nothing.\n";
 #endif
diff --git a/cmds/statsd/tests/OringDurationTracker_test.cpp b/cmds/statsd/tests/metrics/OringDurationTracker_test.cpp
similarity index 90%
rename from cmds/statsd/tests/OringDurationTracker_test.cpp
rename to cmds/statsd/tests/metrics/OringDurationTracker_test.cpp
index 0b79819..338d55d 100644
--- a/cmds/statsd/tests/OringDurationTracker_test.cpp
+++ b/cmds/statsd/tests/metrics/OringDurationTracker_test.cpp
@@ -12,18 +12,17 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-#include <gmock/gmock.h>
-#include <gtest/gtest.h>
-
+#include "metrics_test_helper.h"
 #include "src/condition/ConditionWizard.h"
 #include "src/metrics/duration_helper/OringDurationTracker.h"
 
+#include <gmock/gmock.h>
+#include <gtest/gtest.h>
 #include <stdio.h>
 #include <set>
 #include <unordered_map>
 #include <vector>
 
-using namespace android::os::statsd;
 using namespace testing;
 using android::sp;
 using std::set;
@@ -31,14 +30,9 @@
 using std::vector;
 
 #ifdef __ANDROID__
-
-class MockConditionWizard : public ConditionWizard {
-public:
-    MOCK_METHOD2(
-            query,
-            ConditionState(const int conditionIndex,
-                           const std::map<std::string, HashableDimensionKey>& conditionParameters));
-};
+namespace android {
+namespace os {
+namespace statsd {
 
 TEST(OringDurationTrackerTest, TestDurationOverlap) {
     sp<MockConditionWizard> wizard = new NaggyMock<MockConditionWizard>();
@@ -93,7 +87,9 @@
     EXPECT_EQ(1u, buckets.size());
     EXPECT_EQ(5, buckets[0].duration_nanos());
 }
-
+}  // namespace statsd
+}  // namespace os
+}  // namespace android
 #else
 GTEST_LOG_(INFO) << "This test does nothing.\n";
 #endif
diff --git a/cmds/statsd/tests/metrics/metrics_test_helper.h b/cmds/statsd/tests/metrics/metrics_test_helper.h
new file mode 100644
index 0000000..5fd7d62
--- /dev/null
+++ b/cmds/statsd/tests/metrics/metrics_test_helper.h
@@ -0,0 +1,35 @@
+// 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.
+#pragma once
+
+#include "src/condition/ConditionWizard.h"
+
+#include <gmock/gmock.h>
+#include <gtest/gtest.h>
+
+namespace android {
+namespace os {
+namespace statsd {
+
+class MockConditionWizard : public ConditionWizard {
+public:
+    MOCK_METHOD2(
+            query,
+            ConditionState(const int conditionIndex,
+                           const std::map<std::string, HashableDimensionKey>& conditionParameters));
+};
+
+}  // namespace statsd
+}  // namespace os
+}  // namespace android
diff --git a/cmds/svc/svc b/cmds/svc/svc
index 27111cd..07b50fe 100755
--- a/cmds/svc/svc
+++ b/cmds/svc/svc
@@ -1,3 +1,4 @@
+#!/system/bin/sh
 # Script to start "am" on the device, which has a very rudimentary
 # shell.
 #
diff --git a/cmds/telecom/telecom b/cmds/telecom/telecom
index 9efdcfd..a19036b 100755
--- a/cmds/telecom/telecom
+++ b/cmds/telecom/telecom
@@ -1,3 +1,4 @@
+#!/system/bin/sh
 # Script to start "telecom" on the device
 #
 base=/system
diff --git a/cmds/uiautomator/cmds/uiautomator/uiautomator b/cmds/uiautomator/cmds/uiautomator/uiautomator
index 86a1dba..889c2b5 100755
--- a/cmds/uiautomator/cmds/uiautomator/uiautomator
+++ b/cmds/uiautomator/cmds/uiautomator/uiautomator
@@ -1,3 +1,4 @@
+#!/system/bin/sh
 #
 # Copyright (C) 2012 The Android Open Source Project
 #
diff --git a/cmds/vr/vr b/cmds/vr/vr
index a279007..dbde02a 100755
--- a/cmds/vr/vr
+++ b/cmds/vr/vr
@@ -1,3 +1,4 @@
+#!/system/bin/sh
 # Script to start "vr" on the device
 #
 base=/system
diff --git a/cmds/wm/wm b/cmds/wm/wm
index f7a5bc7..16d6bd6 100755
--- a/cmds/wm/wm
+++ b/cmds/wm/wm
@@ -1,3 +1,4 @@
+#!/system/bin/sh
 # Script to start "wm" on the device, which has a very rudimentary
 # shell.
 #
diff --git a/core/java/android/app/Notification.java b/core/java/android/app/Notification.java
index 8226e0f..d5d95fb 100644
--- a/core/java/android/app/Notification.java
+++ b/core/java/android/app/Notification.java
@@ -22,6 +22,7 @@
 import android.annotation.DrawableRes;
 import android.annotation.IntDef;
 import android.annotation.NonNull;
+import android.annotation.Nullable;
 import android.annotation.RequiresPermission;
 import android.annotation.SdkConstant;
 import android.annotation.SdkConstant.SdkConstantType;
@@ -3900,7 +3901,7 @@
             final Bundle ex = mN.extras;
             updateBackgroundColor(contentView);
             bindNotificationHeader(contentView, p.ambient);
-            bindLargeIcon(contentView);
+            bindLargeIcon(contentView, p.hideLargeIcon, p.alwaysShowReply);
             boolean showProgress = handleProgressBar(p.hasProgress, contentView, ex);
             if (p.title != null) {
                 contentView.setViewVisibility(R.id.title, View.VISIBLE);
@@ -4110,11 +4111,13 @@
             }
         }
 
-        private void bindLargeIcon(RemoteViews contentView) {
+        private void bindLargeIcon(RemoteViews contentView, boolean hideLargeIcon,
+                boolean alwaysShowReply) {
             if (mN.mLargeIcon == null && mN.largeIcon != null) {
                 mN.mLargeIcon = Icon.createWithBitmap(mN.largeIcon);
             }
-            if (mN.mLargeIcon != null) {
+            boolean showLargeIcon = mN.mLargeIcon != null && !hideLargeIcon;
+            if (showLargeIcon) {
                 contentView.setViewVisibility(R.id.right_icon, View.VISIBLE);
                 contentView.setImageViewIcon(R.id.right_icon, mN.mLargeIcon);
                 processLargeLegacyIcon(mN.mLargeIcon, contentView);
@@ -4122,32 +4125,45 @@
                 contentView.setViewLayoutMarginEndDimen(R.id.line1, endMargin);
                 contentView.setViewLayoutMarginEndDimen(R.id.text, endMargin);
                 contentView.setViewLayoutMarginEndDimen(R.id.progress, endMargin);
-                // Bind the reply action
-                Action action = findReplyAction();
-                contentView.setViewVisibility(R.id.reply_icon_action, action != null
-                        ? View.VISIBLE
-                        : View.GONE);
+            }
+            // Bind the reply action
+            Action action = findReplyAction();
 
-                if (action != null) {
-                    int contrastColor = resolveContrastColor();
+            boolean actionVisible = action != null && (showLargeIcon || alwaysShowReply);
+            int replyId = showLargeIcon ? R.id.reply_icon_action : R.id.right_icon;
+            if (actionVisible) {
+                // We're only showing the icon as big if we're hiding the large icon
+                int contrastColor = resolveContrastColor();
+                int iconColor;
+                if (showLargeIcon) {
                     contentView.setDrawableTint(R.id.reply_icon_action,
                             true /* targetBackground */,
                             contrastColor, PorterDuff.Mode.SRC_ATOP);
-                    int iconColor = NotificationColorUtil.isColorLight(contrastColor)
-                            ? Color.BLACK : Color.WHITE;
-                    contentView.setDrawableTint(R.id.reply_icon_action,
-                            false /* targetBackground */,
-                            iconColor, PorterDuff.Mode.SRC_ATOP);
                     contentView.setOnClickPendingIntent(R.id.right_icon,
                             action.actionIntent);
-                    contentView.setOnClickPendingIntent(R.id.reply_icon_action,
-                            action.actionIntent);
                     contentView.setRemoteInputs(R.id.right_icon, action.mRemoteInputs);
-                    contentView.setRemoteInputs(R.id.reply_icon_action, action.mRemoteInputs);
-
+                    iconColor = NotificationColorUtil.isColorLight(contrastColor)
+                            ? Color.BLACK : Color.WHITE;
+                } else {
+                    contentView.setImageViewResource(R.id.right_icon,
+                            R.drawable.ic_reply_notification_large);
+                    contentView.setViewVisibility(R.id.right_icon, View.VISIBLE);
+                    iconColor = contrastColor;
                 }
+                contentView.setDrawableTint(replyId,
+                        false /* targetBackground */,
+                        iconColor,
+                        PorterDuff.Mode.SRC_ATOP);
+                contentView.setOnClickPendingIntent(replyId,
+                        action.actionIntent);
+                contentView.setRemoteInputs(replyId, action.mRemoteInputs);
+            } else {
+                contentView.setRemoteInputs(R.id.right_icon, null);
             }
-            contentView.setViewVisibility(R.id.right_icon_container, mN.mLargeIcon != null
+            contentView.setViewVisibility(R.id.reply_icon_action, actionVisible && showLargeIcon
+                    ? View.VISIBLE
+                    : View.GONE);
+            contentView.setViewVisibility(R.id.right_icon_container, actionVisible || showLargeIcon
                     ? View.VISIBLE
                     : View.GONE);
         }
@@ -6055,18 +6071,12 @@
         protected void restoreFromExtras(Bundle extras) {
             super.restoreFromExtras(extras);
 
-            mMessages.clear();
-            mHistoricMessages.clear();
             mUserDisplayName = extras.getCharSequence(EXTRA_SELF_DISPLAY_NAME);
             mConversationTitle = extras.getCharSequence(EXTRA_CONVERSATION_TITLE);
             Parcelable[] messages = extras.getParcelableArray(EXTRA_MESSAGES);
-            if (messages != null && messages instanceof Parcelable[]) {
-                mMessages = Message.getMessagesFromBundleArray(messages);
-            }
+            mMessages = Message.getMessagesFromBundleArray(messages);
             Parcelable[] histMessages = extras.getParcelableArray(EXTRA_HISTORIC_MESSAGES);
-            if (histMessages != null && histMessages instanceof Parcelable[]) {
-                mHistoricMessages = Message.getMessagesFromBundleArray(histMessages);
-            }
+            mHistoricMessages = Message.getMessagesFromBundleArray(histMessages);
         }
 
         /**
@@ -6074,38 +6084,34 @@
          */
         @Override
         public RemoteViews makeContentView(boolean increasedHeight) {
-            if (!increasedHeight) {
-                Message m = findLatestIncomingMessage();
-                CharSequence title = mConversationTitle != null
-                        ? mConversationTitle
-                        : (m == null) ? null : m.mSender;
-                CharSequence text = (m == null)
-                        ? null
-                        : mConversationTitle != null ? makeMessageLine(m, mBuilder) : m.mText;
-
-                return mBuilder.applyStandardTemplate(mBuilder.getBaseLayoutResource(),
-                        mBuilder.mParams.reset().hasProgress(false).title(title).text(text));
-            } else {
-                mBuilder.mOriginalActions = mBuilder.mActions;
-                mBuilder.mActions = new ArrayList<>();
-                RemoteViews remoteViews = makeBigContentView();
-                mBuilder.mActions = mBuilder.mOriginalActions;
-                mBuilder.mOriginalActions = null;
-                return remoteViews;
-            }
+            mBuilder.mOriginalActions = mBuilder.mActions;
+            mBuilder.mActions = new ArrayList<>();
+            RemoteViews remoteViews = makeBigContentView();
+            mBuilder.mActions = mBuilder.mOriginalActions;
+            mBuilder.mOriginalActions = null;
+            return remoteViews;
         }
 
         private Message findLatestIncomingMessage() {
-            for (int i = mMessages.size() - 1; i >= 0; i--) {
-                Message m = mMessages.get(i);
+            return findLatestIncomingMessage(mMessages);
+        }
+
+        /**
+         * @hide
+         */
+        @Nullable
+        public static Message findLatestIncomingMessage(
+                List<Message> messages) {
+            for (int i = messages.size() - 1; i >= 0; i--) {
+                Message m = messages.get(i);
                 // Incoming messages have a non-empty sender.
                 if (!TextUtils.isEmpty(m.mSender)) {
                     return m;
                 }
             }
-            if (!mMessages.isEmpty()) {
+            if (!messages.isEmpty()) {
                 // No incoming messages, fall back to outgoing message
-                return mMessages.get(mMessages.size() - 1);
+                return messages.get(messages.size() - 1);
             }
             return null;
         }
@@ -6115,118 +6121,82 @@
          */
         @Override
         public RemoteViews makeBigContentView() {
-            CharSequence title = !TextUtils.isEmpty(super.mBigContentTitle)
+            CharSequence conversationTitle = !TextUtils.isEmpty(super.mBigContentTitle)
                     ? super.mBigContentTitle
                     : mConversationTitle;
-            boolean hasTitle = !TextUtils.isEmpty(title);
-
-            if (mMessages.size() == 1) {
-                // Special case for a single message: Use the big text style
-                // so the collapsed and expanded versions match nicely.
-                CharSequence bigTitle;
-                CharSequence text;
-                if (hasTitle) {
-                    bigTitle = title;
-                    text = makeMessageLine(mMessages.get(0), mBuilder);
-                } else {
-                    bigTitle = mMessages.get(0).mSender;
-                    text = mMessages.get(0).mText;
-                }
-                RemoteViews contentView = mBuilder.applyStandardTemplateWithActions(
-                        mBuilder.getBigTextLayoutResource(),
-                        mBuilder.mParams.reset().hasProgress(false).title(bigTitle).text(null));
-                BigTextStyle.applyBigTextContentView(mBuilder, contentView, text);
-                return contentView;
+            boolean isOneToOne = TextUtils.isEmpty(conversationTitle);
+            if (isOneToOne) {
+                // Let's add the conversationTitle in case we didn't have one before and all
+                // messages are from the same sender
+                conversationTitle = createConversationTitleFromMessages();
+            } else if (hasOnlyWhiteSpaceSenders()) {
+                isOneToOne = true;
             }
-
+            boolean hasTitle = !TextUtils.isEmpty(conversationTitle);
             RemoteViews contentView = mBuilder.applyStandardTemplateWithActions(
                     mBuilder.getMessagingLayoutResource(),
-                    mBuilder.mParams.reset().hasProgress(false).title(title).text(null));
-
-            int[] rowIds = {R.id.inbox_text0, R.id.inbox_text1, R.id.inbox_text2, R.id.inbox_text3,
-                    R.id.inbox_text4, R.id.inbox_text5, R.id.inbox_text6};
-
-            // Make sure all rows are gone in case we reuse a view.
-            for (int rowId : rowIds) {
-                contentView.setViewVisibility(rowId, View.GONE);
-            }
-
-            int i=0;
-            contentView.setViewLayoutMarginBottomDimen(R.id.line1,
-                    hasTitle ? R.dimen.notification_messaging_spacing : 0);
-            contentView.setInt(R.id.notification_messaging, "setNumIndentLines",
-                    !mBuilder.mN.hasLargeIcon() ? 0 : (hasTitle ? 1 : 2));
-
-            int contractedChildId = View.NO_ID;
-            Message contractedMessage = findLatestIncomingMessage();
-            int firstHistoricMessage = Math.max(0, mHistoricMessages.size()
-                    - (rowIds.length - mMessages.size()));
-            while (firstHistoricMessage + i < mHistoricMessages.size() && i < rowIds.length) {
-                Message m = mHistoricMessages.get(firstHistoricMessage + i);
-                int rowId = rowIds[i];
-
-                contentView.setTextViewText(rowId, makeMessageLine(m, mBuilder));
-
-                if (contractedMessage == m) {
-                    contractedChildId = rowId;
-                }
-
-                i++;
-            }
-
-            int firstMessage = Math.max(0, mMessages.size() - rowIds.length);
-            while (firstMessage + i < mMessages.size() && i < rowIds.length) {
-                Message m = mMessages.get(firstMessage + i);
-                int rowId = rowIds[i];
-
-                contentView.setViewVisibility(rowId, View.VISIBLE);
-                contentView.setTextViewText(rowId, mBuilder.processTextSpans(
-                        makeMessageLine(m, mBuilder)));
-                mBuilder.setTextViewColorSecondary(contentView, rowId);
-
-                if (contractedMessage == m) {
-                    contractedChildId = rowId;
-                }
-
-                i++;
-            }
-            // Clear the remaining views for reapply. Ensures that historic message views can
-            // reliably be identified as being GONE and having non-null text.
-            while (i < rowIds.length) {
-                int rowId = rowIds[i];
-                contentView.setTextViewText(rowId, null);
-                i++;
-            }
-
-            // Record this here to allow transformation between the contracted and expanded views.
-            contentView.setInt(R.id.notification_messaging, "setContractedChildId",
-                    contractedChildId);
+                    mBuilder.mParams.reset().hasProgress(false).title(conversationTitle).text(null)
+                            .hideLargeIcon(isOneToOne).alwaysShowReply(true));
+            addExtras(mBuilder.mN.extras);
+            contentView.setInt(R.id.status_bar_latest_event_content, "setLayoutColor",
+                    mBuilder.resolveContrastColor());
+            contentView.setIcon(R.id.status_bar_latest_event_content, "setLargeIcon",
+                    mBuilder.mN.mLargeIcon);
+            contentView.setBoolean(R.id.status_bar_latest_event_content, "setIsOneToOne",
+                    isOneToOne);
+            contentView.setBundle(R.id.status_bar_latest_event_content, "setData",
+                    mBuilder.mN.extras);
             return contentView;
         }
 
-        private CharSequence makeMessageLine(Message m, Builder builder) {
-            BidiFormatter bidi = BidiFormatter.getInstance();
-            SpannableStringBuilder sb = new SpannableStringBuilder();
-            boolean colorize = builder.isColorized();
-            TextAppearanceSpan colorSpan;
-            CharSequence messageName;
-            if (TextUtils.isEmpty(m.mSender)) {
-                CharSequence replyName = mUserDisplayName == null ? "" : mUserDisplayName;
-                sb.append(bidi.unicodeWrap(replyName),
-                        makeFontColorSpan(colorize
-                                ? builder.getPrimaryTextColor()
-                                : mBuilder.resolveContrastColor()),
-                        0 /* flags */);
-            } else {
-                sb.append(bidi.unicodeWrap(m.mSender),
-                        makeFontColorSpan(colorize
-                                ? builder.getPrimaryTextColor()
-                                : Color.BLACK),
-                        0 /* flags */);
+        private boolean hasOnlyWhiteSpaceSenders() {
+            for (int i = 0; i < mMessages.size(); i++) {
+                Message m = mMessages.get(i);
+                CharSequence sender = m.getSender();
+                if (!isWhiteSpace(sender)) {
+                    return false;
+                }
             }
-            CharSequence text = m.mText == null ? "" : m.mText;
-            sb.append("  ").append(bidi.unicodeWrap(text));
-            return sb;
+            return true;
+        }
+
+        private boolean isWhiteSpace(CharSequence sender) {
+            if (TextUtils.isEmpty(sender)) {
+                return true;
+            }
+            if (sender.toString().matches("^\\s*$")) {
+                return true;
+            }
+            // Let's check if we only have 0 whitespace chars. Some apps did this as a workaround
+            // For the presentation that we had.
+            for (int i = 0; i < sender.length(); i++) {
+                char c = sender.charAt(i);
+                if (c != '\u200B') {
+                    return false;
+                }
+            }
+            return true;
+        }
+
+        private CharSequence createConversationTitleFromMessages() {
+            ArraySet<CharSequence> names = new ArraySet<>();
+            for (int i = 0; i < mMessages.size(); i++) {
+                Message m = mMessages.get(i);
+                CharSequence sender = m.getSender();
+                if (sender != null) {
+                    names.add(sender);
+                }
+            }
+            SpannableStringBuilder title = new SpannableStringBuilder();
+            int size = names.size();
+            for (int i = 0; i < size; i++) {
+                CharSequence name = names.valueAt(i);
+                if (!TextUtils.isEmpty(title)) {
+                    title.append(", ");
+                }
+                title.append(BidiFormatter.getInstance().unicodeWrap(name));
+            }
+            return title;
         }
 
         /**
@@ -6234,19 +6204,9 @@
          */
         @Override
         public RemoteViews makeHeadsUpContentView(boolean increasedHeight) {
-            if (increasedHeight) {
-                return makeBigContentView();
-            }
-            Message m = findLatestIncomingMessage();
-            CharSequence title = mConversationTitle != null
-                    ? mConversationTitle
-                    : (m == null) ? null : m.mSender;
-            CharSequence text = (m == null)
-                    ? null
-                    : mConversationTitle != null ? makeMessageLine(m, mBuilder) : m.mText;
-
-            return mBuilder.applyStandardTemplateWithActions(mBuilder.getBigBaseLayoutResource(),
-                    mBuilder.mParams.reset().hasProgress(false).title(title).text(text));
+            RemoteViews remoteViews = makeBigContentView();
+            remoteViews.setInt(R.id.notification_messaging, "setMaxDisplayedLines", 1);
+            return remoteViews;
         }
 
         private static TextAppearanceSpan makeFontColorSpan(int color) {
@@ -6394,7 +6354,15 @@
                 return bundles;
             }
 
-            static List<Message> getMessagesFromBundleArray(Parcelable[] bundles) {
+            /**
+             * @return A list of messages read from the bundles.
+             *
+             * @hide
+             */
+            public static List<Message> getMessagesFromBundleArray(Parcelable[] bundles) {
+                if (bundles == null) {
+                    return new ArrayList<>();
+                }
                 List<Message> messages = new ArrayList<>(bundles.length);
                 for (int i = 0; i < bundles.length; i++) {
                     if (bundles[i] instanceof Bundle) {
@@ -8487,6 +8455,8 @@
         boolean ambient = false;
         CharSequence title;
         CharSequence text;
+        boolean hideLargeIcon;
+        public boolean alwaysShowReply;
 
         final StandardTemplateParams reset() {
             hasProgress = true;
@@ -8511,6 +8481,16 @@
             return this;
         }
 
+        final StandardTemplateParams alwaysShowReply(boolean alwaysShowReply) {
+            this.alwaysShowReply = alwaysShowReply;
+            return this;
+        }
+
+        final StandardTemplateParams hideLargeIcon(boolean hideLargeIcon) {
+            this.hideLargeIcon = hideLargeIcon;
+            return this;
+        }
+
         final StandardTemplateParams ambient(boolean ambient) {
             Preconditions.checkState(title == null && text == null, "must set ambient before text");
             this.ambient = ambient;
@@ -8527,7 +8507,6 @@
                 text = extras.getCharSequence(EXTRA_TEXT);
             }
             this.text = b.processLegacyText(text, ambient);
-
             return this;
         }
     }
diff --git a/core/java/android/net/ConnectivityManager.java b/core/java/android/net/ConnectivityManager.java
index d7ecc81..8071e8b 100644
--- a/core/java/android/net/ConnectivityManager.java
+++ b/core/java/android/net/ConnectivityManager.java
@@ -619,6 +619,35 @@
      */
     public static final int NETID_UNSET = 0;
 
+    /**
+     * Private DNS Mode values.
+     *
+     * The "private_dns_mode" global setting stores a String value which is
+     * expected to be one of the following.
+     */
+
+    /**
+     * @hide
+     */
+    public static final String PRIVATE_DNS_MODE_OFF = "off";
+    /**
+     * @hide
+     */
+    public static final String PRIVATE_DNS_MODE_OPPORTUNISTIC = "opportunistic";
+    /**
+     * @hide
+     */
+    public static final String PRIVATE_DNS_MODE_PROVIDER_HOSTNAME = "hostname";
+    /**
+     * The default Private DNS mode.
+     *
+     * This may change from release to release or may become dependent upon
+     * the capabilities of the underlying platform.
+     *
+     * @hide
+     */
+    public static final String PRIVATE_DNS_DEFAULT_MODE = PRIVATE_DNS_MODE_OPPORTUNISTIC;
+
     private final IConnectivityManager mService;
     /**
      * A kludge to facilitate static access where a Context pointer isn't available, like in the
diff --git a/core/java/android/os/Debug.java b/core/java/android/os/Debug.java
index 2e62eb6..2acf36f 100644
--- a/core/java/android/os/Debug.java
+++ b/core/java/android/os/Debug.java
@@ -16,11 +16,14 @@
 
 package android.os;
 
+import android.annotation.NonNull;
+import android.annotation.Nullable;
 import android.app.AppGlobals;
 import android.content.Context;
 import android.util.Log;
 
 import com.android.internal.util.FastPrintWriter;
+import com.android.internal.util.Preconditions;
 import com.android.internal.util.TypedProperties;
 
 import dalvik.system.VMDebug;
@@ -2347,4 +2350,24 @@
     public static String getCaller() {
         return getCaller(Thread.currentThread().getStackTrace(), 0);
     }
+
+    /**
+     * Attach a library as a jvmti agent to the current runtime.
+     *
+     * @param library library containing the agent
+     * @param options options passed to the agent
+     *
+     * @throws IOException If the agent could not be attached
+     */
+    public static void attachJvmtiAgent(@NonNull String library, @Nullable String options)
+            throws IOException {
+        Preconditions.checkNotNull(library);
+        Preconditions.checkArgument(!library.contains("="));
+
+        if (options == null) {
+            VMDebug.attachAgent(library);
+        } else {
+            VMDebug.attachAgent(library + "=" + options);
+        }
+    }
 }
diff --git a/core/java/android/os/Parcel.java b/core/java/android/os/Parcel.java
index c2cf3967..10adb5a 100644
--- a/core/java/android/os/Parcel.java
+++ b/core/java/android/os/Parcel.java
@@ -2020,8 +2020,6 @@
     @Deprecated
     static native void closeFileDescriptor(FileDescriptor desc) throws IOException;
 
-    static native void clearFileDescriptor(FileDescriptor desc);
-
     /**
      * Read a byte value from the parcel at the current dataPosition().
      */
diff --git a/core/java/android/os/ParcelFileDescriptor.java b/core/java/android/os/ParcelFileDescriptor.java
index 7f588ad..7556f09 100644
--- a/core/java/android/os/ParcelFileDescriptor.java
+++ b/core/java/android/os/ParcelFileDescriptor.java
@@ -683,7 +683,7 @@
                 throw new IllegalStateException("Already closed");
             }
             final int fd = getFd();
-            Parcel.clearFileDescriptor(mFd);
+            mFd.setInt$(-1);
             writeCommStatusAndClose(Status.DETACHED, null);
             mClosed = true;
             mGuard.close();
diff --git a/core/java/android/os/StrictMode.java b/core/java/android/os/StrictMode.java
index 53d87ff..d5820b6 100644
--- a/core/java/android/os/StrictMode.java
+++ b/core/java/android/os/StrictMode.java
@@ -29,6 +29,8 @@
 import android.content.pm.ApplicationInfo;
 import android.net.TrafficStats;
 import android.net.Uri;
+import android.os.StrictMode.ThreadPolicy;
+import android.os.StrictMode.VmPolicy;
 import android.os.strictmode.CleartextNetworkViolation;
 import android.os.strictmode.ContentUriWithoutPermissionViolation;
 import android.os.strictmode.CustomViolation;
@@ -54,6 +56,7 @@
 import android.view.IWindowManager;
 
 import com.android.internal.annotations.GuardedBy;
+import com.android.internal.os.BackgroundThread;
 import com.android.internal.os.RuntimeInit;
 import com.android.internal.util.FastPrintWriter;
 import com.android.internal.util.HexDump;
@@ -1533,7 +1536,6 @@
 
             if (violationMaskSubset != 0) {
                 violationMaskSubset |= info.getViolationBit();
-                final int savedPolicyMask = getThreadPolicyMask();
 
                 final boolean justDropBox = (info.mPolicy & THREAD_PENALTY_MASK) == PENALTY_DROPBOX;
                 if (justDropBox) {
@@ -1544,29 +1546,8 @@
                     // isn't always super fast, despite the implementation
                     // in the ActivityManager trying to be mostly async.
                     dropboxViolationAsync(violationMaskSubset, info);
-                    return;
-                }
-
-                // Normal synchronous call to the ActivityManager.
-                try {
-                    // First, remove any policy before we call into the Activity Manager,
-                    // otherwise we'll infinite recurse as we try to log policy violations
-                    // to disk, thus violating policy, thus requiring logging, etc...
-                    // We restore the current policy below, in the finally block.
-                    setThreadPolicyMask(0);
-
-                    ActivityManager.getService()
-                            .handleApplicationStrictModeViolation(
-                                    RuntimeInit.getApplicationObject(), violationMaskSubset, info);
-                } catch (RemoteException e) {
-                    if (e instanceof DeadObjectException) {
-                        // System process is dead; ignore
-                    } else {
-                        Log.e(TAG, "RemoteException trying to handle StrictMode violation", e);
-                    }
-                } finally {
-                    // Restore the policy.
-                    setThreadPolicyMask(savedPolicyMask);
+                } else {
+                    handleApplicationStrictModeViolation(violationMaskSubset, info);
                 }
             }
 
@@ -1598,28 +1579,39 @@
 
         if (LOG_V) Log.d(TAG, "Dropboxing async; in-flight=" + outstanding);
 
-        new Thread("callActivityManagerForStrictModeDropbox") {
-            public void run() {
-                Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
-                try {
-                    IActivityManager am = ActivityManager.getService();
-                    if (am == null) {
-                        Log.d(TAG, "No activity manager; failed to Dropbox violation.");
-                    } else {
-                        am.handleApplicationStrictModeViolation(
-                                RuntimeInit.getApplicationObject(), violationMaskSubset, info);
-                    }
-                } catch (RemoteException e) {
-                    if (e instanceof DeadObjectException) {
-                        // System process is dead; ignore
-                    } else {
-                        Log.e(TAG, "RemoteException handling StrictMode violation", e);
-                    }
-                }
-                int outstanding = sDropboxCallsInFlight.decrementAndGet();
-                if (LOG_V) Log.d(TAG, "Dropbox complete; in-flight=" + outstanding);
+        BackgroundThread.getHandler().post(() -> {
+            handleApplicationStrictModeViolation(violationMaskSubset, info);
+            int outstandingInner = sDropboxCallsInFlight.decrementAndGet();
+            if (LOG_V) Log.d(TAG, "Dropbox complete; in-flight=" + outstandingInner);
+        });
+    }
+
+    private static void handleApplicationStrictModeViolation(int violationMaskSubset,
+            ViolationInfo info) {
+        final int oldMask = getThreadPolicyMask();
+        try {
+            // First, remove any policy before we call into the Activity Manager,
+            // otherwise we'll infinite recurse as we try to log policy violations
+            // to disk, thus violating policy, thus requiring logging, etc...
+            // We restore the current policy below, in the finally block.
+            setThreadPolicyMask(0);
+
+            IActivityManager am = ActivityManager.getService();
+            if (am == null) {
+                Log.w(TAG, "No activity manager; failed to Dropbox violation.");
+            } else {
+                am.handleApplicationStrictModeViolation(
+                        RuntimeInit.getApplicationObject(), violationMaskSubset, info);
             }
-        }.start();
+        } catch (RemoteException e) {
+            if (e instanceof DeadObjectException) {
+                // System process is dead; ignore
+            } else {
+                Log.e(TAG, "RemoteException handling StrictMode violation", e);
+            }
+        } finally {
+            setThreadPolicyMask(oldMask);
+        }
     }
 
     private static class AndroidCloseGuardReporter implements CloseGuard.Reporter {
@@ -1908,31 +1900,7 @@
         }
 
         if (penaltyDropbox && lastViolationTime == 0) {
-            // The violationMask, passed to ActivityManager, is a
-            // subset of the original StrictMode policy bitmask, with
-            // only the bit violated and penalty bits to be executed
-            // by the ActivityManagerService remaining set.
-            final int savedPolicyMask = getThreadPolicyMask();
-            try {
-                // First, remove any policy before we call into the Activity Manager,
-                // otherwise we'll infinite recurse as we try to log policy violations
-                // to disk, thus violating policy, thus requiring logging, etc...
-                // We restore the current policy below, in the finally block.
-                setThreadPolicyMask(0);
-
-                ActivityManager.getService()
-                        .handleApplicationStrictModeViolation(
-                                RuntimeInit.getApplicationObject(), violationMaskSubset, info);
-            } catch (RemoteException e) {
-                if (e instanceof DeadObjectException) {
-                    // System process is dead; ignore
-                } else {
-                    Log.e(TAG, "RemoteException trying to handle StrictMode violation", e);
-                }
-            } finally {
-                // Restore the policy.
-                setThreadPolicyMask(savedPolicyMask);
-            }
+            handleApplicationStrictModeViolation(violationMaskSubset, info);
         }
 
         if (penaltyDeath) {
diff --git a/core/java/android/os/storage/IStorageManager.aidl b/core/java/android/os/storage/IStorageManager.aidl
index e865ed1..0b76eec 100644
--- a/core/java/android/os/storage/IStorageManager.aidl
+++ b/core/java/android/os/storage/IStorageManager.aidl
@@ -17,6 +17,7 @@
 package android.os.storage;
 
 import android.content.pm.IPackageMoveObserver;
+import android.os.IVoldTaskListener;
 import android.os.ParcelFileDescriptor;
 import android.os.storage.DiskInfo;
 import android.os.storage.IStorageEventListener;
@@ -165,7 +166,7 @@
     void forgetAllVolumes() = 56;
     String getPrimaryStorageUuid() = 57;
     void setPrimaryStorageUuid(in String volumeUuid, IPackageMoveObserver callback) = 58;
-    long benchmark(in String volId) = 59;
+    void benchmark(in String volId, IVoldTaskListener listener) = 59;
     void setDebugFlags(int flags, int mask) = 60;
     void createUserKey(int userId, int serialNumber, boolean ephemeral) = 61;
     void destroyUserKey(int userId) = 62;
@@ -177,7 +178,7 @@
     boolean isConvertibleToFBE() = 68;
     void addUserKeyAuth(int userId, int serialNumber, in byte[] token, in byte[] secret) = 70;
     void fixateNewestUserKeyAuth(int userId) = 71;
-    void fstrim(int flags) = 72;
+    void fstrim(int flags, IVoldTaskListener listener) = 72;
     AppFuseMount mountProxyFileDescriptorBridge() = 73;
     ParcelFileDescriptor openProxyFileDescriptor(int mountPointId, int fileId, int mode) = 74;
     long getCacheQuotaBytes(String volumeUuid, int uid) = 75;
diff --git a/core/java/android/os/storage/StorageManager.java b/core/java/android/os/storage/StorageManager.java
index 6594cd0..0b007dd 100644
--- a/core/java/android/os/storage/StorageManager.java
+++ b/core/java/android/os/storage/StorageManager.java
@@ -42,10 +42,12 @@
 import android.os.FileUtils;
 import android.os.Handler;
 import android.os.IVold;
+import android.os.IVoldTaskListener;
 import android.os.Looper;
 import android.os.Message;
 import android.os.ParcelFileDescriptor;
 import android.os.ParcelableException;
+import android.os.PersistableBundle;
 import android.os.ProxyFileDescriptorCallback;
 import android.os.RemoteException;
 import android.os.ServiceManager;
@@ -87,7 +89,9 @@
 import java.util.List;
 import java.util.Objects;
 import java.util.UUID;
+import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.ThreadFactory;
+import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicInteger;
 
 /**
@@ -884,9 +888,32 @@
     }
 
     /** {@hide} */
+    @Deprecated
     public long benchmark(String volId) {
+        final CompletableFuture<PersistableBundle> result = new CompletableFuture<>();
+        benchmark(volId, new IVoldTaskListener.Stub() {
+            @Override
+            public void onStatus(int status, PersistableBundle extras) {
+                // Ignored
+            }
+
+            @Override
+            public void onFinished(int status, PersistableBundle extras) {
+                result.complete(extras);
+            }
+        });
         try {
-            return mStorageManager.benchmark(volId);
+            // Convert ms to ns
+            return result.get(3, TimeUnit.MINUTES).getLong("run", Long.MAX_VALUE) * 1000000;
+        } catch (Exception e) {
+            return Long.MAX_VALUE;
+        }
+    }
+
+    /** {@hide} */
+    public void benchmark(String volId, IVoldTaskListener listener) {
+        try {
+            mStorageManager.benchmark(volId, listener);
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
         }
diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java
index 433878e..2501f22 100755
--- a/core/java/android/provider/Settings.java
+++ b/core/java/android/provider/Settings.java
@@ -9297,11 +9297,20 @@
         public static final String DEFAULT_DNS_SERVER = "default_dns_server";
 
         /**
-         * Whether to disable DNS over TLS (boolean)
+         * The requested Private DNS mode (string), and an accompanying specifier (string).
+         *
+         * Currently, the specifier holds the chosen provider name when the mode requests
+         * a specific provider. It may be used to store the provider name even when the
+         * mode changes so that temporarily disabling and re-enabling the specific
+         * provider mode does not necessitate retyping the provider hostname.
          *
          * @hide
          */
-        public static final String DNS_TLS_DISABLED = "dns_tls_disabled";
+        public static final String PRIVATE_DNS_MODE = "private_dns_mode";
+        /**
+         * @hide
+         */
+        public static final String PRIVATE_DNS_SPECIFIER = "private_dns_specifier";
 
         /** {@hide} */
         public static final String
@@ -10387,7 +10396,9 @@
             DOCK_AUDIO_MEDIA_ENABLED,
             ENCODED_SURROUND_OUTPUT,
             LOW_POWER_MODE_TRIGGER_LEVEL,
-            BLUETOOTH_ON
+            BLUETOOTH_ON,
+            PRIVATE_DNS_MODE,
+            PRIVATE_DNS_SPECIFIER
         };
 
         /** @hide */
diff --git a/core/java/android/service/dreams/DreamService.java b/core/java/android/service/dreams/DreamService.java
index 6a15ade..2a245d0 100644
--- a/core/java/android/service/dreams/DreamService.java
+++ b/core/java/android/service/dreams/DreamService.java
@@ -680,8 +680,8 @@
      *
      * @return The screen state to use while dozing, such as {@link Display#STATE_ON},
      * {@link Display#STATE_DOZE}, {@link Display#STATE_DOZE_SUSPEND},
-     * or {@link Display#STATE_OFF}, or {@link Display#STATE_UNKNOWN} for the default
-     * behavior.
+     * {@link Display#STATE_ON_SUSPEND}, {@link Display#STATE_OFF}, or {@link Display#STATE_UNKNOWN}
+     * for the default behavior.
      *
      * @see #setDozeScreenState
      * @hide For use by system UI components only.
@@ -700,12 +700,18 @@
      * perform transitions between states while dozing to conserve power and
      * achieve various effects.
      * </p><p>
-     * It is recommended that the state be set to {@link Display#STATE_DOZE_SUSPEND}
-     * once the dream has completely finished drawing and before it releases its wakelock
-     * to allow the display hardware to be fully suspended.  While suspended, the
-     * display will preserve its on-screen contents or hand off control to dedicated
-     * doze hardware if the devices supports it.  If the doze suspend state is
-     * used, the dream must make sure to set the mode back
+     * Some devices will have dedicated hardware ("Sidekick") to animate
+     * the display content while the CPU sleeps. If the dream and the hardware support
+     * this, {@link Display#STATE_ON_SUSPEND} or {@link Display#STATE_DOZE_SUSPEND}
+     * will switch control to the Sidekick.
+     * </p><p>
+     * If not using Sidekick, it is recommended that the state be set to
+     * {@link Display#STATE_DOZE_SUSPEND} once the dream has completely
+     * finished drawing and before it releases its wakelock
+     * to allow the display hardware to be fully suspended.  While suspended,
+     * the display will preserve its on-screen contents.
+     * </p><p>
+     * If the doze suspend state is used, the dream must make sure to set the mode back
      * to {@link Display#STATE_DOZE} or {@link Display#STATE_ON} before drawing again
      * since the display updates may be ignored and not seen by the user otherwise.
      * </p><p>
@@ -716,8 +722,8 @@
      *
      * @param state The screen state to use while dozing, such as {@link Display#STATE_ON},
      * {@link Display#STATE_DOZE}, {@link Display#STATE_DOZE_SUSPEND},
-     * or {@link Display#STATE_OFF}, or {@link Display#STATE_UNKNOWN} for the default
-     * behavior.
+     * {@link Display#STATE_ON_SUSPEND}, {@link Display#STATE_OFF}, or {@link Display#STATE_UNKNOWN}
+     * for the default behavior.
      *
      * @hide For use by system UI components only.
      */
diff --git a/core/java/android/view/Display.java b/core/java/android/view/Display.java
index e7c3f92..6a44cdb 100644
--- a/core/java/android/view/Display.java
+++ b/core/java/android/view/Display.java
@@ -294,11 +294,10 @@
 
     /**
      * Display state: The display is dozing in a suspended low power state; it is still
-     * on but is optimized for showing static system-provided content while the device
-     * is non-interactive.  This mode may be used to conserve even more power by allowing
-     * the hardware to stop applying frame buffer updates from the graphics subsystem or
-     * to take over the display and manage it autonomously to implement low power always-on
-     * display functionality.
+     * on but the CPU is not updating it. This may be used in one of two ways: to show
+     * static system-provided content while the device is non-interactive, or to allow
+     * a "Sidekick" compute resource to update the display. For this reason, the
+     * CPU must not control the display in this mode.
      *
      * @see #getState
      * @see android.os.PowerManager#isInteractive
@@ -313,6 +312,18 @@
      */
     public static final int STATE_VR = 5;
 
+    /**
+     * Display state: The display is in a suspended full power state; it is still
+     * on but the CPU is not updating it. This may be used in one of two ways: to show
+     * static system-provided content while the device is non-interactive, or to allow
+     * a "Sidekick" compute resource to update the display. For this reason, the
+     * CPU must not control the display in this mode.
+     *
+     * @see #getState
+     * @see android.os.PowerManager#isInteractive
+     */
+    public static final int STATE_ON_SUSPEND = 6;
+
     /* The color mode constants defined below must be kept in sync with the ones in
      * system/core/include/system/graphics-base.h */
 
@@ -994,7 +1005,7 @@
      * Gets the state of the display, such as whether it is on or off.
      *
      * @return The state of the display: one of {@link #STATE_OFF}, {@link #STATE_ON},
-     * {@link #STATE_DOZE}, {@link #STATE_DOZE_SUSPEND}, or
+     * {@link #STATE_DOZE}, {@link #STATE_DOZE_SUSPEND}, {@link #STATE_ON_SUSPEND}, or
      * {@link #STATE_UNKNOWN}.
      */
     public int getState() {
@@ -1113,6 +1124,8 @@
                 return "DOZE_SUSPEND";
             case STATE_VR:
                 return "VR";
+            case STATE_ON_SUSPEND:
+                return "ON_SUSPEND";
             default:
                 return Integer.toString(state);
         }
@@ -1120,11 +1133,11 @@
 
     /**
      * Returns true if display updates may be suspended while in the specified
-     * display power state.
+     * display power state. In SUSPEND states, updates are absolutely forbidden.
      * @hide
      */
     public static boolean isSuspendedState(int state) {
-        return state == STATE_OFF || state == STATE_DOZE_SUSPEND;
+        return state == STATE_OFF || state == STATE_DOZE_SUSPEND || state == STATE_ON_SUSPEND;
     }
 
     /**
diff --git a/core/java/android/view/SurfaceControl.java b/core/java/android/view/SurfaceControl.java
index cd84147..5641009 100644
--- a/core/java/android/view/SurfaceControl.java
+++ b/core/java/android/view/SurfaceControl.java
@@ -295,6 +295,12 @@
     public static final int POWER_MODE_DOZE_SUSPEND = 3;
 
     /**
+     * Display power mode on: used while putting the screen into a suspended
+     * full power mode.  Use only with {@link SurfaceControl#setDisplayPowerMode}.
+     */
+    public static final int POWER_MODE_ON_SUSPEND = 4;
+
+    /**
      * A value for windowType used to indicate that the window should be omitted from screenshots
      * and display mirroring. A temporary workaround until we express such things with
      * the hierarchy.
diff --git a/core/java/com/android/internal/os/BatteryStatsImpl.java b/core/java/com/android/internal/os/BatteryStatsImpl.java
index 0535ebe..2ec64a5 100644
--- a/core/java/com/android/internal/os/BatteryStatsImpl.java
+++ b/core/java/com/android/internal/os/BatteryStatsImpl.java
@@ -9626,7 +9626,8 @@
     }
 
     public boolean isScreenOn(int state) {
-        return state == Display.STATE_ON || state == Display.STATE_VR;
+        return state == Display.STATE_ON || state == Display.STATE_VR
+            || state == Display.STATE_ON_SUSPEND;
     }
 
     public boolean isScreenOff(int state) {
diff --git a/core/java/com/android/internal/widget/ImageFloatingTextView.java b/core/java/com/android/internal/widget/ImageFloatingTextView.java
index 7870333..09f7282 100644
--- a/core/java/com/android/internal/widget/ImageFloatingTextView.java
+++ b/core/java/com/android/internal/widget/ImageFloatingTextView.java
@@ -176,8 +176,4 @@
         }
         return false;
     }
-
-    public int getLayoutHeight() {
-        return getLayout().getHeight();
-    }
 }
diff --git a/core/java/com/android/internal/widget/MessagingGroup.java b/core/java/com/android/internal/widget/MessagingGroup.java
new file mode 100644
index 0000000..792f921
--- /dev/null
+++ b/core/java/com/android/internal/widget/MessagingGroup.java
@@ -0,0 +1,393 @@
+/*
+ * 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.internal.widget;
+
+import android.annotation.AttrRes;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.StyleRes;
+import android.content.Context;
+import android.graphics.Color;
+import android.graphics.ColorFilter;
+import android.graphics.PorterDuff;
+import android.graphics.PorterDuffColorFilter;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.Icon;
+import android.text.TextUtils;
+import android.util.AttributeSet;
+import android.util.Pools;
+import android.view.Gravity;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewParent;
+import android.view.ViewTreeObserver;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.RemoteViews;
+
+import com.android.internal.R;
+import com.android.internal.util.NotificationColorUtil;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * A message of a {@link MessagingLayout}.
+ */
+@RemoteViews.RemoteView
+public class MessagingGroup extends LinearLayout implements MessagingLinearLayout.MessagingChild {
+    private static Pools.SimplePool<MessagingGroup> sInstancePool
+            = new Pools.SynchronizedPool<>(10);
+    private MessagingLinearLayout mMessageContainer;
+    private ImageFloatingTextView mSenderName;
+    private ImageView mAvatarView;
+    private String mAvatarSymbol = "";
+    private int mLayoutColor;
+    private CharSequence mAvatarName = "";
+    private Icon mAvatarIcon;
+    private ColorFilter mMessageBackgroundFilter;
+    private int mTextColor;
+    private List<MessagingMessage> mMessages;
+    private ArrayList<MessagingMessage> mAddedMessages = new ArrayList<>();
+    private boolean mFirstLayout;
+    private boolean mIsHidingAnimated;
+
+    public MessagingGroup(@NonNull Context context) {
+        super(context);
+    }
+
+    public MessagingGroup(@NonNull Context context, @Nullable AttributeSet attrs) {
+        super(context, attrs);
+    }
+
+    public MessagingGroup(@NonNull Context context, @Nullable AttributeSet attrs,
+            @AttrRes int defStyleAttr) {
+        super(context, attrs, defStyleAttr);
+    }
+
+    public MessagingGroup(@NonNull Context context, @Nullable AttributeSet attrs,
+            @AttrRes int defStyleAttr, @StyleRes int defStyleRes) {
+        super(context, attrs, defStyleAttr, defStyleRes);
+    }
+
+    @Override
+    protected void onFinishInflate() {
+        super.onFinishInflate();
+        mMessageContainer = findViewById(R.id.group_message_container);
+        mSenderName = findViewById(R.id.message_name);
+        mSenderName.addOnLayoutChangeListener(MessagingLayout.MESSAGING_PROPERTY_ANIMATOR);
+        mAvatarView = findViewById(R.id.message_icon);
+    }
+
+    public void setSender(CharSequence sender) {
+        if (sender == null) {
+            mAvatarView.setVisibility(GONE);
+            mSenderName.setVisibility(GONE);
+            setGravity(Gravity.END);
+            mMessageBackgroundFilter = new PorterDuffColorFilter(mLayoutColor,
+                    PorterDuff.Mode.SRC_ATOP);
+            mTextColor = NotificationColorUtil.isColorLight(mLayoutColor) ? getNormalTextColor()
+                    : Color.WHITE;
+        } else {
+            mSenderName.setText(sender);
+            mAvatarView.setVisibility(VISIBLE);
+            mSenderName.setVisibility(VISIBLE);
+            setGravity(Gravity.START);
+            mMessageBackgroundFilter = null;
+            mTextColor = getNormalTextColor();
+        }
+    }
+
+    private int getNormalTextColor() {
+        return mContext.getColor(R.color.notification_primary_text_color_light);
+    }
+
+    public void setAvatar(Icon icon) {
+        mAvatarIcon = icon;
+        mAvatarView.setImageIcon(icon);
+        mAvatarSymbol = "";
+        mLayoutColor = 0;
+        mAvatarName = "";
+    }
+
+    static MessagingGroup createGroup(MessagingLinearLayout layout) {;
+        MessagingGroup createdGroup = sInstancePool.acquire();
+        if (createdGroup == null) {
+            createdGroup = (MessagingGroup) LayoutInflater.from(layout.getContext()).inflate(
+                    R.layout.notification_template_messaging_group, layout,
+                    false);
+            createdGroup.addOnLayoutChangeListener(MessagingLayout.MESSAGING_PROPERTY_ANIMATOR);
+        }
+        layout.addView(createdGroup);
+        return createdGroup;
+    }
+
+    public void removeMessage(MessagingMessage messagingMessage) {
+        mMessageContainer.removeView(messagingMessage);
+        Runnable recycleRunnable = () -> {
+            mMessageContainer.removeTransientView(messagingMessage);
+            messagingMessage.recycle();
+            if (mMessageContainer.getChildCount() == 0
+                    && mMessageContainer.getTransientViewCount() == 0) {
+                ViewParent parent = getParent();
+                if (parent instanceof ViewGroup) {
+                    ((ViewGroup) parent).removeView(MessagingGroup.this);
+                }
+                setAvatar(null);
+                mAvatarView.setAlpha(1.0f);
+                mAvatarView.setTranslationY(0.0f);
+                mSenderName.setAlpha(1.0f);
+                mSenderName.setTranslationY(0.0f);
+                sInstancePool.release(MessagingGroup.this);
+            }
+        };
+        if (isShown()) {
+            mMessageContainer.addTransientView(messagingMessage, 0);
+            performRemoveAnimation(messagingMessage, recycleRunnable);
+            if (mMessageContainer.getChildCount() == 0) {
+                removeGroupAnimated(null);
+            }
+        } else {
+            recycleRunnable.run();
+        }
+
+    }
+
+    private void removeGroupAnimated(Runnable endAction) {
+        MessagingPropertyAnimator.fadeOut(mAvatarView, null);
+        MessagingPropertyAnimator.startLocalTranslationTo(mAvatarView,
+                (int) (-getHeight() * 0.5f), MessagingLayout.FAST_OUT_LINEAR_IN);
+        MessagingPropertyAnimator.fadeOut(mSenderName, null);
+        MessagingPropertyAnimator.startLocalTranslationTo(mSenderName,
+                (int) (-getHeight() * 0.5f), MessagingLayout.FAST_OUT_LINEAR_IN);
+        boolean endActionTriggered = false;
+        for (int i = mMessageContainer.getChildCount() - 1; i >= 0; i--) {
+            View child = mMessageContainer.getChildAt(i);
+            if (child.getVisibility() == View.GONE) {
+                continue;
+            }
+            final ViewGroup.LayoutParams lp = child.getLayoutParams();
+            if (lp instanceof MessagingLinearLayout.LayoutParams
+                    && ((MessagingLinearLayout.LayoutParams) lp).hide
+                    && !((MessagingLinearLayout.LayoutParams) lp).visibleBefore) {
+                continue;
+            }
+            Runnable childEndAction = endActionTriggered ? null : endAction;
+            performRemoveAnimation(child, childEndAction);
+            endActionTriggered = true;
+        }
+        if (!endActionTriggered && endAction != null) {
+            endAction.run();
+        }
+    }
+
+    public void performRemoveAnimation(View message,
+            Runnable recycleRunnable) {
+        MessagingPropertyAnimator.fadeOut(message, recycleRunnable);
+        MessagingPropertyAnimator.startLocalTranslationTo(message,
+                (int) (-getHeight() * 0.5f), MessagingLayout.FAST_OUT_LINEAR_IN);
+    }
+
+    public CharSequence getSenderName() {
+        return mSenderName.getText();
+    }
+
+    public void setSenderVisible(boolean visible) {
+        mSenderName.setVisibility(visible ? VISIBLE : GONE);
+    }
+
+    public static void dropCache() {
+        sInstancePool = new Pools.SynchronizedPool<>(10);
+    }
+
+    @Override
+    public int getMeasuredType() {
+        boolean hasNormal = false;
+        for (int i = mMessageContainer.getChildCount() - 1; i >= 0; i--) {
+            View child = mMessageContainer.getChildAt(i);
+            if (child instanceof MessagingLinearLayout.MessagingChild) {
+                int type = ((MessagingLinearLayout.MessagingChild) child).getMeasuredType();
+                if (type == MEASURED_TOO_SMALL) {
+                    if (hasNormal) {
+                        return MEASURED_SHORTENED;
+                    } else {
+                        return MEASURED_TOO_SMALL;
+                    }
+                } else if (type == MEASURED_SHORTENED) {
+                    return MEASURED_SHORTENED;
+                } else {
+                    hasNormal = true;
+                }
+            }
+        }
+        return MEASURED_NORMAL;
+    }
+
+    @Override
+    public int getConsumedLines() {
+        int result = 0;
+        for (int i = 0; i < mMessageContainer.getChildCount(); i++) {
+            View child = mMessageContainer.getChildAt(i);
+            if (child instanceof MessagingLinearLayout.MessagingChild) {
+                result += ((MessagingLinearLayout.MessagingChild) child).getConsumedLines();
+            }
+        }
+        // A group is usually taking up quite some space with the padding and the name, let's add 1
+        return result + 1;
+    }
+
+    @Override
+    public void setMaxDisplayedLines(int lines) {
+        mMessageContainer.setMaxDisplayedLines(lines);
+    }
+
+    @Override
+    public void hideAnimated() {
+        setIsHidingAnimated(true);
+        removeGroupAnimated(() -> setIsHidingAnimated(false));
+    }
+
+    @Override
+    public boolean isHidingAnimated() {
+        return mIsHidingAnimated;
+    }
+
+    private void setIsHidingAnimated(boolean isHiding) {
+        ViewParent parent = getParent();
+        mIsHidingAnimated = isHiding;
+        invalidate();
+        if (parent instanceof ViewGroup) {
+            ((ViewGroup) parent).invalidate();
+        }
+    }
+
+    public Icon getAvatarSymbolIfMatching(CharSequence avatarName, String avatarSymbol,
+            int layoutColor) {
+        if (mAvatarName.equals(avatarName) && mAvatarSymbol.equals(avatarSymbol)
+                && layoutColor == mLayoutColor) {
+            return mAvatarIcon;
+        }
+        return null;
+    }
+
+    public void setCreatedAvatar(Icon cachedIcon, CharSequence avatarName, String avatarSymbol,
+            int layoutColor) {
+        if (!mAvatarName.equals(avatarName) || !mAvatarSymbol.equals(avatarSymbol)
+                || layoutColor != mLayoutColor) {
+            setAvatar(cachedIcon);
+            mAvatarSymbol = avatarSymbol;
+            mLayoutColor = layoutColor;
+            mAvatarName = avatarName;
+        }
+    }
+
+    public void setLayoutColor(int layoutColor) {
+        mLayoutColor = layoutColor;
+    }
+
+    public void setMessages(List<MessagingMessage> group) {
+        // Let's now make sure all children are added and in the correct order
+        for (int messageIndex = 0; messageIndex < group.size(); messageIndex++) {
+            MessagingMessage message = group.get(messageIndex);
+            if (message.getGroup() != this) {
+                message.setMessagingGroup(this);
+                ViewParent parent = mMessageContainer.getParent();
+                if (parent instanceof ViewGroup) {
+                    ((ViewGroup) parent).removeView(message);
+                }
+                mMessageContainer.addView(message, messageIndex);
+                mAddedMessages.add(message);
+            }
+            if (messageIndex != mMessageContainer.indexOfChild(message)) {
+                mMessageContainer.removeView(message);
+                mMessageContainer.addView(message, messageIndex);
+            }
+            // Let's make sure the message color is correct
+            Drawable targetDrawable = message.getBackground();
+
+            if (targetDrawable != null) {
+                targetDrawable.mutate().setColorFilter(mMessageBackgroundFilter);
+            }
+            message.setTextColor(mTextColor);
+        }
+        mMessages = group;
+    }
+
+    @Override
+    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+        super.onLayout(changed, left, top, right, bottom);
+        if (!mAddedMessages.isEmpty()) {
+            getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
+                @Override
+                public boolean onPreDraw() {
+                    for (MessagingMessage message : mAddedMessages) {
+                        if (!message.isShown()) {
+                            continue;
+                        }
+                        MessagingPropertyAnimator.fadeIn(message);
+                        if (!mFirstLayout) {
+                            MessagingPropertyAnimator.startLocalTranslationFrom(message,
+                                    message.getHeight(), MessagingLayout.LINEAR_OUT_SLOW_IN);
+                        }
+                    }
+                    mAddedMessages.clear();
+                    getViewTreeObserver().removeOnPreDrawListener(this);
+                    return true;
+                }
+            });
+        }
+        mFirstLayout = false;
+    }
+
+    /**
+     * Calculates the group compatibility between this and another group.
+     *
+     * @param otherGroup the other group to compare it with
+     *
+     * @return 0 if the groups are totally incompatible or 1 + the number of matching messages if
+     *         they match.
+     */
+    public int calculateGroupCompatibility(MessagingGroup otherGroup) {
+        if (TextUtils.equals(getSenderName(),otherGroup.getSenderName())) {
+            int result = 1;
+            for (int i = 0; i < mMessages.size() && i < otherGroup.mMessages.size(); i++) {
+                MessagingMessage ownMessage = mMessages.get(mMessages.size() - 1 - i);
+                MessagingMessage otherMessage = otherGroup.mMessages.get(
+                        otherGroup.mMessages.size() - 1 - i);
+                if (!ownMessage.sameAs(otherMessage)) {
+                    return result;
+                }
+                result++;
+            }
+            return result;
+        }
+        return 0;
+    }
+
+    public View getSender() {
+        return mSenderName;
+    }
+
+    public View getAvatar() {
+        return mAvatarView;
+    }
+
+    public MessagingLinearLayout getMessageContainer() {
+        return mMessageContainer;
+    }
+}
diff --git a/core/java/com/android/internal/widget/MessagingLayout.java b/core/java/com/android/internal/widget/MessagingLayout.java
new file mode 100644
index 0000000..2acdc01
--- /dev/null
+++ b/core/java/com/android/internal/widget/MessagingLayout.java
@@ -0,0 +1,444 @@
+/*
+ * 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.internal.widget;
+
+import android.annotation.AttrRes;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.StyleRes;
+import android.app.Notification;
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.Rect;
+import android.graphics.drawable.Icon;
+import android.os.Bundle;
+import android.os.Parcelable;
+import android.text.TextUtils;
+import android.util.ArrayMap;
+import android.util.AttributeSet;
+import android.util.DisplayMetrics;
+import android.view.RemotableViewMethod;
+import android.view.View;
+import android.view.ViewTreeObserver;
+import android.view.animation.Interpolator;
+import android.view.animation.PathInterpolator;
+import android.widget.FrameLayout;
+import android.widget.RemoteViews;
+import android.widget.TextView;
+
+import com.android.internal.R;
+import com.android.internal.graphics.ColorUtils;
+import com.android.internal.util.NotificationColorUtil;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.function.Consumer;
+
+/**
+ * A custom-built layout for the Notification.MessagingStyle allows dynamic addition and removal
+ * messages and adapts the layout accordingly.
+ */
+@RemoteViews.RemoteView
+public class MessagingLayout extends FrameLayout {
+
+    private static final float COLOR_SHIFT_AMOUNT = 60;
+    private static final Consumer<MessagingMessage> REMOVE_MESSAGE
+            = MessagingMessage::removeMessage;
+    public static final Interpolator LINEAR_OUT_SLOW_IN = new PathInterpolator(0f, 0f, 0.2f, 1f);
+    public static final Interpolator FAST_OUT_LINEAR_IN = new PathInterpolator(0.4f, 0f, 1f, 1f);
+    public static final Interpolator FAST_OUT_SLOW_IN = new PathInterpolator(0.4f, 0f, 0.2f, 1f);
+    public static final OnLayoutChangeListener MESSAGING_PROPERTY_ANIMATOR
+            = new MessagingPropertyAnimator();
+    private List<MessagingMessage> mMessages = new ArrayList<>();
+    private List<MessagingMessage> mHistoricMessages = new ArrayList<>();
+    private MessagingLinearLayout mMessagingLinearLayout;
+    private View mContractedMessage;
+    private boolean mShowHistoricMessages;
+    private ArrayList<MessagingGroup> mGroups = new ArrayList<>();
+    private TextView mTitleView;
+    private int mLayoutColor;
+    private int mAvatarSize;
+    private Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
+    private Paint mTextPaint = new Paint();
+    private CharSequence mConversationTitle;
+    private Icon mLargeIcon;
+    private boolean mIsOneToOne;
+    private ArrayList<MessagingGroup> mAddedGroups = new ArrayList<>();
+
+    public MessagingLayout(@NonNull Context context) {
+        super(context);
+    }
+
+    public MessagingLayout(@NonNull Context context, @Nullable AttributeSet attrs) {
+        super(context, attrs);
+    }
+
+    public MessagingLayout(@NonNull Context context, @Nullable AttributeSet attrs,
+            @AttrRes int defStyleAttr) {
+        super(context, attrs, defStyleAttr);
+    }
+
+    public MessagingLayout(@NonNull Context context, @Nullable AttributeSet attrs,
+            @AttrRes int defStyleAttr, @StyleRes int defStyleRes) {
+        super(context, attrs, defStyleAttr, defStyleRes);
+    }
+
+    @Override
+    protected void onFinishInflate() {
+        super.onFinishInflate();
+        mMessagingLinearLayout = findViewById(R.id.notification_messaging);
+        mMessagingLinearLayout.setMessagingLayout(this);
+        // We still want to clip, but only on the top, since views can temporarily out of bounds
+        // during transitions.
+        DisplayMetrics displayMetrics = getResources().getDisplayMetrics();
+        Rect rect = new Rect(0, 0, displayMetrics.widthPixels, displayMetrics.heightPixels);
+        mMessagingLinearLayout.setClipBounds(rect);
+        mTitleView = findViewById(R.id.title);
+        mAvatarSize = getResources().getDimensionPixelSize(R.dimen.messaging_avatar_size);
+        mTextPaint.setTextAlign(Paint.Align.CENTER);
+        mTextPaint.setAntiAlias(true);
+    }
+
+    @RemotableViewMethod
+    public void setLargeIcon(Icon icon) {
+        mLargeIcon = icon;
+    }
+
+    @RemotableViewMethod
+    public void setData(Bundle extras) {
+        Parcelable[] messages = extras.getParcelableArray(Notification.EXTRA_MESSAGES);
+        List<Notification.MessagingStyle.Message> newMessages
+                = Notification.MessagingStyle.Message.getMessagesFromBundleArray(messages);
+        Parcelable[] histMessages = extras.getParcelableArray(Notification.EXTRA_HISTORIC_MESSAGES);
+        List<Notification.MessagingStyle.Message> newHistoricMessages
+                = Notification.MessagingStyle.Message.getMessagesFromBundleArray(histMessages);
+        mConversationTitle = null;
+        TextView headerText = findViewById(R.id.header_text);
+        if (headerText != null) {
+            mConversationTitle = headerText.getText();
+        }
+        bind(newMessages, newHistoricMessages);
+    }
+
+    private void bind(List<Notification.MessagingStyle.Message> newMessages,
+            List<Notification.MessagingStyle.Message> newHistoricMessages) {
+
+        List<MessagingMessage> historicMessages = createMessages(newHistoricMessages,
+                true /* isHistoric */);
+        List<MessagingMessage> messages = createMessages(newMessages, false /* isHistoric */);
+        addMessagesToGroups(historicMessages, messages);
+
+        // Let's remove the remaining messages
+        mMessages.forEach(REMOVE_MESSAGE);
+        mHistoricMessages.forEach(REMOVE_MESSAGE);
+
+        mMessages = messages;
+        mHistoricMessages = historicMessages;
+
+        updateContractedMessage();
+        updateHistoricMessageVisibility();
+        updateTitleAndNamesDisplay();
+    }
+
+    private void updateTitleAndNamesDisplay() {
+        ArrayMap<CharSequence, String> uniqueNames = new ArrayMap<>();
+        ArrayMap<Character, CharSequence> uniqueCharacters = new ArrayMap<>();
+        for (int i = 0; i < mGroups.size(); i++) {
+            MessagingGroup group = mGroups.get(i);
+            CharSequence senderName = group.getSenderName();
+            if (TextUtils.isEmpty(senderName)) {
+                continue;
+            }
+            boolean visible = !mIsOneToOne;
+            group.setSenderVisible(visible);
+            if ((visible || mLargeIcon == null) && !uniqueNames.containsKey(senderName)) {
+                char c = senderName.charAt(0);
+                if (uniqueCharacters.containsKey(c)) {
+                    // this character was already used, lets make it more unique. We first need to
+                    // resolve the existing character if it exists
+                    CharSequence existingName = uniqueCharacters.get(c);
+                    if (existingName != null) {
+                        uniqueNames.put(existingName, findNameSplit((String) existingName));
+                        uniqueCharacters.put(c, null);
+                    }
+                    uniqueNames.put(senderName, findNameSplit((String) senderName));
+                } else {
+                    uniqueNames.put(senderName, Character.toString(c));
+                    uniqueCharacters.put(c, senderName);
+                }
+            }
+        }
+
+        // Now that we have the correct symbols, let's look what we have cached
+        ArrayMap<CharSequence, Icon> cachedAvatars = new ArrayMap<>();
+        for (int i = 0; i < mGroups.size(); i++) {
+            // Let's now set the avatars
+            MessagingGroup group = mGroups.get(i);
+            CharSequence senderName = group.getSenderName();
+            if (TextUtils.isEmpty(senderName) || (mIsOneToOne && mLargeIcon != null)) {
+                continue;
+            }
+            String symbol = uniqueNames.get(senderName);
+            Icon cachedIcon = group.getAvatarSymbolIfMatching(senderName,
+                    symbol, mLayoutColor);
+            if (cachedIcon != null) {
+                cachedAvatars.put(senderName, cachedIcon);
+            }
+        }
+
+        for (int i = 0; i < mGroups.size(); i++) {
+            // Let's now set the avatars
+            MessagingGroup group = mGroups.get(i);
+            CharSequence senderName = group.getSenderName();
+            if (TextUtils.isEmpty(senderName)) {
+                continue;
+            }
+            if (mIsOneToOne && mLargeIcon != null) {
+                group.setAvatar(mLargeIcon);
+            } else {
+                Icon cachedIcon = cachedAvatars.get(senderName);
+                if (cachedIcon == null) {
+                    cachedIcon = createAvatarSymbol(senderName, uniqueNames.get(senderName),
+                            mLayoutColor);
+                    cachedAvatars.put(senderName, cachedIcon);
+                }
+                group.setCreatedAvatar(cachedIcon, senderName, uniqueNames.get(senderName),
+                        mLayoutColor);
+            }
+        }
+    }
+
+    public Icon createAvatarSymbol(CharSequence senderName, String symbol, int layoutColor) {
+        Bitmap bitmap = Bitmap.createBitmap(mAvatarSize, mAvatarSize, Bitmap.Config.ARGB_8888);
+        Canvas canvas = new Canvas(bitmap);
+        float radius = mAvatarSize / 2.0f;
+        int color = findColor(senderName, layoutColor);
+        mPaint.setColor(color);
+        canvas.drawCircle(radius, radius, radius, mPaint);
+        boolean needDarkText  = ColorUtils.calculateLuminance(color) > 0.5f;
+        mTextPaint.setColor(needDarkText ? Color.BLACK : Color.WHITE);
+        mTextPaint.setTextSize(symbol.length() == 1 ? mAvatarSize * 0.75f : mAvatarSize * 0.4f);
+        int yPos = (int) (radius - ((mTextPaint.descent() + mTextPaint.ascent()) / 2)) ;
+        canvas.drawText(symbol, radius, yPos, mTextPaint);
+        return Icon.createWithBitmap(bitmap);
+    }
+
+    private int findColor(CharSequence senderName, int layoutColor) {
+        double luminance = NotificationColorUtil.calculateLuminance(layoutColor);
+        float shift = Math.abs(senderName.hashCode()) % 5 / 4.0f - 0.5f;
+
+        // we need to offset the range if the luminance is too close to the borders
+        shift += Math.max(COLOR_SHIFT_AMOUNT / 2.0f / 100 - luminance, 0);
+        shift -= Math.max(COLOR_SHIFT_AMOUNT / 2.0f / 100 - (1.0f - luminance), 0);
+        return NotificationColorUtil.getShiftedColor(layoutColor,
+                (int) (shift * COLOR_SHIFT_AMOUNT));
+    }
+
+    private String findNameSplit(String existingName) {
+        String[] split = existingName.split(" ");
+        if (split.length > 1) {
+            return Character.toString(split[0].charAt(0))
+                    + Character.toString(split[1].charAt(0));
+        }
+        return existingName.substring(0, 1);
+    }
+
+    @RemotableViewMethod
+    public void setLayoutColor(int color) {
+        mLayoutColor = color;
+    }
+
+    @RemotableViewMethod
+    public void setIsOneToOne(boolean oneToOne) {
+        mIsOneToOne = oneToOne;
+    }
+
+    private void addMessagesToGroups(List<MessagingMessage> historicMessages,
+            List<MessagingMessage> messages) {
+        // Let's first find our groups!
+        List<List<MessagingMessage>> groups = new ArrayList<>();
+        List<CharSequence> senders = new ArrayList<>();
+
+        // Lets first find the groups
+        findGroups(historicMessages, messages, groups, senders);
+
+        // Let's now create the views and reorder them accordingly
+        createGroupViews(groups, senders);
+    }
+
+    private void createGroupViews(List<List<MessagingMessage>> groups, List<CharSequence> senders) {
+        mGroups.clear();
+        for (int groupIndex = 0; groupIndex < groups.size(); groupIndex++) {
+            List<MessagingMessage> group = groups.get(groupIndex);
+            MessagingGroup newGroup = null;
+            // we'll just take the first group that exists or create one there is none
+            for (int messageIndex = group.size() - 1; messageIndex >= 0; messageIndex--) {
+                MessagingMessage message = group.get(messageIndex);
+                newGroup = message.getGroup();
+                if (newGroup != null) {
+                    break;
+                }
+            }
+            if (newGroup == null) {
+                newGroup = MessagingGroup.createGroup(mMessagingLinearLayout);
+                mAddedGroups.add(newGroup);
+            }
+            newGroup.setLayoutColor(mLayoutColor);
+            newGroup.setSender(senders.get(groupIndex));
+            mGroups.add(newGroup);
+
+            if (mMessagingLinearLayout.indexOfChild(newGroup) != groupIndex) {
+                mMessagingLinearLayout.removeView(newGroup);
+                mMessagingLinearLayout.addView(newGroup, groupIndex);
+            }
+            newGroup.setMessages(group);
+        }
+    }
+
+    private void findGroups(List<MessagingMessage> historicMessages,
+            List<MessagingMessage> messages, List<List<MessagingMessage>> groups,
+            List<CharSequence> senders) {
+        CharSequence currentSender = null;
+        List<MessagingMessage> currentGroup = null;
+        int histSize = historicMessages.size();
+        for (int i = 0; i < histSize + messages.size(); i++) {
+            MessagingMessage message;
+            if (i < histSize) {
+                message = historicMessages.get(i);
+            } else {
+                message = messages.get(i - histSize);
+            }
+            boolean isNewGroup = currentGroup == null;
+            CharSequence sender = message.getMessage().getSender();
+            isNewGroup |= !TextUtils.equals(sender, currentSender);
+            if (isNewGroup) {
+                currentGroup = new ArrayList<>();
+                groups.add(currentGroup);
+                senders.add(sender);
+                currentSender = sender;
+            }
+            currentGroup.add(message);
+        }
+    }
+
+    private void updateContractedMessage() {
+        for (int i = mMessages.size() - 1; i >= 0; i--) {
+            MessagingMessage m = mMessages.get(i);
+            // Incoming messages have a non-empty sender.
+            if (!TextUtils.isEmpty(m.getMessage().getSender())) {
+                mContractedMessage = m;
+                return;
+            }
+        }
+        if (!mMessages.isEmpty()) {
+            // No incoming messages, fall back to outgoing message
+            mContractedMessage = mMessages.get(mMessages.size() - 1);
+            return;
+        }
+        mContractedMessage = null;
+    }
+
+    /**
+     * Creates new messages, reusing existing ones if they are available.
+     *
+     * @param newMessages the messages to parse.
+     */
+    private List<MessagingMessage> createMessages(
+            List<Notification.MessagingStyle.Message> newMessages, boolean historic) {
+        List<MessagingMessage> result = new ArrayList<>();;
+        for (int i = 0; i < newMessages.size(); i++) {
+            Notification.MessagingStyle.Message m = newMessages.get(i);
+            MessagingMessage message = findAndRemoveMatchingMessage(m);
+            if (message == null) {
+                message = MessagingMessage.createMessage(this, m);
+                message.addOnLayoutChangeListener(MESSAGING_PROPERTY_ANIMATOR);
+            }
+            message.setIsHistoric(historic);
+            result.add(message);
+        }
+        return result;
+    }
+
+    private MessagingMessage findAndRemoveMatchingMessage(Notification.MessagingStyle.Message m) {
+        for (int i = 0; i < mMessages.size(); i++) {
+            MessagingMessage existing = mMessages.get(i);
+            if (existing.sameAs(m)) {
+                mMessages.remove(i);
+                return existing;
+            }
+        }
+        for (int i = 0; i < mHistoricMessages.size(); i++) {
+            MessagingMessage existing = mHistoricMessages.get(i);
+            if (existing.sameAs(m)) {
+                mHistoricMessages.remove(i);
+                return existing;
+            }
+        }
+        return null;
+    }
+
+    public void showHistoricMessages(boolean show) {
+        mShowHistoricMessages = show;
+        updateHistoricMessageVisibility();
+    }
+
+    private void updateHistoricMessageVisibility() {
+        for (int i = 0; i < mHistoricMessages.size(); i++) {
+            MessagingMessage existing = mHistoricMessages.get(i);
+            existing.setVisibility(mShowHistoricMessages ? VISIBLE : GONE);
+        }
+    }
+
+    @Override
+    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+        super.onLayout(changed, left, top, right, bottom);
+        if (!mAddedGroups.isEmpty()) {
+            getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
+                @Override
+                public boolean onPreDraw() {
+                    for (MessagingGroup group : mAddedGroups) {
+                        if (!group.isShown()) {
+                            continue;
+                        }
+                        MessagingPropertyAnimator.fadeIn(group.getAvatar());
+                        MessagingPropertyAnimator.fadeIn(group.getSender());
+                        MessagingPropertyAnimator.startLocalTranslationFrom(group,
+                                group.getHeight(), LINEAR_OUT_SLOW_IN);
+                    }
+                    mAddedGroups.clear();
+                    getViewTreeObserver().removeOnPreDrawListener(this);
+                    return true;
+                }
+            });
+        }
+    }
+
+    public View getContractedMessage() {
+        return mContractedMessage;
+    }
+
+    public MessagingLinearLayout getMessagingLinearLayout() {
+        return mMessagingLinearLayout;
+    }
+
+    public ArrayList<MessagingGroup> getMessagingGroups() {
+        return mGroups;
+    }
+}
diff --git a/core/java/com/android/internal/widget/MessagingLinearLayout.java b/core/java/com/android/internal/widget/MessagingLinearLayout.java
index 70473a0..f0ef370 100644
--- a/core/java/com/android/internal/widget/MessagingLinearLayout.java
+++ b/core/java/com/android/internal/widget/MessagingLinearLayout.java
@@ -36,28 +36,14 @@
 @RemoteViews.RemoteView
 public class MessagingLinearLayout extends ViewGroup {
 
-    private static final int NOT_MEASURED_BEFORE = -1;
     /**
      * Spacing to be applied between views.
      */
     private int mSpacing;
 
-    /**
-     * The maximum height allowed.
-     */
-    private int mMaxHeight;
+    private int mMaxDisplayedLines = Integer.MAX_VALUE;
 
-    private int mIndentLines;
-
-    /**
-     * Id of the child that's also visible in the contracted layout.
-     */
-    private int mContractedChildId;
-    /**
-     * The last measured with in a layout pass if it was measured before or
-     * {@link #NOT_MEASURED_BEFORE} if this is the first layout pass.
-     */
-    private int mLastMeasuredWidth = NOT_MEASURED_BEFORE;
+    private MessagingLayout mMessagingLayout;
 
     public MessagingLinearLayout(Context context, @Nullable AttributeSet attrs) {
         super(context, attrs);
@@ -79,7 +65,6 @@
         a.recycle();
     }
 
-
     @Override
     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
         // This is essentially a bottom-up linear layout that only adds children that fit entirely
@@ -91,118 +76,67 @@
                 break;
         }
         int widthSize = MeasureSpec.getSize(widthMeasureSpec);
-        boolean recalculateVisibility = mLastMeasuredWidth == NOT_MEASURED_BEFORE
-                || getMeasuredHeight() != targetHeight
-                || mLastMeasuredWidth != widthSize;
-
-        final int count = getChildCount();
-        if (recalculateVisibility) {
-            // We only need to recalculate the view visibilities if the view wasn't measured already
-            // in this pass, otherwise we may drop messages here already since we are measured
-            // exactly with what we returned before, which was optimized already with the
-            // line-indents.
-            for (int i = 0; i < count; ++i) {
-                final View child = getChildAt(i);
-                final LayoutParams lp = (LayoutParams) child.getLayoutParams();
-                lp.hide = true;
-            }
-
-            int totalHeight = mPaddingTop + mPaddingBottom;
-            boolean first = true;
-
-            // Starting from the bottom: we measure every view as if it were the only one. If it still
-
-            // fits, we take it, otherwise we stop there.
-            for (int i = count - 1; i >= 0 && totalHeight < targetHeight; i--) {
-                if (getChildAt(i).getVisibility() == GONE) {
-                    continue;
-                }
-                final View child = getChildAt(i);
-                LayoutParams lp = (LayoutParams) getChildAt(i).getLayoutParams();
-                ImageFloatingTextView textChild = null;
-                if (child instanceof ImageFloatingTextView) {
-                    // Pretend we need the image padding for all views, we don't know which
-                    // one will end up needing to do this (might end up not using all the space,
-                    // but calculating this exactly would be more expensive).
-                    textChild = (ImageFloatingTextView) child;
-                    textChild.setNumIndentLines(mIndentLines == 2 ? 3 : mIndentLines);
-                }
-
-                int spacing = first ? 0 : mSpacing;
-                measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, totalHeight
-                        - mPaddingTop - mPaddingBottom + spacing);
-
-                final int childHeight = child.getMeasuredHeight();
-                int newHeight = Math.max(totalHeight, totalHeight + childHeight + lp.topMargin +
-                        lp.bottomMargin + spacing);
-                first = false;
-                boolean measuredTooSmall = false;
-                if (textChild != null) {
-                    measuredTooSmall = childHeight < textChild.getLayoutHeight()
-                            + textChild.getPaddingTop() + textChild.getPaddingBottom();
-                }
-
-                if (newHeight <= targetHeight && !measuredTooSmall) {
-                    totalHeight = newHeight;
-                    lp.hide = false;
-                } else {
-                    break;
-                }
-            }
-        }
 
         // Now that we know which views to take, fix up the indents and see what width we get.
         int measuredWidth = mPaddingLeft + mPaddingRight;
-        int imageLines = mIndentLines;
-        // Need to redo the height because it may change due to changing indents.
-        int totalHeight = mPaddingTop + mPaddingBottom;
-        boolean first = true;
-        for (int i = 0; i < count; i++) {
+        final int count = getChildCount();
+        int totalHeight;
+        for (int i = 0; i < count; ++i) {
             final View child = getChildAt(i);
             final LayoutParams lp = (LayoutParams) child.getLayoutParams();
-
-            if (child.getVisibility() == GONE || lp.hide) {
-                continue;
-            }
-
-            if (child instanceof ImageFloatingTextView) {
-                ImageFloatingTextView textChild = (ImageFloatingTextView) child;
-                if (imageLines == 2 && textChild.getLineCount() > 2) {
-                    // HACK: If we need indent for two lines, and they're coming from the same
-                    // view, we need extra spacing to compensate for the lack of margins,
-                    // so add an extra line of indent.
-                    imageLines = 3;
-                }
-                boolean changed = textChild.setNumIndentLines(Math.max(0, imageLines));
-                if (changed || !recalculateVisibility) {
-                    final int childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec,
-                            mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin,
-                            lp.width);
-                    // we want to measure it at most as high as it is currently, otherwise we'll
-                    // drop later lines
-                    final int childHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec,
-                            targetHeight - child.getMeasuredHeight(), lp.height);
-
-                    child.measure(childWidthMeasureSpec, childHeightMeasureSpec);;
-                }
-                imageLines -= textChild.getLineCount();
-            }
-
-            measuredWidth = Math.max(measuredWidth,
-                    child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin
-                            + mPaddingLeft + mPaddingRight);
-            totalHeight = Math.max(totalHeight, totalHeight + child.getMeasuredHeight() +
-                    lp.topMargin + lp.bottomMargin + (first ? 0 : mSpacing));
-            first = false;
+            lp.hide = true;
         }
 
+        totalHeight = mPaddingTop + mPaddingBottom;
+        boolean first = true;
+        int linesRemaining = mMaxDisplayedLines;
+
+        // Starting from the bottom: we measure every view as if it were the only one. If it still
+        // fits, we take it, otherwise we stop there.
+        for (int i = count - 1; i >= 0 && totalHeight < targetHeight; i--) {
+            if (getChildAt(i).getVisibility() == GONE) {
+                continue;
+            }
+            final View child = getChildAt(i);
+            LayoutParams lp = (LayoutParams) getChildAt(i).getLayoutParams();
+            MessagingChild messagingChild = null;
+            if (child instanceof MessagingChild) {
+                messagingChild = (MessagingChild) child;
+                messagingChild.setMaxDisplayedLines(linesRemaining);
+            }
+            int spacing = first ? 0 : mSpacing;
+            measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, totalHeight
+                    - mPaddingTop - mPaddingBottom + spacing);
+
+            final int childHeight = child.getMeasuredHeight();
+            int newHeight = Math.max(totalHeight, totalHeight + childHeight + lp.topMargin +
+                    lp.bottomMargin + spacing);
+            first = false;
+            int measureType = MessagingChild.MEASURED_NORMAL;
+            if (messagingChild != null) {
+                measureType = messagingChild.getMeasuredType();
+                linesRemaining -= messagingChild.getConsumedLines();
+            }
+            boolean isShortened = measureType == MessagingChild.MEASURED_SHORTENED;
+            boolean isTooSmall = measureType == MessagingChild.MEASURED_TOO_SMALL;
+            if (newHeight <= targetHeight && !isTooSmall) {
+                totalHeight = newHeight;
+                measuredWidth = Math.max(measuredWidth,
+                        child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin
+                                + mPaddingLeft + mPaddingRight);
+                lp.hide = false;
+                if (isShortened || linesRemaining <= 0) {
+                    break;
+                }
+            } else {
+                break;
+            }
+        }
 
         setMeasuredDimension(
                 resolveSize(Math.max(getSuggestedMinimumWidth(), measuredWidth),
                         widthMeasureSpec),
-                resolveSize(Math.max(getSuggestedMinimumHeight(), totalHeight),
-                        heightMeasureSpec));
-        mLastMeasuredWidth = widthSize;
+                Math.max(getSuggestedMinimumHeight(), totalHeight));
     }
 
     @Override
@@ -221,14 +155,23 @@
         childTop = mPaddingTop;
 
         boolean first = true;
-
+        final boolean shown = isShown();
         for (int i = 0; i < count; i++) {
             final View child = getChildAt(i);
-            final LayoutParams lp = (LayoutParams) child.getLayoutParams();
-
-            if (child.getVisibility() == GONE || lp.hide) {
+            if (child.getVisibility() == GONE) {
                 continue;
             }
+            final LayoutParams lp = (LayoutParams) child.getLayoutParams();
+            MessagingChild messagingChild = (MessagingChild) child;
+            if (lp.hide) {
+                if (shown && lp.visibleBefore) {
+                    messagingChild.hideAnimated();
+                }
+                lp.visibleBefore = false;
+                continue;
+            } else {
+                lp.visibleBefore = true;
+            }
 
             final int childWidth = child.getMeasuredWidth();
             final int childHeight = child.getMeasuredHeight();
@@ -251,14 +194,16 @@
 
             first = false;
         }
-        mLastMeasuredWidth = NOT_MEASURED_BEFORE;
     }
 
     @Override
     protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
         final LayoutParams lp = (LayoutParams) child.getLayoutParams();
         if (lp.hide) {
-            return true;
+            MessagingChild messagingChild = (MessagingChild) child;
+            if (!messagingChild.isHidingAnimated()) {
+                return true;
+            }
         }
         return super.drawChild(canvas, child, drawingTime);
     }
@@ -284,31 +229,37 @@
     }
 
     /**
-     * Sets how many lines should be indented to avoid a floating image.
+     * Sets how many lines should be displayed at most
      */
     @RemotableViewMethod
-    public void setNumIndentLines(int numberLines) {
-        mIndentLines = numberLines;
+    public void setMaxDisplayedLines(int numberLines) {
+        mMaxDisplayedLines = numberLines;
     }
 
-    /**
-     * Set id of the child that's also visible in the contracted layout.
-     */
-    @RemotableViewMethod
-    public void setContractedChildId(int contractedChildId) {
-        mContractedChildId = contractedChildId;
+    public void setMessagingLayout(MessagingLayout layout) {
+        mMessagingLayout = layout;
     }
 
-    /**
-     * Get id of the child that's also visible in the contracted layout.
-     */
-    public int getContractedChildId() {
-        return mContractedChildId;
+    public MessagingLayout getMessagingLayout() {
+        return mMessagingLayout;
+    }
+
+    public interface MessagingChild {
+        int MEASURED_NORMAL = 0;
+        int MEASURED_SHORTENED = 1;
+        int MEASURED_TOO_SMALL = 2;
+
+        int getMeasuredType();
+        int getConsumedLines();
+        void setMaxDisplayedLines(int lines);
+        void hideAnimated();
+        boolean isHidingAnimated();
     }
 
     public static class LayoutParams extends MarginLayoutParams {
 
-        boolean hide = false;
+        public boolean hide = false;
+        public boolean visibleBefore = false;
 
         public LayoutParams(Context c, AttributeSet attrs) {
             super(c, attrs);
diff --git a/core/java/com/android/internal/widget/MessagingMessage.java b/core/java/com/android/internal/widget/MessagingMessage.java
new file mode 100644
index 0000000..f09621f
--- /dev/null
+++ b/core/java/com/android/internal/widget/MessagingMessage.java
@@ -0,0 +1,197 @@
+/*
+ * 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.internal.widget;
+
+import android.annotation.AttrRes;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.StyleRes;
+import android.app.Notification;
+import android.content.Context;
+import android.text.Layout;
+import android.util.AttributeSet;
+import android.util.Pools;
+import android.view.LayoutInflater;
+import android.view.ViewGroup;
+import android.view.ViewParent;
+import android.widget.RemoteViews;
+
+import com.android.internal.R;
+
+import java.util.Objects;
+
+/**
+ * A message of a {@link MessagingLayout}.
+ */
+@RemoteViews.RemoteView
+public class MessagingMessage extends ImageFloatingTextView implements
+        MessagingLinearLayout.MessagingChild {
+
+    private static Pools.SimplePool<MessagingMessage> sInstancePool
+            = new Pools.SynchronizedPool<>(10);
+    private Notification.MessagingStyle.Message mMessage;
+    private MessagingGroup mGroup;
+    private boolean mIsHistoric;
+    private boolean mIsHidingAnimated;
+
+    public MessagingMessage(@NonNull Context context) {
+        super(context);
+    }
+
+    public MessagingMessage(@NonNull Context context, @Nullable AttributeSet attrs) {
+        super(context, attrs);
+    }
+
+    public MessagingMessage(@NonNull Context context, @Nullable AttributeSet attrs,
+            @AttrRes int defStyleAttr) {
+        super(context, attrs, defStyleAttr);
+    }
+
+    public MessagingMessage(@NonNull Context context, @Nullable AttributeSet attrs,
+            @AttrRes int defStyleAttr, @StyleRes int defStyleRes) {
+        super(context, attrs, defStyleAttr, defStyleRes);
+    }
+
+    private void setMessage(Notification.MessagingStyle.Message message) {
+        mMessage = message;
+        setText(message.getText());
+    }
+
+    public Notification.MessagingStyle.Message getMessage() {
+        return mMessage;
+    }
+
+    boolean sameAs(Notification.MessagingStyle.Message message) {
+        if (!Objects.equals(message.getText(), mMessage.getText())) {
+            return false;
+        }
+        if (!Objects.equals(message.getSender(), mMessage.getSender())) {
+            return false;
+        }
+        if (!Objects.equals(message.getTimestamp(), mMessage.getTimestamp())) {
+            return false;
+        }
+        return true;
+    }
+
+    boolean sameAs(MessagingMessage message) {
+        return sameAs(message.getMessage());
+    }
+
+    static MessagingMessage createMessage(MessagingLayout layout,
+            Notification.MessagingStyle.Message m) {
+        MessagingLinearLayout messagingLinearLayout = layout.getMessagingLinearLayout();
+        MessagingMessage createdMessage = sInstancePool.acquire();
+        if (createdMessage == null) {
+            createdMessage = (MessagingMessage) LayoutInflater.from(layout.getContext()).inflate(
+                    R.layout.notification_template_messaging_message, messagingLinearLayout,
+                    false);
+        }
+        createdMessage.setMessage(m);
+        return createdMessage;
+    }
+
+    public void removeMessage() {
+        mGroup.removeMessage(this);
+    }
+
+    public void recycle() {
+        mGroup = null;
+        mMessage = null;
+        setAlpha(1.0f);
+        setTranslationY(0);
+        sInstancePool.release(this);
+    }
+
+    public void setMessagingGroup(MessagingGroup group) {
+        mGroup = group;
+    }
+
+    public static void dropCache() {
+        sInstancePool = new Pools.SynchronizedPool<>(10);
+    }
+
+    public void setIsHistoric(boolean isHistoric) {
+        mIsHistoric = isHistoric;
+    }
+
+    public MessagingGroup getGroup() {
+        return mGroup;
+    }
+
+    @Override
+    public int getMeasuredType() {
+        boolean measuredTooSmall = getMeasuredHeight()
+                < getLayoutHeight() + getPaddingTop() + getPaddingBottom();
+        if (measuredTooSmall) {
+            return MEASURED_TOO_SMALL;
+        } else {
+            Layout layout = getLayout();
+            if (layout == null) {
+                return MEASURED_TOO_SMALL;
+            }
+            if (layout.getEllipsisCount(layout.getLineCount() - 1) > 0) {
+                return MEASURED_SHORTENED;
+            } else {
+                return MEASURED_NORMAL;
+            }
+        }
+    }
+
+    @Override
+    public void hideAnimated() {
+        setIsHidingAnimated(true);
+        mGroup.performRemoveAnimation(this, () -> setIsHidingAnimated(false));
+    }
+
+    private void setIsHidingAnimated(boolean isHiding) {
+        ViewParent parent = getParent();
+        mIsHidingAnimated = isHiding;
+        invalidate();
+        if (parent instanceof ViewGroup) {
+            ((ViewGroup) parent).invalidate();
+        }
+    }
+
+    @Override
+    public boolean isHidingAnimated() {
+        return mIsHidingAnimated;
+    }
+
+    @Override
+    public void setMaxDisplayedLines(int lines) {
+        setMaxLines(lines);
+    }
+
+    @Override
+    public int getConsumedLines() {
+        return getLineCount();
+    }
+
+    public int getLayoutHeight() {
+        Layout layout = getLayout();
+        if (layout == null) {
+            return 0;
+        }
+        return layout.getHeight();
+    }
+
+    @Override
+    public boolean hasOverlappingRendering() {
+        return false;
+    }
+}
diff --git a/core/java/com/android/internal/widget/MessagingPropertyAnimator.java b/core/java/com/android/internal/widget/MessagingPropertyAnimator.java
new file mode 100644
index 0000000..7c3ab7f
--- /dev/null
+++ b/core/java/com/android/internal/widget/MessagingPropertyAnimator.java
@@ -0,0 +1,233 @@
+/*
+ * 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.internal.widget;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.ObjectAnimator;
+import android.util.IntProperty;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.animation.Interpolator;
+import android.view.animation.PathInterpolator;
+
+import com.android.internal.R;
+
+/**
+ * A listener that automatically starts animations when the layout bounds change.
+ */
+public class MessagingPropertyAnimator implements View.OnLayoutChangeListener {
+    static final long APPEAR_ANIMATION_LENGTH = 210;
+    private static final Interpolator ALPHA_IN = new PathInterpolator(0.4f, 0f, 1f, 1f);
+    public static final Interpolator ALPHA_OUT = new PathInterpolator(0f, 0f, 0.8f, 1f);
+    private static final int TAG_LOCAL_TRANSLATION_ANIMATOR = R.id.tag_local_translation_y_animator;
+    private static final int TAG_LOCAL_TRANSLATION_Y = R.id.tag_local_translation_y;
+    private static final int TAG_LAYOUT_TOP = R.id.tag_layout_top;
+    private static final int TAG_ALPHA_ANIMATOR = R.id.tag_alpha_animator;
+    private static final ViewClippingUtil.ClippingParameters CLIPPING_PARAMETERS =
+            view -> view.getId() == com.android.internal.R.id.notification_messaging;
+    private static final IntProperty<View> LOCAL_TRANSLATION_Y =
+            new IntProperty<View>("localTranslationY") {
+                @Override
+                public void setValue(View object, int value) {
+                    setLocalTranslationY(object, value);
+                }
+
+                @Override
+                public Integer get(View object) {
+                    return getLocalTranslationY(object);
+                }
+            };
+
+    @Override
+    public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft,
+            int oldTop, int oldRight, int oldBottom) {
+        int oldHeight = oldBottom - oldTop;
+        Integer layoutTop = (Integer) v.getTag(TAG_LAYOUT_TOP);
+        if (layoutTop != null) {
+            oldTop = layoutTop;
+        }
+        int topChange = oldTop - top;
+        if (oldHeight == 0 || topChange == 0 || !v.isShown() || isGone(v)) {
+            // First layout
+            return;
+        }
+        if (layoutTop != null) {
+            v.setTagInternal(TAG_LAYOUT_TOP, top);
+        }
+        int newHeight = bottom - top;
+        int heightDifference = oldHeight - newHeight;
+        // Only add the difference if the height changes and it's getting smaller
+        heightDifference = Math.max(heightDifference, 0);
+        startLocalTranslationFrom(v, topChange + heightDifference + getLocalTranslationY(v));
+    }
+
+    private boolean isGone(View view) {
+        if (view.getVisibility() == View.GONE) {
+            return true;
+        }
+        final ViewGroup.LayoutParams lp = view.getLayoutParams();
+        if (lp instanceof MessagingLinearLayout.LayoutParams
+                && ((MessagingLinearLayout.LayoutParams) lp).hide) {
+            return true;
+        }
+        return false;
+    }
+
+    public static void startLocalTranslationFrom(View v, int startTranslation) {
+        startLocalTranslationFrom(v, startTranslation, MessagingLayout.FAST_OUT_SLOW_IN);
+    }
+
+    public static void startLocalTranslationFrom(View v, int startTranslation,
+            Interpolator interpolator) {
+        startLocalTranslation(v, startTranslation, 0, interpolator);
+    }
+
+    public static void startLocalTranslationTo(View v, int endTranslation,
+            Interpolator interpolator) {
+        startLocalTranslation(v, getLocalTranslationY(v), endTranslation, interpolator);
+    }
+
+    public static int getLocalTranslationY(View v) {
+        Integer tag = (Integer) v.getTag(TAG_LOCAL_TRANSLATION_Y);
+        if (tag == null) {
+            return 0;
+        }
+        return tag;
+    }
+
+    private static void setLocalTranslationY(View v, int value) {
+        v.setTagInternal(TAG_LOCAL_TRANSLATION_Y, value);
+        updateTopAndBottom(v);
+    }
+
+    private static void updateTopAndBottom(View v) {
+        int layoutTop = (int) v.getTag(TAG_LAYOUT_TOP);
+        int localTranslation = getLocalTranslationY(v);
+        int height = v.getHeight();
+        v.setTop(layoutTop + localTranslation);
+        v.setBottom(layoutTop + height + localTranslation);
+    }
+
+    private static void startLocalTranslation(final View v, int start, int end,
+            Interpolator interpolator) {
+        ObjectAnimator existing = (ObjectAnimator) v.getTag(TAG_LOCAL_TRANSLATION_ANIMATOR);
+        if (existing != null) {
+            existing.cancel();
+        }
+        ObjectAnimator animator = ObjectAnimator.ofInt(v, LOCAL_TRANSLATION_Y, start, end);
+        Integer layoutTop = (Integer) v.getTag(TAG_LAYOUT_TOP);
+        if (layoutTop == null) {
+            layoutTop = v.getTop();
+            v.setTagInternal(TAG_LAYOUT_TOP, layoutTop);
+        }
+        setLocalTranslationY(v, start);
+        animator.setInterpolator(interpolator);
+        animator.setDuration(APPEAR_ANIMATION_LENGTH);
+        animator.addListener(new AnimatorListenerAdapter() {
+            public boolean mCancelled;
+
+            @Override
+            public void onAnimationEnd(Animator animation) {
+                v.setTagInternal(TAG_LOCAL_TRANSLATION_ANIMATOR, null);
+                setClippingDeactivated(v, false);
+                if (!mCancelled) {
+                    setLocalTranslationY(v, 0);
+                    v.setTagInternal(TAG_LAYOUT_TOP, null);
+                }
+            }
+
+            @Override
+            public void onAnimationCancel(Animator animation) {
+                mCancelled = true;
+            }
+        });
+        setClippingDeactivated(v, true);
+        v.setTagInternal(TAG_LOCAL_TRANSLATION_ANIMATOR, animator);
+        animator.start();
+    }
+
+    public static void fadeIn(final View v) {
+        ObjectAnimator existing = (ObjectAnimator) v.getTag(TAG_ALPHA_ANIMATOR);
+        if (existing != null) {
+            existing.cancel();
+        }
+        if (v.getVisibility() == View.INVISIBLE) {
+            v.setVisibility(View.VISIBLE);
+        }
+        ObjectAnimator animator = ObjectAnimator.ofFloat(v, View.ALPHA,
+                0.0f, 1.0f);
+        v.setAlpha(0.0f);
+        animator.setInterpolator(ALPHA_IN);
+        animator.setDuration(APPEAR_ANIMATION_LENGTH);
+        animator.addListener(new AnimatorListenerAdapter() {
+            @Override
+            public void onAnimationEnd(Animator animation) {
+                v.setTagInternal(TAG_ALPHA_ANIMATOR, null);
+                updateLayerType(v, false /* animating */);
+            }
+        });
+        updateLayerType(v, true /* animating */);
+        v.setTagInternal(TAG_ALPHA_ANIMATOR, animator);
+        animator.start();
+    }
+
+    private static void updateLayerType(View view, boolean animating) {
+        if (view.hasOverlappingRendering() && animating) {
+            view.setLayerType(View.LAYER_TYPE_HARDWARE, null);
+        } else if (view.getLayerType() == View.LAYER_TYPE_HARDWARE) {
+            view.setLayerType(View.LAYER_TYPE_NONE, null);
+        }
+    }
+
+    public static void fadeOut(final View view, Runnable endAction) {
+        ObjectAnimator existing = (ObjectAnimator) view.getTag(TAG_ALPHA_ANIMATOR);
+        if (existing != null) {
+            existing.cancel();
+        }
+        ObjectAnimator animator = ObjectAnimator.ofFloat(view, View.ALPHA,
+                view.getAlpha(), 0.0f);
+        animator.setInterpolator(ALPHA_OUT);
+        animator.setDuration(APPEAR_ANIMATION_LENGTH);
+        animator.addListener(new AnimatorListenerAdapter() {
+            @Override
+            public void onAnimationEnd(Animator animation) {
+                view.setTagInternal(TAG_ALPHA_ANIMATOR, null);
+                updateLayerType(view, false /* animating */);
+                if (endAction != null) {
+                    endAction.run();
+                }
+            }
+        });
+        updateLayerType(view, true /* animating */);
+        view.setTagInternal(TAG_ALPHA_ANIMATOR, animator);
+        animator.start();
+    }
+
+    public static void setClippingDeactivated(final View transformedView, boolean deactivated) {
+        ViewClippingUtil.setClippingDeactivated(transformedView, deactivated,
+                CLIPPING_PARAMETERS);
+    }
+
+    public static boolean isAnimatingTranslation(View v) {
+        return v.getTag(TAG_LOCAL_TRANSLATION_ANIMATOR) != null;
+    }
+
+    public static boolean isAnimatingAlpha(View v) {
+        return v.getTag(TAG_ALPHA_ANIMATOR) != null;
+    }
+}
diff --git a/core/java/com/android/internal/widget/RemeasuringLinearLayout.java b/core/java/com/android/internal/widget/RemeasuringLinearLayout.java
new file mode 100644
index 0000000..e352b45
--- /dev/null
+++ b/core/java/com/android/internal/widget/RemeasuringLinearLayout.java
@@ -0,0 +1,68 @@
+/*
+ * 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.internal.widget;
+
+import android.annotation.Nullable;
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.View;
+import android.widget.LinearLayout;
+import android.widget.RemoteViews;
+
+/**
+ * A LinearLayout that sets it's height again after the last measure pass. This is needed for
+ * MessagingLayouts where groups need to be able to snap it's height to.
+ */
+@RemoteViews.RemoteView
+public class RemeasuringLinearLayout extends LinearLayout {
+
+    public RemeasuringLinearLayout(Context context) {
+        super(context);
+    }
+
+    public RemeasuringLinearLayout(Context context, @Nullable AttributeSet attrs) {
+        super(context, attrs);
+    }
+
+    public RemeasuringLinearLayout(Context context, @Nullable AttributeSet attrs,
+            int defStyleAttr) {
+        super(context, attrs, defStyleAttr);
+    }
+
+    public RemeasuringLinearLayout(Context context, AttributeSet attrs, int defStyleAttr,
+            int defStyleRes) {
+        super(context, attrs, defStyleAttr, defStyleRes);
+    }
+
+    @Override
+    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+        int count = getChildCount();
+        int height = 0;
+        for (int i = 0; i < count; ++i) {
+            final View child = getChildAt(i);
+            if (child == null || child.getVisibility() == View.GONE) {
+                continue;
+            }
+
+            final LayoutParams lp = (LayoutParams) child.getLayoutParams();
+            height = Math.max(height, height + child.getMeasuredHeight() + lp.topMargin +
+                    lp.bottomMargin);
+        }
+        setMeasuredDimension(getMeasuredWidth(), height);
+    }
+}
diff --git a/core/java/com/android/internal/widget/ViewClippingUtil.java b/core/java/com/android/internal/widget/ViewClippingUtil.java
new file mode 100644
index 0000000..59bbed4
--- /dev/null
+++ b/core/java/com/android/internal/widget/ViewClippingUtil.java
@@ -0,0 +1,108 @@
+/*
+ * 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.internal.widget;
+
+import android.util.ArraySet;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewParent;
+
+import com.android.internal.R;
+
+/**
+ * A utility class that allows to clip views and their parents to allow for better transitions
+ */
+public class ViewClippingUtil {
+    private static final int CLIP_CLIPPING_SET = R.id.clip_children_set_tag;
+    private static final int CLIP_CHILDREN_TAG = R.id.clip_children_tag;
+    private static final int CLIP_TO_PADDING = R.id.clip_to_padding_tag;
+
+    public static void setClippingDeactivated(final View transformedView, boolean deactivated,
+        ClippingParameters clippingParameters) {
+        if (!deactivated && !clippingParameters.isClippingEnablingAllowed(transformedView)) {
+            return;
+        }
+        if (!(transformedView.getParent() instanceof ViewGroup)) {
+            return;
+        }
+        ViewGroup parent = (ViewGroup) transformedView.getParent();
+        while (true) {
+            if (!deactivated && !clippingParameters.isClippingEnablingAllowed(transformedView)) {
+                return;
+            }
+            ArraySet<View> clipSet = (ArraySet<View>) parent.getTag(CLIP_CLIPPING_SET);
+            if (clipSet == null) {
+                clipSet = new ArraySet<>();
+                parent.setTagInternal(CLIP_CLIPPING_SET, clipSet);
+            }
+            Boolean clipChildren = (Boolean) parent.getTag(CLIP_CHILDREN_TAG);
+            if (clipChildren == null) {
+                clipChildren = parent.getClipChildren();
+                parent.setTagInternal(CLIP_CHILDREN_TAG, clipChildren);
+            }
+            Boolean clipToPadding = (Boolean) parent.getTag(CLIP_TO_PADDING);
+            if (clipToPadding == null) {
+                clipToPadding = parent.getClipToPadding();
+                parent.setTagInternal(CLIP_TO_PADDING, clipToPadding);
+            }
+            if (!deactivated) {
+                clipSet.remove(transformedView);
+                if (clipSet.isEmpty()) {
+                    parent.setClipChildren(clipChildren);
+                    parent.setClipToPadding(clipToPadding);
+                    parent.setTagInternal(CLIP_CLIPPING_SET, null);
+                    clippingParameters.onClippingStateChanged(parent, true);
+                }
+            } else {
+                clipSet.add(transformedView);
+                parent.setClipChildren(false);
+                parent.setClipToPadding(false);
+                clippingParameters.onClippingStateChanged(parent, false);
+            }
+            if (clippingParameters.shouldFinish(parent)) {
+                return;
+            }
+            final ViewParent viewParent = parent.getParent();
+            if (viewParent instanceof ViewGroup) {
+                parent = (ViewGroup) viewParent;
+            } else {
+                return;
+            }
+        }
+    }
+
+    public interface ClippingParameters {
+        /**
+         * Should we stop clipping at this view? If true is returned, {@param view} is the last view
+         * where clipping is activated / deactivated.
+         */
+        boolean shouldFinish(View view);
+
+        /**
+         * Is it allowed to enable clipping on this view.
+         */
+        default boolean isClippingEnablingAllowed(View view) {
+            return !MessagingPropertyAnimator.isAnimatingTranslation(view);
+        }
+
+        /**
+         * A method that is called whenever the view starts clipping again / stops clipping to the
+         * children and padding.
+         */
+        default void onClippingStateChanged(View view, boolean isClipping) {};
+    }
+}
diff --git a/core/jni/android_os_Parcel.cpp b/core/jni/android_os_Parcel.cpp
index f0ac79a..d18c172 100644
--- a/core/jni/android_os_Parcel.cpp
+++ b/core/jni/android_os_Parcel.cpp
@@ -554,18 +554,6 @@
     }
 }
 
-static void android_os_Parcel_clearFileDescriptor(JNIEnv* env, jclass clazz, jobject object)
-{
-    if (object == NULL) {
-        jniThrowNullPointerException(env, NULL);
-        return;
-    }
-    int fd = jniGetFDFromFileDescriptor(env, object);
-    if (fd >= 0) {
-        jniSetFileDescriptorOfFD(env, object, -1);
-    }
-}
-
 static jlong android_os_Parcel_create(JNIEnv* env, jclass clazz)
 {
     Parcel* parcel = new Parcel();
@@ -811,7 +799,6 @@
     {"openFileDescriptor",        "(Ljava/lang/String;I)Ljava/io/FileDescriptor;", (void*)android_os_Parcel_openFileDescriptor},
     {"dupFileDescriptor",         "(Ljava/io/FileDescriptor;)Ljava/io/FileDescriptor;", (void*)android_os_Parcel_dupFileDescriptor},
     {"closeFileDescriptor",       "(Ljava/io/FileDescriptor;)V", (void*)android_os_Parcel_closeFileDescriptor},
-    {"clearFileDescriptor",       "(Ljava/io/FileDescriptor;)V", (void*)android_os_Parcel_clearFileDescriptor},
 
     {"nativeCreate",              "()J", (void*)android_os_Parcel_create},
     {"nativeFreeBuffer",          "(J)J", (void*)android_os_Parcel_freeBuffer},
diff --git a/core/res/res/drawable/ic_reply_notification_large.xml b/core/res/res/drawable/ic_reply_notification_large.xml
new file mode 100644
index 0000000..e75afdd
--- /dev/null
+++ b/core/res/res/drawable/ic_reply_notification_large.xml
@@ -0,0 +1,29 @@
+<!--
+  ~ 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
+  -->
+<inset xmlns:android="http://schemas.android.com/apk/res/android"
+       android:inset="8dp">
+    <vector android:width="24dp"
+            android:height="24dp"
+            android:viewportHeight="24.0"
+            android:viewportWidth="24.0">
+        <path
+            android:fillColor="#FFFFFFFF"
+            android:pathData="M10.0,9.0L10.0,5.0l-7.0,7.0 7.0,7.0l0.0,-4.1c5.0,0.0 8.5,1.6 11.0,5.1 -1.0,-5.0 -4.0,-10.0 -11.0,-11.0z"/>
+        <path
+            android:fillColor="#00000000"
+            android:pathData="M0 0h24v24H0z"/>
+    </vector>
+</inset>
diff --git a/core/res/res/drawable/messaging_message_background.xml b/core/res/res/drawable/messaging_message_background.xml
new file mode 100644
index 0000000..8a2096a
--- /dev/null
+++ b/core/res/res/drawable/messaging_message_background.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ 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
+  -->
+
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+    android:shape="rectangle"
+    android:tint="#14000000">
+    <corners android:radius="4dp" />
+    <padding android:bottom="6dp"
+        android:left="8dp"
+        android:right="8dp"
+        android:top="6dp" />
+</shape>
diff --git a/core/res/res/layout/chooser_row.xml b/core/res/res/layout/chooser_row.xml
index 6c1271d..cf81260 100644
--- a/core/res/res/layout/chooser_row.xml
+++ b/core/res/res/layout/chooser_row.xml
@@ -22,8 +22,7 @@
               android:layout_height="100dp"
               android:gravity="start|top"
               android:paddingStart="@dimen/chooser_grid_padding"
-              android:paddingEnd="@dimen/chooser_grid_padding"
-              android:weightSum="4">
+              android:paddingEnd="@dimen/chooser_grid_padding">
 
 </LinearLayout>
 
diff --git a/core/res/res/layout/notification_material_action.xml b/core/res/res/layout/notification_material_action.xml
index 548ee05..3c9f6ee 100644
--- a/core/res/res/layout/notification_material_action.xml
+++ b/core/res/res/layout/notification_material_action.xml
@@ -25,6 +25,7 @@
     android:layout_marginStart="4dp"
     android:textColor="@color/notification_default_color"
     android:singleLine="true"
+    android:textAlignment="viewStart"
     android:ellipsize="end"
     android:background="@drawable/notification_material_action_background"
     />
diff --git a/core/res/res/layout/notification_material_action_tombstone.xml b/core/res/res/layout/notification_material_action_tombstone.xml
index 1f59ea0..817f298 100644
--- a/core/res/res/layout/notification_material_action_tombstone.xml
+++ b/core/res/res/layout/notification_material_action_tombstone.xml
@@ -26,6 +26,7 @@
     android:textColor="#555555"
     android:singleLine="true"
     android:ellipsize="end"
+    android:textAlignment="viewStart"
     android:alpha="0.5"
     android:enabled="false"
     android:background="@drawable/notification_material_action_background"
diff --git a/core/res/res/layout/notification_template_material_messaging.xml b/core/res/res/layout/notification_template_material_messaging.xml
index fd5154a..f72230b 100644
--- a/core/res/res/layout/notification_template_material_messaging.xml
+++ b/core/res/res/layout/notification_template_material_messaging.xml
@@ -14,13 +14,17 @@
   ~ See the License for the specific language governing permissions and
   ~ limitations under the License
   -->
-<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+<com.android.internal.widget.MessagingLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
     android:id="@+id/status_bar_latest_event_content"
     android:layout_width="match_parent"
     android:layout_height="wrap_content"
     android:tag="messaging"
     >
-    <include layout="@layout/notification_template_header" />
+    <include layout="@layout/notification_template_header"
+             android:layout_width="wrap_content"
+             android:layout_height="@dimen/notification_header_height"
+             android:layout_marginEnd="56dp"/>
     <LinearLayout
             android:id="@+id/notification_action_list_margin_target"
             android:layout_width="match_parent"
@@ -39,7 +43,6 @@
             android:paddingEnd="@dimen/notification_content_margin_end"
             android:minHeight="@dimen/notification_min_content_height"
             android:layout_marginBottom="@dimen/notification_content_margin_bottom"
-            android:clipToPadding="false"
             android:orientation="vertical"
             >
             <include layout="@layout/notification_template_part_line1"
@@ -50,31 +53,14 @@
                 android:id="@+id/notification_messaging"
                 android:layout_width="match_parent"
                 android:layout_height="wrap_content"
-                android:spacing="@dimen/notification_messaging_spacing" >
-                <com.android.internal.widget.ImageFloatingTextView android:id="@+id/inbox_text0"
-                    style="@style/Widget.Material.Notification.MessagingText"
-                    />
-                <com.android.internal.widget.ImageFloatingTextView android:id="@+id/inbox_text1"
-                    style="@style/Widget.Material.Notification.MessagingText"
-                    />
-                <com.android.internal.widget.ImageFloatingTextView android:id="@+id/inbox_text2"
-                    style="@style/Widget.Material.Notification.MessagingText"
-                    />
-                <com.android.internal.widget.ImageFloatingTextView android:id="@+id/inbox_text3"
-                    style="@style/Widget.Material.Notification.MessagingText"
-                    />
-                <com.android.internal.widget.ImageFloatingTextView android:id="@+id/inbox_text4"
-                    style="@style/Widget.Material.Notification.MessagingText"
-                    />
-                <com.android.internal.widget.ImageFloatingTextView android:id="@+id/inbox_text5"
-                    style="@style/Widget.Material.Notification.MessagingText"
-                    />
-                <com.android.internal.widget.ImageFloatingTextView android:id="@+id/inbox_text6"
-                    style="@style/Widget.Material.Notification.MessagingText"
-                    />
-            </com.android.internal.widget.MessagingLinearLayout>
+                android:layout_marginTop="8dp"
+                android:spacing="@dimen/notification_messaging_spacing" />
         </LinearLayout>
     </LinearLayout>
     <include layout="@layout/notification_material_action_list" />
-    <include layout="@layout/notification_template_right_icon" />
-</FrameLayout>
+    <include layout="@layout/notification_template_right_icon"
+             android:layout_width="wrap_content"
+             android:layout_height="wrap_content"
+             android:layout_marginTop="18dp"
+             android:layout_gravity="top|end"/>
+</com.android.internal.widget.MessagingLayout>
diff --git a/core/res/res/layout/notification_template_messaging_group.xml b/core/res/res/layout/notification_template_messaging_group.xml
new file mode 100644
index 0000000..8973ceb
--- /dev/null
+++ b/core/res/res/layout/notification_template_messaging_group.xml
@@ -0,0 +1,50 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2016 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
+  -->
+<com.android.internal.widget.MessagingGroup
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/status_bar_latest_event_content"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:orientation="horizontal" >
+    <ImageView
+        android:id="@+id/message_icon"
+        android:layout_width="@dimen/messaging_avatar_size"
+        android:layout_height="@dimen/messaging_avatar_size"
+        android:layout_marginEnd="8dp"
+        android:scaleType="centerCrop"
+        android:importantForAccessibility="no" />
+    <com.android.internal.widget.RemeasuringLinearLayout
+        android:id="@+id/message_group_and_sender_container"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:orientation="vertical">
+        <com.android.internal.widget.MessagingLinearLayout
+            android:id="@+id/group_message_container"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:spacing="2dp"
+            android:layout_weight="1"/>
+        <com.android.internal.widget.ImageFloatingTextView
+            android:id="@+id/message_name"
+            style="@style/Widget.Material.Notification.MessagingName"
+            android:layout_width="wrap_content"
+            android:paddingStart="8dp"
+            android:paddingEnd="8dp"
+            android:paddingTop="2dp"
+        />
+    </com.android.internal.widget.RemeasuringLinearLayout>
+</com.android.internal.widget.MessagingGroup>
diff --git a/core/res/res/layout/notification_template_messaging_message.xml b/core/res/res/layout/notification_template_messaging_message.xml
new file mode 100644
index 0000000..ab6466c
--- /dev/null
+++ b/core/res/res/layout/notification_template_messaging_message.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ 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
+  -->
+<com.android.internal.widget.MessagingMessage
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/message_text"
+    style="@style/Widget.Material.Notification.MessagingText"
+/>
diff --git a/core/res/res/layout/notification_template_right_icon.xml b/core/res/res/layout/notification_template_right_icon.xml
index d379256..8fb2887 100644
--- a/core/res/res/layout/notification_template_right_icon.xml
+++ b/core/res/res/layout/notification_template_right_icon.xml
@@ -19,12 +19,12 @@
              android:id="@+id/right_icon_container"
              android:layout_width="wrap_content"
              android:layout_height="wrap_content"
+             android:layout_marginTop="36dp"
              android:layout_gravity="top|end">
     <ImageView android:id="@+id/right_icon"
                android:layout_width="@dimen/notification_right_icon_size"
                android:layout_height="@dimen/notification_right_icon_size"
                android:layout_gravity="top|end"
-               android:layout_marginTop="36dp"
                android:layout_marginEnd="@dimen/notification_content_margin_end"
                android:scaleType="centerCrop"
                android:importantForAccessibility="no" />
@@ -32,7 +32,7 @@
                android:layout_width="16dp"
                android:layout_height="16dp"
                android:layout_gravity="top|end"
-               android:layout_marginTop="64dp"
+               android:layout_marginTop="28dp"
                android:layout_marginEnd="12dp"
                android:background="@drawable/notification_reply_background"
                android:src="@drawable/ic_reply_notification"
diff --git a/core/res/res/values/dimens.xml b/core/res/res/values/dimens.xml
index 947fcf1..946216c 100644
--- a/core/res/res/values/dimens.xml
+++ b/core/res/res/values/dimens.xml
@@ -605,6 +605,8 @@
     <!-- The size of the right icon image when on low ram -->
     <dimen name="notification_right_icon_size_low_ram">40dp</dimen>
 
+    <dimen name="messaging_avatar_size">24dp</dimen>
+
     <!-- Max width/height of the autofill data set picker as a fraction of the screen width/height -->
     <dimen name="autofill_dataset_picker_max_size">90%</dimen>
 
diff --git a/core/res/res/values/ids.xml b/core/res/res/values/ids.xml
index cd3624d..b40117e 100644
--- a/core/res/res/values/ids.xml
+++ b/core/res/res/values/ids.xml
@@ -139,6 +139,27 @@
   <!-- Accessibility action identifier for {@link android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction#ACTION_MOVE_WINDOW}. -->
   <item type="id" name="accessibilityActionMoveWindow" />
 
+  <!-- A tag used to save an animator in local y translation -->
+  <item type="id" name="tag_local_translation_y_animator" />
+
+  <!-- A tag used to save the local translation y -->
+  <item type="id" name="tag_local_translation_y" />
+
+  <!-- A tag used to save the original top of a view -->
+  <item type="id" name="tag_layout_top" />
+
+  <!-- A tag used to save an animator in alpha -->
+  <item type="id" name="tag_alpha_animator" />
+
+  <!-- A tag used to save the clip children in a tag -->
+  <item type="id" name="clip_children_tag" />
+
+  <!-- A tag used to save the set of deactivated children that clip -->
+  <item type="id" name="clip_children_set_tag" />
+
+  <!-- A tag used to save the clip to padding in a tag -->
+  <item type="id" name="clip_to_padding_tag" />
+
   <!-- Action used to manually trigger an autofill request -->
   <item type="id" name="autofill" />
 
diff --git a/core/res/res/values/styles_material.xml b/core/res/res/values/styles_material.xml
index cddf99a..2cd4dcb 100644
--- a/core/res/res/values/styles_material.xml
+++ b/core/res/res/values/styles_material.xml
@@ -505,8 +505,17 @@
         <item name="layout_width">match_parent</item>
         <item name="layout_height">wrap_content</item>
         <item name="ellipsize">end</item>
-        <item name="visibility">gone</item>
         <item name="textAppearance">@style/TextAppearance.Material.Notification</item>
+        <item name="background">@drawable/messaging_message_background</item>
+    </style>
+
+    <style name="Widget.Material.Notification.MessagingName" parent="Widget.Material.Light.TextView">
+        <item name="layout_width">wrap_content</item>
+        <item name="layout_height">wrap_content</item>
+        <item name="ellipsize">end</item>
+        <item name="textAppearance">@style/TextAppearance.Material.Notification</item>
+        <item name="textColor">@color/notification_primary_text_color_light</item>
+        <item name="textSize">12sp</item>
     </style>
 
     <!-- Widget Styles -->
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index 32758e8..fd0012d 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -3105,6 +3105,22 @@
   <java-symbol type="dimen" name="chooser_service_spacing" />
   <java-symbol type="bool" name="config_showSysuiShutdown" />
 
+  <java-symbol type="layout" name="notification_template_messaging_message" />
+  <java-symbol type="layout" name="notification_template_messaging_group" />
+  <java-symbol type="id" name="message_text" />
+  <java-symbol type="id" name="message_name" />
+  <java-symbol type="id" name="message_icon" />
+  <java-symbol type="id" name="group_message_container" />
+  <java-symbol type="id" name="tag_local_translation_y_animator" />
+  <java-symbol type="id" name="tag_local_translation_y" />
+  <java-symbol type="id" name="tag_layout_top" />
+  <java-symbol type="id" name="tag_alpha_animator" />
+  <java-symbol type="id" name="clip_children_set_tag" />
+  <java-symbol type="id" name="clip_to_padding_tag" />
+  <java-symbol type="id" name="clip_children_tag" />
+  <java-symbol type="drawable" name="ic_reply_notification_large" />
+  <java-symbol type="dimen" name="messaging_avatar_size" />
+
   <java-symbol type="integer" name="config_stableDeviceDisplayWidth" />
   <java-symbol type="integer" name="config_stableDeviceDisplayHeight" />
   <java-symbol type="bool" name="config_display_no_service_when_sim_unready" />
diff --git a/core/tests/coretests/src/android/provider/SettingsBackupTest.java b/core/tests/coretests/src/android/provider/SettingsBackupTest.java
index ebe0527..1002939 100644
--- a/core/tests/coretests/src/android/provider/SettingsBackupTest.java
+++ b/core/tests/coretests/src/android/provider/SettingsBackupTest.java
@@ -180,7 +180,6 @@
                     Settings.Global.DNS_RESOLVER_MIN_SAMPLES,
                     Settings.Global.DNS_RESOLVER_SAMPLE_VALIDITY_SECONDS,
                     Settings.Global.DNS_RESOLVER_SUCCESS_THRESHOLD_PERCENT,
-                    Settings.Global.DNS_TLS_DISABLED,
                     Settings.Global.DOCK_SOUNDS_ENABLED_WHEN_ACCESSIBILITY,
                     Settings.Global.DOWNLOAD_MAX_BYTES_OVER_MOBILE,
                     Settings.Global.DOWNLOAD_RECOMMENDED_MAX_BYTES_OVER_MOBILE,
diff --git a/core/tests/coretests/src/android/widget/TextViewActivityTest.java b/core/tests/coretests/src/android/widget/TextViewActivityTest.java
index bbdbdb1..0245570 100644
--- a/core/tests/coretests/src/android/widget/TextViewActivityTest.java
+++ b/core/tests/coretests/src/android/widget/TextViewActivityTest.java
@@ -49,6 +49,11 @@
 
 import static org.hamcrest.Matchers.anyOf;
 import static org.hamcrest.Matchers.is;
+import static org.mockito.Matchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
 
 import android.app.Activity;
 import android.app.Instrumentation;
@@ -633,15 +638,21 @@
 
     @Test
     public void testSetSelectionAndActionMode() throws Throwable {
+        final TextView textView = mActivity.findViewById(R.id.textview);
+        final ActionMode.Callback amCallback = mock(ActionMode.Callback.class);
+        when(amCallback.onCreateActionMode(any(ActionMode.class), any(Menu.class)))
+                .thenReturn(true);
+        when(amCallback.onPrepareActionMode(any(ActionMode.class), any(Menu.class)))
+                .thenReturn(true);
+        textView.setCustomSelectionActionModeCallback(amCallback);
+
         final String text = "abc def";
         onView(withId(R.id.textview)).perform(replaceText(text));
-
-        final TextView textView = mActivity.findViewById(R.id.textview);
         mActivityRule.runOnUiThread(
                 () -> Selection.setSelection((Spannable) textView.getText(), 0, 3));
         mInstrumentation.waitForIdleSync();
         // Don't automatically start action mode.
-        // TODO: Implement assertActionModeNotStarted()
+        verify(amCallback, never()).onCreateActionMode(any(ActionMode.class), any(Menu.class));
         // Make sure that "Select All" is included in the selection action mode when the entire text
         // is not selected.
         onView(withId(R.id.textview)).perform(longPressOnTextAtIndex(text.indexOf('e')));
diff --git a/core/tests/coretests/src/com/android/internal/widget/MessagingLinearLayoutTest.java b/core/tests/coretests/src/com/android/internal/widget/MessagingLinearLayoutTest.java
index dfe8511..3919fdd 100644
--- a/core/tests/coretests/src/com/android/internal/widget/MessagingLinearLayoutTest.java
+++ b/core/tests/coretests/src/com/android/internal/widget/MessagingLinearLayoutTest.java
@@ -23,11 +23,11 @@
 import android.content.Context;
 import android.support.test.InstrumentationRegistry;
 import android.support.test.filters.SmallTest;
+import android.text.Layout;
 import android.view.LayoutInflater;
 import android.view.View.MeasureSpec;
 
 import com.android.frameworks.coretests.R;
-import com.google.common.base.Function;
 
 import org.junit.Before;
 import org.junit.Test;
@@ -52,33 +52,28 @@
 
     @Test
     public void testSingleChild() {
-        FakeImageFloatingTextView child = fakeChild((i) -> 3);
+        FakeImageFloatingTextView child = fakeChild(3);
 
-        mView.setNumIndentLines(2);
         mView.addView(child);
 
         mView.measure(WIDTH_SPEC, HEIGHT_SPEC);
         mView.layout(0, 0, mView.getMeasuredWidth(), mView.getMeasuredHeight());
 
-        assertEquals(3, child.getNumIndentLines());
         assertFalse(child.isHidden());
         assertEquals(150, mView.getMeasuredHeight());
     }
 
     @Test
     public void testLargeSmall() {
-        FakeImageFloatingTextView child1 = fakeChild((i) -> 3);
-        FakeImageFloatingTextView child2 = fakeChild((i) -> 1);
+        FakeImageFloatingTextView child1 = fakeChild(3);
+        FakeImageFloatingTextView child2 = fakeChild(1);
 
-        mView.setNumIndentLines(2);
         mView.addView(child1);
         mView.addView(child2);
 
         mView.measure(WIDTH_SPEC, HEIGHT_SPEC);
         mView.layout(0, 0, mView.getMeasuredWidth(), mView.getMeasuredHeight());
 
-        assertEquals(3, child1.getNumIndentLines());
-        assertEquals(0, child2.getNumIndentLines());
         assertFalse("child1 should not be hidden", child1.isHidden());
         assertFalse("child2 should not be hidden", child2.isHidden());
         assertEquals(205, mView.getMeasuredHeight());
@@ -86,18 +81,15 @@
 
     @Test
     public void testSmallSmall() {
-        FakeImageFloatingTextView child1 = fakeChild((i) -> 1);
-        FakeImageFloatingTextView child2 = fakeChild((i) -> 1);
+        FakeImageFloatingTextView child1 = fakeChild(1);
+        FakeImageFloatingTextView child2 = fakeChild(1);
 
-        mView.setNumIndentLines(2);
         mView.addView(child1);
         mView.addView(child2);
 
         mView.measure(WIDTH_SPEC, HEIGHT_SPEC);
         mView.layout(0, 0, mView.getMeasuredWidth(), mView.getMeasuredHeight());
 
-        assertEquals(2, child1.getNumIndentLines());
-        assertEquals(1, child2.getNumIndentLines());
         assertFalse("child1 should not be hidden", child1.isHidden());
         assertFalse("child2 should not be hidden", child2.isHidden());
         assertEquals(105, mView.getMeasuredHeight());
@@ -105,17 +97,15 @@
 
     @Test
     public void testLargeLarge() {
-        FakeImageFloatingTextView child1 = fakeChild((i) -> 7);
-        FakeImageFloatingTextView child2 = fakeChild((i) -> 7);
+        FakeImageFloatingTextView child1 = fakeChild(7);
+        FakeImageFloatingTextView child2 = fakeChild(7);
 
-        mView.setNumIndentLines(2);
         mView.addView(child1);
         mView.addView(child2);
 
         mView.measure(WIDTH_SPEC, HEIGHT_SPEC);
         mView.layout(0, 0, mView.getMeasuredWidth(), mView.getMeasuredHeight());
 
-        assertEquals(3, child2.getNumIndentLines());
         assertTrue("child1 should be hidden", child1.isHidden());
         assertFalse("child2 should not be hidden", child2.isHidden());
         assertEquals(350, mView.getMeasuredHeight());
@@ -123,10 +113,9 @@
 
     @Test
     public void testLargeSmall_largeWrapsWith3indentbutNotFullHeight_andHitsMax() {
-        FakeImageFloatingTextView child1 = fakeChild((i) -> i > 2 ? 7 : 6);
-        FakeImageFloatingTextView child2 = fakeChild((i) -> 1);
+        FakeImageFloatingTextView child1 = fakeChild(7);
+        FakeImageFloatingTextView child2 = fakeChild(1);
 
-        mView.setNumIndentLines(2);
         mView.addView(child1);
         mView.addView(child2);
 
@@ -135,51 +124,18 @@
 
         assertFalse("child1 should not be hidden", child1.isHidden());
         assertFalse("child2 should not be hidden", child2.isHidden());
-        assertEquals(355, mView.getMeasuredHeight());
-        assertEquals(3, child1.getNumIndentLines());
-        assertEquals(0, child2.getNumIndentLines());
+        assertEquals(355, mView.getMeasuredHeight());;
     }
 
-    @Test
-    public void testLargeSmall_largeWrapsWith3indentbutnot3() {
-        FakeImageFloatingTextView child1 = fakeChild((i) -> i > 2 ? 4 : 3);
-        FakeImageFloatingTextView child2 = fakeChild((i) -> 1);
-
-        mView.setNumIndentLines(2);
-        mView.addView(child1);
-        mView.addView(child2);
-
-        mView.measure(WIDTH_SPEC, HEIGHT_SPEC);
-        mView.layout(0, 0, mView.getMeasuredWidth(), mView.getMeasuredHeight());
-
-        assertFalse("child1 should not be hidden", child1.isHidden());
-        assertFalse("child2 should not be hidden", child2.isHidden());
-        assertEquals(255, mView.getMeasuredHeight());
-        assertEquals(3, child1.getNumIndentLines());
-        assertEquals(0, child2.getNumIndentLines());
-    }
-
-    private class FakeImageFloatingTextView extends ImageFloatingTextView {
+    private class FakeImageFloatingTextView extends MessagingMessage {
 
         public static final int LINE_HEIGHT = 50;
-        private final Function<Integer, Integer> mLinesForIndent;
-        private int mNumIndentLines;
+        private final int mNumLines;
 
         public FakeImageFloatingTextView(Context context,
-                Function<Integer, Integer> linesForIndent) {
+                int linesForIndent) {
             super(context, null, 0, 0);
-            mLinesForIndent = linesForIndent;
-        }
-
-        @Override
-        public boolean setNumIndentLines(int lines) {
-            boolean changed = (mNumIndentLines != lines);
-            mNumIndentLines = lines;
-            return changed;
-        }
-
-        public int getNumIndentLines() {
-            return mNumIndentLines;
+            mNumLines = linesForIndent;
         }
 
         @Override
@@ -195,6 +151,20 @@
                             heightMeasureSpec)));
         }
 
+        public int getMeasuredType() {
+            boolean measuredTooSmall = getMeasuredHeight()
+                    < getLayoutHeight() + getPaddingTop() + getPaddingBottom();
+            if (measuredTooSmall) {
+                return MEASURED_TOO_SMALL;
+            } else {
+                if (getMeasuredHeight() == getDesiredHeight()) {
+                    return MEASURED_NORMAL;
+                } else {
+                    return MEASURED_SHORTENED;
+                }
+            }
+        }
+
         private int clampToMultiplesOfLineHeight(int size) {
             if (size <= LINE_HEIGHT) {
                 return size;
@@ -204,7 +174,7 @@
 
         @Override
         public int getLineCount() {
-            return mLinesForIndent.apply(mNumIndentLines);
+            return mNumLines;
         }
 
         public int getDesiredHeight() {
@@ -229,7 +199,7 @@
         }
     }
 
-    private FakeImageFloatingTextView fakeChild(Function<Integer,Integer> linesForIndent) {
-        return new FakeImageFloatingTextView(mContext, linesForIndent);
+    private FakeImageFloatingTextView fakeChild(int numLines) {
+        return new FakeImageFloatingTextView(mContext, numLines);
     }
 }
diff --git a/data/etc/privapp-permissions-platform.xml b/data/etc/privapp-permissions-platform.xml
index 82aa803..f7e0b46 100644
--- a/data/etc/privapp-permissions-platform.xml
+++ b/data/etc/privapp-permissions-platform.xml
@@ -328,6 +328,7 @@
     <privapp-permissions package="com.android.systemui">
         <permission name="android.permission.BATTERY_STATS"/>
         <permission name="android.permission.BIND_APPWIDGET"/>
+        <permission name="android.permission.BIND_SLICE" />
         <permission name="android.permission.BLUETOOTH_PRIVILEGED"/>
         <permission name="android.permission.CHANGE_COMPONENT_ENABLED_STATE"/>
         <permission name="android.permission.CHANGE_DEVICE_IDLE_TEMP_WHITELIST"/>
diff --git a/libs/hwui/SkiaCanvas.cpp b/libs/hwui/SkiaCanvas.cpp
index 70dfa86..508869a 100644
--- a/libs/hwui/SkiaCanvas.cpp
+++ b/libs/hwui/SkiaCanvas.cpp
@@ -521,7 +521,11 @@
                          float sweepAngle, bool useCenter, const SkPaint& paint) {
     if (CC_UNLIKELY(paint.nothingToDraw())) return;
     SkRect arc = SkRect::MakeLTRB(left, top, right, bottom);
-    mCanvas->drawArc(arc, startAngle, sweepAngle, useCenter, paint);
+    if (fabs(sweepAngle) >= 360.0f) {
+        mCanvas->drawOval(arc, paint);
+    } else {
+        mCanvas->drawArc(arc, startAngle, sweepAngle, useCenter, paint);
+    }
 }
 
 void SkiaCanvas::drawPath(const SkPath& path, const SkPaint& paint) {
diff --git a/media/jni/Android.bp b/media/jni/Android.bp
index 1d85c97..597336b 100644
--- a/media/jni/Android.bp
+++ b/media/jni/Android.bp
@@ -35,6 +35,7 @@
         "libutils",
         "libbinder",
         "libmedia",
+        "libmedia_omx",
         "libmediametrics",
         "libmediadrm",
         "libmidi",
diff --git a/packages/MtpDocumentsProvider/res/values-mr/strings.xml b/packages/MtpDocumentsProvider/res/values-mr/strings.xml
index 89a9d14..d581e10 100644
--- a/packages/MtpDocumentsProvider/res/values-mr/strings.xml
+++ b/packages/MtpDocumentsProvider/res/values-mr/strings.xml
@@ -19,7 +19,7 @@
     <string name="app_label" msgid="6271216747302322594">"MTP होस्ट"</string>
     <string name="downloads_app_label" msgid="7120690641874849726">"डाउनलोड"</string>
     <string name="root_name" msgid="5819495383921089536">"<xliff:g id="DEVICE_MODEL">%1$s</xliff:g> <xliff:g id="STORAGE_NAME">%2$s</xliff:g>"</string>
-    <string name="accessing_notification_title" msgid="3030133609230917944">"<xliff:g id="DEVICE_MODEL">%1$s</xliff:g> मधून फायलींंमध्ये प्रवेश करीत आहे"</string>
+    <string name="accessing_notification_title" msgid="3030133609230917944">"<xliff:g id="DEVICE_MODEL">%1$s</xliff:g> मधून फायलींंमध्ये प्रवेश करत आहे"</string>
     <string name="error_busy_device" msgid="3997316850357386589">"दुसरे डिव्हाइस व्यस्त आहे. ते उपलब्‍ध होईपर्यंत तुम्ही फायली ट्रांसफर करू शकत नाही."</string>
     <string name="error_locked_device" msgid="7557872102188356147">"कोणत्याही फायली आढळल्या नाहीत. दुसरे डिव्हाइस कदाचित बंद असू शकते. तसे असल्यास, ते अनलॉक करा आणि पुन्हा प्रयत्न करा."</string>
 </resources>
diff --git a/packages/PrintSpooler/res/values-mr/strings.xml b/packages/PrintSpooler/res/values-mr/strings.xml
index 05eb853..862d193 100644
--- a/packages/PrintSpooler/res/values-mr/strings.xml
+++ b/packages/PrintSpooler/res/values-mr/strings.xml
@@ -34,7 +34,7 @@
     <string name="print_preview" msgid="8010217796057763343">"मुद्रण पूर्वावलोकन"</string>
     <string name="install_for_print_preview" msgid="6366303997385509332">"पूर्वावलोकनासाठी पीडीएफ व्ह्यूअर इंस्टॉल करा"</string>
     <string name="printing_app_crashed" msgid="854477616686566398">"प्रिंटिंग अ‍ॅप क्रॅश झाले"</string>
-    <string name="generating_print_job" msgid="3119608742651698916">"मुद्रण कार्य व्‍युत्‍पन्न करीत आहे"</string>
+    <string name="generating_print_job" msgid="3119608742651698916">"मुद्रण कार्य व्‍युत्‍पन्न करत आहे"</string>
     <string name="save_as_pdf" msgid="5718454119847596853">"पीडीएफ म्‍हणून सेव्ह करा"</string>
     <string name="all_printers" msgid="5018829726861876202">"सर्व प्रिंटर..."</string>
     <string name="print_dialog" msgid="32628687461331979">"मुद्रण संवाद"</string>
@@ -79,8 +79,8 @@
       <item quantity="one"><xliff:g id="COUNT_1">%1$s</xliff:g> प्रिंटर शोधण्यासाठी इंस्टॉल करा</item>
       <item quantity="other"><xliff:g id="COUNT_1">%1$s</xliff:g> प्रिंटर शोधण्यासाठी इंस्टॉल करा</item>
     </plurals>
-    <string name="printing_notification_title_template" msgid="295903957762447362">"<xliff:g id="PRINT_JOB_NAME">%1$s</xliff:g> मुद्रण करीत आहे"</string>
-    <string name="cancelling_notification_title_template" msgid="1821759594704703197">"<xliff:g id="PRINT_JOB_NAME">%1$s</xliff:g> रद्द करीत आहे"</string>
+    <string name="printing_notification_title_template" msgid="295903957762447362">"<xliff:g id="PRINT_JOB_NAME">%1$s</xliff:g> मुद्रण करत आहे"</string>
+    <string name="cancelling_notification_title_template" msgid="1821759594704703197">"<xliff:g id="PRINT_JOB_NAME">%1$s</xliff:g> रद्द करत आहे"</string>
     <string name="failed_notification_title_template" msgid="2256217208186530973">"प्रिंटर एरर <xliff:g id="PRINT_JOB_NAME">%1$s</xliff:g>"</string>
     <string name="blocked_notification_title_template" msgid="1175435827331588646">"प्रिंटरने <xliff:g id="PRINT_JOB_NAME">%1$s</xliff:g> अवरोधित केले"</string>
     <string name="cancel" msgid="4373674107267141885">"रद्द करा"</string>
diff --git a/packages/SettingsLib/res/values-en-rXC/strings.xml b/packages/SettingsLib/res/values-en-rXC/strings.xml
index a3430fc..2a4983a 100644
--- a/packages/SettingsLib/res/values-en-rXC/strings.xml
+++ b/packages/SettingsLib/res/values-en-rXC/strings.xml
@@ -99,6 +99,13 @@
     <string name="bluetooth_pairing_pin_error_message" msgid="8337234855188925274">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‏‎‎‏‎‎‎‏‎‏‏‏‏‏‏‏‏‏‎‎‏‏‏‎‏‏‎‎‏‏‏‏‎‎‏‏‏‎‏‏‏‎‏‏‎‏‏‎‏‎‎‎‎‎‏‎‎‎‏‎‏‎‏‎‎‎‏‎‏‏‎‏‎‏‏‎‏‎‎Couldn\'t pair with ‎‏‎‎‏‏‎<xliff:g id="DEVICE_NAME">%1$s</xliff:g>‎‏‎‎‏‏‏‎ because of an incorrect PIN or passkey.‎‏‎‎‏‎"</string>
     <string name="bluetooth_pairing_device_down_error_message" msgid="7870998403045801381">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‏‎‎‏‎‎‎‏‎‏‏‏‏‏‏‏‏‎‏‏‎‏‎‎‏‏‏‎‏‏‎‏‏‎‎‏‏‏‎‏‎‏‏‎‎‎‎‏‏‎‎‎‎‏‎‎‏‏‎‏‏‎‎‎‏‏‏‏‎‏‏‎‏‎‎‏‎‏‎Can\'t communicate with ‎‏‎‎‏‏‎<xliff:g id="DEVICE_NAME">%1$s</xliff:g>‎‏‎‎‏‏‏‎.‎‏‎‎‏‎"</string>
     <string name="bluetooth_pairing_rejected_error_message" msgid="1648157108520832454">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‏‎‎‏‎‎‎‏‎‏‏‏‏‎‏‏‎‏‏‎‏‏‎‏‏‏‏‏‎‏‏‎‏‏‏‎‎‎‏‎‏‎‎‎‎‏‏‎‏‎‏‏‎‏‏‎‏‎‏‏‎‏‎‎‏‏‎‏‏‏‎‎‎‏‏‎‎Pairing rejected by ‎‏‎‎‏‏‎<xliff:g id="DEVICE_NAME">%1$s</xliff:g>‎‏‎‎‏‏‏‎.‎‏‎‎‏‎"</string>
+    <string name="bluetooth_talkback_computer" msgid="4875089335641234463">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‏‎‎‏‎‎‎‏‎‏‏‏‏‏‏‏‎‎‎‎‏‏‏‎‏‎‎‏‏‏‏‏‎‎‏‎‏‏‏‏‏‎‏‎‏‏‏‏‏‎‎‎‏‏‎‎‏‏‎‎‎‎‏‏‎‏‏‏‎‎‎‎‎‏‏‏‏‏‎Computer‎‏‎‎‏‎"</string>
+    <string name="bluetooth_talkback_headset" msgid="5140152177885220949">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‏‎‎‏‎‎‎‏‎‏‏‏‏‏‏‏‎‎‎‏‏‏‎‏‎‏‎‏‎‏‎‏‏‏‏‏‎‏‎‎‏‎‏‏‎‎‎‎‎‎‏‏‏‎‎‏‎‎‏‏‎‎‏‎‎‏‎‎‎‎‎‏‎‏‎‏‎‏‎Headset‎‏‎‎‏‎"</string>
+    <string name="bluetooth_talkback_phone" msgid="4260255181240622896">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‏‎‎‏‎‎‎‏‎‏‏‏‏‏‎‏‏‏‎‏‏‎‎‎‏‏‏‏‏‎‏‏‏‎‏‏‏‏‎‎‎‎‎‏‏‏‏‏‏‏‎‏‎‎‏‎‏‏‏‎‏‎‏‎‏‏‏‏‏‎‎‏‏‎‎‎‎‎Phone‎‏‎‎‏‎"</string>
+    <string name="bluetooth_talkback_imaging" msgid="551146170554589119">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‏‎‎‏‎‎‎‏‎‏‏‏‎‏‏‏‏‏‏‎‏‎‎‏‏‎‎‎‎‏‎‎‎‎‏‎‎‎‎‏‎‏‏‎‏‎‎‎‏‏‎‎‏‏‏‎‎‏‎‎‎‏‏‏‏‏‏‎‏‏‏‏‏‏‎Imaging‎‏‎‎‏‎"</string>
+    <string name="bluetooth_talkback_headphone" msgid="26580326066627664">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‏‎‎‏‎‎‎‏‎‏‏‎‏‏‏‏‎‏‏‏‏‎‎‏‏‎‏‏‏‎‏‎‏‎‏‎‏‎‎‏‏‎‏‏‎‏‎‎‏‏‎‏‏‏‎‎‏‎‎‏‎‎‎‏‎‏‎‎‎‎‎Headphone‎‏‎‎‏‎"</string>
+    <string name="bluetooth_talkback_input_peripheral" msgid="5165842622743212268">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‏‎‎‏‎‎‎‏‎‏‏‏‏‏‏‏‎‎‎‏‏‏‏‎‏‏‎‎‎‎‏‏‎‎‎‎‏‎‎‏‏‏‏‏‏‎‏‏‎‏‏‏‏‎‎‎‏‎‏‏‏‎‎‏‏‏‎‎‎‎‏‏‏‎‏‏‎‎‎Input Peripheral‎‏‎‎‏‎"</string>
+    <string name="bluetooth_talkback_bluetooth" msgid="5615463912185280812">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‏‎‎‏‎‎‎‏‎‏‏‏‏‏‏‏‎‎‏‏‎‏‏‏‏‎‏‏‏‎‎‎‏‎‎‎‏‎‏‎‏‎‏‏‎‏‎‎‎‏‎‏‏‎‏‏‎‏‎‏‎‎‎‎‏‏‏‎‎‏‎‎‏‎‏‏‎‎‎Bluetooth‎‏‎‎‏‎"</string>
     <string name="accessibility_wifi_off" msgid="1166761729660614716">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‏‎‎‏‎‎‎‏‎‏‏‏‏‎‏‏‎‎‎‎‎‎‏‏‎‎‎‏‎‎‏‎‏‎‏‏‏‎‎‏‏‏‎‎‏‎‎‎‎‎‎‏‏‏‏‏‎‎‎‎‏‏‏‏‏‎‎‎‎‎‏‏‏‏‎‎‎Wifi off.‎‏‎‎‏‎"</string>
     <string name="accessibility_no_wifi" msgid="8834610636137374508">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‏‎‎‏‎‎‎‏‎‏‏‏‏‏‏‏‏‏‏‎‏‎‏‎‎‏‏‎‏‎‏‏‎‏‎‏‏‏‏‎‎‏‎‎‎‎‏‏‎‏‎‏‏‏‏‏‎‏‏‎‏‏‎‎‏‎‎‎‏‏‎‎‏‎‏‏‎‎‎Wifi disconnected.‎‏‎‎‏‎"</string>
     <string name="accessibility_wifi_one_bar" msgid="4869376278894301820">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‏‎‎‏‎‎‎‏‎‏‏‏‏‏‏‏‎‎‎‎‏‏‏‎‎‏‎‎‏‏‎‏‏‏‏‏‏‏‏‏‏‎‏‏‎‏‎‎‏‏‎‏‎‏‎‎‏‏‎‏‎‏‎‏‎‏‎‎‏‎‎‏‏‏‏‏‎‎‎Wifi one bar.‎‏‎‎‏‎"</string>
diff --git a/packages/SettingsLib/res/values-es-rUS/strings.xml b/packages/SettingsLib/res/values-es-rUS/strings.xml
index ea62383..f351e70 100644
--- a/packages/SettingsLib/res/values-es-rUS/strings.xml
+++ b/packages/SettingsLib/res/values-es-rUS/strings.xml
@@ -99,6 +99,13 @@
     <string name="bluetooth_pairing_pin_error_message" msgid="8337234855188925274">"No se pudo sincronizar con <xliff:g id="DEVICE_NAME">%1$s</xliff:g> debido a que el PIN o la clave de acceso son incorrectos."</string>
     <string name="bluetooth_pairing_device_down_error_message" msgid="7870998403045801381">"No se puede establecer la comunicación con <xliff:g id="DEVICE_NAME">%1$s</xliff:g>."</string>
     <string name="bluetooth_pairing_rejected_error_message" msgid="1648157108520832454">"Vínculo rechazado por <xliff:g id="DEVICE_NAME">%1$s</xliff:g>"</string>
+    <string name="bluetooth_talkback_computer" msgid="4875089335641234463">"Computadora"</string>
+    <string name="bluetooth_talkback_headset" msgid="5140152177885220949">"Auriculares"</string>
+    <string name="bluetooth_talkback_phone" msgid="4260255181240622896">"Teléfono"</string>
+    <string name="bluetooth_talkback_imaging" msgid="551146170554589119">"Imágenes"</string>
+    <string name="bluetooth_talkback_headphone" msgid="26580326066627664">"Auriculares"</string>
+    <string name="bluetooth_talkback_input_peripheral" msgid="5165842622743212268">"Periférico de entrada"</string>
+    <string name="bluetooth_talkback_bluetooth" msgid="5615463912185280812">"Bluetooth"</string>
     <string name="accessibility_wifi_off" msgid="1166761729660614716">"Wi-Fi inhabilitado"</string>
     <string name="accessibility_no_wifi" msgid="8834610636137374508">"Wi-Fi desconectado"</string>
     <string name="accessibility_wifi_one_bar" msgid="4869376278894301820">"Una barra de Wi-Fi"</string>
diff --git a/packages/SettingsLib/res/values-mr/strings.xml b/packages/SettingsLib/res/values-mr/strings.xml
index 42ee581..17d7191 100644
--- a/packages/SettingsLib/res/values-mr/strings.xml
+++ b/packages/SettingsLib/res/values-mr/strings.xml
@@ -22,7 +22,7 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="wifi_fail_to_scan" msgid="1265540342578081461">"नेटवर्कसाठी स्कॅन करू शकत नाही"</string>
     <string name="wifi_security_none" msgid="7985461072596594400">"काहीही नाही"</string>
-    <string name="wifi_remembered" msgid="4955746899347821096">"जतन केले"</string>
+    <string name="wifi_remembered" msgid="4955746899347821096">"सेव्ह केले"</string>
     <string name="wifi_disabled_generic" msgid="4259794910584943386">"अक्षम"</string>
     <string name="wifi_disabled_network_failure" msgid="2364951338436007124">"IP कॉन्फिगरेशन अयशस्वी"</string>
     <string name="wifi_disabled_by_recommendation_provider" msgid="5168315140978066096">"कमी दर्जाच्या नेटवर्कमुळे कनेक्ट केलेले नाही"</string>
@@ -34,7 +34,7 @@
     <string name="wifi_not_in_range" msgid="1136191511238508967">"परिक्षेत्रामध्ये नाही"</string>
     <string name="wifi_no_internet_no_reconnect" msgid="5724903347310541706">"स्वयंचलितपणे कनेक्ट करणार नाही"</string>
     <string name="wifi_no_internet" msgid="3880396223819116454">"इंटरनेट अॅक्सेस नाही"</string>
-    <string name="saved_network" msgid="4352716707126620811">"<xliff:g id="NAME">%1$s</xliff:g> द्वारे जतन केले"</string>
+    <string name="saved_network" msgid="4352716707126620811">"<xliff:g id="NAME">%1$s</xliff:g> द्वारे सेव्ह केले"</string>
     <string name="connected_via_network_scorer" msgid="5713793306870815341">"%1$s द्वारे स्वयंचलितपणे कनेक्ट केले"</string>
     <string name="connected_via_network_scorer_default" msgid="7867260222020343104">"नेटवर्क रेटिंग प्रदात्याद्वारे स्वयंचलितपणे कनेक्ट केले"</string>
     <string name="connected_via_passpoint" msgid="2826205693803088747">"%1$s द्वारे कनेक्‍ट केले"</string>
@@ -52,7 +52,7 @@
     <string name="preference_summary_default_combination" msgid="8532964268242666060">"<xliff:g id="STATE">%1$s</xliff:g> / <xliff:g id="DESCRIPTION">%2$s</xliff:g>"</string>
     <string name="bluetooth_disconnected" msgid="6557104142667339895">"डिस्कनेक्ट केले"</string>
     <string name="bluetooth_disconnecting" msgid="8913264760027764974">"डिस्कनेक्ट करत आहे..."</string>
-    <string name="bluetooth_connecting" msgid="8555009514614320497">"कनेक्ट करीत आहे..."</string>
+    <string name="bluetooth_connecting" msgid="8555009514614320497">"कनेक्ट करत आहे..."</string>
     <string name="bluetooth_connected" msgid="6038755206916626419">"कनेक्ट केले"</string>
     <string name="bluetooth_pairing" msgid="1426882272690346242">"जोडत आहे…"</string>
     <string name="bluetooth_connected_no_headset" msgid="2866994875046035609">"कनेक्ट केले (फोन नाही)"</string>
@@ -99,6 +99,13 @@
     <string name="bluetooth_pairing_pin_error_message" msgid="8337234855188925274">"अयोग्य पिन किंवा पासकीमुळे <xliff:g id="DEVICE_NAME">%1$s</xliff:g> सह जोडू शकलो नाही."</string>
     <string name="bluetooth_pairing_device_down_error_message" msgid="7870998403045801381">"<xliff:g id="DEVICE_NAME">%1$s</xliff:g> शी संप्रेषण करू शकत नाही."</string>
     <string name="bluetooth_pairing_rejected_error_message" msgid="1648157108520832454">"<xliff:g id="DEVICE_NAME">%1$s</xliff:g> द्वारे पेअरींग नाकारले."</string>
+    <string name="bluetooth_talkback_computer" msgid="4875089335641234463">"संगणक"</string>
+    <string name="bluetooth_talkback_headset" msgid="5140152177885220949">"हेडसेट"</string>
+    <string name="bluetooth_talkback_phone" msgid="4260255181240622896">"फोन"</string>
+    <string name="bluetooth_talkback_imaging" msgid="551146170554589119">"इमेज"</string>
+    <string name="bluetooth_talkback_headphone" msgid="26580326066627664">"हेडफोन"</string>
+    <string name="bluetooth_talkback_input_peripheral" msgid="5165842622743212268">"इनपुट परिधीय"</string>
+    <string name="bluetooth_talkback_bluetooth" msgid="5615463912185280812">"ब्लूटूथ"</string>
     <string name="accessibility_wifi_off" msgid="1166761729660614716">"वाय फाय बंद."</string>
     <string name="accessibility_no_wifi" msgid="8834610636137374508">"वाय फाय डिस्कनेक्ट झाले."</string>
     <string name="accessibility_wifi_one_bar" msgid="4869376278894301820">"वाय फाय एक बार."</string>
@@ -135,7 +142,7 @@
     <string name="tts_play_example_summary" msgid="8029071615047894486">"उच्चार संश्लेषणाचे एक छोटेसे प्रात्यक्षिक प्ले करा"</string>
     <string name="tts_install_data_title" msgid="4264378440508149986">"व्हॉइस डेटा इंस्टॉल करा"</string>
     <string name="tts_install_data_summary" msgid="5742135732511822589">"उच्चार संश्लेषणासाठी आवश्यक आवाज डेटा इंस्टॉल करा"</string>
-    <string name="tts_engine_security_warning" msgid="8786238102020223650">"हे उच्चार संश्लेषण इंजिन संकेतशब्द आणि क्रेडिट कार्ड नंबर यासारख्या वैयक्तिक मजकुरासह, बोलला जाणारा सर्व मजकूर संकलित करण्यात सक्षम होऊ शकते. हे <xliff:g id="TTS_PLUGIN_ENGINE_NAME">%s</xliff:g> इंजिनवरून येते. या उच्चार संश्लेषण इंजिनचा वापर सक्षम करायचा?"</string>
+    <string name="tts_engine_security_warning" msgid="8786238102020223650">"हे उच्चार संश्लेषण इंजिन पासवर्ड आणि क्रेडिट कार्ड नंबर यासारख्या वैयक्तिक मजकुरासह, बोलला जाणारा सर्व मजकूर संकलित करण्यात सक्षम होऊ शकते. हे <xliff:g id="TTS_PLUGIN_ENGINE_NAME">%s</xliff:g> इंजिनवरून येते. या उच्चार संश्लेषण इंजिनचा वापर सक्षम करायचा?"</string>
     <string name="tts_engine_network_required" msgid="1190837151485314743">"या भाषेस टेक्‍स्‍ट टू स्‍पीचसाठी एका नेटवर्क कनेक्शनची आवश्यकता आहे."</string>
     <string name="tts_default_sample_string" msgid="4040835213373086322">"हे उच्चार संश्लेषणाचे एक उदाहरण आहे"</string>
     <string name="tts_status_title" msgid="7268566550242584413">"डीफॉल्ट भाषा स्थिती"</string>
@@ -245,7 +252,7 @@
     <string name="debug_debugging_category" msgid="6781250159513471316">"डीबग करणे"</string>
     <string name="debug_app" msgid="8349591734751384446">"डीबग अॅप निवडा"</string>
     <string name="debug_app_not_set" msgid="718752499586403499">"कोणतेही डीबग अॅप्लिकेशन सेट नाही"</string>
-    <string name="debug_app_set" msgid="2063077997870280017">"अॅप्लिकेशन डीबग करीत आहे: <xliff:g id="APP_NAME">%1$s</xliff:g>"</string>
+    <string name="debug_app_set" msgid="2063077997870280017">"अॅप्लिकेशन डीबग करत आहे: <xliff:g id="APP_NAME">%1$s</xliff:g>"</string>
     <string name="select_application" msgid="5156029161289091703">"अॅप्लिकेशन निवडा"</string>
     <string name="no_application" msgid="2813387563129153880">"काहीही नाही"</string>
     <string name="wait_for_debugger" msgid="1202370874528893091">"डीबगरची प्रतीक्षा करा"</string>
@@ -303,11 +310,11 @@
     <string name="force_resizable_activities_summary" msgid="6667493494706124459">"manifest मूल्यांकडे दुर्लक्ष करून, एकाधिक-विंडोसाठी सर्व क्रियाकलापांचा आकार बदलण्यायोग्य करा."</string>
     <string name="enable_freeform_support" msgid="1461893351278940416">"freeform विंडो सक्षम करा"</string>
     <string name="enable_freeform_support_summary" msgid="8247310463288834487">"प्रायोगिक मुक्तस्वरूपाच्या विंडोसाठी समर्थन सक्षम करा."</string>
-    <string name="local_backup_password_title" msgid="3860471654439418822">"डेस्कटॉप बॅकअप संकेतशब्द"</string>
+    <string name="local_backup_password_title" msgid="3860471654439418822">"डेस्कटॉप बॅकअप पासवर्ड"</string>
     <string name="local_backup_password_summary_none" msgid="6951095485537767956">"डेस्कटॉप पूर्ण बॅक अप सध्या संरक्षित नाहीत"</string>
-    <string name="local_backup_password_summary_change" msgid="5376206246809190364">"डेस्कटॉपच्या पूर्ण बॅकअपसाठी असलेला संकेतशब्द बदलण्यासाठी किंवा काढण्यासाठी टॅप  करा"</string>
+    <string name="local_backup_password_summary_change" msgid="5376206246809190364">"डेस्कटॉपच्या पूर्ण बॅकअपसाठी असलेला पासवर्ड बदलण्यासाठी किंवा काढण्यासाठी टॅप  करा"</string>
     <string name="local_backup_password_toast_success" msgid="582016086228434290">"नवीन बॅक अप पासवर्ड सेट झाला"</string>
-    <string name="local_backup_password_toast_confirmation_mismatch" msgid="7805892532752708288">"नवीन संकेतशब्द आणि पुष्टीकरण जुळत नाही"</string>
+    <string name="local_backup_password_toast_confirmation_mismatch" msgid="7805892532752708288">"नवीन पासवर्ड आणि पुष्टीकरण जुळत नाही"</string>
     <string name="local_backup_password_toast_validation_failure" msgid="5646377234895626531">"बॅक अप पासवर्ड सेट करणे अयशस्वी"</string>
   <string-array name="color_mode_names">
     <item msgid="2425514299220523812">"सशक्त (डीफॉल्ट)"</item>
@@ -382,13 +389,13 @@
     <string name="screen_zoom_summary_custom" msgid="5611979864124160447">"सानुकूल करा (<xliff:g id="DENSITYDPI">%d</xliff:g>)"</string>
     <string name="help_feedback_label" msgid="6815040660801785649">"मदत आणि अभिप्राय"</string>
     <string name="content_description_menu_button" msgid="8182594799812351266">"मेनू"</string>
-    <string name="retail_demo_reset_message" msgid="118771671364131297">"डेमो मोडमध्ये फॅक्टरी रीसेट करण्यासाठी पासवर्ड प्रविष्ट करा"</string>
+    <string name="retail_demo_reset_message" msgid="118771671364131297">"डेमो मोडमध्ये फॅक्टरी रीसेट करण्यासाठी पासवर्ड एंटर करा"</string>
     <string name="retail_demo_reset_next" msgid="8356731459226304963">"पुढील"</string>
-    <string name="retail_demo_reset_title" msgid="696589204029930100">"संकेतशब्द आवश्यक"</string>
+    <string name="retail_demo_reset_title" msgid="696589204029930100">"पासवर्ड आवश्यक"</string>
     <string name="active_input_method_subtypes" msgid="3596398805424733238">"सक्रिय इनपुट पद्धती"</string>
     <string name="use_system_language_to_select_input_method_subtypes" msgid="5747329075020379587">"सिस्टम भाषा वापरा"</string>
     <string name="failed_to_open_app_settings_toast" msgid="1251067459298072462">"<xliff:g id="SPELL_APPLICATION_NAME">%1$s</xliff:g> साठी सेटिंग्ज उघडण्यात अयशस्वी"</string>
-    <string name="ime_security_warning" msgid="4135828934735934248">"ही इनपुट पद्धत संकेतशब्द आणि क्रेडिट कार्ड नंबर यासह, आपण टाइप करता तो सर्व मजकूर संकलित करण्यात सक्षम होऊ शकते. ही <xliff:g id="IME_APPLICATION_NAME">%1$s</xliff:g> अॅपवरून येते. ही इनपुट पद्धत वापरायची?"</string>
+    <string name="ime_security_warning" msgid="4135828934735934248">"ही इनपुट पद्धत पासवर्ड आणि क्रेडिट कार्ड नंबर यासह, आपण टाइप करता तो सर्व मजकूर संकलित करण्यात सक्षम होऊ शकते. ही <xliff:g id="IME_APPLICATION_NAME">%1$s</xliff:g> अॅपवरून येते. ही इनपुट पद्धत वापरायची?"</string>
     <string name="direct_boot_unaware_dialog_message" msgid="7870273558547549125">"टीप: रीबूट केल्यानंतर, तुम्ही आपला फोन अनलॉक करे पर्यंत हे अॅप सुरू होऊ शकत नाही"</string>
     <string name="ims_reg_title" msgid="7609782759207241443">"IMS नोंदणी स्थिती"</string>
     <string name="ims_reg_status_registered" msgid="933003316932739188">"नोंदवलेले"</string>
diff --git a/packages/SettingsLib/res/values/strings.xml b/packages/SettingsLib/res/values/strings.xml
index 360848f..4bb16ff 100644
--- a/packages/SettingsLib/res/values/strings.xml
+++ b/packages/SettingsLib/res/values/strings.xml
@@ -546,11 +546,13 @@
     <!-- [CHAR LIMIT=NONE] Label for displaying Bluetooth Audio Codec Parameters while streaming -->
     <string name="bluetooth_select_a2dp_codec_streaming_label">Streaming: <xliff:g id="streaming_parameter">%1$s</xliff:g></string>
 
-    <!-- Title of the developer option for DNS over TLS. -->
-    <string name="dns_tls">DNS over TLS</string>
-    <!-- Summary to explain the developer option for DNS over TLS.  This allows the user to
-    request that the system attempt TLS with all DNS servers, or none. -->
-    <string name="dns_tls_summary">If enabled, attempt DNS over TLS on port 853.</string>
+    <!-- Developer option setting for Private DNS -->
+    <string name="select_private_dns_configuration_title">Private DNS</string>
+    <string name="select_private_dns_configuration_dialog_title">Select Private DNS Mode</string>
+    <string name="private_dns_mode_off">Off</string>
+    <string name="private_dns_mode_opportunistic">Opportunistic</string>
+    <string name="private_dns_mode_provider">Private DNS provider hostname</string>
+    <string name="private_dns_mode_provider_hostname_hint">Enter hostname of DNS provider</string>
 
     <!-- setting Checkbox summary whether to show options for wireless display certification  -->
     <string name="wifi_display_certification_summary">Show options for wireless display certification</string>
diff --git a/packages/SettingsLib/src/com/android/settingslib/datetime/ZoneGetter.java b/packages/SettingsLib/src/com/android/settingslib/datetime/ZoneGetter.java
index 1cb255b..a8262c8 100644
--- a/packages/SettingsLib/src/com/android/settingslib/datetime/ZoneGetter.java
+++ b/packages/SettingsLib/src/com/android/settingslib/datetime/ZoneGetter.java
@@ -20,6 +20,7 @@
 import android.content.res.XmlResourceParser;
 import android.icu.text.TimeZoneFormat;
 import android.icu.text.TimeZoneNames;
+import android.support.annotation.VisibleForTesting;
 import android.support.v4.text.BidiFormatter;
 import android.support.v4.text.TextDirectionHeuristicsCompat;
 import android.text.SpannableString;
@@ -32,6 +33,8 @@
 
 import com.android.settingslib.R;
 
+import libcore.util.TimeZoneFinder;
+
 import org.xmlpull.v1.XmlPullParserException;
 
 import java.util.ArrayList;
@@ -43,7 +46,6 @@
 import java.util.Map;
 import java.util.Set;
 import java.util.TimeZone;
-import libcore.util.TimeZoneFinder;
 
 /**
  * ZoneGetter is the utility class to get time zone and zone list, and both of them have display
@@ -350,7 +352,8 @@
         return gmtText;
     }
 
-    private static final class ZoneGetterData {
+    @VisibleForTesting
+    public static final class ZoneGetterData {
         public final String[] olsonIdsToDisplay;
         public final CharSequence[] gmtOffsetTexts;
         public final TimeZone[] timeZones;
@@ -377,9 +380,13 @@
             }
 
             // Create a lookup of local zone IDs.
-            List<String> zoneIds =
-                    TimeZoneFinder.getInstance().lookupTimeZoneIdsByCountry(locale.getCountry());
+            final List<String> zoneIds = lookupTimeZoneIdsByCountry(locale.getCountry());
             localZoneIds = new HashSet<>(zoneIds);
         }
+
+        @VisibleForTesting
+        public List<String> lookupTimeZoneIdsByCountry(String country) {
+            return TimeZoneFinder.getInstance().lookupTimeZoneIdsByCountry(country);
+        }
     }
 }
diff --git a/packages/SystemUI/AndroidManifest.xml b/packages/SystemUI/AndroidManifest.xml
index 7b8d0db..29ecac0 100644
--- a/packages/SystemUI/AndroidManifest.xml
+++ b/packages/SystemUI/AndroidManifest.xml
@@ -120,6 +120,7 @@
     <uses-permission android:name="android.permission.TRUST_LISTENER" />
     <uses-permission android:name="android.permission.USE_FINGERPRINT" />
     <uses-permission android:name="android.permission.RESET_FINGERPRINT_LOCKOUT" />
+    <uses-permission android:name="android.permission.BIND_SLICE" />
 
     <!-- Needed for WallpaperManager.clear in ImageWallpaper.updateWallpaperLocked -->
     <uses-permission android:name="android.permission.SET_WALLPAPER"/>
@@ -222,7 +223,7 @@
              -->
         <service android:name="SystemUIService"
             android:exported="true"
-            />
+        />
 
         <!-- Recents depends on every user having their own SystemUI process, so on user switch,
              ensure that the process is created by starting this service.
@@ -568,6 +569,11 @@
                 android:resource="@xml/fileprovider" />
         </provider>
 
+        <provider android:name=".keyguard.KeyguardSliceProvider"
+                  android:authorities="com.android.systemui.keyguard"
+                  android:exported="true">
+        </provider>
+
         <receiver
             android:name=".statusbar.KeyboardShortcutsReceiver">
             <intent-filter>
diff --git a/packages/SystemUI/res-keyguard/layout/keyguard_emergency_carrier_area.xml b/packages/SystemUI/res-keyguard/layout/keyguard_emergency_carrier_area.xml
index 39cba74..3018a02 100644
--- a/packages/SystemUI/res-keyguard/layout/keyguard_emergency_carrier_area.xml
+++ b/packages/SystemUI/res-keyguard/layout/keyguard_emergency_carrier_area.xml
@@ -33,8 +33,10 @@
         android:layout_width="wrap_content"
         android:layout_height="wrap_content"
         style="@style/Keyguard.TextView"
+        android:singleLine="true"
         android:ellipsize="marquee"
         android:visibility="gone"
+        android:gravity="center"
         androidprv:allCaps="@bool/kg_use_all_caps" />
 
     <com.android.keyguard.EmergencyButton
diff --git a/packages/SystemUI/res-keyguard/layout/keyguard_sim_pin_view.xml b/packages/SystemUI/res-keyguard/layout/keyguard_sim_pin_view.xml
index 97c8965..947f27d 100644
--- a/packages/SystemUI/res-keyguard/layout/keyguard_sim_pin_view.xml
+++ b/packages/SystemUI/res-keyguard/layout/keyguard_sim_pin_view.xml
@@ -32,6 +32,7 @@
             android:id="@+id/keyguard_sim"
             android:layout_width="match_parent"
             android:layout_height="wrap_content"
+            android:tint="@color/background_protected"
             android:src="@drawable/ic_lockscreen_sim"/>
 
     <include layout="@layout/keyguard_message_area"
diff --git a/packages/SystemUI/res-keyguard/layout/keyguard_sim_puk_view.xml b/packages/SystemUI/res-keyguard/layout/keyguard_sim_puk_view.xml
index d4c5d74..6f270b4 100644
--- a/packages/SystemUI/res-keyguard/layout/keyguard_sim_puk_view.xml
+++ b/packages/SystemUI/res-keyguard/layout/keyguard_sim_puk_view.xml
@@ -33,6 +33,7 @@
             android:id="@+id/keyguard_sim"
             android:layout_width="match_parent"
             android:layout_height="wrap_content"
+            android:tint="@color/background_protected"
             android:src="@drawable/ic_lockscreen_sim"/>
 
     <include layout="@layout/keyguard_message_area"
diff --git a/packages/SystemUI/res-keyguard/layout/keyguard_status_area.xml b/packages/SystemUI/res-keyguard/layout/keyguard_status_area.xml
index 56fb73f..020cfee 100644
--- a/packages/SystemUI/res-keyguard/layout/keyguard_status_area.xml
+++ b/packages/SystemUI/res-keyguard/layout/keyguard_status_area.xml
@@ -18,34 +18,37 @@
 -->
 
 <!-- This is a view that shows general status information in Keyguard. -->
-<LinearLayout
+<com.android.keyguard.KeyguardSliceView
     xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:androidprv="http://schemas.android.com/apk/res-auto"
-    android:layout_width="match_parent"
+    android:layout_marginStart="16dp"
+    android:layout_marginEnd="16dp"
+    android:layout_width="wrap_content"
     android:layout_height="wrap_content"
-    android:orientation="horizontal"
-    android:gravity="center">
-    <com.android.systemui.statusbar.policy.DateView
-        android:id="@+id/date_view"
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content"
-        android:textColor="?attr/wallpaperTextColor"
-        style="@style/widget_label"
-        android:letterSpacing="0.05"
-        android:gravity="center"
-        />
-    <TextView android:id="@+id/alarm_status"
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content"
-        android:drawablePadding="6dp"
-        android:drawableStart="@drawable/ic_access_alarms_big"
-        android:drawableTint="?attr/wallpaperTextColorSecondary"
-        android:drawableTintMode="src_in"
-        android:textColor="?attr/wallpaperTextColorSecondary"
-        android:letterSpacing="0.05"
-        style="@style/widget_label"
-        android:layout_marginStart="6dp"
-        android:gravity="center"
-        android:visibility="gone"
-        />
-</LinearLayout>
+    android:layout_marginTop="@dimen/date_owner_info_margin"
+    android:layout_gravity="center_horizontal"
+    android:paddingTop="4dp"
+    android:clipToPadding="false"
+    android:orientation="vertical"
+    android:layout_centerHorizontal="true">
+    <TextView android:id="@+id/title"
+              android:layout_width="match_parent"
+              android:layout_height="wrap_content"
+              android:singleLine="true"
+              android:ellipsize="end"
+              android:fadingEdge="horizontal"
+              android:gravity="center"
+              android:textSize="22sp"
+              android:textColor="?attr/wallpaperTextColor"
+    />
+    <TextView android:id="@+id/text"
+              android:layout_width="match_parent"
+              android:layout_height="wrap_content"
+              android:singleLine="true"
+              android:gravity="center"
+              android:visibility="gone"
+              android:textSize="16sp"
+              android:textColor="?attr/wallpaperTextColor"
+              android:layout_marginTop="4dp"
+              android:ellipsize="end"
+    />
+</com.android.keyguard.KeyguardSliceView>
\ No newline at end of file
diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml
index 9901f6f..c678111f 100644
--- a/packages/SystemUI/res/values/dimens.xml
+++ b/packages/SystemUI/res/values/dimens.xml
@@ -95,6 +95,10 @@
     <!-- Height of a heads up notification in the status bar -->
     <dimen name="notification_max_heads_up_height_increased">188dp</dimen>
 
+    <!-- Height of a messaging notifications with actions at least. Not that this is an upper bound
+         and the notification won't use this much, but is measured with wrap_content -->
+    <dimen name="notification_messaging_actions_min_height">196dp</dimen>
+
     <!-- a threshold in dp per second that is considered fast scrolling -->
     <dimen name="scroll_fast_threshold">1500dp</dimen>
 
diff --git a/packages/SystemUI/res/values/ids.xml b/packages/SystemUI/res/values/ids.xml
index e5f8029..cfd95b4 100644
--- a/packages/SystemUI/res/values/ids.xml
+++ b/packages/SystemUI/res/values/ids.xml
@@ -66,9 +66,6 @@
 
     <!-- For notification icons for which targetSdk < L, this caches whether the icon is grayscale -->
     <item type="id" name="icon_is_grayscale" />
-    <item type="id" name="clip_children_tag" />
-    <item type="id" name="clip_children_set_tag" />
-    <item type="id" name="clip_to_padding_tag" />
     <item type="id" name="image_icon_tag" />
     <item type="id" name="contains_transformed_view" />
     <item type="id" name="is_clicked_heads_up_tag" />
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSliceView.java b/packages/SystemUI/src/com/android/keyguard/KeyguardSliceView.java
new file mode 100644
index 0000000..c18f9b6
--- /dev/null
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSliceView.java
@@ -0,0 +1,160 @@
+/*
+ * 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.keyguard;
+
+import android.app.PendingIntent;
+import android.app.slice.Slice;
+import android.app.slice.SliceItem;
+import android.app.slice.SliceQuery;
+import android.content.Context;
+import android.database.ContentObserver;
+import android.graphics.Color;
+import android.net.Uri;
+import android.os.Handler;
+import android.util.AttributeSet;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import com.android.internal.graphics.ColorUtils;
+import com.android.systemui.R;
+import com.android.systemui.keyguard.KeyguardSliceProvider;
+
+/**
+ * View visible under the clock on the lock screen and AoD.
+ */
+public class KeyguardSliceView extends LinearLayout {
+
+    private final Uri mKeyguardSliceUri;
+    private TextView mTitle;
+    private TextView mText;
+    private Slice mSlice;
+    private PendingIntent mSliceAction;
+    private int mTextColor;
+    private float mDarkAmount = 0;
+
+    private final ContentObserver mObserver;
+
+    public KeyguardSliceView(Context context) {
+        this(context, null, 0);
+    }
+
+    public KeyguardSliceView(Context context, AttributeSet attrs) {
+        this(context, attrs, 0);
+    }
+
+    public KeyguardSliceView(Context context, AttributeSet attrs, int defStyle) {
+        super(context, attrs, defStyle);
+        mObserver = new KeyguardSliceObserver(new Handler());
+        mKeyguardSliceUri = Uri.parse(KeyguardSliceProvider.KEYGUARD_SLICE_URI);;
+    }
+
+    @Override
+    protected void onFinishInflate() {
+        super.onFinishInflate();
+        mTitle = findViewById(R.id.title);
+        mText = findViewById(R.id.text);
+        mTextColor = mTitle.getCurrentTextColor();
+    }
+
+    @Override
+    protected void onAttachedToWindow() {
+        super.onAttachedToWindow();
+
+        // Set initial content
+        showSlice(Slice.bindSlice(getContext().getContentResolver(), mKeyguardSliceUri));
+
+        // Make sure we always have the most current slice
+        getContext().getContentResolver().registerContentObserver(mKeyguardSliceUri,
+                false /* notifyDescendants */, mObserver);
+    }
+
+    @Override
+    protected void onDetachedFromWindow() {
+        super.onDetachedFromWindow();
+
+        getContext().getContentResolver().unregisterContentObserver(mObserver);
+    }
+
+    private void showSlice(Slice slice) {
+        // Items will be wrapped into an action when they have tap targets.
+        SliceItem actionSlice = SliceQuery.find(slice, SliceItem.TYPE_ACTION);
+        if (actionSlice != null) {
+            mSlice = actionSlice.getSlice();
+            mSliceAction = actionSlice.getAction();
+        } else {
+            mSlice = slice;
+            mSliceAction = null;
+        }
+
+        if (mSlice == null) {
+            setVisibility(GONE);
+            return;
+        }
+
+        SliceItem title = SliceQuery.find(mSlice, SliceItem.TYPE_TEXT, Slice.HINT_TITLE, null);
+        if (title == null) {
+            mTitle.setVisibility(GONE);
+        } else {
+            mTitle.setVisibility(VISIBLE);
+            mTitle.setText(title.getText());
+        }
+
+        SliceItem text = SliceQuery.find(mSlice, SliceItem.TYPE_TEXT, null, Slice.HINT_TITLE);
+        if (text == null) {
+            mText.setVisibility(GONE);
+        } else {
+            mText.setVisibility(VISIBLE);
+            mText.setText(text.getText());
+        }
+
+        final int visibility = title == null && text == null ? GONE : VISIBLE;
+        if (visibility != getVisibility()) {
+            setVisibility(visibility);
+        }
+    }
+
+    public void setDark(float darkAmount) {
+        mDarkAmount = darkAmount;
+        updateTextColors();
+    }
+
+    public void setTextColor(int textColor) {
+        mTextColor = textColor;
+    }
+
+    private void updateTextColors() {
+        final int blendedColor = ColorUtils.blendARGB(mTextColor, Color.WHITE, mDarkAmount);
+        mTitle.setTextColor(blendedColor);
+        mText.setTextColor(blendedColor);
+    }
+
+    private class KeyguardSliceObserver extends ContentObserver {
+        KeyguardSliceObserver(Handler handler) {
+            super(handler);
+        }
+
+        @Override
+        public void onChange(boolean selfChange) {
+            this.onChange(selfChange, null);
+        }
+
+        @Override
+        public void onChange(boolean selfChange, Uri uri) {
+            showSlice(Slice.bindSlice(getContext().getContentResolver(), mKeyguardSliceUri));
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardStatusView.java b/packages/SystemUI/src/com/android/keyguard/KeyguardStatusView.java
index bc2a59d..78cf2b9 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardStatusView.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardStatusView.java
@@ -19,11 +19,9 @@
 import android.app.ActivityManager;
 import android.app.AlarmManager;
 import android.content.Context;
-import android.content.res.ColorStateList;
 import android.content.res.Configuration;
 import android.content.res.Resources;
 import android.graphics.Color;
-import android.graphics.PorterDuff;
 import android.os.Handler;
 import android.os.Looper;
 import android.os.UserHandle;
@@ -42,8 +40,8 @@
 
 import com.android.internal.util.ArrayUtils;
 import com.android.internal.widget.LockPatternUtils;
+import com.android.settingslib.Utils;
 import com.android.systemui.ChargingView;
-import com.android.systemui.statusbar.policy.DateView;
 
 import java.util.Locale;
 
@@ -55,13 +53,11 @@
     private final LockPatternUtils mLockPatternUtils;
     private final AlarmManager mAlarmManager;
 
-    private TextView mAlarmStatusView;
-    private DateView mDateView;
     private TextClock mClockView;
     private TextView mOwnerInfo;
     private ViewGroup mClockContainer;
     private ChargingView mBatteryDoze;
-    private View mKeyguardStatusArea;
+    private KeyguardSliceView mKeyguardSlice;
     private Runnable mPendingMarqueeStart;
     private Handler mHandler;
 
@@ -69,8 +65,6 @@
     private boolean mPulsing;
     private float mDarkAmount = 0;
     private int mTextColor;
-    private int mDateTextColor;
-    private int mAlarmTextColor;
 
     private KeyguardUpdateMonitorCallback mInfoCallback = new KeyguardUpdateMonitorCallback() {
 
@@ -141,7 +135,6 @@
 
     private void setEnableMarqueeImpl(boolean enabled) {
         if (DEBUG) Log.v(TAG, (enabled ? "Enable" : "Disable") + " transport text marquee");
-        if (mAlarmStatusView != null) mAlarmStatusView.setSelected(enabled);
         if (mOwnerInfo != null) mOwnerInfo.setSelected(enabled);
     }
 
@@ -149,8 +142,6 @@
     protected void onFinishInflate() {
         super.onFinishInflate();
         mClockContainer = findViewById(R.id.keyguard_clock_container);
-        mAlarmStatusView = findViewById(R.id.alarm_status);
-        mDateView = findViewById(R.id.date_view);
         mClockView = findViewById(R.id.clock_view);
         mClockView.setShowCurrentUserTime(true);
         if (KeyguardClockAccessibilityDelegate.isNeeded(mContext)) {
@@ -158,11 +149,9 @@
         }
         mOwnerInfo = findViewById(R.id.owner_info);
         mBatteryDoze = findViewById(R.id.battery_doze);
-        mKeyguardStatusArea = findViewById(R.id.keyguard_status_area);
-        mVisibleInDoze = new View[]{mBatteryDoze, mClockView, mKeyguardStatusArea};
+        mKeyguardSlice = findViewById(R.id.keyguard_status_area);
+        mVisibleInDoze = new View[]{mBatteryDoze, mClockView, mKeyguardSlice};
         mTextColor = mClockView.getCurrentTextColor();
-        mDateTextColor = mDateView.getCurrentTextColor();
-        mAlarmTextColor = mAlarmStatusView.getCurrentTextColor();
 
         boolean shouldMarquee = KeyguardUpdateMonitor.getInstance(mContext).isDeviceInteractive();
         setEnableMarquee(shouldMarquee);
@@ -184,8 +173,6 @@
         layoutParams.bottomMargin = getResources().getDimensionPixelSize(
                 R.dimen.bottom_text_spacing_digital);
         mClockView.setLayoutParams(layoutParams);
-        mDateView.setTextSize(TypedValue.COMPLEX_UNIT_PX,
-                getResources().getDimensionPixelSize(R.dimen.widget_label_font_size));
         if (mOwnerInfo != null) {
             mOwnerInfo.setTextSize(TypedValue.COMPLEX_UNIT_PX,
                     getResources().getDimensionPixelSize(R.dimen.widget_label_font_size));
@@ -193,8 +180,6 @@
     }
 
     public void refreshTime() {
-        mDateView.setDatePattern(Patterns.dateViewSkel);
-
         mClockView.setFormat12Hour(Patterns.clockView12);
         mClockView.setFormat24Hour(Patterns.clockView24);
     }
@@ -205,23 +190,11 @@
         Patterns.update(mContext, nextAlarm != null);
 
         refreshTime();
-        refreshAlarmStatus(nextAlarm);
-    }
-
-    void refreshAlarmStatus(AlarmManager.AlarmClockInfo nextAlarm) {
-        if (nextAlarm != null) {
-            String alarm = formatNextAlarm(mContext, nextAlarm);
-            mAlarmStatusView.setText(alarm);
-            mAlarmStatusView.setContentDescription(
-                    getResources().getString(R.string.keyguard_accessibility_next_alarm, alarm));
-            mAlarmStatusView.setVisibility(View.VISIBLE);
-        } else {
-            mAlarmStatusView.setVisibility(View.GONE);
-        }
     }
 
     public int getClockBottom() {
-        return mKeyguardStatusArea.getBottom();
+        return mKeyguardSlice.getVisibility() == VISIBLE ? mKeyguardSlice.getBottom()
+                : mClockView.getBottom();
     }
 
     public float getClockTextSize() {
@@ -341,11 +314,8 @@
 
         updateDozeVisibleViews();
         mBatteryDoze.setDark(dark);
+        mKeyguardSlice.setDark(darkAmount);
         mClockView.setTextColor(ColorUtils.blendARGB(mTextColor, Color.WHITE, darkAmount));
-        mDateView.setTextColor(ColorUtils.blendARGB(mDateTextColor, Color.WHITE, darkAmount));
-        int blendedAlarmColor = ColorUtils.blendARGB(mAlarmTextColor, Color.WHITE, darkAmount);
-        mAlarmStatusView.setTextColor(blendedAlarmColor);
-        mAlarmStatusView.setCompoundDrawableTintList(ColorStateList.valueOf(blendedAlarmColor));
     }
 
     public void setPulsing(boolean pulsing) {
diff --git a/packages/SystemUI/src/com/android/systemui/doze/DozeScreenStatePreventingAdapter.java b/packages/SystemUI/src/com/android/systemui/doze/DozeScreenStatePreventingAdapter.java
index 5d0a9d7..03b0082 100644
--- a/packages/SystemUI/src/com/android/systemui/doze/DozeScreenStatePreventingAdapter.java
+++ b/packages/SystemUI/src/com/android/systemui/doze/DozeScreenStatePreventingAdapter.java
@@ -33,8 +33,10 @@
 
     @Override
     public void setDozeScreenState(int state) {
-        if (state == Display.STATE_DOZE || state == Display.STATE_DOZE_SUSPEND) {
+        if (state == Display.STATE_DOZE) {
             state = Display.STATE_ON;
+        } else if (state == Display.STATE_DOZE_SUSPEND) {
+            state = Display.STATE_ON_SUSPEND;
         }
         super.setDozeScreenState(state);
     }
diff --git a/packages/SystemUI/src/com/android/systemui/doze/DozeService.java b/packages/SystemUI/src/com/android/systemui/doze/DozeService.java
index 98b1106..6650cc6 100644
--- a/packages/SystemUI/src/com/android/systemui/doze/DozeService.java
+++ b/packages/SystemUI/src/com/android/systemui/doze/DozeService.java
@@ -92,6 +92,7 @@
 
     @Override
     protected void dumpOnHandler(FileDescriptor fd, PrintWriter pw, String[] args) {
+        super.dumpOnHandler(fd, pw, args);
         if (mDozeMachine != null) {
             mDozeMachine.dump(pw);
         }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardSliceProvider.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardSliceProvider.java
new file mode 100644
index 0000000..03018f7
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardSliceProvider.java
@@ -0,0 +1,159 @@
+/*
+ * 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.systemui.keyguard;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.icu.text.DateFormat;
+import android.icu.text.DisplayContext;
+import android.net.Uri;
+import android.os.Handler;
+import android.app.slice.Slice;
+import android.app.slice.SliceProvider;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.systemui.R;
+
+import java.util.Date;
+import java.util.Locale;
+
+/**
+ * Simple Slice provider that shows the current date.
+ */
+public class KeyguardSliceProvider extends SliceProvider {
+
+    public static final String KEYGUARD_SLICE_URI = "content://com.android.systemui.keyguard/main";
+
+    private final Date mCurrentTime = new Date();
+    protected final Uri mSliceUri;
+    private final Handler mHandler;
+    private String mDatePattern;
+    private DateFormat mDateFormat;
+    private String mLastText;
+    private boolean mRegistered;
+    private boolean mRegisteredEveryMinute;
+
+    /**
+     * Receiver responsible for time ticking and updating the date format.
+     */
+    @VisibleForTesting
+    final BroadcastReceiver mIntentReceiver = new BroadcastReceiver() {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            final String action = intent.getAction();
+            if (Intent.ACTION_TIME_TICK.equals(action)
+                    || Intent.ACTION_DATE_CHANGED.equals(action)
+                    || Intent.ACTION_TIME_CHANGED.equals(action)
+                    || Intent.ACTION_TIMEZONE_CHANGED.equals(action)
+                    || Intent.ACTION_LOCALE_CHANGED.equals(action)) {
+                if (Intent.ACTION_LOCALE_CHANGED.equals(action)
+                        || Intent.ACTION_TIMEZONE_CHANGED.equals(action)) {
+                    // need to get a fresh date format
+                    mHandler.post(KeyguardSliceProvider.this::cleanDateFormat);
+                }
+                mHandler.post(KeyguardSliceProvider.this::updateClock);
+            }
+        }
+    };
+
+    public KeyguardSliceProvider() {
+        this(new Handler());
+    }
+
+    @VisibleForTesting
+    KeyguardSliceProvider(Handler handler) {
+        mHandler = handler;
+        mSliceUri = Uri.parse(KEYGUARD_SLICE_URI);
+    }
+
+    @Override
+    public Slice onBindSlice(Uri sliceUri) {
+        return new Slice.Builder(sliceUri).addText(mLastText, Slice.HINT_TITLE).build();
+    }
+
+    @Override
+    public boolean onCreate() {
+
+        mDatePattern = getContext().getString(R.string.system_ui_date_pattern);
+
+        registerClockUpdate(false /* everyMinute */);
+        updateClock();
+        return true;
+    }
+
+    protected void registerClockUpdate(boolean everyMinute) {
+        if (mRegistered) {
+            if (mRegisteredEveryMinute == everyMinute) {
+                return;
+            } else {
+                unregisterClockUpdate();
+            }
+        }
+
+        IntentFilter filter = new IntentFilter();
+        if (everyMinute) {
+            filter.addAction(Intent.ACTION_TIME_TICK);
+        }
+        filter.addAction(Intent.ACTION_DATE_CHANGED);
+        filter.addAction(Intent.ACTION_TIME_CHANGED);
+        filter.addAction(Intent.ACTION_TIMEZONE_CHANGED);
+        filter.addAction(Intent.ACTION_LOCALE_CHANGED);
+        getContext().registerReceiver(mIntentReceiver, filter, null /* permission*/,
+                null /* scheduler */);
+        mRegistered = true;
+        mRegisteredEveryMinute = everyMinute;
+    }
+
+    protected void unregisterClockUpdate() {
+        if (!mRegistered) {
+            return;
+        }
+        getContext().unregisterReceiver(mIntentReceiver);
+        mRegistered = false;
+    }
+
+    @VisibleForTesting
+    boolean isRegistered() {
+        return mRegistered;
+    }
+
+    protected void updateClock() {
+        final String text = getFormattedDate();
+        if (!text.equals(mLastText)) {
+            mLastText = text;
+            getContext().getContentResolver().notifyChange(mSliceUri, null /* observer */);
+        }
+    }
+
+    protected String getFormattedDate() {
+        if (mDateFormat == null) {
+            final Locale l = Locale.getDefault();
+            DateFormat format = DateFormat.getInstanceForSkeleton(mDatePattern, l);
+            format.setContext(DisplayContext.CAPITALIZATION_FOR_STANDALONE);
+            mDateFormat = format;
+        }
+        mCurrentTime.setTime(System.currentTimeMillis());
+        return mDateFormat.format(mCurrentTime);
+    }
+
+    @VisibleForTesting
+    void cleanDateFormat() {
+        mDateFormat = null;
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/ExpandableNotificationRow.java b/packages/SystemUI/src/com/android/systemui/statusbar/ExpandableNotificationRow.java
index 6c5f4b2..8ff950e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/ExpandableNotificationRow.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/ExpandableNotificationRow.java
@@ -69,6 +69,7 @@
 import com.android.systemui.statusbar.notification.HybridNotificationView;
 import com.android.systemui.statusbar.notification.NotificationInflater;
 import com.android.systemui.statusbar.notification.NotificationUtils;
+import com.android.systemui.statusbar.notification.NotificationViewWrapper;
 import com.android.systemui.statusbar.notification.VisualStabilityManager;
 import com.android.systemui.statusbar.phone.NotificationGroupManager;
 import com.android.systemui.statusbar.phone.StatusBar;
@@ -453,6 +454,11 @@
         } else {
             headsUpheight = mMaxHeadsUpHeight;
         }
+        NotificationViewWrapper headsUpWrapper = layout.getVisibleWrapper(
+                NotificationContentView.VISIBLE_TYPE_HEADSUP);
+        if (headsUpWrapper != null) {
+            headsUpheight = Math.max(headsUpheight, headsUpWrapper.getMinLayoutHeight());
+        }
         layout.setHeights(minHeight, headsUpheight, mNotificationMaxHeight,
                 mNotificationAmbientHeight);
     }
@@ -1256,16 +1262,21 @@
     }
 
     private void initDimens() {
-        mNotificationMinHeightLegacy = getFontScaledHeight(R.dimen.notification_min_height_legacy);
-        mNotificationMinHeight = getFontScaledHeight(R.dimen.notification_min_height);
-        mNotificationMinHeightLarge = getFontScaledHeight(
+        mNotificationMinHeightLegacy = NotificationUtils.getFontScaledHeight(mContext,
+                R.dimen.notification_min_height_legacy);
+        mNotificationMinHeight = NotificationUtils.getFontScaledHeight(mContext,
+                R.dimen.notification_min_height);
+        mNotificationMinHeightLarge = NotificationUtils.getFontScaledHeight(mContext,
                 R.dimen.notification_min_height_increased);
-        mNotificationMaxHeight = getFontScaledHeight(R.dimen.notification_max_height);
-        mNotificationAmbientHeight = getFontScaledHeight(R.dimen.notification_ambient_height);
-        mMaxHeadsUpHeightLegacy = getFontScaledHeight(
+        mNotificationMaxHeight = NotificationUtils.getFontScaledHeight(mContext,
+                R.dimen.notification_max_height);
+        mNotificationAmbientHeight = NotificationUtils.getFontScaledHeight(mContext,
+                R.dimen.notification_ambient_height);
+        mMaxHeadsUpHeightLegacy = NotificationUtils.getFontScaledHeight(mContext,
                 R.dimen.notification_max_heads_up_height_legacy);
-        mMaxHeadsUpHeight = getFontScaledHeight(R.dimen.notification_max_heads_up_height);
-        mMaxHeadsUpHeightIncreased = getFontScaledHeight(
+        mMaxHeadsUpHeight = NotificationUtils.getFontScaledHeight(mContext,
+                R.dimen.notification_max_heads_up_height);
+        mMaxHeadsUpHeightIncreased = NotificationUtils.getFontScaledHeight(mContext,
                 R.dimen.notification_max_heads_up_height_increased);
 
         Resources res = getResources();
@@ -1280,17 +1291,6 @@
     }
 
     /**
-     * @param dimenId the dimen to look up
-     * @return the font scaled dimen as if it were in sp but doesn't shrink sizes below dp
-     */
-    private int getFontScaledHeight(int dimenId) {
-        int dimensionPixelSize = getResources().getDimensionPixelSize(dimenId);
-        float factor = Math.max(1.0f, getResources().getDisplayMetrics().scaledDensity /
-                getResources().getDisplayMetrics().density);
-        return (int) (dimensionPixelSize * factor);
-    }
-
-    /**
      * Resets this view so it can be re-used for an updated notification.
      */
     public void reset() {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/ViewTransformationHelper.java b/packages/SystemUI/src/com/android/systemui/statusbar/ViewTransformationHelper.java
index 5353005..09b11c2 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/ViewTransformationHelper.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/ViewTransformationHelper.java
@@ -35,7 +35,8 @@
 /**
  * A view that can be transformed to and from.
  */
-public class ViewTransformationHelper implements TransformableView {
+public class ViewTransformationHelper implements TransformableView,
+        TransformState.TransformInfo {
 
     private static final int TAG_CONTAINS_TRANSFORMED_VIEW = R.id.contains_transformed_view;
 
@@ -59,7 +60,7 @@
     public TransformState getCurrentState(int fadingView) {
         View view = mTransformedViews.get(fadingView);
         if (view != null && view.getVisibility() != View.GONE) {
-            return TransformState.createFrom(view);
+            return TransformState.createFrom(view, this);
         }
         return null;
     }
@@ -88,6 +89,7 @@
                         endRunnable.run();
                     }
                     setVisible(false);
+                    mViewTransformationAnimation = null;
                 } else {
                     abortTransformations();
                 }
@@ -245,7 +247,7 @@
     }
 
     public void resetTransformedView(View view) {
-        TransformState state = TransformState.createFrom(view);
+        TransformState state = TransformState.createFrom(view, this);
         state.setVisible(true /* visible */, true /* force */);
         state.recycle();
     }
@@ -257,6 +259,11 @@
         return new ArraySet<>(mTransformedViews.values());
     }
 
+    @Override
+    public boolean isAnimating() {
+        return mViewTransformationAnimation != null && mViewTransformationAnimation.isRunning();
+    }
+
     public static abstract class CustomTransformation {
         /**
          * Transform a state to the given view
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/ImageTransformState.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/ImageTransformState.java
index 92f26d6..8227b77 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/ImageTransformState.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/ImageTransformState.java
@@ -39,8 +39,8 @@
     private Icon mIcon;
 
     @Override
-    public void initFrom(View view) {
-        super.initFrom(view);
+    public void initFrom(View view, TransformInfo transformInfo) {
+        super.initFrom(view, transformInfo);
         if (view instanceof ImageView) {
             mIcon = (Icon) view.getTag(ICON_TAG);
         }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/MessagingLayoutTransformState.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/MessagingLayoutTransformState.java
new file mode 100644
index 0000000..4c2a2f5
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/MessagingLayoutTransformState.java
@@ -0,0 +1,405 @@
+/*
+ * 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.systemui.statusbar.notification;
+
+import android.content.res.Resources;
+import android.util.Pools;
+import android.view.View;
+import android.view.ViewGroup;
+
+import com.android.internal.widget.MessagingGroup;
+import com.android.internal.widget.MessagingLayout;
+import com.android.internal.widget.MessagingLinearLayout;
+import com.android.internal.widget.MessagingMessage;
+import com.android.internal.widget.MessagingPropertyAnimator;
+import com.android.internal.widget.ViewClippingUtil;
+import com.android.systemui.Interpolators;
+import com.android.systemui.statusbar.ExpandableNotificationRow;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+
+/**
+ * A transform state of the action list
+*/
+public class MessagingLayoutTransformState extends TransformState {
+
+    private static Pools.SimplePool<MessagingLayoutTransformState> sInstancePool
+            = new Pools.SimplePool<>(40);
+    private MessagingLinearLayout mMessageContainer;
+    private MessagingLayout mMessagingLayout;
+    private HashMap<MessagingGroup, MessagingGroup> mGroupMap = new HashMap<>();
+    private float mRelativeTranslationOffset;
+
+    public static MessagingLayoutTransformState obtain() {
+        MessagingLayoutTransformState instance = sInstancePool.acquire();
+        if (instance != null) {
+            return instance;
+        }
+        return new MessagingLayoutTransformState();
+    }
+
+    @Override
+    public void initFrom(View view, TransformInfo transformInfo) {
+        super.initFrom(view, transformInfo);
+        if (mTransformedView instanceof MessagingLinearLayout) {
+            mMessageContainer = (MessagingLinearLayout) mTransformedView;
+            mMessagingLayout = mMessageContainer.getMessagingLayout();
+            Resources resources = view.getContext().getResources();
+            mRelativeTranslationOffset = resources.getDisplayMetrics().density * 8;
+        }
+    }
+
+    @Override
+    public boolean transformViewTo(TransformState otherState, float transformationAmount) {
+        if (otherState instanceof MessagingLayoutTransformState) {
+            // It's a party! Let's transform between these two layouts!
+            transformViewInternal((MessagingLayoutTransformState) otherState, transformationAmount,
+                    true /* to */);
+            return true;
+        } else {
+            return super.transformViewTo(otherState, transformationAmount);
+        }
+    }
+
+    @Override
+    public void transformViewFrom(TransformState otherState, float transformationAmount) {
+        if (otherState instanceof MessagingLayoutTransformState) {
+            // It's a party! Let's transform between these two layouts!
+            transformViewInternal((MessagingLayoutTransformState) otherState, transformationAmount,
+                    false /* to */);
+        } else {
+            super.transformViewFrom(otherState, transformationAmount);
+        }
+    }
+
+    private void transformViewInternal(MessagingLayoutTransformState mlt,
+            float transformationAmount, boolean to) {
+        ArrayList<MessagingGroup> ownGroups = filterHiddenGroups(
+                mMessagingLayout.getMessagingGroups());
+        ArrayList<MessagingGroup> otherGroups = filterHiddenGroups(
+                mlt.mMessagingLayout.getMessagingGroups());
+        HashMap<MessagingGroup, MessagingGroup> pairs = findPairs(ownGroups, otherGroups);
+        MessagingGroup lastPairedGroup = null;
+        float currentTranslation = 0;
+        float transformationDistanceRemaining = 0;
+        for (int i = ownGroups.size() - 1; i >= 0; i--) {
+            MessagingGroup ownGroup = ownGroups.get(i);
+            MessagingGroup matchingGroup = pairs.get(ownGroup);
+            if (!isGone(ownGroup)) {
+                if (matchingGroup != null) {
+                    transformGroups(ownGroup, matchingGroup, transformationAmount, to);
+                    if (lastPairedGroup == null) {
+                        lastPairedGroup = ownGroup;
+                        if (to){
+                            float totalTranslation = ownGroup.getTop() - matchingGroup.getTop();
+                            transformationDistanceRemaining
+                                    = matchingGroup.getAvatar().getTranslationY();
+                            currentTranslation = transformationDistanceRemaining - totalTranslation;
+                        } else {
+                            float totalTranslation = matchingGroup.getTop() - ownGroup.getTop();
+                            currentTranslation = ownGroup.getAvatar().getTranslationY();
+                            transformationDistanceRemaining = currentTranslation - totalTranslation;
+                        }
+                    }
+                } else {
+                    float groupTransformationAmount = transformationAmount;
+                    if (lastPairedGroup != null) {
+                        adaptGroupAppear(ownGroup, transformationAmount, currentTranslation,
+                                to);
+                        int distance = lastPairedGroup.getTop() - ownGroup.getTop();
+                        float transformationDistance = mTransformInfo.isAnimating()
+                                ? distance
+                                : ownGroup.getHeight() * 0.75f;
+                        float translationProgress = transformationDistanceRemaining
+                                - (distance - transformationDistance);
+                        groupTransformationAmount =
+                                translationProgress / transformationDistance;
+                        groupTransformationAmount = Math.max(0.0f, Math.min(1.0f,
+                                groupTransformationAmount));
+                        if (to) {
+                            groupTransformationAmount = 1.0f - groupTransformationAmount;
+                        }
+                    }
+                    if (to) {
+                        disappear(ownGroup, groupTransformationAmount);
+                    } else {
+                        appear(ownGroup, groupTransformationAmount);
+                    }
+                }
+            }
+        }
+    }
+
+    private void appear(MessagingGroup ownGroup, float transformationAmount) {
+        MessagingLinearLayout ownMessages = ownGroup.getMessageContainer();
+        for (int j = 0; j < ownMessages.getChildCount(); j++) {
+            View child = ownMessages.getChildAt(j);
+            if (isGone(child)) {
+                continue;
+            }
+            appear(child, transformationAmount);
+            setClippingDeactivated(child, true);
+        }
+        appear(ownGroup.getAvatar(), transformationAmount);
+        appear(ownGroup.getSender(), transformationAmount);
+        setClippingDeactivated(ownGroup.getSender(), true);
+        setClippingDeactivated(ownGroup.getAvatar(), true);
+    }
+
+    private void adaptGroupAppear(MessagingGroup ownGroup, float transformationAmount,
+            float overallTranslation, boolean to) {
+        float relativeOffset;
+        if (to) {
+            relativeOffset = transformationAmount * mRelativeTranslationOffset;
+        } else {
+            relativeOffset = (1.0f - transformationAmount) * mRelativeTranslationOffset;
+        }
+        if (ownGroup.getSender().getVisibility() != View.GONE) {
+            relativeOffset *= 0.5f;
+        }
+        ownGroup.getMessageContainer().setTranslationY(relativeOffset);
+        ownGroup.setTranslationY(overallTranslation * 0.85f);
+    }
+
+    private void disappear(MessagingGroup ownGroup, float transformationAmount) {
+        MessagingLinearLayout ownMessages = ownGroup.getMessageContainer();
+        for (int j = 0; j < ownMessages.getChildCount(); j++) {
+            View child = ownMessages.getChildAt(j);
+            if (isGone(child)) {
+                continue;
+            }
+            disappear(child, transformationAmount);
+            setClippingDeactivated(child, true);
+        }
+        disappear(ownGroup.getAvatar(), transformationAmount);
+        disappear(ownGroup.getSender(), transformationAmount);
+        setClippingDeactivated(ownGroup.getSender(), true);
+        setClippingDeactivated(ownGroup.getAvatar(), true);
+    }
+
+    private void appear(View child, float transformationAmount) {
+        if (child.getVisibility() == View.GONE) {
+            return;
+        }
+        TransformState ownState = TransformState.createFrom(child, mTransformInfo);
+        ownState.appear(transformationAmount, null);
+        ownState.recycle();
+    }
+
+    private void disappear(View child, float transformationAmount) {
+        if (child.getVisibility() == View.GONE) {
+            return;
+        }
+        TransformState ownState = TransformState.createFrom(child, mTransformInfo);
+        ownState.disappear(transformationAmount, null);
+        ownState.recycle();
+    }
+
+    private ArrayList<MessagingGroup> filterHiddenGroups(
+            ArrayList<MessagingGroup> groups) {
+        ArrayList<MessagingGroup> result = new ArrayList<>(groups);
+        for (int i = 0; i < result.size(); i++) {
+            MessagingGroup messagingGroup = result.get(i);
+            if (isGone(messagingGroup)) {
+                result.remove(i);
+                i--;
+            }
+        }
+        return result;
+    }
+
+    private void transformGroups(MessagingGroup ownGroup, MessagingGroup otherGroup,
+            float transformationAmount, boolean to) {
+        transformView(transformationAmount, to, ownGroup.getSender(), otherGroup.getSender(),
+                true /* sameAsAny */);
+        transformView(transformationAmount, to, ownGroup.getAvatar(), otherGroup.getAvatar(),
+                true /* sameAsAny */);
+        MessagingLinearLayout ownMessages = ownGroup.getMessageContainer();
+        MessagingLinearLayout otherMessages = otherGroup.getMessageContainer();
+        float previousTranslation = 0;
+        for (int i = 0; i < ownMessages.getChildCount(); i++) {
+            View child = ownMessages.getChildAt(ownMessages.getChildCount() - 1 - i);
+            if (isGone(child)) {
+                continue;
+            }
+            int otherIndex = otherMessages.getChildCount() - 1 - i;
+            View otherChild = null;
+            if (otherIndex >= 0) {
+                otherChild = otherMessages.getChildAt(otherIndex);
+                if (isGone(otherChild)) {
+                    otherChild = null;
+                }
+            }
+            if (otherChild == null) {
+                float distanceToTop = child.getTop() + child.getHeight() + previousTranslation;
+                transformationAmount = distanceToTop / child.getHeight();
+                transformationAmount = Math.max(0.0f, Math.min(1.0f, transformationAmount));
+                if (to) {
+                    transformationAmount = 1.0f - transformationAmount;
+                }
+            }
+            transformView(transformationAmount, to, child, otherChild, false /* sameAsAny */);
+            if (otherChild == null) {
+                child.setTranslationY(previousTranslation);
+                setClippingDeactivated(child, true);
+            } else if (to) {
+                float totalTranslation = child.getTop() + ownGroup.getTop()
+                        - otherChild.getTop() - otherChild.getTop();
+                previousTranslation = otherChild.getTranslationY() - totalTranslation;
+            } else {
+                previousTranslation = child.getTranslationY();
+            }
+        }
+    }
+
+    private void transformView(float transformationAmount, boolean to, View ownView,
+            View otherView, boolean sameAsAny) {
+        TransformState ownState = TransformState.createFrom(ownView, mTransformInfo);
+        if (!mTransformInfo.isAnimating()) {
+            ownState.setDefaultInterpolator(Interpolators.LINEAR);
+        }
+        ownState.setIsSameAsAnyView(sameAsAny);
+        if (to) {
+            if (otherView != null) {
+                TransformState otherState = TransformState.createFrom(otherView, mTransformInfo);
+                ownState.transformViewTo(otherState, transformationAmount);
+                otherState.recycle();
+            } else {
+                ownState.disappear(transformationAmount, null);
+            }
+        } else {
+            if (otherView != null) {
+                TransformState otherState = TransformState.createFrom(otherView, mTransformInfo);
+                ownState.transformViewFrom(otherState, transformationAmount);
+                otherState.recycle();
+            } else {
+                ownState.appear(transformationAmount, null);
+            }
+        }
+        ownState.recycle();
+    }
+
+    private HashMap<MessagingGroup, MessagingGroup> findPairs(ArrayList<MessagingGroup> ownGroups,
+            ArrayList<MessagingGroup> otherGroups) {
+        mGroupMap.clear();
+        int lastMatch = Integer.MAX_VALUE;
+        for (int i = ownGroups.size() - 1; i >= 0; i--) {
+            MessagingGroup ownGroup = ownGroups.get(i);
+            MessagingGroup bestMatch = null;
+            int bestCompatibility = 0;
+            for (int j = Math.min(otherGroups.size(), lastMatch) - 1; j >= 0; j--) {
+                MessagingGroup otherGroup = otherGroups.get(j);
+                int compatibility = ownGroup.calculateGroupCompatibility(otherGroup);
+                if (compatibility > bestCompatibility) {
+                    bestCompatibility = compatibility;
+                    bestMatch = otherGroup;
+                    lastMatch = j;
+                }
+            }
+            if (bestMatch != null) {
+                mGroupMap.put(ownGroup, bestMatch);
+            }
+        }
+        return mGroupMap;
+    }
+
+    private boolean isGone(View view) {
+        if (view.getVisibility() == View.GONE) {
+            return true;
+        }
+        final ViewGroup.LayoutParams lp = view.getLayoutParams();
+        if (lp instanceof MessagingLinearLayout.LayoutParams
+                && ((MessagingLinearLayout.LayoutParams) lp).hide) {
+            return true;
+        }
+        return false;
+    }
+
+    @Override
+    public void setVisible(boolean visible, boolean force) {
+        resetTransformedView();
+        ArrayList<MessagingGroup> ownGroups = mMessagingLayout.getMessagingGroups();
+        for (int i = 0; i < ownGroups.size(); i++) {
+            MessagingGroup ownGroup = ownGroups.get(i);
+            if (!isGone(ownGroup)) {
+                MessagingLinearLayout ownMessages = ownGroup.getMessageContainer();
+                for (int j = 0; j < ownMessages.getChildCount(); j++) {
+                    MessagingMessage child = (MessagingMessage) ownMessages.getChildAt(j);
+                    setVisible(child, visible, force);
+                }
+                setVisible(ownGroup.getAvatar(), visible, force);
+                setVisible(ownGroup.getSender(), visible, force);
+            }
+        }
+    }
+
+    private void setVisible(View child, boolean visible, boolean force) {
+        if (isGone(child) || MessagingPropertyAnimator.isAnimatingAlpha(child)) {
+            return;
+        }
+        TransformState ownState = TransformState.createFrom(child, mTransformInfo);
+        ownState.setVisible(visible, force);
+        ownState.recycle();
+    }
+
+    @Override
+    protected void resetTransformedView() {
+        super.resetTransformedView();
+        ArrayList<MessagingGroup> ownGroups = mMessagingLayout.getMessagingGroups();
+        for (int i = 0; i < ownGroups.size(); i++) {
+            MessagingGroup ownGroup = ownGroups.get(i);
+            if (!isGone(ownGroup)) {
+                MessagingLinearLayout ownMessages = ownGroup.getMessageContainer();
+                for (int j = 0; j < ownMessages.getChildCount(); j++) {
+                    View child = ownMessages.getChildAt(j);
+                    if (isGone(child)) {
+                        continue;
+                    }
+                    resetTransformedView(child);
+                    setClippingDeactivated(child, false);
+                }
+                resetTransformedView(ownGroup.getAvatar());
+                resetTransformedView(ownGroup.getSender());
+                setClippingDeactivated(ownGroup.getAvatar(), false);
+                setClippingDeactivated(ownGroup.getSender(), false);
+                ownGroup.setTranslationY(0);
+                ownGroup.getMessageContainer().setTranslationY(0);
+            }
+        }
+    }
+
+    private void resetTransformedView(View child) {
+        TransformState ownState = TransformState.createFrom(child, mTransformInfo);
+        ownState.resetTransformedView();
+        ownState.recycle();
+    }
+
+    @Override
+    protected void reset() {
+        super.reset();
+        mMessageContainer = null;
+        mMessagingLayout = null;
+    }
+
+    @Override
+    public void recycle() {
+        super.recycle();
+        mGroupMap.clear();;
+        sInstancePool.release(this);
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationMessagingTemplateViewWrapper.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationMessagingTemplateViewWrapper.java
index f6ee1ca..fb5644f 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationMessagingTemplateViewWrapper.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationMessagingTemplateViewWrapper.java
@@ -16,7 +16,9 @@
 
 package com.android.systemui.statusbar.notification;
 
+import com.android.internal.widget.MessagingLayout;
 import com.android.internal.widget.MessagingLinearLayout;
+import com.android.systemui.R;
 import com.android.systemui.statusbar.ExpandableNotificationRow;
 import com.android.systemui.statusbar.TransformableView;
 
@@ -32,41 +34,20 @@
  */
 public class NotificationMessagingTemplateViewWrapper extends NotificationTemplateViewWrapper {
 
-    private View mContractedMessage;
-    private ArrayList<View> mHistoricMessages = new ArrayList<View>();
+    private final int mMinHeightWithActions;
+    private MessagingLayout mMessagingLayout;
+    private MessagingLinearLayout mMessagingLinearLayout;
 
     protected NotificationMessagingTemplateViewWrapper(Context ctx, View view,
             ExpandableNotificationRow row) {
         super(ctx, view, row);
+        mMessagingLayout = (MessagingLayout) view;
+        mMinHeightWithActions = NotificationUtils.getFontScaledHeight(ctx,
+                R.dimen.notification_messaging_actions_min_height);
     }
 
     private void resolveViews() {
-        mContractedMessage = null;
-
-        View container = mView.findViewById(com.android.internal.R.id.notification_messaging);
-        if (container instanceof MessagingLinearLayout
-                && ((MessagingLinearLayout) container).getChildCount() > 0) {
-            MessagingLinearLayout messagingContainer = (MessagingLinearLayout) container;
-
-            int childCount = messagingContainer.getChildCount();
-            for (int i = 0; i < childCount; i++) {
-                View child = messagingContainer.getChildAt(i);
-
-                if (child.getVisibility() == View.GONE
-                        && child instanceof TextView
-                        && !TextUtils.isEmpty(((TextView) child).getText())) {
-                    mHistoricMessages.add(child);
-                }
-
-                // Only consider the first visible child - transforming to a position other than the
-                // first looks bad because we have to move across other messages that are fading in.
-                if (child.getId() == messagingContainer.getContractedChildId()) {
-                    mContractedMessage = child;
-                } else if (child.getVisibility() == View.VISIBLE) {
-                    break;
-                }
-            }
-        }
+        mMessagingLinearLayout = mMessagingLayout.getMessagingLinearLayout();
     }
 
     @Override
@@ -81,16 +62,22 @@
     protected void updateTransformedTypes() {
         // This also clears the existing types
         super.updateTransformedTypes();
-        if (mContractedMessage != null) {
-            mTransformationHelper.addTransformedView(TransformableView.TRANSFORMING_VIEW_TEXT,
-                    mContractedMessage);
+        if (mMessagingLinearLayout != null) {
+            mTransformationHelper.addTransformedView(mMessagingLinearLayout.getId(),
+                    mMessagingLinearLayout);
         }
     }
 
     @Override
     public void setRemoteInputVisible(boolean visible) {
-        for (int i = 0; i < mHistoricMessages.size(); i++) {
-            mHistoricMessages.get(i).setVisibility(visible ? View.VISIBLE : View.GONE);
+        mMessagingLayout.showHistoricMessages(visible);
+    }
+
+    @Override
+    public int getMinLayoutHeight() {
+        if (mActionsContainer != null && mActionsContainer.getVisibility() != View.GONE) {
+            return mMinHeightWithActions;
         }
+        return super.getMinLayoutHeight();
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationTemplateViewWrapper.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationTemplateViewWrapper.java
index bb979eb..fd085d9 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationTemplateViewWrapper.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationTemplateViewWrapper.java
@@ -41,7 +41,7 @@
     private ProgressBar mProgressBar;
     private TextView mTitle;
     private TextView mText;
-    private View mActionsContainer;
+    protected View mActionsContainer;
     private View mReplyAction;
     private Rect mTmpRect = new Rect();
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationUtils.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationUtils.java
index 3115361..af393c9 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationUtils.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationUtils.java
@@ -66,4 +66,14 @@
                 Settings.System.HAPTIC_FEEDBACK_ENABLED, 0, UserHandle.USER_CURRENT) == 0;
     }
 
+    /**
+     * @param dimenId the dimen to look up
+     * @return the font scaled dimen as if it were in sp but doesn't shrink sizes below dp
+     */
+    public static int getFontScaledHeight(Context context, int dimenId) {
+        int dimensionPixelSize = context.getResources().getDimensionPixelSize(dimenId);
+        float factor = Math.max(1.0f, context.getResources().getDisplayMetrics().scaledDensity /
+                context.getResources().getDisplayMetrics().density);
+        return (int) (dimensionPixelSize * factor);
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationViewWrapper.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationViewWrapper.java
index 5200d69..1cd5f15 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationViewWrapper.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationViewWrapper.java
@@ -190,4 +190,8 @@
     public boolean disallowSingleClick(float x, float y) {
         return false;
     }
+
+    public int getMinLayoutHeight() {
+        return 0;
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/TextViewTransformState.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/TextViewTransformState.java
index c4aabe4..178c813 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/TextViewTransformState.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/TextViewTransformState.java
@@ -33,8 +33,8 @@
     private TextView mText;
 
     @Override
-    public void initFrom(View view) {
-        super.initFrom(view);
+    public void initFrom(View view, TransformInfo transformInfo) {
+        super.initFrom(view, transformInfo);
         if (view instanceof TextView) {
             mText = (TextView) view;
         }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/TransformState.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/TransformState.java
index bafedff..ad07af0 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/TransformState.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/TransformState.java
@@ -26,6 +26,8 @@
 import android.widget.ProgressBar;
 import android.widget.TextView;
 
+import com.android.internal.widget.MessagingPropertyAnimator;
+import com.android.internal.widget.ViewClippingUtil;
 import com.android.systemui.Interpolators;
 import com.android.systemui.R;
 import com.android.systemui.statusbar.CrossFadeHelper;
@@ -43,23 +45,46 @@
     public static final int TRANSFORM_ALL = TRANSFORM_X | TRANSFORM_Y;
 
     private static final float UNDEFINED = -1f;
-    private static final int CLIP_CLIPPING_SET = R.id.clip_children_set_tag;
-    private static final int CLIP_CHILDREN_TAG = R.id.clip_children_tag;
-    private static final int CLIP_TO_PADDING = R.id.clip_to_padding_tag;
     private static final int TRANSFORMATION_START_X = R.id.transformation_start_x_tag;
     private static final int TRANSFORMATION_START_Y = R.id.transformation_start_y_tag;
     private static final int TRANSFORMATION_START_SCLALE_X = R.id.transformation_start_scale_x_tag;
     private static final int TRANSFORMATION_START_SCLALE_Y = R.id.transformation_start_scale_y_tag;
     private static Pools.SimplePool<TransformState> sInstancePool = new Pools.SimplePool<>(40);
+    private static ViewClippingUtil.ClippingParameters CLIPPING_PARAMETERS
+            = new ViewClippingUtil.ClippingParameters() {
+        @Override
+        public boolean shouldFinish(View view) {
+            if (view instanceof ExpandableNotificationRow) {
+                ExpandableNotificationRow row = (ExpandableNotificationRow) view;
+                return !row.isChildInGroup();
+            }
+            return false;
+        }
+
+        @Override
+        public void onClippingStateChanged(View view, boolean isClipping) {
+            if (view instanceof ExpandableNotificationRow) {
+                ExpandableNotificationRow row = (ExpandableNotificationRow) view;
+                if (isClipping) {
+                    row.setClipToActualHeight(true);
+                } else if (row.isChildInGroup()) {
+                    row.setClipToActualHeight(false);
+                }
+            }
+        }
+    };
 
     protected View mTransformedView;
+    protected TransformInfo mTransformInfo;
     private int[] mOwnPosition = new int[2];
     private boolean mSameAsAny;
     private float mTransformationEndY = UNDEFINED;
     private float mTransformationEndX = UNDEFINED;
+    private Interpolator mDefaultInterpolator = Interpolators.FAST_OUT_SLOW_IN;
 
-    public void initFrom(View view) {
+    public void initFrom(View view, TransformInfo transformInfo) {
         mTransformedView = view;
+        mTransformInfo = transformInfo;
     }
 
     /**
@@ -108,13 +133,16 @@
         final View transformedView = mTransformedView;
         boolean transformX = (transformationFlags & TRANSFORM_X) != 0;
         boolean transformY = (transformationFlags & TRANSFORM_Y) != 0;
-        boolean transformScale = transformScale(otherState);
+        boolean differentHeight = otherState.getViewHeight() != getViewHeight();
+        boolean differentWidth = otherState.getViewWidth() != getViewWidth();
+        boolean transformScale = transformScale(otherState) && (differentHeight || differentWidth);
         // lets animate the positions correctly
         if (transformationAmount == 0.0f
                 || transformX && getTransformationStartX() == UNDEFINED
                 || transformY && getTransformationStartY() == UNDEFINED
-                || transformScale && getTransformationStartScaleX() == UNDEFINED
-                || transformScale && getTransformationStartScaleY() == UNDEFINED) {
+                || transformScale && getTransformationStartScaleX() == UNDEFINED && differentWidth
+                || transformScale && getTransformationStartScaleY() == UNDEFINED
+                        && differentHeight) {
             int[] otherPosition;
             if (transformationAmount != 0.0f) {
                 otherPosition = otherState.getLaidOutLocationOnScreen();
@@ -132,14 +160,14 @@
                 }
                 // we also want to animate the scale if we're the same
                 View otherView = otherState.getTransformedView();
-                if (transformScale && otherState.getViewWidth() != getViewWidth()) {
+                if (transformScale && differentWidth) {
                     setTransformationStartScaleX(otherState.getViewWidth() * otherView.getScaleX()
                             / (float) getViewWidth());
                     transformedView.setPivotX(0);
                 } else {
                     setTransformationStartScaleX(UNDEFINED);
                 }
-                if (transformScale && otherState.getViewHeight() != getViewHeight()) {
+                if (transformScale && differentHeight) {
                     setTransformationStartScaleY(otherState.getViewHeight() * otherView.getScaleY()
                             / (float) getViewHeight());
                     transformedView.setPivotY(0);
@@ -159,7 +187,7 @@
             }
             setClippingDeactivated(transformedView, true);
         }
-        float interpolatedValue = Interpolators.FAST_OUT_SLOW_IN.getInterpolation(
+        float interpolatedValue = mDefaultInterpolator.getInterpolation(
                 transformationAmount);
         if (transformX) {
             float interpolation = interpolatedValue;
@@ -297,7 +325,7 @@
             }
             setClippingDeactivated(transformedView, true);
         }
-        float interpolatedValue = Interpolators.FAST_OUT_SLOW_IN.getInterpolation(
+        float interpolatedValue = mDefaultInterpolator.getInterpolation(
                 transformationAmount);
         int[] otherStablePosition = otherState.getLaidOutLocationOnScreen();
         int[] ownPosition = getLaidOutLocationOnScreen();
@@ -354,59 +382,8 @@
         }
     }
 
-    public static void setClippingDeactivated(final View transformedView, boolean deactivated) {
-        if (!(transformedView.getParent() instanceof ViewGroup)) {
-            return;
-        }
-        ViewGroup view = (ViewGroup) transformedView.getParent();
-        while (true) {
-            ArraySet<View> clipSet = (ArraySet<View>) view.getTag(CLIP_CLIPPING_SET);
-            if (clipSet == null) {
-                clipSet = new ArraySet<>();
-                view.setTag(CLIP_CLIPPING_SET, clipSet);
-            }
-            Boolean clipChildren = (Boolean) view.getTag(CLIP_CHILDREN_TAG);
-            if (clipChildren == null) {
-                clipChildren = view.getClipChildren();
-                view.setTag(CLIP_CHILDREN_TAG, clipChildren);
-            }
-            Boolean clipToPadding = (Boolean) view.getTag(CLIP_TO_PADDING);
-            if (clipToPadding == null) {
-                clipToPadding = view.getClipToPadding();
-                view.setTag(CLIP_TO_PADDING, clipToPadding);
-            }
-            ExpandableNotificationRow row = view instanceof ExpandableNotificationRow
-                    ? (ExpandableNotificationRow) view
-                    : null;
-            if (!deactivated) {
-                clipSet.remove(transformedView);
-                if (clipSet.isEmpty()) {
-                    view.setClipChildren(clipChildren);
-                    view.setClipToPadding(clipToPadding);
-                    view.setTag(CLIP_CLIPPING_SET, null);
-                    if (row != null) {
-                        row.setClipToActualHeight(true);
-                    }
-                }
-            } else {
-                clipSet.add(transformedView);
-                view.setClipChildren(false);
-                view.setClipToPadding(false);
-                if (row != null && row.isChildInGroup()) {
-                    // We still want to clip to the parent's height
-                    row.setClipToActualHeight(false);
-                }
-            }
-            if (row != null && !row.isChildInGroup()) {
-                return;
-            }
-            final ViewParent parent = view.getParent();
-            if (parent instanceof ViewGroup) {
-                view = (ViewGroup) parent;
-            } else {
-                return;
-            }
-        }
+    protected void setClippingDeactivated(final View transformedView, boolean deactivated) {
+        ViewClippingUtil.setClippingDeactivated(transformedView, deactivated, CLIPPING_PARAMETERS);
     }
 
     public int[] getLaidOutLocationOnScreen() {
@@ -423,6 +400,9 @@
         // remove scale
         mOwnPosition[0] -= (1.0f - mTransformedView.getScaleX()) * mTransformedView.getPivotX();
         mOwnPosition[1] -= (1.0f - mTransformedView.getScaleY()) * mTransformedView.getPivotY();
+
+        // Remove local translations
+        mOwnPosition[1] -= MessagingPropertyAnimator.getLocalTranslationY(mTransformedView);
         return mOwnPosition;
     }
 
@@ -444,20 +424,26 @@
         CrossFadeHelper.fadeOut(mTransformedView, transformationAmount);
     }
 
-    public static TransformState createFrom(View view) {
+    public static TransformState createFrom(View view,
+            TransformInfo transformInfo) {
         if (view instanceof TextView) {
             TextViewTransformState result = TextViewTransformState.obtain();
-            result.initFrom(view);
+            result.initFrom(view, transformInfo);
             return result;
         }
         if (view.getId() == com.android.internal.R.id.actions_container) {
             ActionListTransformState result = ActionListTransformState.obtain();
-            result.initFrom(view);
+            result.initFrom(view, transformInfo);
+            return result;
+        }
+        if (view.getId() == com.android.internal.R.id.notification_messaging) {
+            MessagingLayoutTransformState result = MessagingLayoutTransformState.obtain();
+            result.initFrom(view, transformInfo);
             return result;
         }
         if (view instanceof ImageView) {
             ImageTransformState result = ImageTransformState.obtain();
-            result.initFrom(view);
+            result.initFrom(view, transformInfo);
             if (view.getId() == com.android.internal.R.id.reply_icon_action) {
                 ((TransformState) result).setIsSameAsAnyView(true);
             }
@@ -465,15 +451,15 @@
         }
         if (view instanceof ProgressBar) {
             ProgressTransformState result = ProgressTransformState.obtain();
-            result.initFrom(view);
+            result.initFrom(view, transformInfo);
             return result;
         }
         TransformState result = obtain();
-        result.initFrom(view);
+        result.initFrom(view, transformInfo);
         return result;
     }
 
-    private void setIsSameAsAnyView(boolean sameAsAny) {
+    public void setIsSameAsAnyView(boolean sameAsAny) {
         mSameAsAny = sameAsAny;
     }
 
@@ -533,6 +519,7 @@
         mSameAsAny = false;
         mTransformationEndX = UNDEFINED;
         mTransformationEndY = UNDEFINED;
+        mDefaultInterpolator = Interpolators.FAST_OUT_SLOW_IN;
     }
 
     public void setVisible(boolean visible, boolean force) {
@@ -578,4 +565,12 @@
     public View getTransformedView() {
         return mTransformedView;
     }
+
+    public void setDefaultInterpolator(Interpolator interpolator) {
+        mDefaultInterpolator = interpolator;
+    }
+
+    public interface TransformInfo {
+        boolean isAnimating();
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/LightBarController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/LightBarController.java
index d226fed..1c361ca 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/LightBarController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/LightBarController.java
@@ -175,9 +175,8 @@
     private boolean isLight(int vis, int barMode, int flag) {
         boolean isTransparentBar = (barMode == MODE_TRANSPARENT
                 || barMode == MODE_LIGHTS_OUT_TRANSPARENT);
-        boolean allowLight = isTransparentBar && !mBatteryController.isPowerSave();
         boolean light = (vis & flag) != 0;
-        return allowLight && light;
+        return isTransparentBar && light;
     }
 
     private boolean animateChange() {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java
index 4a58c6d..46e3aa1 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java
@@ -146,6 +146,8 @@
 import com.android.internal.statusbar.StatusBarIcon;
 import com.android.internal.util.NotificationMessagingUtil;
 import com.android.internal.widget.LockPatternUtils;
+import com.android.internal.widget.MessagingGroup;
+import com.android.internal.widget.MessagingMessage;
 import com.android.keyguard.KeyguardHostView.OnDismissAction;
 import com.android.keyguard.KeyguardUpdateMonitor;
 import com.android.keyguard.KeyguardUpdateMonitorCallback;
@@ -273,7 +275,7 @@
     public static final boolean ENABLE_CHILD_NOTIFICATIONS
             = SystemProperties.getBoolean("debug.child_notifs", true);
     public static final boolean FORCE_REMOTE_INPUT_HISTORY =
-            SystemProperties.getBoolean("debug.force_remoteinput_history", false);
+            SystemProperties.getBoolean("debug.force_remoteinput_history", true);
     private static final boolean ENABLE_LOCK_SCREEN_ALLOW_REMOTE_INPUT = false;
 
     protected static final int MSG_HIDE_RECENT_APPS = 1020;
@@ -1232,6 +1234,8 @@
     }
 
     public void onDensityOrFontScaleChanged() {
+        MessagingMessage.dropCache();
+        MessagingGroup.dropCache();
         // start old BaseStatusBar.onDensityOrFontScaleChanged().
         if (!KeyguardUpdateMonitor.getInstance(mContext).isSwitchingUser()) {
             updateNotificationsOnDensityOrFontScaleChanged();
@@ -1685,8 +1689,9 @@
             clearCurrentMediaNotification();
             updateMediaMetaData(true, true);
         }
-        if (FORCE_REMOTE_INPUT_HISTORY && mRemoteInputController.isSpinning(key)) {
-            Entry entry = mNotificationData.get(key);
+        Entry entry = mNotificationData.get(key);
+        if (FORCE_REMOTE_INPUT_HISTORY && mRemoteInputController.isSpinning(key)
+                && entry.row != null && !entry.row.isDismissed()) {
             StatusBarNotification sbn = entry.notification;
 
             Notification.Builder b = Notification.Builder
@@ -1722,6 +1727,7 @@
                 deferRemoval = false;
             }
             if (updated) {
+                Log.w(TAG, "Keeping notification around after sending remote input "+ entry.key);
                 mKeysKeptForRemoteInput.add(entry.key);
                 return;
             }
@@ -1731,7 +1737,6 @@
             mHeadsUpEntriesToRemoveOnSwitch.add(mHeadsUpManager.getEntry(key));
             return;
         }
-        Entry entry = mNotificationData.get(key);
 
         if (entry != null && mRemoteInputController.isRemoteInputActive(entry)
                 && (entry.row != null && !entry.row.isDismissed())) {
@@ -3278,12 +3283,8 @@
     }
 
     void checkBarMode(int mode, int windowState, BarTransitions transitions) {
-        final boolean powerSave = mBatteryController.isPowerSave();
         final boolean anim = !mNoAnimationOnNextBarModeChange && mDeviceInteractive
-                && windowState != WINDOW_STATE_HIDDEN && !powerSave;
-        if (powerSave && getBarState() == StatusBarState.SHADE) {
-            mode = MODE_WARNING;
-        }
+                && windowState != WINDOW_STATE_HIDDEN;
         transitions.transitionTo(mode, anim);
     }
 
@@ -7117,7 +7118,7 @@
         mAllowLockscreenRemoteInput = allowLockscreenRemoteInput;
     }
 
-    private void updateLockscreenNotificationSetting() {
+    protected void updateLockscreenNotificationSetting() {
         final boolean show = Settings.Secure.getIntForUser(mContext.getContentResolver(),
                 Settings.Secure.LOCK_SCREEN_SHOW_NOTIFICATIONS,
                 1,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/doze/DozeScreenStatePreventingAdapterTest.java b/packages/SystemUI/tests/src/com/android/systemui/doze/DozeScreenStatePreventingAdapterTest.java
index 720e9f0..a17a95f 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/doze/DozeScreenStatePreventingAdapterTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/doze/DozeScreenStatePreventingAdapterTest.java
@@ -70,7 +70,7 @@
     @Test
     public void forwards_setDozeScreenState_doze_suspend() throws Exception {
         mWrapper.setDozeScreenState(Display.STATE_DOZE_SUSPEND);
-        verify(mInner).setDozeScreenState(Display.STATE_ON);
+        verify(mInner).setDozeScreenState(Display.STATE_ON_SUSPEND);
     }
 
     @Test
@@ -95,4 +95,4 @@
 
         assertSame(mInner, DozeScreenStatePreventingAdapter.wrapIfNeeded(mInner, params));
     }
-}
\ No newline at end of file
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/doze/DozeSuspendScreenStatePreventingAdapterTest.java b/packages/SystemUI/tests/src/com/android/systemui/doze/DozeSuspendScreenStatePreventingAdapterTest.java
index d78e739..ed93561 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/doze/DozeSuspendScreenStatePreventingAdapterTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/doze/DozeSuspendScreenStatePreventingAdapterTest.java
@@ -74,6 +74,12 @@
     }
 
     @Test
+    public void forwards_setDozeScreenState_on_suspend() throws Exception {
+        mWrapper.setDozeScreenState(Display.STATE_ON_SUSPEND);
+        verify(mInner).setDozeScreenState(Display.STATE_ON_SUSPEND);
+    }
+
+    @Test
     public void forwards_requestWakeUp() throws Exception {
         mWrapper.requestWakeUp();
         verify(mInner).requestWakeUp();
@@ -95,4 +101,4 @@
 
         assertSame(mInner, DozeSuspendScreenStatePreventingAdapter.wrapIfNeeded(mInner, params));
     }
-}
\ No newline at end of file
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardSliceProviderTest.java b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardSliceProviderTest.java
new file mode 100644
index 0000000..4437aac
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardSliceProviderTest.java
@@ -0,0 +1,108 @@
+/*
+ * 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.systemui.keyguard;
+
+import android.app.slice.Slice;
+import android.app.slice.SliceItem;
+import android.app.slice.SliceQuery;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Handler;
+import android.support.test.filters.SmallTest;
+import android.testing.AndroidTestingRunner;
+import android.testing.TestableLooper;
+import android.testing.TestableLooper.RunWithLooper;
+
+import com.android.systemui.SysuiTestCase;
+
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@SmallTest
+@RunWith(AndroidTestingRunner.class)
+@RunWithLooper(setAsMainLooper = true)
+public class KeyguardSliceProviderTest extends SysuiTestCase {
+
+    private TestableKeyguardSliceProvider mProvider;
+
+    @Before
+    public void setup() {
+        mProvider = new TestableKeyguardSliceProvider();
+        mProvider.attachInfo(getContext(), null);
+    }
+
+    @Test
+    public void registersClockUpdate() {
+        Assert.assertTrue("registerClockUpdate should have been called during initialization.",
+                mProvider.isRegistered());
+    }
+
+    @Test
+    public void unregisterClockUpdate() {
+        mProvider.unregisterClockUpdate();
+        Assert.assertFalse("Clock updates should have been unregistered.",
+                mProvider.isRegistered());
+    }
+
+    @Test
+    public void returnsValidSlice() {
+        Slice slice = mProvider.onBindSlice(Uri.parse(KeyguardSliceProvider.KEYGUARD_SLICE_URI));
+        SliceItem text = SliceQuery.find(slice, SliceItem.TYPE_TEXT, Slice.HINT_TITLE,
+                null /* nonHints */);
+        Assert.assertNotNull("Slice must provide a title.", text);
+    }
+
+    @Test
+    public void cleansDateFormat() {
+        mProvider.mIntentReceiver.onReceive(getContext(), new Intent(Intent.ACTION_TIMEZONE_CHANGED));
+        TestableLooper.get(this).processAllMessages();
+        Assert.assertEquals("Date format should have been cleaned.", 1 /* expected */,
+                mProvider.mCleanDateFormatInvokations);
+    }
+
+    @Test
+    public void updatesClock() {
+        mProvider.mIntentReceiver.onReceive(getContext(), new Intent(Intent.ACTION_TIME_TICK));
+        TestableLooper.get(this).processAllMessages();
+        Assert.assertEquals("Clock should have been updated.", 2 /* expected */,
+                mProvider.mUpdateClockInvokations);
+    }
+
+    private class TestableKeyguardSliceProvider extends KeyguardSliceProvider {
+        int mCleanDateFormatInvokations;
+        int mUpdateClockInvokations;
+
+        TestableKeyguardSliceProvider() {
+            super(new Handler(TestableLooper.get(KeyguardSliceProviderTest.this).getLooper()));
+        }
+
+        @Override
+        void cleanDateFormat() {
+            super.cleanDateFormat();
+            mCleanDateFormatInvokations++;
+        }
+
+        @Override
+        protected void updateClock() {
+            super.updateClock();
+            mUpdateClockInvokations++;
+        }
+    }
+
+}
diff --git a/services/core/java/com/android/server/BatteryService.java b/services/core/java/com/android/server/BatteryService.java
index 1b61866..ea0ed27 100644
--- a/services/core/java/com/android/server/BatteryService.java
+++ b/services/core/java/com/android/server/BatteryService.java
@@ -38,7 +38,7 @@
 import android.content.pm.PackageManager;
 import android.hidl.manager.V1_0.IServiceManager;
 import android.hidl.manager.V1_0.IServiceNotification;
-import android.hardware.health.V2_0.HealthInfo;
+import android.hardware.health.V1_0.HealthInfo;
 import android.hardware.health.V2_0.IHealthInfoCallback;
 import android.hardware.health.V2_0.IHealth;
 import android.hardware.health.V2_0.Result;
@@ -49,6 +49,7 @@
 import android.os.Binder;
 import android.os.FileUtils;
 import android.os.Handler;
+import android.os.HandlerThread;
 import android.os.IBatteryPropertiesListener;
 import android.os.IBatteryPropertiesRegistrar;
 import android.os.IBinder;
@@ -75,6 +76,7 @@
 import java.util.Arrays;
 import java.util.List;
 import java.util.NoSuchElementException;
+import java.util.Objects;
 import java.util.concurrent.atomic.AtomicReference;
 
 /**
@@ -176,6 +178,7 @@
     private HealthServiceWrapper mHealthServiceWrapper;
     private HealthHalCallback mHealthHalCallback;
     private BatteryPropertiesRegistrar mBatteryPropertiesRegistrar;
+    private HandlerThread mHandlerThread;
 
     public BatteryService(Context context) {
         super(context);
@@ -308,16 +311,16 @@
     private boolean isPoweredLocked(int plugTypeSet) {
         // assume we are powered if battery state is unknown so
         // the "stay on while plugged in" option will work.
-        if (mHealthInfo.legacy.batteryStatus == BatteryManager.BATTERY_STATUS_UNKNOWN) {
+        if (mHealthInfo.batteryStatus == BatteryManager.BATTERY_STATUS_UNKNOWN) {
             return true;
         }
-        if ((plugTypeSet & BatteryManager.BATTERY_PLUGGED_AC) != 0 && mHealthInfo.legacy.chargerAcOnline) {
+        if ((plugTypeSet & BatteryManager.BATTERY_PLUGGED_AC) != 0 && mHealthInfo.chargerAcOnline) {
             return true;
         }
-        if ((plugTypeSet & BatteryManager.BATTERY_PLUGGED_USB) != 0 && mHealthInfo.legacy.chargerUsbOnline) {
+        if ((plugTypeSet & BatteryManager.BATTERY_PLUGGED_USB) != 0 && mHealthInfo.chargerUsbOnline) {
             return true;
         }
-        if ((plugTypeSet & BatteryManager.BATTERY_PLUGGED_WIRELESS) != 0 && mHealthInfo.legacy.chargerWirelessOnline) {
+        if ((plugTypeSet & BatteryManager.BATTERY_PLUGGED_WIRELESS) != 0 && mHealthInfo.chargerWirelessOnline) {
             return true;
         }
         return false;
@@ -334,15 +337,15 @@
          *   (becomes <= mLowBatteryWarningLevel).
          */
         return !plugged
-                && mHealthInfo.legacy.batteryStatus != BatteryManager.BATTERY_STATUS_UNKNOWN
-                && mHealthInfo.legacy.batteryLevel <= mLowBatteryWarningLevel
+                && mHealthInfo.batteryStatus != BatteryManager.BATTERY_STATUS_UNKNOWN
+                && mHealthInfo.batteryLevel <= mLowBatteryWarningLevel
                 && (oldPlugged || mLastBatteryLevel > mLowBatteryWarningLevel);
     }
 
     private void shutdownIfNoPowerLocked() {
         // shut down gracefully if our battery is critically low and we are not powered.
         // wait until the system has booted before attempting to display the shutdown dialog.
-        if (mHealthInfo.legacy.batteryLevel == 0 && !isPoweredLocked(BatteryManager.BATTERY_PLUGGED_ANY)) {
+        if (mHealthInfo.batteryLevel == 0 && !isPoweredLocked(BatteryManager.BATTERY_PLUGGED_ANY)) {
             mHandler.post(new Runnable() {
                 @Override
                 public void run() {
@@ -363,7 +366,7 @@
         // shut down gracefully if temperature is too high (> 68.0C by default)
         // wait until the system has booted before attempting to display the
         // shutdown dialog.
-        if (mHealthInfo.legacy.batteryTemperature > mShutdownBatteryTemperature) {
+        if (mHealthInfo.batteryTemperature > mShutdownBatteryTemperature) {
             mHandler.post(new Runnable() {
                 @Override
                 public void run() {
@@ -396,37 +399,34 @@
     }
 
     private static void copy(HealthInfo dst, HealthInfo src) {
-        dst.legacy.chargerAcOnline = src.legacy.chargerAcOnline;
-        dst.legacy.chargerUsbOnline = src.legacy.chargerUsbOnline;
-        dst.legacy.chargerWirelessOnline = src.legacy.chargerWirelessOnline;
-        dst.legacy.maxChargingCurrent = src.legacy.maxChargingCurrent;
-        dst.legacy.maxChargingVoltage = src.legacy.maxChargingVoltage;
-        dst.legacy.batteryStatus = src.legacy.batteryStatus;
-        dst.legacy.batteryHealth = src.legacy.batteryHealth;
-        dst.legacy.batteryPresent = src.legacy.batteryPresent;
-        dst.legacy.batteryLevel = src.legacy.batteryLevel;
-        dst.legacy.batteryVoltage = src.legacy.batteryVoltage;
-        dst.legacy.batteryTemperature = src.legacy.batteryTemperature;
-        dst.legacy.batteryCurrent = src.legacy.batteryCurrent;
-        dst.legacy.batteryCycleCount = src.legacy.batteryCycleCount;
-        dst.legacy.batteryFullCharge = src.legacy.batteryFullCharge;
-        dst.legacy.batteryChargeCounter = src.legacy.batteryChargeCounter;
-        dst.legacy.batteryTechnology = src.legacy.batteryTechnology;
-        dst.batteryCurrentAverage = src.batteryCurrentAverage;
-        dst.batteryCapacity = src.batteryCapacity;
-        dst.energyCounter = src.energyCounter;
+        dst.chargerAcOnline = src.chargerAcOnline;
+        dst.chargerUsbOnline = src.chargerUsbOnline;
+        dst.chargerWirelessOnline = src.chargerWirelessOnline;
+        dst.maxChargingCurrent = src.maxChargingCurrent;
+        dst.maxChargingVoltage = src.maxChargingVoltage;
+        dst.batteryStatus = src.batteryStatus;
+        dst.batteryHealth = src.batteryHealth;
+        dst.batteryPresent = src.batteryPresent;
+        dst.batteryLevel = src.batteryLevel;
+        dst.batteryVoltage = src.batteryVoltage;
+        dst.batteryTemperature = src.batteryTemperature;
+        dst.batteryCurrent = src.batteryCurrent;
+        dst.batteryCycleCount = src.batteryCycleCount;
+        dst.batteryFullCharge = src.batteryFullCharge;
+        dst.batteryChargeCounter = src.batteryChargeCounter;
+        dst.batteryTechnology = src.batteryTechnology;
     }
 
     private void processValuesLocked(boolean force) {
         boolean logOutlier = false;
         long dischargeDuration = 0;
 
-        mBatteryLevelCritical = (mHealthInfo.legacy.batteryLevel <= mCriticalBatteryLevel);
-        if (mHealthInfo.legacy.chargerAcOnline) {
+        mBatteryLevelCritical = (mHealthInfo.batteryLevel <= mCriticalBatteryLevel);
+        if (mHealthInfo.chargerAcOnline) {
             mPlugType = BatteryManager.BATTERY_PLUGGED_AC;
-        } else if (mHealthInfo.legacy.chargerUsbOnline) {
+        } else if (mHealthInfo.chargerUsbOnline) {
             mPlugType = BatteryManager.BATTERY_PLUGGED_USB;
-        } else if (mHealthInfo.legacy.chargerWirelessOnline) {
+        } else if (mHealthInfo.chargerWirelessOnline) {
             mPlugType = BatteryManager.BATTERY_PLUGGED_WIRELESS;
         } else {
             mPlugType = BATTERY_PLUGGED_NONE;
@@ -441,10 +441,10 @@
 
         // Let the battery stats keep track of the current level.
         try {
-            mBatteryStats.setBatteryState(mHealthInfo.legacy.batteryStatus, mHealthInfo.legacy.batteryHealth,
-                    mPlugType, mHealthInfo.legacy.batteryLevel, mHealthInfo.legacy.batteryTemperature,
-                    mHealthInfo.legacy.batteryVoltage, mHealthInfo.legacy.batteryChargeCounter,
-                    mHealthInfo.legacy.batteryFullCharge);
+            mBatteryStats.setBatteryState(mHealthInfo.batteryStatus, mHealthInfo.batteryHealth,
+                    mPlugType, mHealthInfo.batteryLevel, mHealthInfo.batteryTemperature,
+                    mHealthInfo.batteryVoltage, mHealthInfo.batteryChargeCounter,
+                    mHealthInfo.batteryFullCharge);
         } catch (RemoteException e) {
             // Should never happen.
         }
@@ -452,16 +452,16 @@
         shutdownIfNoPowerLocked();
         shutdownIfOverTempLocked();
 
-        if (force || (mHealthInfo.legacy.batteryStatus != mLastBatteryStatus ||
-                mHealthInfo.legacy.batteryHealth != mLastBatteryHealth ||
-                mHealthInfo.legacy.batteryPresent != mLastBatteryPresent ||
-                mHealthInfo.legacy.batteryLevel != mLastBatteryLevel ||
+        if (force || (mHealthInfo.batteryStatus != mLastBatteryStatus ||
+                mHealthInfo.batteryHealth != mLastBatteryHealth ||
+                mHealthInfo.batteryPresent != mLastBatteryPresent ||
+                mHealthInfo.batteryLevel != mLastBatteryLevel ||
                 mPlugType != mLastPlugType ||
-                mHealthInfo.legacy.batteryVoltage != mLastBatteryVoltage ||
-                mHealthInfo.legacy.batteryTemperature != mLastBatteryTemperature ||
-                mHealthInfo.legacy.maxChargingCurrent != mLastMaxChargingCurrent ||
-                mHealthInfo.legacy.maxChargingVoltage != mLastMaxChargingVoltage ||
-                mHealthInfo.legacy.batteryChargeCounter != mLastChargeCounter ||
+                mHealthInfo.batteryVoltage != mLastBatteryVoltage ||
+                mHealthInfo.batteryTemperature != mLastBatteryTemperature ||
+                mHealthInfo.maxChargingCurrent != mLastMaxChargingCurrent ||
+                mHealthInfo.maxChargingVoltage != mLastMaxChargingVoltage ||
+                mHealthInfo.batteryChargeCounter != mLastChargeCounter ||
                 mInvalidCharger != mLastInvalidCharger)) {
 
             if (mPlugType != mLastPlugType) {
@@ -470,33 +470,33 @@
 
                     // There's no value in this data unless we've discharged at least once and the
                     // battery level has changed; so don't log until it does.
-                    if (mDischargeStartTime != 0 && mDischargeStartLevel != mHealthInfo.legacy.batteryLevel) {
+                    if (mDischargeStartTime != 0 && mDischargeStartLevel != mHealthInfo.batteryLevel) {
                         dischargeDuration = SystemClock.elapsedRealtime() - mDischargeStartTime;
                         logOutlier = true;
                         EventLog.writeEvent(EventLogTags.BATTERY_DISCHARGE, dischargeDuration,
-                                mDischargeStartLevel, mHealthInfo.legacy.batteryLevel);
+                                mDischargeStartLevel, mHealthInfo.batteryLevel);
                         // make sure we see a discharge event before logging again
                         mDischargeStartTime = 0;
                     }
                 } else if (mPlugType == BATTERY_PLUGGED_NONE) {
                     // charging -> discharging or we just powered up
                     mDischargeStartTime = SystemClock.elapsedRealtime();
-                    mDischargeStartLevel = mHealthInfo.legacy.batteryLevel;
+                    mDischargeStartLevel = mHealthInfo.batteryLevel;
                 }
             }
-            if (mHealthInfo.legacy.batteryStatus != mLastBatteryStatus ||
-                    mHealthInfo.legacy.batteryHealth != mLastBatteryHealth ||
-                    mHealthInfo.legacy.batteryPresent != mLastBatteryPresent ||
+            if (mHealthInfo.batteryStatus != mLastBatteryStatus ||
+                    mHealthInfo.batteryHealth != mLastBatteryHealth ||
+                    mHealthInfo.batteryPresent != mLastBatteryPresent ||
                     mPlugType != mLastPlugType) {
                 EventLog.writeEvent(EventLogTags.BATTERY_STATUS,
-                        mHealthInfo.legacy.batteryStatus, mHealthInfo.legacy.batteryHealth, mHealthInfo.legacy.batteryPresent ? 1 : 0,
-                        mPlugType, mHealthInfo.legacy.batteryTechnology);
+                        mHealthInfo.batteryStatus, mHealthInfo.batteryHealth, mHealthInfo.batteryPresent ? 1 : 0,
+                        mPlugType, mHealthInfo.batteryTechnology);
             }
-            if (mHealthInfo.legacy.batteryLevel != mLastBatteryLevel) {
+            if (mHealthInfo.batteryLevel != mLastBatteryLevel) {
                 // Don't do this just from voltage or temperature changes, that is
                 // too noisy.
                 EventLog.writeEvent(EventLogTags.BATTERY_LEVEL,
-                        mHealthInfo.legacy.batteryLevel, mHealthInfo.legacy.batteryVoltage, mHealthInfo.legacy.batteryTemperature);
+                        mHealthInfo.batteryLevel, mHealthInfo.batteryVoltage, mHealthInfo.batteryTemperature);
             }
             if (mBatteryLevelCritical && !mLastBatteryLevelCritical &&
                     mPlugType == BATTERY_PLUGGED_NONE) {
@@ -509,16 +509,16 @@
             if (!mBatteryLevelLow) {
                 // Should we now switch in to low battery mode?
                 if (mPlugType == BATTERY_PLUGGED_NONE
-                        && mHealthInfo.legacy.batteryLevel <= mLowBatteryWarningLevel) {
+                        && mHealthInfo.batteryLevel <= mLowBatteryWarningLevel) {
                     mBatteryLevelLow = true;
                 }
             } else {
                 // Should we now switch out of low battery mode?
                 if (mPlugType != BATTERY_PLUGGED_NONE) {
                     mBatteryLevelLow = false;
-                } else if (mHealthInfo.legacy.batteryLevel >= mLowBatteryCloseWarningLevel)  {
+                } else if (mHealthInfo.batteryLevel >= mLowBatteryCloseWarningLevel)  {
                     mBatteryLevelLow = false;
-                } else if (force && mHealthInfo.legacy.batteryLevel >= mLowBatteryWarningLevel) {
+                } else if (force && mHealthInfo.batteryLevel >= mLowBatteryWarningLevel) {
                     // If being forced, the previous state doesn't matter, we will just
                     // absolutely check to see if we are now above the warning level.
                     mBatteryLevelLow = false;
@@ -565,7 +565,7 @@
                     }
                 });
             } else if (mSentLowBatteryBroadcast &&
-                    mHealthInfo.legacy.batteryLevel >= mLowBatteryCloseWarningLevel) {
+                    mHealthInfo.batteryLevel >= mLowBatteryCloseWarningLevel) {
                 mSentLowBatteryBroadcast = false;
                 final Intent statusIntent = new Intent(Intent.ACTION_BATTERY_OKAY);
                 statusIntent.setFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT);
@@ -591,16 +591,16 @@
                 logOutlierLocked(dischargeDuration);
             }
 
-            mLastBatteryStatus = mHealthInfo.legacy.batteryStatus;
-            mLastBatteryHealth = mHealthInfo.legacy.batteryHealth;
-            mLastBatteryPresent = mHealthInfo.legacy.batteryPresent;
-            mLastBatteryLevel = mHealthInfo.legacy.batteryLevel;
+            mLastBatteryStatus = mHealthInfo.batteryStatus;
+            mLastBatteryHealth = mHealthInfo.batteryHealth;
+            mLastBatteryPresent = mHealthInfo.batteryPresent;
+            mLastBatteryLevel = mHealthInfo.batteryLevel;
             mLastPlugType = mPlugType;
-            mLastBatteryVoltage = mHealthInfo.legacy.batteryVoltage;
-            mLastBatteryTemperature = mHealthInfo.legacy.batteryTemperature;
-            mLastMaxChargingCurrent = mHealthInfo.legacy.maxChargingCurrent;
-            mLastMaxChargingVoltage = mHealthInfo.legacy.maxChargingVoltage;
-            mLastChargeCounter = mHealthInfo.legacy.batteryChargeCounter;
+            mLastBatteryVoltage = mHealthInfo.batteryVoltage;
+            mLastBatteryTemperature = mHealthInfo.batteryTemperature;
+            mLastMaxChargingCurrent = mHealthInfo.maxChargingCurrent;
+            mLastMaxChargingVoltage = mHealthInfo.maxChargingVoltage;
+            mLastChargeCounter = mHealthInfo.batteryChargeCounter;
             mLastBatteryLevelCritical = mBatteryLevelCritical;
             mLastInvalidCharger = mInvalidCharger;
         }
@@ -612,23 +612,23 @@
         intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY
                 | Intent.FLAG_RECEIVER_REPLACE_PENDING);
 
-        int icon = getIconLocked(mHealthInfo.legacy.batteryLevel);
+        int icon = getIconLocked(mHealthInfo.batteryLevel);
 
         intent.putExtra(BatteryManager.EXTRA_SEQUENCE, mSequence);
-        intent.putExtra(BatteryManager.EXTRA_STATUS, mHealthInfo.legacy.batteryStatus);
-        intent.putExtra(BatteryManager.EXTRA_HEALTH, mHealthInfo.legacy.batteryHealth);
-        intent.putExtra(BatteryManager.EXTRA_PRESENT, mHealthInfo.legacy.batteryPresent);
-        intent.putExtra(BatteryManager.EXTRA_LEVEL, mHealthInfo.legacy.batteryLevel);
+        intent.putExtra(BatteryManager.EXTRA_STATUS, mHealthInfo.batteryStatus);
+        intent.putExtra(BatteryManager.EXTRA_HEALTH, mHealthInfo.batteryHealth);
+        intent.putExtra(BatteryManager.EXTRA_PRESENT, mHealthInfo.batteryPresent);
+        intent.putExtra(BatteryManager.EXTRA_LEVEL, mHealthInfo.batteryLevel);
         intent.putExtra(BatteryManager.EXTRA_SCALE, BATTERY_SCALE);
         intent.putExtra(BatteryManager.EXTRA_ICON_SMALL, icon);
         intent.putExtra(BatteryManager.EXTRA_PLUGGED, mPlugType);
-        intent.putExtra(BatteryManager.EXTRA_VOLTAGE, mHealthInfo.legacy.batteryVoltage);
-        intent.putExtra(BatteryManager.EXTRA_TEMPERATURE, mHealthInfo.legacy.batteryTemperature);
-        intent.putExtra(BatteryManager.EXTRA_TECHNOLOGY, mHealthInfo.legacy.batteryTechnology);
+        intent.putExtra(BatteryManager.EXTRA_VOLTAGE, mHealthInfo.batteryVoltage);
+        intent.putExtra(BatteryManager.EXTRA_TEMPERATURE, mHealthInfo.batteryTemperature);
+        intent.putExtra(BatteryManager.EXTRA_TECHNOLOGY, mHealthInfo.batteryTechnology);
         intent.putExtra(BatteryManager.EXTRA_INVALID_CHARGER, mInvalidCharger);
-        intent.putExtra(BatteryManager.EXTRA_MAX_CHARGING_CURRENT, mHealthInfo.legacy.maxChargingCurrent);
-        intent.putExtra(BatteryManager.EXTRA_MAX_CHARGING_VOLTAGE, mHealthInfo.legacy.maxChargingVoltage);
-        intent.putExtra(BatteryManager.EXTRA_CHARGE_COUNTER, mHealthInfo.legacy.batteryChargeCounter);
+        intent.putExtra(BatteryManager.EXTRA_MAX_CHARGING_CURRENT, mHealthInfo.maxChargingCurrent);
+        intent.putExtra(BatteryManager.EXTRA_MAX_CHARGING_VOLTAGE, mHealthInfo.maxChargingVoltage);
+        intent.putExtra(BatteryManager.EXTRA_CHARGE_COUNTER, mHealthInfo.batteryChargeCounter);
         if (DEBUG) {
             Slog.d(TAG, "Sending ACTION_BATTERY_CHANGED. scale:" + BATTERY_SCALE
                     + ", info:" + mHealthInfo.toString());
@@ -692,14 +692,14 @@
                 long durationThreshold = Long.parseLong(durationThresholdString);
                 int dischargeThreshold = Integer.parseInt(dischargeThresholdString);
                 if (duration <= durationThreshold &&
-                        mDischargeStartLevel - mHealthInfo.legacy.batteryLevel >= dischargeThreshold) {
+                        mDischargeStartLevel - mHealthInfo.batteryLevel >= dischargeThreshold) {
                     // If the discharge cycle is bad enough we want to know about it.
                     logBatteryStatsLocked();
                 }
                 if (DEBUG) Slog.v(TAG, "duration threshold: " + durationThreshold +
                         " discharge threshold: " + dischargeThreshold);
                 if (DEBUG) Slog.v(TAG, "duration: " + duration + " discharge: " +
-                        (mDischargeStartLevel - mHealthInfo.legacy.batteryLevel));
+                        (mDischargeStartLevel - mHealthInfo.batteryLevel));
             } catch (NumberFormatException e) {
                 Slog.e(TAG, "Invalid DischargeThresholds GService string: " +
                         durationThresholdString + " or " + dischargeThresholdString);
@@ -708,14 +708,14 @@
     }
 
     private int getIconLocked(int level) {
-        if (mHealthInfo.legacy.batteryStatus == BatteryManager.BATTERY_STATUS_CHARGING) {
+        if (mHealthInfo.batteryStatus == BatteryManager.BATTERY_STATUS_CHARGING) {
             return com.android.internal.R.drawable.stat_sys_battery_charge;
-        } else if (mHealthInfo.legacy.batteryStatus == BatteryManager.BATTERY_STATUS_DISCHARGING) {
+        } else if (mHealthInfo.batteryStatus == BatteryManager.BATTERY_STATUS_DISCHARGING) {
             return com.android.internal.R.drawable.stat_sys_battery;
-        } else if (mHealthInfo.legacy.batteryStatus == BatteryManager.BATTERY_STATUS_NOT_CHARGING
-                || mHealthInfo.legacy.batteryStatus == BatteryManager.BATTERY_STATUS_FULL) {
+        } else if (mHealthInfo.batteryStatus == BatteryManager.BATTERY_STATUS_NOT_CHARGING
+                || mHealthInfo.batteryStatus == BatteryManager.BATTERY_STATUS_FULL) {
             if (isPoweredLocked(BatteryManager.BATTERY_PLUGGED_ANY)
-                    && mHealthInfo.legacy.batteryLevel >= 100) {
+                    && mHealthInfo.batteryLevel >= 100) {
                 return com.android.internal.R.drawable.stat_sys_battery_charge;
             } else {
                 return com.android.internal.R.drawable.stat_sys_battery;
@@ -779,9 +779,9 @@
                 if (!mUpdatesStopped) {
                     copy(mLastHealthInfo, mHealthInfo);
                 }
-                mHealthInfo.legacy.chargerAcOnline = false;
-                mHealthInfo.legacy.chargerUsbOnline = false;
-                mHealthInfo.legacy.chargerWirelessOnline = false;
+                mHealthInfo.chargerAcOnline = false;
+                mHealthInfo.chargerUsbOnline = false;
+                mHealthInfo.chargerWirelessOnline = false;
                 long ident = Binder.clearCallingIdentity();
                 try {
                     mUpdatesStopped = true;
@@ -813,25 +813,25 @@
                     boolean update = true;
                     switch (key) {
                         case "present":
-                            mHealthInfo.legacy.batteryPresent = Integer.parseInt(value) != 0;
+                            mHealthInfo.batteryPresent = Integer.parseInt(value) != 0;
                             break;
                         case "ac":
-                            mHealthInfo.legacy.chargerAcOnline = Integer.parseInt(value) != 0;
+                            mHealthInfo.chargerAcOnline = Integer.parseInt(value) != 0;
                             break;
                         case "usb":
-                            mHealthInfo.legacy.chargerUsbOnline = Integer.parseInt(value) != 0;
+                            mHealthInfo.chargerUsbOnline = Integer.parseInt(value) != 0;
                             break;
                         case "wireless":
-                            mHealthInfo.legacy.chargerWirelessOnline = Integer.parseInt(value) != 0;
+                            mHealthInfo.chargerWirelessOnline = Integer.parseInt(value) != 0;
                             break;
                         case "status":
-                            mHealthInfo.legacy.batteryStatus = Integer.parseInt(value);
+                            mHealthInfo.batteryStatus = Integer.parseInt(value);
                             break;
                         case "level":
-                            mHealthInfo.legacy.batteryLevel = Integer.parseInt(value);
+                            mHealthInfo.batteryLevel = Integer.parseInt(value);
                             break;
                         case "temp":
-                            mHealthInfo.legacy.batteryTemperature = Integer.parseInt(value);
+                            mHealthInfo.batteryTemperature = Integer.parseInt(value);
                             break;
                         case "invalid":
                             mInvalidCharger = Integer.parseInt(value);
@@ -890,20 +890,20 @@
                 if (mUpdatesStopped) {
                     pw.println("  (UPDATES STOPPED -- use 'reset' to restart)");
                 }
-                pw.println("  AC powered: " + mHealthInfo.legacy.chargerAcOnline);
-                pw.println("  USB powered: " + mHealthInfo.legacy.chargerUsbOnline);
-                pw.println("  Wireless powered: " + mHealthInfo.legacy.chargerWirelessOnline);
-                pw.println("  Max charging current: " + mHealthInfo.legacy.maxChargingCurrent);
-                pw.println("  Max charging voltage: " + mHealthInfo.legacy.maxChargingVoltage);
-                pw.println("  Charge counter: " + mHealthInfo.legacy.batteryChargeCounter);
-                pw.println("  status: " + mHealthInfo.legacy.batteryStatus);
-                pw.println("  health: " + mHealthInfo.legacy.batteryHealth);
-                pw.println("  present: " + mHealthInfo.legacy.batteryPresent);
-                pw.println("  level: " + mHealthInfo.legacy.batteryLevel);
+                pw.println("  AC powered: " + mHealthInfo.chargerAcOnline);
+                pw.println("  USB powered: " + mHealthInfo.chargerUsbOnline);
+                pw.println("  Wireless powered: " + mHealthInfo.chargerWirelessOnline);
+                pw.println("  Max charging current: " + mHealthInfo.maxChargingCurrent);
+                pw.println("  Max charging voltage: " + mHealthInfo.maxChargingVoltage);
+                pw.println("  Charge counter: " + mHealthInfo.batteryChargeCounter);
+                pw.println("  status: " + mHealthInfo.batteryStatus);
+                pw.println("  health: " + mHealthInfo.batteryHealth);
+                pw.println("  present: " + mHealthInfo.batteryPresent);
+                pw.println("  level: " + mHealthInfo.batteryLevel);
                 pw.println("  scale: " + BATTERY_SCALE);
-                pw.println("  voltage: " + mHealthInfo.legacy.batteryVoltage);
-                pw.println("  temperature: " + mHealthInfo.legacy.batteryTemperature);
-                pw.println("  technology: " + mHealthInfo.legacy.batteryTechnology);
+                pw.println("  voltage: " + mHealthInfo.batteryVoltage);
+                pw.println("  temperature: " + mHealthInfo.batteryTemperature);
+                pw.println("  technology: " + mHealthInfo.batteryTechnology);
             } else {
                 Shell shell = new Shell();
                 shell.exec(mBinderService, null, fd, null, args, null, new ResultReceiver(null));
@@ -917,25 +917,25 @@
         synchronized (mLock) {
             proto.write(BatteryServiceDumpProto.ARE_UPDATES_STOPPED, mUpdatesStopped);
             int batteryPluggedValue = BatteryManagerProto.PLUG_TYPE_NONE;
-            if (mHealthInfo.legacy.chargerAcOnline) {
+            if (mHealthInfo.chargerAcOnline) {
                 batteryPluggedValue = BatteryManagerProto.PLUG_TYPE_AC;
-            } else if (mHealthInfo.legacy.chargerUsbOnline) {
+            } else if (mHealthInfo.chargerUsbOnline) {
                 batteryPluggedValue = BatteryManagerProto.PLUG_TYPE_USB;
-            } else if (mHealthInfo.legacy.chargerWirelessOnline) {
+            } else if (mHealthInfo.chargerWirelessOnline) {
                 batteryPluggedValue = BatteryManagerProto.PLUG_TYPE_WIRELESS;
             }
             proto.write(BatteryServiceDumpProto.PLUGGED, batteryPluggedValue);
-            proto.write(BatteryServiceDumpProto.MAX_CHARGING_CURRENT, mHealthInfo.legacy.maxChargingCurrent);
-            proto.write(BatteryServiceDumpProto.MAX_CHARGING_VOLTAGE, mHealthInfo.legacy.maxChargingVoltage);
-            proto.write(BatteryServiceDumpProto.CHARGE_COUNTER, mHealthInfo.legacy.batteryChargeCounter);
-            proto.write(BatteryServiceDumpProto.STATUS, mHealthInfo.legacy.batteryStatus);
-            proto.write(BatteryServiceDumpProto.HEALTH, mHealthInfo.legacy.batteryHealth);
-            proto.write(BatteryServiceDumpProto.IS_PRESENT, mHealthInfo.legacy.batteryPresent);
-            proto.write(BatteryServiceDumpProto.LEVEL, mHealthInfo.legacy.batteryLevel);
+            proto.write(BatteryServiceDumpProto.MAX_CHARGING_CURRENT, mHealthInfo.maxChargingCurrent);
+            proto.write(BatteryServiceDumpProto.MAX_CHARGING_VOLTAGE, mHealthInfo.maxChargingVoltage);
+            proto.write(BatteryServiceDumpProto.CHARGE_COUNTER, mHealthInfo.batteryChargeCounter);
+            proto.write(BatteryServiceDumpProto.STATUS, mHealthInfo.batteryStatus);
+            proto.write(BatteryServiceDumpProto.HEALTH, mHealthInfo.batteryHealth);
+            proto.write(BatteryServiceDumpProto.IS_PRESENT, mHealthInfo.batteryPresent);
+            proto.write(BatteryServiceDumpProto.LEVEL, mHealthInfo.batteryLevel);
             proto.write(BatteryServiceDumpProto.SCALE, BATTERY_SCALE);
-            proto.write(BatteryServiceDumpProto.VOLTAGE, mHealthInfo.legacy.batteryVoltage);
-            proto.write(BatteryServiceDumpProto.TEMPERATURE, mHealthInfo.legacy.batteryTemperature);
-            proto.write(BatteryServiceDumpProto.TECHNOLOGY, mHealthInfo.legacy.batteryTechnology);
+            proto.write(BatteryServiceDumpProto.VOLTAGE, mHealthInfo.batteryVoltage);
+            proto.write(BatteryServiceDumpProto.TEMPERATURE, mHealthInfo.batteryTemperature);
+            proto.write(BatteryServiceDumpProto.TECHNOLOGY, mHealthInfo.batteryTechnology);
         }
         proto.flush();
     }
@@ -976,8 +976,8 @@
          * Synchronize on BatteryService.
          */
         public void updateLightsLocked() {
-            final int level = mHealthInfo.legacy.batteryLevel;
-            final int status = mHealthInfo.legacy.batteryStatus;
+            final int level = mHealthInfo.batteryLevel;
+            final int status = mHealthInfo.batteryStatus;
             if (level < mLowBatteryWarningLevel) {
                 if (status == BatteryManager.BATTERY_STATUS_CHARGING) {
                     // Solid red when battery is charging
@@ -1154,7 +1154,7 @@
         @Override
         public int getBatteryLevel() {
             synchronized (mLock) {
-                return mHealthInfo.legacy.batteryLevel;
+                return mHealthInfo.batteryLevel;
             }
         }
 
@@ -1195,14 +1195,13 @@
                 Arrays.asList(INSTANCE_VENDOR, INSTANCE_HEALTHD);
 
         private final IServiceNotification mNotification = new Notification();
+        private final HandlerThread mHandlerThread = new HandlerThread("HealthServiceRefresh");
         // These variables are fixed after init.
         private Callback mCallback;
         private IHealthSupplier mHealthSupplier;
         private String mInstanceName;
 
-        private final Object mLastServiceSetLock = new Object();
         // Last IHealth service received.
-        // set must be also be guarded with mLastServiceSetLock to ensure ordering.
         private final AtomicReference<IHealth> mLastService = new AtomicReference<>();
 
         /**
@@ -1220,6 +1219,10 @@
          * Start monitoring registration of new IHealth services. Only instances that are in
          * {@code sAllInstances} and in device / framework manifest are used. This function should
          * only be called once.
+         *
+         * mCallback.onRegistration() is called synchronously (aka in init thread) before
+         * this method returns.
+         *
          * @throws RemoteException transaction error when talking to IServiceManager
          * @throws NoSuchElementException if one of the following cases:
          *         - No service manager;
@@ -1240,40 +1243,48 @@
             mCallback = callback;
             mHealthSupplier = healthSupplier;
 
-            traceBegin("HealthInitGetManager");
-            try {
-                manager = managerSupplier.get();
-            } finally {
-                traceEnd();
-            }
+            // Initialize mLastService and call callback for the first time (in init thread)
+            IHealth newService = null;
             for (String name : sAllInstances) {
-                traceBegin("HealthInitGetTransport_" + name);
+                traceBegin("HealthInitGetService_" + name);
                 try {
-                    if (manager.getTransport(IHealth.kInterfaceName, name) !=
-                            IServiceManager.Transport.EMPTY) {
-                        mInstanceName = name;
-                        break;
-                    }
+                    newService = healthSupplier.get(name);
+                } catch (NoSuchElementException ex) {
+                    /* ignored, handled below */
                 } finally {
                     traceEnd();
                 }
+                if (newService != null) {
+                    mInstanceName = name;
+                    mLastService.set(newService);
+                    break;
+                }
             }
 
-            if (mInstanceName == null) {
+            if (mInstanceName == null || newService == null) {
                 throw new NoSuchElementException(String.format(
                         "No IHealth service instance among %s is available. Perhaps no permission?",
                         sAllInstances.toString()));
             }
+            mCallback.onRegistration(null, newService, mInstanceName);
 
+            // Register for future service registrations
             traceBegin("HealthInitRegisterNotification");
+            mHandlerThread.start();
             try {
-                manager.registerForNotifications(IHealth.kInterfaceName, mInstanceName, mNotification);
+                managerSupplier.get().registerForNotifications(
+                        IHealth.kInterfaceName, mInstanceName, mNotification);
             } finally {
                 traceEnd();
             }
             Slog.i(TAG, "health: HealthServiceWrapper listening to instance " + mInstanceName);
         }
 
+        @VisibleForTesting
+        HandlerThread getHandlerThread() {
+            return mHandlerThread;
+        }
+
         interface Callback {
             /**
              * This function is invoked asynchronously when a new and related IServiceNotification
@@ -1302,7 +1313,7 @@
          */
         interface IHealthSupplier {
             default IHealth get(String name) throws NoSuchElementException, RemoteException {
-                return IHealth.getService(name);
+                return IHealth.getService(name, true /* retry */);
             }
         }
 
@@ -1312,18 +1323,27 @@
                     boolean preexisting) {
                 if (!IHealth.kInterfaceName.equals(interfaceName)) return;
                 if (!mInstanceName.equals(instanceName)) return;
-                try {
-                    // ensures the order of multiple onRegistration on different threads.
-                    synchronized (mLastServiceSetLock) {
-                        IHealth newService = mHealthSupplier.get(instanceName);
-                        IHealth oldService = mLastService.getAndSet(newService);
-                        Slog.i(TAG, "health: new instance registered " + instanceName);
-                        mCallback.onRegistration(oldService, newService, instanceName);
+
+                // This runnable only runs on mHandlerThread and ordering is ensured, hence
+                // no locking is needed inside the runnable.
+                mHandlerThread.getThreadHandler().post(new Runnable() {
+                    @Override
+                    public void run() {
+                        try {
+                            IHealth newService = mHealthSupplier.get(mInstanceName);
+                            IHealth oldService = mLastService.getAndSet(newService);
+
+                            // preexisting may be inaccurate (race). Check for equality here.
+                            if (Objects.equals(newService, oldService)) return;
+
+                            Slog.i(TAG, "health: new instance registered " + mInstanceName);
+                            mCallback.onRegistration(oldService, newService, mInstanceName);
+                        } catch (NoSuchElementException | RemoteException ex) {
+                            Slog.e(TAG, "health: Cannot get instance '" + mInstanceName
+                                    + "': " + ex.getMessage() + ". Perhaps no permission?");
+                        }
                     }
-                } catch (NoSuchElementException | RemoteException ex) {
-                    Slog.e(TAG, "health: Cannot get instance '" + instanceName + "': " +
-                           ex.getMessage() + ". Perhaps no permission?");
-                }
+                });
             }
         }
     }
diff --git a/services/core/java/com/android/server/NetworkManagementService.java b/services/core/java/com/android/server/NetworkManagementService.java
index c60d7b0..8a15ded 100644
--- a/services/core/java/com/android/server/NetworkManagementService.java
+++ b/services/core/java/com/android/server/NetworkManagementService.java
@@ -20,6 +20,9 @@
 import static android.Manifest.permission.DUMP;
 import static android.Manifest.permission.NETWORK_STACK;
 import static android.Manifest.permission.SHUTDOWN;
+import static android.net.ConnectivityManager.PRIVATE_DNS_DEFAULT_MODE;
+import static android.net.ConnectivityManager.PRIVATE_DNS_MODE_OPPORTUNISTIC;
+import static android.net.ConnectivityManager.PRIVATE_DNS_MODE_PROVIDER_HOSTNAME;
 import static android.net.NetworkPolicyManager.FIREWALL_CHAIN_DOZABLE;
 import static android.net.NetworkPolicyManager.FIREWALL_CHAIN_NAME_DOZABLE;
 import static android.net.NetworkPolicyManager.FIREWALL_CHAIN_NAME_NONE;
@@ -92,6 +95,7 @@
 import android.telephony.PhoneStateListener;
 import android.telephony.SubscriptionManager;
 import android.telephony.TelephonyManager;
+import android.text.TextUtils;
 import android.util.Log;
 import android.util.Slog;
 import android.util.SparseBooleanArray;
@@ -1946,9 +1950,9 @@
     public void setDnsConfigurationForNetwork(int netId, String[] servers, String domains) {
         mContext.enforceCallingOrSelfPermission(CONNECTIVITY_INTERNAL, TAG);
 
-        ContentResolver resolver = mContext.getContentResolver();
+        final ContentResolver cr = mContext.getContentResolver();
 
-        int sampleValidity = Settings.Global.getInt(resolver,
+        int sampleValidity = Settings.Global.getInt(cr,
                 Settings.Global.DNS_RESOLVER_SAMPLE_VALIDITY_SECONDS,
                 DNS_RESOLVER_DEFAULT_SAMPLE_VALIDITY_SECONDS);
         if (sampleValidity < 0 || sampleValidity > 65535) {
@@ -1957,7 +1961,7 @@
             sampleValidity = DNS_RESOLVER_DEFAULT_SAMPLE_VALIDITY_SECONDS;
         }
 
-        int successThreshold = Settings.Global.getInt(resolver,
+        int successThreshold = Settings.Global.getInt(cr,
                 Settings.Global.DNS_RESOLVER_SUCCESS_THRESHOLD_PERCENT,
                 DNS_RESOLVER_DEFAULT_SUCCESS_THRESHOLD_PERCENT);
         if (successThreshold < 0 || successThreshold > 100) {
@@ -1966,9 +1970,9 @@
             successThreshold = DNS_RESOLVER_DEFAULT_SUCCESS_THRESHOLD_PERCENT;
         }
 
-        int minSamples = Settings.Global.getInt(resolver,
+        int minSamples = Settings.Global.getInt(cr,
                 Settings.Global.DNS_RESOLVER_MIN_SAMPLES, DNS_RESOLVER_DEFAULT_MIN_SAMPLES);
-        int maxSamples = Settings.Global.getInt(resolver,
+        int maxSamples = Settings.Global.getInt(cr,
                 Settings.Global.DNS_RESOLVER_MAX_SAMPLES, DNS_RESOLVER_DEFAULT_MAX_SAMPLES);
         if (minSamples < 0 || minSamples > maxSamples || maxSamples > 64) {
             Slog.w(TAG, "Invalid sample count (min, max)=(" + minSamples + ", " + maxSamples +
@@ -1980,8 +1984,24 @@
 
         final String[] domainStrs = domains == null ? new String[0] : domains.split(" ");
         final int[] params = { sampleValidity, successThreshold, minSamples, maxSamples };
-        final boolean useTls = Settings.Global.getInt(resolver,
-                Settings.Global.DNS_TLS_DISABLED, 0) == 0;
+        final boolean useTls = shouldUseTls(cr);
+        // TODO: Populate tlsHostname once it's decided how the hostname's IP
+        // addresses will be resolved:
+        //
+        //     [1] network-provided DNS servers are included here with the
+        //         hostname and netd will use the network-provided servers to
+        //         resolve the hostname and fix up its internal structures, or
+        //
+        //     [2] network-provided DNS servers are included here without the
+        //         hostname, the ConnectivityService layer resolves the given
+        //         hostname, and then reconfigures netd with this information.
+        //
+        // In practice, there will always be a need for ConnectivityService or
+        // the captive portal app to use the network-provided services to make
+        // some queries. This argues in favor of [1], in concert with another
+        // mechanism, perhaps setting a high bit in the netid, to indicate
+        // via existing DNS APIs which set of servers (network-provided or
+        // non-network-provided private DNS) should be queried.
         final String tlsHostname = "";
         final String[] tlsFingerprints = new String[0];
         try {
@@ -1992,6 +2012,15 @@
         }
     }
 
+    private static boolean shouldUseTls(ContentResolver cr) {
+        String privateDns = Settings.Global.getString(cr, Settings.Global.PRIVATE_DNS_MODE);
+        if (TextUtils.isEmpty(privateDns)) {
+            privateDns = PRIVATE_DNS_DEFAULT_MODE;
+        }
+        return privateDns.equals(PRIVATE_DNS_MODE_OPPORTUNISTIC) ||
+               privateDns.startsWith(PRIVATE_DNS_MODE_PROVIDER_HOSTNAME);
+    }
+
     @Override
     public void addVpnUidRanges(int netId, UidRange[] ranges) {
         mContext.enforceCallingOrSelfPermission(CONNECTIVITY_INTERNAL, TAG);
diff --git a/services/core/java/com/android/server/RescueParty.java b/services/core/java/com/android/server/RescueParty.java
index 1924a86..a9f190c 100644
--- a/services/core/java/com/android/server/RescueParty.java
+++ b/services/core/java/com/android/server/RescueParty.java
@@ -16,6 +16,8 @@
 
 package com.android.server;
 
+import static com.android.server.pm.PackageManagerServiceUtils.logCriticalInfo;
+
 import android.content.ContentResolver;
 import android.content.Context;
 import android.os.Build;
@@ -146,7 +148,7 @@
         SystemProperties.set(PROP_RESCUE_LEVEL, Integer.toString(level));
 
         EventLogTags.writeRescueLevel(level, triggerUid);
-        PackageManagerService.logCriticalInfo(Log.WARN, "Incremented rescue level to "
+        logCriticalInfo(Log.WARN, "Incremented rescue level to "
                 + levelToString(level) + " triggered by UID " + triggerUid);
     }
 
@@ -166,12 +168,12 @@
         try {
             executeRescueLevelInternal(context, level);
             EventLogTags.writeRescueSuccess(level);
-            PackageManagerService.logCriticalInfo(Log.DEBUG,
+            logCriticalInfo(Log.DEBUG,
                     "Finished rescue level " + levelToString(level));
         } catch (Throwable t) {
             final String msg = ExceptionUtils.getCompleteMessage(t);
             EventLogTags.writeRescueFailure(level, msg);
-            PackageManagerService.logCriticalInfo(Log.ERROR,
+            logCriticalInfo(Log.ERROR,
                     "Failed rescue level " + levelToString(level) + ": " + msg);
         }
     }
diff --git a/services/core/java/com/android/server/StorageManagerService.java b/services/core/java/com/android/server/StorageManagerService.java
index bfbce40..75e8000 100644
--- a/services/core/java/com/android/server/StorageManagerService.java
+++ b/services/core/java/com/android/server/StorageManagerService.java
@@ -56,6 +56,7 @@
 import android.os.Handler;
 import android.os.HandlerThread;
 import android.os.IBinder;
+import android.os.IProgressListener;
 import android.os.IStoraged;
 import android.os.IVold;
 import android.os.IVoldListener;
@@ -570,7 +571,7 @@
                     }
 
                     // TODO: Reintroduce shouldBenchmark() test
-                    fstrim(0);
+                    fstrim(0, null);
 
                     // invoke the completion callback, if any
                     // TODO: fstrim is non-blocking, so remove this useless callback
@@ -1576,21 +1577,19 @@
     }
 
     @Override
-    public long benchmark(String volId) {
+    public void benchmark(String volId, IVoldTaskListener listener) {
         enforcePermission(android.Manifest.permission.MOUNT_FORMAT_FILESYSTEMS);
 
-        // TODO: refactor for callers to provide a listener
         try {
-            final CompletableFuture<PersistableBundle> result = new CompletableFuture<>();
             mVold.benchmark(volId, new IVoldTaskListener.Stub() {
                 @Override
                 public void onStatus(int status, PersistableBundle extras) {
-                    // Not currently used
+                    dispatchOnStatus(listener, status, extras);
                 }
 
                 @Override
                 public void onFinished(int status, PersistableBundle extras) {
-                    result.complete(extras);
+                    dispatchOnFinished(listener, status, extras);
 
                     final String path = extras.getString("path");
                     final String ident = extras.getString("ident");
@@ -1611,10 +1610,8 @@
                     }
                 }
             });
-            return result.get(3, TimeUnit.MINUTES).getLong("run", Long.MAX_VALUE);
-        } catch (Exception e) {
-            Slog.wtf(TAG, e);
-            return Long.MAX_VALUE;
+        } catch (RemoteException e) {
+            throw e.rethrowAsRuntimeException();
         }
     }
 
@@ -1742,13 +1739,15 @@
     }
 
     @Override
-    public void fstrim(int flags) {
+    public void fstrim(int flags, IVoldTaskListener listener) {
         enforcePermission(android.Manifest.permission.MOUNT_FORMAT_FILESYSTEMS);
 
         try {
             mVold.fstrim(flags, new IVoldTaskListener.Stub() {
                 @Override
                 public void onStatus(int status, PersistableBundle extras) {
+                    dispatchOnStatus(listener, status, extras);
+
                     // Ignore trim failures
                     if (status != 0) return;
 
@@ -1770,12 +1769,13 @@
 
                 @Override
                 public void onFinished(int status, PersistableBundle extras) {
-                    // Not currently used
+                    dispatchOnFinished(listener, status, extras);
+
                     // TODO: benchmark when desired
                 }
             });
-        } catch (Exception e) {
-            Slog.wtf(TAG, e);
+        } catch (RemoteException e) {
+            throw e.rethrowAsRuntimeException();
         }
     }
 
@@ -3239,6 +3239,26 @@
         }
     }
 
+    private void dispatchOnStatus(IVoldTaskListener listener, int status,
+            PersistableBundle extras) {
+        if (listener != null) {
+            try {
+                listener.onStatus(status, extras);
+            } catch (RemoteException ignored) {
+            }
+        }
+    }
+
+    private void dispatchOnFinished(IVoldTaskListener listener, int status,
+            PersistableBundle extras) {
+        if (listener != null) {
+            try {
+                listener.onFinished(status, extras);
+            } catch (RemoteException ignored) {
+            }
+        }
+    }
+
     private static class Callbacks extends Handler {
         private static final int MSG_STORAGE_STATE_CHANGED = 1;
         private static final int MSG_VOLUME_STATE_CHANGED = 2;
diff --git a/services/core/java/com/android/server/am/UserController.java b/services/core/java/com/android/server/am/UserController.java
index 2df5dc9..9c649e3 100644
--- a/services/core/java/com/android/server/am/UserController.java
+++ b/services/core/java/com/android/server/am/UserController.java
@@ -89,6 +89,7 @@
 import com.android.internal.util.ArrayUtils;
 import com.android.internal.util.Preconditions;
 import com.android.internal.widget.LockPatternUtils;
+import com.android.server.FgThread;
 import com.android.server.LocalServices;
 import com.android.server.SystemServiceManager;
 import com.android.server.pm.UserManagerService;
@@ -369,13 +370,17 @@
         // Prepare app storage before we go any further
         uss.mUnlockProgress.setProgress(5,
                     mInjector.getContext().getString(R.string.android_start_title));
-        mInjector.getUserManager().onBeforeUnlockUser(userId);
-        uss.mUnlockProgress.setProgress(20);
 
-        // Dispatch unlocked to system services; when fully dispatched,
-        // that calls through to the next "unlocked" phase
-        mHandler.obtainMessage(SYSTEM_USER_UNLOCK_MSG, userId, 0, uss)
-                .sendToTarget();
+        // Call onBeforeUnlockUser on a worker thread that allows disk I/O
+        FgThread.getHandler().post(() -> {
+            mInjector.getUserManager().onBeforeUnlockUser(userId);
+            uss.mUnlockProgress.setProgress(20);
+
+            // Dispatch unlocked to system services; when fully dispatched,
+            // that calls through to the next "unlocked" phase
+            mHandler.obtainMessage(SYSTEM_USER_UNLOCK_MSG, userId, 0, uss)
+                    .sendToTarget();
+        });
     }
 
     /**
@@ -1819,7 +1824,10 @@
             case SYSTEM_USER_UNLOCK_MSG:
                 final int userId = msg.arg1;
                 mInjector.getSystemServiceManager().unlockUser(userId);
-                mInjector.loadUserRecents(userId);
+                // Loads recents on a worker thread that allows disk I/O
+                FgThread.getHandler().post(() -> {
+                    mInjector.loadUserRecents(userId);
+                });
                 if (userId == UserHandle.USER_SYSTEM) {
                     mInjector.startPersistentApps(PackageManager.MATCH_DIRECT_BOOT_UNAWARE);
                 }
diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java
index 5eb2a8d..3f23737 100644
--- a/services/core/java/com/android/server/audio/AudioService.java
+++ b/services/core/java/com/android/server/audio/AudioService.java
@@ -122,6 +122,7 @@
 import android.view.KeyEvent;
 import android.view.accessibility.AccessibilityManager;
 
+import com.android.internal.annotations.GuardedBy;
 import com.android.internal.util.DumpUtils;
 import com.android.internal.util.XmlUtils;
 import com.android.server.EventLogTags;
@@ -398,8 +399,9 @@
      * {@link AudioManager#RINGER_MODE_SILENT}, or
      * {@link AudioManager#RINGER_MODE_VIBRATE}.
      */
-    // protected by mSettingsLock
+    @GuardedBy("mSettingsLock")
     private int mRingerMode;  // internal ringer mode, affects muting of underlying streams
+    @GuardedBy("mSettingsLock")
     private int mRingerModeExternal = -1;  // reported ringer mode to outside clients (AudioManager)
 
     /** @see System#MODE_RINGER_STREAMS_AFFECTED */
@@ -929,8 +931,11 @@
         mForceUseLogger.log(new ForceUseEvent(AudioSystem.FOR_RECORD, mForcedUseForComm,
                 "onAudioServerDied"));
         AudioSystem.setForceUse(AudioSystem.FOR_RECORD, mForcedUseForComm);
-        final int forSys = mCameraSoundForced ?
-                AudioSystem.FORCE_SYSTEM_ENFORCED : AudioSystem.FORCE_NONE;
+        final int forSys;
+        synchronized (mSettingsLock) {
+            forSys = mCameraSoundForced ?
+                    AudioSystem.FORCE_SYSTEM_ENFORCED : AudioSystem.FORCE_NONE;
+        }
         mForceUseLogger.log(new ForceUseEvent(AudioSystem.FOR_SYSTEM, forSys,
                 "onAudioServerDied"));
         AudioSystem.setForceUse(AudioSystem.FOR_SYSTEM, forSys);
@@ -3797,6 +3802,7 @@
         return (mRingerModeMutedStreams & (1 << streamType)) != 0;
     }
 
+    @GuardedBy("mSettingsLock")
     private boolean updateRingerModeAffectedStreams() {
         int ringerModeAffectedStreams = Settings.System.getIntForUser(mContentResolver,
                 Settings.System.MODE_RINGER_STREAMS_AFFECTED,
@@ -3810,12 +3816,10 @@
             ringerModeAffectedStreams = mRingerModeDelegate
                     .getRingerModeAffectedStreams(ringerModeAffectedStreams);
         }
-        synchronized (mCameraSoundForced) {
-            if (mCameraSoundForced) {
-                ringerModeAffectedStreams &= ~(1 << AudioSystem.STREAM_SYSTEM_ENFORCED);
-            } else {
-                ringerModeAffectedStreams |= (1 << AudioSystem.STREAM_SYSTEM_ENFORCED);
-            }
+        if (mCameraSoundForced) {
+            ringerModeAffectedStreams &= ~(1 << AudioSystem.STREAM_SYSTEM_ENFORCED);
+        } else {
+            ringerModeAffectedStreams |= (1 << AudioSystem.STREAM_SYSTEM_ENFORCED);
         }
         if (mStreamVolumeAlias[AudioSystem.STREAM_DTMF] == AudioSystem.STREAM_RING) {
             ringerModeAffectedStreams |= (1 << AudioSystem.STREAM_DTMF);
@@ -4185,7 +4189,6 @@
     //  2   mSetModeDeathHandlers
     //  3     mSettingsLock
     //  4       VolumeStreamState.class
-    //  5         mCameraSoundForced
     public class VolumeStreamState {
         private final int mStreamType;
         private final int mIndexMin;
@@ -4252,27 +4255,28 @@
         }
 
         public void readSettings() {
-            synchronized (VolumeStreamState.class) {
-                // force maximum volume on all streams if fixed volume property is set
-                if (mUseFixedVolume) {
-                    mIndexMap.put(AudioSystem.DEVICE_OUT_DEFAULT, mIndexMax);
-                    return;
-                }
-                // do not read system stream volume from settings: this stream is always aliased
-                // to another stream type and its volume is never persisted. Values in settings can
-                // only be stale values
-                if ((mStreamType == AudioSystem.STREAM_SYSTEM) ||
-                        (mStreamType == AudioSystem.STREAM_SYSTEM_ENFORCED)) {
-                    int index = 10 * AudioSystem.DEFAULT_STREAM_VOLUME[mStreamType];
-                    synchronized (mCameraSoundForced) {
+            synchronized (mSettingsLock) {
+                synchronized (VolumeStreamState.class) {
+                    // force maximum volume on all streams if fixed volume property is set
+                    if (mUseFixedVolume) {
+                        mIndexMap.put(AudioSystem.DEVICE_OUT_DEFAULT, mIndexMax);
+                        return;
+                    }
+                    // do not read system stream volume from settings: this stream is always aliased
+                    // to another stream type and its volume is never persisted. Values in settings can
+                    // only be stale values
+                    if ((mStreamType == AudioSystem.STREAM_SYSTEM) ||
+                            (mStreamType == AudioSystem.STREAM_SYSTEM_ENFORCED)) {
+                        int index = 10 * AudioSystem.DEFAULT_STREAM_VOLUME[mStreamType];
                         if (mCameraSoundForced) {
                             index = mIndexMax;
                         }
+                        mIndexMap.put(AudioSystem.DEVICE_OUT_DEFAULT, index);
+                        return;
                     }
-                    mIndexMap.put(AudioSystem.DEVICE_OUT_DEFAULT, index);
-                    return;
                 }
-
+            }
+            synchronized (VolumeStreamState.class) {
                 int remainingDevices = AudioSystem.DEVICE_OUT_ALL;
 
                 for (int i = 0; remainingDevices != 0; i++) {
@@ -4385,34 +4389,34 @@
         public boolean setIndex(int index, int device, String caller) {
             boolean changed = false;
             int oldIndex;
-            synchronized (VolumeStreamState.class) {
-                oldIndex = getIndex(device);
-                index = getValidIndex(index);
-                synchronized (mCameraSoundForced) {
+            synchronized (mSettingsLock) {
+                synchronized (VolumeStreamState.class) {
+                    oldIndex = getIndex(device);
+                    index = getValidIndex(index);
                     if ((mStreamType == AudioSystem.STREAM_SYSTEM_ENFORCED) && mCameraSoundForced) {
                         index = mIndexMax;
                     }
-                }
-                mIndexMap.put(device, index);
+                    mIndexMap.put(device, index);
 
-                changed = oldIndex != index;
-                // Apply change to all streams using this one as alias if:
-                // - the index actually changed OR
-                // - there is no volume index stored for this device on alias stream.
-                // If changing volume of current device, also change volume of current
-                // device on aliased stream
-                final boolean currentDevice = (device == getDeviceForStream(mStreamType));
-                final int numStreamTypes = AudioSystem.getNumStreamTypes();
-                for (int streamType = numStreamTypes - 1; streamType >= 0; streamType--) {
-                    final VolumeStreamState aliasStreamState = mStreamStates[streamType];
-                    if (streamType != mStreamType &&
-                            mStreamVolumeAlias[streamType] == mStreamType &&
-                            (changed || !aliasStreamState.hasIndexForDevice(device))) {
-                        final int scaledIndex = rescaleIndex(index, mStreamType, streamType);
-                        aliasStreamState.setIndex(scaledIndex, device, caller);
-                        if (currentDevice) {
-                            aliasStreamState.setIndex(scaledIndex,
-                                    getDeviceForStream(streamType), caller);
+                    changed = oldIndex != index;
+                    // Apply change to all streams using this one as alias if:
+                    // - the index actually changed OR
+                    // - there is no volume index stored for this device on alias stream.
+                    // If changing volume of current device, also change volume of current
+                    // device on aliased stream
+                    final boolean currentDevice = (device == getDeviceForStream(mStreamType));
+                    final int numStreamTypes = AudioSystem.getNumStreamTypes();
+                    for (int streamType = numStreamTypes - 1; streamType >= 0; streamType--) {
+                        final VolumeStreamState aliasStreamState = mStreamStates[streamType];
+                        if (streamType != mStreamType &&
+                                mStreamVolumeAlias[streamType] == mStreamType &&
+                                (changed || !aliasStreamState.hasIndexForDevice(device))) {
+                            final int scaledIndex = rescaleIndex(index, mStreamType, streamType);
+                            aliasStreamState.setIndex(scaledIndex, device, caller);
+                            if (currentDevice) {
+                                aliasStreamState.setIndex(scaledIndex,
+                                        getDeviceForStream(streamType), caller);
+                            }
                         }
                     }
                 }
@@ -6022,13 +6026,8 @@
 
             boolean cameraSoundForced = readCameraSoundForced();
             synchronized (mSettingsLock) {
-                boolean cameraSoundForcedChanged = false;
-                synchronized (mCameraSoundForced) {
-                    if (cameraSoundForced != mCameraSoundForced) {
-                        mCameraSoundForced = cameraSoundForced;
-                        cameraSoundForcedChanged = true;
-                    }
-                }
+                final boolean cameraSoundForcedChanged = (cameraSoundForced != mCameraSoundForced);
+                mCameraSoundForced = cameraSoundForced;
                 if (cameraSoundForcedChanged) {
                     if (!mIsSingleVolume) {
                         VolumeStreamState s = mStreamStates[AudioSystem.STREAM_SYSTEM_ENFORCED];
@@ -6408,11 +6407,12 @@
     //==========================================================================================
 
     // cached value of com.android.internal.R.bool.config_camera_sound_forced
-    private Boolean mCameraSoundForced;
+    @GuardedBy("mSettingsLock")
+    private boolean mCameraSoundForced;
 
     // called by android.hardware.Camera to populate CameraInfo.canDisableShutterSound
     public boolean isCameraSoundForced() {
-        synchronized (mCameraSoundForced) {
+        synchronized (mSettingsLock) {
             return mCameraSoundForced;
         }
     }
@@ -6734,7 +6734,9 @@
         public void setRingerModeDelegate(RingerModeDelegate delegate) {
             mRingerModeDelegate = delegate;
             if (mRingerModeDelegate != null) {
-                updateRingerModeAffectedStreams();
+                synchronized (mSettingsLock) {
+                    updateRingerModeAffectedStreams();
+                }
                 setRingerModeInternal(getRingerModeInternal(), TAG + ".setRingerModeDelegate");
             }
         }
diff --git a/services/core/java/com/android/server/display/DisplayPowerController.java b/services/core/java/com/android/server/display/DisplayPowerController.java
index f930b52..600bc42 100644
--- a/services/core/java/com/android/server/display/DisplayPowerController.java
+++ b/services/core/java/com/android/server/display/DisplayPowerController.java
@@ -1120,6 +1120,27 @@
             // Dismiss the black surface without fanfare.
             mPowerState.setColorFadeLevel(1.0f);
             mPowerState.dismissColorFade();
+        } else if (target == Display.STATE_ON_SUSPEND) {
+            // Want screen full-power and suspended.
+            // Wait for brightness animation to complete beforehand unless already
+            // suspended because we may not be able to change it after suspension.
+            if (mScreenBrightnessRampAnimator.isAnimating()
+                    && mPowerState.getScreenState() != Display.STATE_ON_SUSPEND) {
+                return;
+            }
+
+            // If not already suspending, temporarily set the state to on until the
+            // screen on is unblocked, then suspend.
+            if (mPowerState.getScreenState() != Display.STATE_ON_SUSPEND) {
+                if (!setScreenState(Display.STATE_ON)) {
+                    return;
+                }
+                setScreenState(Display.STATE_ON_SUSPEND);
+            }
+
+            // Dismiss the black surface without fanfare.
+            mPowerState.setColorFadeLevel(1.0f);
+            mPowerState.dismissColorFade();
         } else {
             // Want screen off.
             mPendingScreenOff = true;
diff --git a/services/core/java/com/android/server/display/LocalDisplayAdapter.java b/services/core/java/com/android/server/display/LocalDisplayAdapter.java
index 9fdc35f..eb9ff58 100644
--- a/services/core/java/com/android/server/display/LocalDisplayAdapter.java
+++ b/services/core/java/com/android/server/display/LocalDisplayAdapter.java
@@ -148,6 +148,8 @@
                 return SurfaceControl.POWER_MODE_DOZE;
             case Display.STATE_DOZE_SUSPEND:
                 return SurfaceControl.POWER_MODE_DOZE_SUSPEND;
+            case Display.STATE_ON_SUSPEND:
+                return SurfaceControl.POWER_MODE_ON_SUSPEND;
             default:
                 return SurfaceControl.POWER_MODE_NORMAL;
         }
@@ -470,6 +472,10 @@
                                     || oldState == Display.STATE_DOZE_SUSPEND) {
                                 setDisplayState(Display.STATE_DOZE);
                                 currentState = Display.STATE_DOZE;
+                            } else if (state == Display.STATE_ON_SUSPEND
+                                    || oldState == Display.STATE_ON_SUSPEND) {
+                                setDisplayState(Display.STATE_ON);
+                                currentState = Display.STATE_ON;
                             } else {
                                 return; // old state and new state is off
                             }
diff --git a/services/core/java/com/android/server/media/AudioPlaybackMonitor.java b/services/core/java/com/android/server/media/AudioPlaybackMonitor.java
deleted file mode 100644
index 791ee82..0000000
--- a/services/core/java/com/android/server/media/AudioPlaybackMonitor.java
+++ /dev/null
@@ -1,289 +0,0 @@
-/*
- * 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.media;
-
-import android.content.Context;
-import android.media.AudioManager.AudioPlaybackCallback;
-import android.media.AudioPlaybackConfiguration;
-import android.media.IAudioService;
-import android.media.IPlaybackConfigDispatcher;
-import android.os.Binder;
-import android.os.RemoteException;
-import android.os.UserHandle;
-import android.util.IntArray;
-import android.util.Log;
-import android.util.SparseArray;
-
-import java.io.PrintWriter;
-import java.util.ArrayList;
-import java.util.HashSet;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-
-/**
- * Monitors changes in audio playback, and notify the newly started audio playback through the
- * {@link OnAudioPlaybackStartedListener} and the activeness change through the
- * {@link OnAudioPlaybackActiveStateListener}.
- */
-class AudioPlaybackMonitor extends IPlaybackConfigDispatcher.Stub {
-    private static boolean DEBUG = MediaSessionService.DEBUG;
-    private static String TAG = "AudioPlaybackMonitor";
-
-    private static AudioPlaybackMonitor sInstance;
-
-    /**
-     * Called when audio playback is started for a given UID.
-     */
-    interface OnAudioPlaybackStartedListener {
-        void onAudioPlaybackStarted(int uid);
-    }
-
-    /**
-     * Called when audio player state is changed.
-     */
-    interface OnAudioPlayerActiveStateChangedListener {
-        void onAudioPlayerActiveStateChanged(int uid, boolean active);
-    }
-
-    private final Object mLock = new Object();
-    private final Context mContext;
-    private final List<OnAudioPlaybackStartedListener> mAudioPlaybackStartedListeners
-            = new ArrayList<>();
-    private final List<OnAudioPlayerActiveStateChangedListener>
-            mAudioPlayerActiveStateChangedListeners = new ArrayList<>();
-    private final Map<Integer, Integer> mAudioPlaybackStates = new HashMap<>();
-    private final Set<Integer> mActiveAudioPlaybackClientUids = new HashSet<>();
-
-    // Sorted array of UIDs that had active audio playback. (i.e. playing an audio/video)
-    // The UID whose audio playback becomes active at the last comes first.
-    // TODO(b/35278867): Find and use unique identifier for apps because apps may share the UID.
-    private final IntArray mSortedAudioPlaybackClientUids = new IntArray();
-
-    static AudioPlaybackMonitor getInstance(Context context, IAudioService audioService) {
-        if (sInstance == null) {
-            sInstance = new AudioPlaybackMonitor(context, audioService);
-        }
-        return sInstance;
-    }
-
-    private AudioPlaybackMonitor(Context context, IAudioService audioService) {
-        mContext = context;
-        try {
-            audioService.registerPlaybackCallback(this);
-        } catch (RemoteException e) {
-            Log.wtf(TAG, "Failed to register playback callback", e);
-        }
-    }
-
-    /**
-     * Called when the {@link AudioPlaybackConfiguration} is updated.
-     * <p>If an app starts audio playback, the app's local media session will be the media button
-     * session. If the app has multiple media sessions, the playback active local session will be
-     * picked.
-     *
-     * @param configs List of the current audio playback configuration
-     */
-    @Override
-    public void dispatchPlaybackConfigChange(List<AudioPlaybackConfiguration> configs,
-            boolean flush) {
-        if (flush) {
-            Binder.flushPendingCommands();
-        }
-        final long token = Binder.clearCallingIdentity();
-        try {
-            List<Integer> newActiveAudioPlaybackClientUids = new ArrayList<>();
-            List<OnAudioPlayerActiveStateChangedListener> audioPlayerActiveStateChangedListeners;
-            List<OnAudioPlaybackStartedListener> audioPlaybackStartedListeners;
-            synchronized (mLock) {
-                // Update mActiveAudioPlaybackClientUids and mSortedAudioPlaybackClientUids,
-                // and find newly activated audio playbacks.
-                mActiveAudioPlaybackClientUids.clear();
-                for (AudioPlaybackConfiguration config : configs) {
-                    // Ignore inactive (i.e. not playing) or PLAYER_TYPE_JAM_SOUNDPOOL
-                    // (i.e. playback from the SoundPool class which is only for sound effects)
-                    // playback.
-                    // Note that we shouldn't ignore PLAYER_TYPE_UNKNOWN because it might be OEM
-                    // specific audio/video players.
-                    if (!config.isActive() || config.getPlayerType()
-                            == AudioPlaybackConfiguration.PLAYER_TYPE_JAM_SOUNDPOOL) {
-                        continue;
-                    }
-
-                    mActiveAudioPlaybackClientUids.add(config.getClientUid());
-                    Integer oldState = mAudioPlaybackStates.get(config.getPlayerInterfaceId());
-                    if (!isActiveState(oldState)) {
-                        if (DEBUG) {
-                            Log.d(TAG, "Found a new active media playback. " +
-                                    AudioPlaybackConfiguration.toLogFriendlyString(config));
-                        }
-                        // New active audio playback.
-                        newActiveAudioPlaybackClientUids.add(config.getClientUid());
-                        int index = mSortedAudioPlaybackClientUids.indexOf(config.getClientUid());
-                        if (index == 0) {
-                            // It's the lastly played music app already. Skip updating.
-                            continue;
-                        } else if (index > 0) {
-                            mSortedAudioPlaybackClientUids.remove(index);
-                        }
-                        mSortedAudioPlaybackClientUids.add(0, config.getClientUid());
-                    }
-                }
-                audioPlayerActiveStateChangedListeners = new ArrayList<>(
-                        mAudioPlayerActiveStateChangedListeners);
-                audioPlaybackStartedListeners = new ArrayList<>(mAudioPlaybackStartedListeners);
-            }
-            // Notify the change of audio playback states.
-            for (AudioPlaybackConfiguration config : configs) {
-                boolean wasActive = isActiveState(
-                        mAudioPlaybackStates.get(config.getPlayerInterfaceId()));
-                boolean isActive = config.isActive();
-                if (wasActive != isActive) {
-                    for (OnAudioPlayerActiveStateChangedListener listener
-                            : audioPlayerActiveStateChangedListeners) {
-                        listener.onAudioPlayerActiveStateChanged(config.getClientUid(),
-                                isActive);
-                    }
-                }
-            }
-            // Notify the start of audio playback
-            for (int uid : newActiveAudioPlaybackClientUids) {
-                for (OnAudioPlaybackStartedListener listener : audioPlaybackStartedListeners) {
-                    listener.onAudioPlaybackStarted(uid);
-                }
-            }
-            mAudioPlaybackStates.clear();
-            for (AudioPlaybackConfiguration config : configs) {
-                mAudioPlaybackStates.put(config.getPlayerInterfaceId(), config.getPlayerState());
-            }
-        } finally {
-            Binder.restoreCallingIdentity(token);
-        }
-    }
-
-    /**
-     * Registers OnAudioPlaybackStartedListener.
-     */
-    public void registerOnAudioPlaybackStartedListener(OnAudioPlaybackStartedListener listener) {
-        synchronized (mLock) {
-            mAudioPlaybackStartedListeners.add(listener);
-        }
-    }
-
-    /**
-     * Unregisters OnAudioPlaybackStartedListener.
-     */
-    public void unregisterOnAudioPlaybackStartedListener(OnAudioPlaybackStartedListener listener) {
-        synchronized (mLock) {
-            mAudioPlaybackStartedListeners.remove(listener);
-        }
-    }
-
-    /**
-     * Registers OnAudioPlayerActiveStateChangedListener.
-     */
-    public void registerOnAudioPlayerActiveStateChangedListener(
-            OnAudioPlayerActiveStateChangedListener listener) {
-        synchronized (mLock) {
-            mAudioPlayerActiveStateChangedListeners.add(listener);
-        }
-    }
-
-    /**
-     * Unregisters OnAudioPlayerActiveStateChangedListener.
-     */
-    public void unregisterOnAudioPlayerActiveStateChangedListener(
-            OnAudioPlayerActiveStateChangedListener listener) {
-        synchronized (mLock) {
-            mAudioPlayerActiveStateChangedListeners.remove(listener);
-        }
-    }
-
-    /**
-     * Returns the sorted list of UIDs that have had active audio playback. (i.e. playing an
-     * audio/video) The UID whose audio playback becomes active at the last comes first.
-     */
-    public IntArray getSortedAudioPlaybackClientUids() {
-        IntArray sortedAudioPlaybackClientUids = new IntArray();
-        synchronized (mLock) {
-            sortedAudioPlaybackClientUids.addAll(mSortedAudioPlaybackClientUids);
-        }
-        return sortedAudioPlaybackClientUids;
-    }
-
-    /**
-     * Returns if the audio playback is active for the uid.
-     */
-    public boolean isPlaybackActive(int uid) {
-        synchronized (mLock) {
-            return mActiveAudioPlaybackClientUids.contains(uid);
-        }
-    }
-
-    /**
-     * Cleans up the sorted list of audio playback client UIDs with given {@param
-     * mediaButtonSessionUid}.
-     * <p>UIDs whose audio playback started after the media button session's audio playback
-     * cannot be the lastly played media app. So they won't needed anymore.
-     *
-     * @param mediaButtonSessionUid UID of the media button session.
-     */
-    public void cleanUpAudioPlaybackUids(int mediaButtonSessionUid) {
-        synchronized (mLock) {
-            int userId = UserHandle.getUserId(mediaButtonSessionUid);
-            for (int i = mSortedAudioPlaybackClientUids.size() - 1; i >= 0; i--) {
-                if (mSortedAudioPlaybackClientUids.get(i) == mediaButtonSessionUid) {
-                    break;
-                }
-                int uid = mSortedAudioPlaybackClientUids.get(i);
-                if (userId == UserHandle.getUserId(uid) && !isPlaybackActive(uid)) {
-                    // Clean up unnecessary UIDs.
-                    // It doesn't need to be managed profile aware because it's just to prevent
-                    // the list from increasing indefinitely. The media button session updating
-                    // shouldn't be affected by cleaning up.
-                    mSortedAudioPlaybackClientUids.remove(i);
-                }
-            }
-        }
-    }
-
-    /**
-     * Dumps {@link AudioPlaybackMonitor}.
-     */
-    public void dump(PrintWriter pw, String prefix) {
-        synchronized (mLock) {
-            pw.println(prefix + "Audio playback (lastly played comes first)");
-            String indent = prefix + "  ";
-            for (int i = 0; i < mSortedAudioPlaybackClientUids.size(); i++) {
-                int uid = mSortedAudioPlaybackClientUids.get(i);
-                pw.print(indent + "uid=" + uid + " packages=");
-                String[] packages = mContext.getPackageManager().getPackagesForUid(uid);
-                if (packages != null && packages.length > 0) {
-                    for (int j = 0; j < packages.length; j++) {
-                        pw.print(packages[j] + " ");
-                    }
-                }
-                pw.println();
-            }
-        }
-    }
-
-    private boolean isActiveState(Integer state) {
-        return state != null && state.equals(AudioPlaybackConfiguration.PLAYER_STATE_STARTED);
-    }
-}
diff --git a/services/core/java/com/android/server/media/AudioPlayerStateMonitor.java b/services/core/java/com/android/server/media/AudioPlayerStateMonitor.java
new file mode 100644
index 0000000..be223f1
--- /dev/null
+++ b/services/core/java/com/android/server/media/AudioPlayerStateMonitor.java
@@ -0,0 +1,327 @@
+/*
+ * 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.media;
+
+import android.annotation.Nullable;
+import android.content.Context;
+import android.media.AudioPlaybackConfiguration;
+import android.media.IAudioService;
+import android.media.IPlaybackConfigDispatcher;
+import android.os.Binder;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.os.RemoteException;
+import android.os.UserHandle;
+import android.util.IntArray;
+import android.util.Log;
+
+import com.android.internal.annotations.GuardedBy;
+
+import java.io.PrintWriter;
+import java.util.HashSet;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Monitors the state changes of audio players.
+ */
+class AudioPlayerStateMonitor extends IPlaybackConfigDispatcher.Stub {
+    private static boolean DEBUG = MediaSessionService.DEBUG;
+    private static String TAG = "AudioPlayerStateMonitor";
+
+    private static AudioPlayerStateMonitor sInstance = new AudioPlayerStateMonitor();
+
+    /**
+     * Called when the state of audio player is changed.
+     */
+    interface OnAudioPlayerStateChangedListener {
+        void onAudioPlayerStateChanged(
+                int uid, int prevState, @Nullable AudioPlaybackConfiguration config);
+    }
+
+    private final static class MessageHandler extends Handler {
+        private static final int MSG_AUDIO_PLAYER_STATE_CHANGED = 1;
+
+        private final OnAudioPlayerStateChangedListener mListsner;
+
+        public MessageHandler(Looper looper, OnAudioPlayerStateChangedListener listener) {
+            super(looper);
+            mListsner = listener;
+        }
+
+        @Override
+        public void handleMessage(Message msg) {
+            switch (msg.what) {
+                case MSG_AUDIO_PLAYER_STATE_CHANGED:
+                    mListsner.onAudioPlayerStateChanged(
+                            msg.arg1, msg.arg2, (AudioPlaybackConfiguration) msg.obj);
+                    break;
+            }
+        }
+
+        public void sendAudioPlayerStateChangedMessage(int uid, int prevState,
+                AudioPlaybackConfiguration config) {
+            obtainMessage(MSG_AUDIO_PLAYER_STATE_CHANGED, uid, prevState, config).sendToTarget();
+        }
+    }
+
+    private final Object mLock = new Object();
+    @GuardedBy("mLock")
+    private final Map<OnAudioPlayerStateChangedListener, MessageHandler> mListenerMap =
+            new HashMap<>();
+    @GuardedBy("mLock")
+    private final Map<Integer, Integer> mAudioPlayerStates = new HashMap<>();
+    @GuardedBy("mLock")
+    private final Map<Integer, HashSet<Integer>> mAudioPlayersForUid = new HashMap<>();
+    // Sorted array of UIDs that had active audio playback. (i.e. playing an audio/video)
+    // The UID whose audio playback becomes active at the last comes first.
+    // TODO(b/35278867): Find and use unique identifier for apps because apps may share the UID.
+    @GuardedBy("mLock")
+    private final IntArray mSortedAudioPlaybackClientUids = new IntArray();
+
+    @GuardedBy("mLock")
+    private boolean mRegisteredToAudioService;
+
+    static AudioPlayerStateMonitor getInstance() {
+        return sInstance;
+    }
+
+    private AudioPlayerStateMonitor() {
+    }
+
+    /**
+     * Called when the {@link AudioPlaybackConfiguration} is updated.
+     * <p>If an app starts audio playback, the app's local media session will be the media button
+     * session. If the app has multiple media sessions, the playback active local session will be
+     * picked.
+     *
+     * @param configs List of the current audio playback configuration
+     */
+    @Override
+    public void dispatchPlaybackConfigChange(List<AudioPlaybackConfiguration> configs,
+            boolean flush) {
+        if (flush) {
+            Binder.flushPendingCommands();
+        }
+        final long token = Binder.clearCallingIdentity();
+        try {
+            final Map<Integer, Integer> prevAudioPlayerStates = new HashMap<>(mAudioPlayerStates);
+            final Map<Integer, HashSet<Integer>> prevAudioPlayersForUid =
+                    new HashMap<>(mAudioPlayersForUid);
+            synchronized (mLock) {
+                mAudioPlayerStates.clear();
+                mAudioPlayersForUid.clear();
+                for (AudioPlaybackConfiguration config : configs) {
+                    int pii = config.getPlayerInterfaceId();
+                    int uid = config.getClientUid();
+                    mAudioPlayerStates.put(pii, config.getPlayerState());
+                    HashSet<Integer> players = mAudioPlayersForUid.get(uid);
+                    if (players == null) {
+                        players = new HashSet<Integer>();
+                        players.add(pii);
+                        mAudioPlayersForUid.put(uid, players);
+                    } else {
+                        players.add(pii);
+                    }
+                }
+                for (AudioPlaybackConfiguration config : configs) {
+                    if (!config.isActive()) {
+                        continue;
+                    }
+
+                    int uid = config.getClientUid();
+                    if (!isActiveState(prevAudioPlayerStates.get(config.getPlayerInterfaceId()))) {
+                        if (DEBUG) {
+                            Log.d(TAG, "Found a new active media playback. " +
+                                    AudioPlaybackConfiguration.toLogFriendlyString(config));
+                        }
+                        // New active audio playback.
+                        int index = mSortedAudioPlaybackClientUids.indexOf(uid);
+                        if (index == 0) {
+                            // It's the lastly played music app already. Skip updating.
+                            continue;
+                        } else if (index > 0) {
+                            mSortedAudioPlaybackClientUids.remove(index);
+                        }
+                        mSortedAudioPlaybackClientUids.add(0, uid);
+                    }
+                }
+                // Notify the change of audio player states.
+                for (AudioPlaybackConfiguration config : configs) {
+                    final Integer prevState = prevAudioPlayerStates.get(config.getPlayerInterfaceId());
+                    final int prevStateInt =
+                            (prevState == null) ? AudioPlaybackConfiguration.PLAYER_STATE_UNKNOWN :
+                                prevState.intValue();
+                    if (prevStateInt != config.getPlayerState()) {
+                        sendAudioPlayerStateChangedMessageLocked(
+                                config.getClientUid(), prevStateInt, config);
+                    }
+                }
+                for (Integer prevUid : prevAudioPlayersForUid.keySet()) {
+                    // If all players for prevUid is removed, notify the prev state was
+                    // PLAYER_STATE_STARTED only when there were a player whose state was
+                    // PLAYER_STATE_STARTED, otherwise any inactive state is okay to notify.
+                    if (!mAudioPlayersForUid.containsKey(prevUid)) {
+                        Set<Integer> prevPlayers = prevAudioPlayersForUid.get(prevUid);
+                        int prevState = AudioPlaybackConfiguration.PLAYER_STATE_UNKNOWN;
+                        for (int pii : prevPlayers) {
+                            Integer state = prevAudioPlayerStates.get(pii);
+                            if (state == null) {
+                                continue;
+                            }
+                            if (state == AudioPlaybackConfiguration.PLAYER_STATE_STARTED) {
+                                prevState = state;
+                                break;
+                            } else if (prevState
+                                    == AudioPlaybackConfiguration.PLAYER_STATE_UNKNOWN) {
+                                prevState = state;
+                            }
+                        }
+                        sendAudioPlayerStateChangedMessageLocked(prevUid, prevState, null);
+                    }
+                }
+            }
+        } finally {
+            Binder.restoreCallingIdentity(token);
+        }
+    }
+
+    /**
+     * Registers OnAudioPlayerStateChangedListener.
+     */
+    public void registerListener(OnAudioPlayerStateChangedListener listener, Handler handler) {
+        synchronized (mLock) {
+            mListenerMap.put(listener, new MessageHandler((handler == null) ?
+                    Looper.myLooper() : handler.getLooper(), listener));
+        }
+    }
+
+    /**
+     * Unregisters OnAudioPlayerStateChangedListener.
+     */
+    public void unregisterListener(OnAudioPlayerStateChangedListener listener) {
+        synchronized (mLock) {
+            mListenerMap.remove(listener);
+        }
+    }
+
+    /**
+     * Returns the sorted list of UIDs that have had active audio playback. (i.e. playing an
+     * audio/video) The UID whose audio playback becomes active at the last comes first.
+     */
+    public IntArray getSortedAudioPlaybackClientUids() {
+        IntArray sortedAudioPlaybackClientUids = new IntArray();
+        synchronized (mLock) {
+            sortedAudioPlaybackClientUids.addAll(mSortedAudioPlaybackClientUids);
+        }
+        return sortedAudioPlaybackClientUids;
+    }
+
+    /**
+     * Returns if the audio playback is active for the uid.
+     */
+    public boolean isPlaybackActive(int uid) {
+        synchronized (mLock) {
+            Set<Integer> players = mAudioPlayersForUid.get(uid);
+            if (players == null) {
+                return false;
+            }
+            for (Integer pii : players) {
+                if (isActiveState(mAudioPlayerStates.get(pii))) {
+                    return true;
+                }
+            }
+            return false;
+        }
+    }
+
+    /**
+     * Cleans up the sorted list of audio playback client UIDs with given {@param
+     * mediaButtonSessionUid}.
+     * <p>UIDs whose audio playback are inactive and have started before the media button session's
+     * audio playback cannot be the lastly played media app. So they won't needed anymore.
+     *
+     * @param mediaButtonSessionUid UID of the media button session.
+     */
+    public void cleanUpAudioPlaybackUids(int mediaButtonSessionUid) {
+        synchronized (mLock) {
+            int userId = UserHandle.getUserId(mediaButtonSessionUid);
+            for (int i = mSortedAudioPlaybackClientUids.size() - 1; i >= 0; i--) {
+                if (mSortedAudioPlaybackClientUids.get(i) == mediaButtonSessionUid) {
+                    break;
+                }
+                int uid = mSortedAudioPlaybackClientUids.get(i);
+                if (userId == UserHandle.getUserId(uid) && !isPlaybackActive(uid)) {
+                    // Clean up unnecessary UIDs.
+                    // It doesn't need to be managed profile aware because it's just to prevent
+                    // the list from increasing indefinitely. The media button session updating
+                    // shouldn't be affected by cleaning up.
+                    mSortedAudioPlaybackClientUids.remove(i);
+                }
+            }
+        }
+    }
+
+    /**
+     * Dumps {@link AudioPlayerStateMonitor}.
+     */
+    public void dump(Context context, PrintWriter pw, String prefix) {
+        synchronized (mLock) {
+            pw.println(prefix + "Audio playback (lastly played comes first)");
+            String indent = prefix + "  ";
+            for (int i = 0; i < mSortedAudioPlaybackClientUids.size(); i++) {
+                int uid = mSortedAudioPlaybackClientUids.get(i);
+                pw.print(indent + "uid=" + uid + " packages=");
+                String[] packages = context.getPackageManager().getPackagesForUid(uid);
+                if (packages != null && packages.length > 0) {
+                    for (int j = 0; j < packages.length; j++) {
+                        pw.print(packages[j] + " ");
+                    }
+                }
+                pw.println();
+            }
+        }
+    }
+
+    public void registerSelfIntoAudioServiceIfNeeded(IAudioService audioService) {
+        synchronized (mLock) {
+            try {
+                if (!mRegisteredToAudioService) {
+                    audioService.registerPlaybackCallback(this);
+                    mRegisteredToAudioService = true;
+                }
+            } catch (RemoteException e) {
+                Log.wtf(TAG, "Failed to register playback callback", e);
+                mRegisteredToAudioService = false;
+            }
+        }
+    }
+
+    private void sendAudioPlayerStateChangedMessageLocked(
+            final int uid, final int prevState, final AudioPlaybackConfiguration config) {
+        for (MessageHandler messageHandler : mListenerMap.values()) {
+            messageHandler.sendAudioPlayerStateChangedMessage(uid, prevState, config);
+        }
+    }
+
+    private static boolean isActiveState(Integer state) {
+        return state != null && state.equals(AudioPlaybackConfiguration.PLAYER_STATE_STARTED);
+    }
+}
diff --git a/services/core/java/com/android/server/media/MediaRouterService.java b/services/core/java/com/android/server/media/MediaRouterService.java
index 1cfd5f0..3c9e1d4 100644
--- a/services/core/java/com/android/server/media/MediaRouterService.java
+++ b/services/core/java/com/android/server/media/MediaRouterService.java
@@ -19,12 +19,14 @@
 import com.android.internal.util.DumpUtils;
 import com.android.server.Watchdog;
 
+import android.annotation.Nullable;
 import android.app.ActivityManager;
 import android.content.BroadcastReceiver;
 import android.content.Context;
 import android.content.Intent;
 import android.content.IntentFilter;
 import android.content.pm.PackageManager;
+import android.media.AudioPlaybackConfiguration;
 import android.media.AudioRoutesInfo;
 import android.media.AudioSystem;
 import android.media.IAudioRoutesObserver;
@@ -96,7 +98,8 @@
     private int mCurrentUserId = -1;
     private boolean mGlobalBluetoothA2dpOn = false;
     private final IAudioService mAudioService;
-    private final AudioPlaybackMonitor mAudioPlaybackMonitor;
+    private final AudioPlayerStateMonitor mAudioPlayerStateMonitor;
+    private final Handler mHandler = new Handler();
     private final AudioRoutesInfo mCurAudioRoutesInfo = new AudioRoutesInfo();
 
     public MediaRouterService(Context context) {
@@ -106,31 +109,57 @@
         mAudioService = IAudioService.Stub.asInterface(
                 ServiceManager.getService(Context.AUDIO_SERVICE));
 
-        mAudioPlaybackMonitor = AudioPlaybackMonitor.getInstance(context, mAudioService);
-        mAudioPlaybackMonitor.registerOnAudioPlayerActiveStateChangedListener(
-                new AudioPlaybackMonitor.OnAudioPlayerActiveStateChangedListener() {
+        mAudioPlayerStateMonitor = AudioPlayerStateMonitor.getInstance();
+        mAudioPlayerStateMonitor.registerListener(
+                new AudioPlayerStateMonitor.OnAudioPlayerStateChangedListener() {
+            static final long WAIT_MS = 500;
+            final Runnable mRestoreBluetoothA2dpRunnable = new Runnable() {
+                @Override
+                public void run() {
+                    restoreBluetoothA2dp();
+                }
+            };
+
             @Override
-            public void onAudioPlayerActiveStateChanged(int uid, boolean active) {
+            public void onAudioPlayerStateChanged(
+                    int uid, int prevState, @Nullable AudioPlaybackConfiguration config) {
+                int restoreUid = -1;
+                boolean active = config == null ? false : config.isActive();
                 if (active) {
-                    restoreRoute(uid);
+                    restoreUid = uid;
+                } else if (prevState != AudioPlaybackConfiguration.PLAYER_STATE_STARTED) {
+                    // Noting to do if the prev state is not an active state.
+                    return;
                 } else {
                     IntArray sortedAudioPlaybackClientUids =
-                            mAudioPlaybackMonitor.getSortedAudioPlaybackClientUids();
-                    boolean restored = false;
-                    for (int i = 0; i < sortedAudioPlaybackClientUids.size(); i++) {
-                        if (mAudioPlaybackMonitor.isPlaybackActive(
+                            mAudioPlayerStateMonitor.getSortedAudioPlaybackClientUids();
+                    for (int i = 0; i < sortedAudioPlaybackClientUids.size(); ++i) {
+                        if (mAudioPlayerStateMonitor.isPlaybackActive(
                                 sortedAudioPlaybackClientUids.get(i))) {
-                            restoreRoute(sortedAudioPlaybackClientUids.get(i));
-                            restored = true;
+                            restoreUid = sortedAudioPlaybackClientUids.get(i);
                             break;
                         }
                     }
-                    if (!restored) {
-                        restoreBluetoothA2dp();
+                }
+
+                mHandler.removeCallbacks(mRestoreBluetoothA2dpRunnable);
+                if (restoreUid >= 0) {
+                    restoreRoute(restoreUid);
+                    if (DEBUG) {
+                        Slog.d(TAG, "onAudioPlayerStateChanged: " + "uid " + uid
+                                + " active " + active + " restoring " + restoreUid);
+                    }
+                } else {
+                    mHandler.postDelayed(mRestoreBluetoothA2dpRunnable, WAIT_MS);
+                    if (DEBUG) {
+                        Slog.d(TAG, "onAudioPlayerStateChanged: " + "uid " + uid
+                                + " active " + active + " delaying");
                     }
                 }
             }
-        });
+        }, mHandler);
+        mAudioPlayerStateMonitor.registerSelfIntoAudioServiceIfNeeded(mAudioService);
+
         AudioRoutesInfo audioRoutes = null;
         try {
             audioRoutes = mAudioService.startWatchingRoutes(new IAudioRoutesObserver.Stub() {
@@ -261,9 +290,14 @@
 
         final long token = Binder.clearCallingIdentity();
         try {
+            ClientRecord clientRecord;
             synchronized (mLock) {
-                return isPlaybackActiveLocked(client);
+                clientRecord = mAllClientRecords.get(client.asBinder());
             }
+            if (clientRecord != null) {
+                return mAudioPlayerStateMonitor.isPlaybackActive(clientRecord.mUid);
+            }
+            return false;
         } finally {
             Binder.restoreCallingIdentity(token);
         }
@@ -480,14 +514,6 @@
         return null;
     }
 
-    private boolean isPlaybackActiveLocked(IMediaRouterClient client) {
-        ClientRecord clientRecord = mAllClientRecords.get(client.asBinder());
-        if (clientRecord != null) {
-            return mAudioPlaybackMonitor.isPlaybackActive(clientRecord.mUid);
-        }
-        return false;
-    }
-
     private void setDiscoveryRequestLocked(IMediaRouterClient client,
             int routeTypes, boolean activeScan) {
         final IBinder binder = client.asBinder();
diff --git a/services/core/java/com/android/server/media/MediaSessionService.java b/services/core/java/com/android/server/media/MediaSessionService.java
index aa65244..f6a81d0 100644
--- a/services/core/java/com/android/server/media/MediaSessionService.java
+++ b/services/core/java/com/android/server/media/MediaSessionService.java
@@ -16,6 +16,7 @@
 
 package com.android.server.media;
 
+import android.annotation.Nullable;
 import android.app.ActivityManager;
 import android.app.INotificationManager;
 import android.app.KeyguardManager;
@@ -31,7 +32,7 @@
 import android.content.pm.UserInfo;
 import android.database.ContentObserver;
 import android.media.AudioManager;
-import android.media.AudioManagerInternal;
+import android.media.AudioPlaybackConfiguration;
 import android.media.AudioSystem;
 import android.media.IAudioService;
 import android.media.IRemoteVolumeController;
@@ -68,7 +69,6 @@
 import android.view.ViewConfiguration;
 
 import com.android.internal.util.DumpUtils;
-import com.android.server.LocalServices;
 import com.android.server.SystemService;
 import com.android.server.Watchdog;
 import com.android.server.Watchdog.Monitor;
@@ -104,7 +104,6 @@
 
     private KeyguardManager mKeyguardManager;
     private IAudioService mAudioService;
-    private AudioManagerInternal mAudioManagerInternal;
     private ContentResolver mContentResolver;
     private SettingsObserver mSettingsObserver;
     private INotificationManager mNotificationManager;
@@ -114,7 +113,7 @@
     // It's always not null after the MediaSessionService is started.
     private FullUserRecord mCurrentFullUserRecord;
     private MediaSessionRecord mGlobalPrioritySession;
-    private AudioPlaybackMonitor mAudioPlaybackMonitor;
+    private AudioPlayerStateMonitor mAudioPlayerStateMonitor;
 
     // Used to notify system UI when remote volume was changed. TODO find a
     // better way to handle this.
@@ -137,11 +136,16 @@
         mKeyguardManager =
                 (KeyguardManager) getContext().getSystemService(Context.KEYGUARD_SERVICE);
         mAudioService = getAudioService();
-        mAudioPlaybackMonitor = AudioPlaybackMonitor.getInstance(getContext(), mAudioService);
-        mAudioPlaybackMonitor.registerOnAudioPlaybackStartedListener(
-                new AudioPlaybackMonitor.OnAudioPlaybackStartedListener() {
+        mAudioPlayerStateMonitor = AudioPlayerStateMonitor.getInstance();
+        mAudioPlayerStateMonitor.registerListener(
+                new AudioPlayerStateMonitor.OnAudioPlayerStateChangedListener() {
             @Override
-            public void onAudioPlaybackStarted(int uid) {
+            public void onAudioPlayerStateChanged(
+                    int uid, int prevState, @Nullable AudioPlaybackConfiguration config) {
+                if (config == null || !config.isActive() || config.getPlayerType()
+                        == AudioPlaybackConfiguration.PLAYER_TYPE_JAM_SOUNDPOOL) {
+                    return;
+                }
                 synchronized (mLock) {
                     FullUserRecord user =
                             getFullUserRecordLocked(UserHandle.getUserId(uid));
@@ -150,8 +154,8 @@
                     }
                 }
             }
-        });
-        mAudioManagerInternal = LocalServices.getService(AudioManagerInternal.class);
+        }, null /* handler */);
+        mAudioPlayerStateMonitor.registerSelfIntoAudioServiceIfNeeded(mAudioService);
         mContentResolver = getContext().getContentResolver();
         mSettingsObserver = new SettingsObserver();
         mSettingsObserver.observe();
@@ -650,7 +654,7 @@
 
         public FullUserRecord(int fullUserId) {
             mFullUserId = fullUserId;
-            mPriorityStack = new MediaSessionStack(mAudioPlaybackMonitor, this);
+            mPriorityStack = new MediaSessionStack(mAudioPlayerStateMonitor, this);
             // Restore the remembered media button receiver before the boot.
             String mediaButtonReceiver = Settings.Secure.getStringForUser(mContentResolver,
                     Settings.System.MEDIA_BUTTON_RECEIVER, mFullUserId);
@@ -1309,7 +1313,7 @@
                 for (int i = 0; i < count; i++) {
                     mUserRecords.valueAt(i).dumpLocked(pw, "");
                 }
-                mAudioPlaybackMonitor.dump(pw, "");
+                mAudioPlayerStateMonitor.dump(getContext(), pw, "");
             }
         }
 
diff --git a/services/core/java/com/android/server/media/MediaSessionStack.java b/services/core/java/com/android/server/media/MediaSessionStack.java
index d9fe72e..719ec36 100644
--- a/services/core/java/com/android/server/media/MediaSessionStack.java
+++ b/services/core/java/com/android/server/media/MediaSessionStack.java
@@ -75,7 +75,7 @@
      */
     private final List<MediaSessionRecord> mSessions = new ArrayList<MediaSessionRecord>();
 
-    private final AudioPlaybackMonitor mAudioPlaybackMonitor;
+    private final AudioPlayerStateMonitor mAudioPlayerStateMonitor;
     private final OnMediaButtonSessionChangedListener mOnMediaButtonSessionChangedListener;
 
     /**
@@ -84,7 +84,6 @@
      */
     private MediaSessionRecord mMediaButtonSession;
 
-    private MediaSessionRecord mCachedDefault;
     private MediaSessionRecord mCachedVolumeDefault;
 
     /**
@@ -93,8 +92,8 @@
     private final SparseArray<ArrayList<MediaSessionRecord>> mCachedActiveLists =
             new SparseArray<>();
 
-    MediaSessionStack(AudioPlaybackMonitor monitor, OnMediaButtonSessionChangedListener listener) {
-        mAudioPlaybackMonitor = monitor;
+    MediaSessionStack(AudioPlayerStateMonitor monitor, OnMediaButtonSessionChangedListener listener) {
+        mAudioPlayerStateMonitor = monitor;
         mOnMediaButtonSessionChangedListener = listener;
     }
 
@@ -187,13 +186,13 @@
         if (DEBUG) {
             Log.d(TAG, "updateMediaButtonSessionIfNeeded, callers=" + Debug.getCallers(2));
         }
-        IntArray audioPlaybackUids = mAudioPlaybackMonitor.getSortedAudioPlaybackClientUids();
+        IntArray audioPlaybackUids = mAudioPlayerStateMonitor.getSortedAudioPlaybackClientUids();
         for (int i = 0; i < audioPlaybackUids.size(); i++) {
             MediaSessionRecord mediaButtonSession =
                     findMediaButtonSession(audioPlaybackUids.get(i));
             if (mediaButtonSession != null) {
                 // Found the media button session.
-                mAudioPlaybackMonitor.cleanUpAudioPlaybackUids(mediaButtonSession.getUid());
+                mAudioPlayerStateMonitor.cleanUpAudioPlaybackUids(mediaButtonSession.getUid());
                 if (mMediaButtonSession != mediaButtonSession) {
                     updateMediaButtonSession(mediaButtonSession);
                 }
@@ -216,7 +215,7 @@
         for (MediaSessionRecord session : mSessions) {
             if (uid == session.getUid()) {
                 if (session.getPlaybackState() != null && session.isPlaybackActive() ==
-                        mAudioPlaybackMonitor.isPlaybackActive(session.getUid())) {
+                        mAudioPlayerStateMonitor.isPlaybackActive(session.getUid())) {
                     // If there's a media session whose PlaybackState matches
                     // the audio playback state, return it immediately.
                     return session;
@@ -376,7 +375,6 @@
     }
 
     private void clearCache(int userId) {
-        mCachedDefault = null;
         mCachedVolumeDefault = null;
         mCachedActiveLists.remove(userId);
         // mCachedActiveLists may also include the list of sessions for UserHandle.USER_ALL,
diff --git a/services/core/java/com/android/server/pm/KeySetManagerService.java b/services/core/java/com/android/server/pm/KeySetManagerService.java
index 3574466..fca9585 100644
--- a/services/core/java/com/android/server/pm/KeySetManagerService.java
+++ b/services/core/java/com/android/server/pm/KeySetManagerService.java
@@ -18,6 +18,8 @@
 
 import static android.content.pm.PackageManager.INSTALL_FAILED_INVALID_APK;
 
+import static com.android.server.pm.PackageManagerService.SCAN_INITIAL;
+
 import com.android.internal.util.Preconditions;
 import android.content.pm.PackageParser;
 import android.util.ArrayMap;
@@ -341,6 +343,41 @@
         return mKeySets.get(id) != null;
     }
 
+    public boolean shouldCheckUpgradeKeySetLocked(PackageSettingBase oldPs, int scanFlags) {
+        // Can't rotate keys during boot or if sharedUser.
+        if (oldPs == null || (scanFlags&SCAN_INITIAL) != 0 || oldPs.isSharedUser()
+                || !oldPs.keySetData.isUsingUpgradeKeySets()) {
+            return false;
+        }
+        // app is using upgradeKeySets; make sure all are valid
+        long[] upgradeKeySets = oldPs.keySetData.getUpgradeKeySets();
+        for (int i = 0; i < upgradeKeySets.length; i++) {
+            if (!isIdValidKeySetId(upgradeKeySets[i])) {
+                Slog.wtf(TAG, "Package "
+                         + (oldPs.name != null ? oldPs.name : "<null>")
+                         + " contains upgrade-key-set reference to unknown key-set: "
+                         + upgradeKeySets[i]
+                         + " reverting to signatures check.");
+                return false;
+            }
+        }
+        return true;
+    }
+
+    public boolean checkUpgradeKeySetLocked(PackageSettingBase oldPS,
+            PackageParser.Package newPkg) {
+        // Upgrade keysets are being used.  Determine if new package has a superset of the
+        // required keys.
+        long[] upgradeKeySets = oldPS.keySetData.getUpgradeKeySets();
+        for (int i = 0; i < upgradeKeySets.length; i++) {
+            Set<PublicKey> upgradeSet = getPublicKeysFromKeySetLPr(upgradeKeySets[i]);
+            if (upgradeSet != null && newPkg.mSigningKeys.containsAll(upgradeSet)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
     /**
      * Fetches the {@link PublicKey public keys} which belong to the specified
      * KeySet id.
diff --git a/services/core/java/com/android/server/pm/PackageManagerService.java b/services/core/java/com/android/server/pm/PackageManagerService.java
index 6c42f4f..6a0ea4ee 100644
--- a/services/core/java/com/android/server/pm/PackageManagerService.java
+++ b/services/core/java/com/android/server/pm/PackageManagerService.java
@@ -102,6 +102,15 @@
 import static com.android.server.pm.InstructionSets.getPrimaryInstructionSet;
 import static com.android.server.pm.PackageManagerServiceCompilerMapping.getCompilerFilterForReason;
 import static com.android.server.pm.PackageManagerServiceCompilerMapping.getDefaultCompilerFilter;
+import static com.android.server.pm.PackageManagerServiceUtils.compareSignatures;
+import static com.android.server.pm.PackageManagerServiceUtils.compressedFileExists;
+import static com.android.server.pm.PackageManagerServiceUtils.decompressFile;
+import static com.android.server.pm.PackageManagerServiceUtils.deriveAbiOverride;
+import static com.android.server.pm.PackageManagerServiceUtils.dumpCriticalInfo;
+import static com.android.server.pm.PackageManagerServiceUtils.getCompressedFiles;
+import static com.android.server.pm.PackageManagerServiceUtils.getLastModifiedTime;
+import static com.android.server.pm.PackageManagerServiceUtils.logCriticalInfo;
+import static com.android.server.pm.PackageManagerServiceUtils.verifySignatures;
 import static com.android.server.pm.permission.PermissionsState.PERMISSION_OPERATION_FAILURE;
 import static com.android.server.pm.permission.PermissionsState.PERMISSION_OPERATION_SUCCESS;
 import static com.android.server.pm.permission.PermissionsState.PERMISSION_OPERATION_SUCCESS_GIDS_CHANGED;
@@ -408,7 +417,7 @@
     private static final boolean DEBUG_FILTERS = false;
     public static final boolean DEBUG_PERMISSIONS = false;
     private static final boolean DEBUG_SHARED_LIBRARIES = false;
-    private static final boolean DEBUG_COMPRESSION = Build.IS_DEBUGGABLE;
+    public static final boolean DEBUG_COMPRESSION = Build.IS_DEBUGGABLE;
 
     // Debug output for dexopting. This is shared between PackageManagerService, OtaDexoptService
     // and PackageDexOptimizer. All these classes have their own flag to allow switching a single
@@ -462,9 +471,9 @@
 
     private static final String STATIC_SHARED_LIB_DELIMITER = "_";
     /** Extension of the compressed packages */
-    private final static String COMPRESSED_EXTENSION = ".gz";
+    public final static String COMPRESSED_EXTENSION = ".gz";
     /** Suffix of stub packages on the system partition */
-    private final static String STUB_SUFFIX = "-Stub";
+    public final static String STUB_SUFFIX = "-Stub";
 
     private static final int[] EMPTY_INT_ARRAY = new int[0];
 
@@ -3076,75 +3085,6 @@
         }
     }
 
-    private int decompressFile(File srcFile, File dstFile) throws ErrnoException {
-        if (DEBUG_COMPRESSION) {
-            Slog.i(TAG, "Decompress file"
-                    + "; src: " + srcFile.getAbsolutePath()
-                    + ", dst: " + dstFile.getAbsolutePath());
-        }
-        try (
-                InputStream fileIn = new GZIPInputStream(new FileInputStream(srcFile));
-                OutputStream fileOut = new FileOutputStream(dstFile, false /*append*/);
-        ) {
-            Streams.copy(fileIn, fileOut);
-            Os.chmod(dstFile.getAbsolutePath(), 0644);
-            return PackageManager.INSTALL_SUCCEEDED;
-        } catch (IOException e) {
-            logCriticalInfo(Log.ERROR, "Failed to decompress file"
-                    + "; src: " + srcFile.getAbsolutePath()
-                    + ", dst: " + dstFile.getAbsolutePath());
-        }
-        return PackageManager.INSTALL_FAILED_INTERNAL_ERROR;
-    }
-
-    private File[] getCompressedFiles(String codePath) {
-        final File stubCodePath = new File(codePath);
-        final String stubName = stubCodePath.getName();
-
-        // The layout of a compressed package on a given partition is as follows :
-        //
-        // Compressed artifacts:
-        //
-        // /partition/ModuleName/foo.gz
-        // /partation/ModuleName/bar.gz
-        //
-        // Stub artifact:
-        //
-        // /partition/ModuleName-Stub/ModuleName-Stub.apk
-        //
-        // In other words, stub is on the same partition as the compressed artifacts
-        // and in a directory that's suffixed with "-Stub".
-        int idx = stubName.lastIndexOf(STUB_SUFFIX);
-        if (idx < 0 || (stubName.length() != (idx + STUB_SUFFIX.length()))) {
-            return null;
-        }
-
-        final File stubParentDir = stubCodePath.getParentFile();
-        if (stubParentDir == null) {
-            Slog.e(TAG, "Unable to determine stub parent dir for codePath: " + codePath);
-            return null;
-        }
-
-        final File compressedPath = new File(stubParentDir, stubName.substring(0, idx));
-        final File[] files = compressedPath.listFiles(new FilenameFilter() {
-            @Override
-            public boolean accept(File dir, String name) {
-                return name.toLowerCase().endsWith(COMPRESSED_EXTENSION);
-            }
-        });
-
-        if (DEBUG_COMPRESSION && files != null && files.length > 0) {
-            Slog.i(TAG, "getCompressedFiles[" + codePath + "]: " + Arrays.toString(files));
-        }
-
-        return files;
-    }
-
-    private boolean compressedFileExists(String codePath) {
-        final File[] compressedFiles = getCompressedFiles(codePath);
-        return compressedFiles != null && compressedFiles.length > 0;
-    }
-
     /**
      * Decompresses the given package on the system image onto
      * the /data partition.
@@ -5385,56 +5325,6 @@
     }
 
     /**
-     * Compares two sets of signatures. Returns:
-     * <br />
-     * {@link PackageManager#SIGNATURE_NEITHER_SIGNED}: if both signature sets are null,
-     * <br />
-     * {@link PackageManager#SIGNATURE_FIRST_NOT_SIGNED}: if the first signature set is null,
-     * <br />
-     * {@link PackageManager#SIGNATURE_SECOND_NOT_SIGNED}: if the second signature set is null,
-     * <br />
-     * {@link PackageManager#SIGNATURE_MATCH}: if the two signature sets are identical,
-     * <br />
-     * {@link PackageManager#SIGNATURE_NO_MATCH}: if the two signature sets differ.
-     */
-    public static int compareSignatures(Signature[] s1, Signature[] s2) {
-        if (s1 == null) {
-            return s2 == null
-                    ? PackageManager.SIGNATURE_NEITHER_SIGNED
-                    : PackageManager.SIGNATURE_FIRST_NOT_SIGNED;
-        }
-
-        if (s2 == null) {
-            return PackageManager.SIGNATURE_SECOND_NOT_SIGNED;
-        }
-
-        if (s1.length != s2.length) {
-            return PackageManager.SIGNATURE_NO_MATCH;
-        }
-
-        // Since both signature sets are of size 1, we can compare without HashSets.
-        if (s1.length == 1) {
-            return s1[0].equals(s2[0]) ?
-                    PackageManager.SIGNATURE_MATCH :
-                    PackageManager.SIGNATURE_NO_MATCH;
-        }
-
-        ArraySet<Signature> set1 = new ArraySet<Signature>();
-        for (Signature sig : s1) {
-            set1.add(sig);
-        }
-        ArraySet<Signature> set2 = new ArraySet<Signature>();
-        for (Signature sig : s2) {
-            set2.add(sig);
-        }
-        // Make sure s2 contains all signatures in s1.
-        if (set1.equals(set2)) {
-            return PackageManager.SIGNATURE_MATCH;
-        }
-        return PackageManager.SIGNATURE_NO_MATCH;
-    }
-
-    /**
      * If the database version for this type of package (internal storage or
      * external storage) is less than the version where package signatures
      * were updated, return true.
@@ -5444,76 +5334,11 @@
         return ver.databaseVersion < DatabaseVersion.SIGNATURE_END_ENTITY;
     }
 
-    /**
-     * Used for backward compatibility to make sure any packages with
-     * certificate chains get upgraded to the new style. {@code existingSigs}
-     * will be in the old format (since they were stored on disk from before the
-     * system upgrade) and {@code scannedSigs} will be in the newer format.
-     */
-    private int compareSignaturesCompat(PackageSignatures existingSigs,
-            PackageParser.Package scannedPkg) {
-        if (!isCompatSignatureUpdateNeeded(scannedPkg)) {
-            return PackageManager.SIGNATURE_NO_MATCH;
-        }
-
-        ArraySet<Signature> existingSet = new ArraySet<Signature>();
-        for (Signature sig : existingSigs.mSignatures) {
-            existingSet.add(sig);
-        }
-        ArraySet<Signature> scannedCompatSet = new ArraySet<Signature>();
-        for (Signature sig : scannedPkg.mSignatures) {
-            try {
-                Signature[] chainSignatures = sig.getChainSignatures();
-                for (Signature chainSig : chainSignatures) {
-                    scannedCompatSet.add(chainSig);
-                }
-            } catch (CertificateEncodingException e) {
-                scannedCompatSet.add(sig);
-            }
-        }
-        /*
-         * Make sure the expanded scanned set contains all signatures in the
-         * existing one.
-         */
-        if (scannedCompatSet.equals(existingSet)) {
-            // Migrate the old signatures to the new scheme.
-            existingSigs.assignSignatures(scannedPkg.mSignatures);
-            // The new KeySets will be re-added later in the scanning process.
-            synchronized (mPackages) {
-                mSettings.mKeySetManagerService.removeAppKeySetDataLPw(scannedPkg.packageName);
-            }
-            return PackageManager.SIGNATURE_MATCH;
-        }
-        return PackageManager.SIGNATURE_NO_MATCH;
-    }
-
     private boolean isRecoverSignatureUpdateNeeded(PackageParser.Package scannedPkg) {
         final VersionInfo ver = getSettingsVersionForPackage(scannedPkg);
         return ver.databaseVersion < DatabaseVersion.SIGNATURE_MALFORMED_RECOVER;
     }
 
-    private int compareSignaturesRecover(PackageSignatures existingSigs,
-            PackageParser.Package scannedPkg) {
-        if (!isRecoverSignatureUpdateNeeded(scannedPkg)) {
-            return PackageManager.SIGNATURE_NO_MATCH;
-        }
-
-        String msg = null;
-        try {
-            if (Signature.areEffectiveMatch(existingSigs.mSignatures, scannedPkg.mSignatures)) {
-                logCriticalInfo(Log.INFO, "Recovered effectively matching certificates for "
-                        + scannedPkg.packageName);
-                return PackageManager.SIGNATURE_MATCH;
-            }
-        } catch (CertificateException e) {
-            msg = e.getMessage();
-        }
-
-        logCriticalInfo(Log.INFO,
-                "Failed to recover certificates for " + scannedPkg.packageName + ": " + msg);
-        return PackageManager.SIGNATURE_NO_MATCH;
-    }
-
     @Override
     public List<String> getAllPackages() {
         final int callingUid = Binder.getCallingUid();
@@ -8237,51 +8062,10 @@
         parallelPackageParser.close();
     }
 
-    private static File getSettingsProblemFile() {
-        File dataDir = Environment.getDataDirectory();
-        File systemDir = new File(dataDir, "system");
-        File fname = new File(systemDir, "uiderrors.txt");
-        return fname;
-    }
-
     public static void reportSettingsProblem(int priority, String msg) {
         logCriticalInfo(priority, msg);
     }
 
-    public static void logCriticalInfo(int priority, String msg) {
-        Slog.println(priority, TAG, msg);
-        EventLogTags.writePmCriticalInfo(msg);
-        try {
-            File fname = getSettingsProblemFile();
-            FileOutputStream out = new FileOutputStream(fname, true);
-            PrintWriter pw = new FastPrintWriter(out);
-            SimpleDateFormat formatter = new SimpleDateFormat();
-            String dateString = formatter.format(new Date(System.currentTimeMillis()));
-            pw.println(dateString + ": " + msg);
-            pw.close();
-            FileUtils.setPermissions(
-                    fname.toString(),
-                    FileUtils.S_IRWXU|FileUtils.S_IRWXG|FileUtils.S_IROTH,
-                    -1, -1);
-        } catch (java.io.IOException e) {
-        }
-    }
-
-    private long getLastModifiedTime(PackageParser.Package pkg, File srcFile) {
-        if (srcFile.isDirectory()) {
-            final File baseFile = new File(pkg.baseCodePath);
-            long maxModifiedTime = baseFile.lastModified();
-            if (pkg.splitCodePaths != null) {
-                for (int i = pkg.splitCodePaths.length - 1; i >=0; --i) {
-                    final File splitFile = new File(pkg.splitCodePaths[i]);
-                    maxModifiedTime = Math.max(maxModifiedTime, splitFile.lastModified());
-                }
-            }
-            return maxModifiedTime;
-        }
-        return srcFile.lastModified();
-    }
-
     private void collectCertificatesLI(PackageSetting ps, PackageParser.Package pkg, File srcFile,
             final int policyFlags) throws PackageManagerException {
         // When upgrading from pre-N MR1, verify the package time stamp using the package
@@ -8294,7 +8078,7 @@
                 && !isCompatSignatureUpdateNeeded(pkg)
                 && !isRecoverSignatureUpdateNeeded(pkg)) {
             long mSigningKeySetId = ps.keySetData.getProperSigningKeySet();
-            KeySetManagerService ksms = mSettings.mKeySetManagerService;
+            final KeySetManagerService ksms = mSettings.mKeySetManagerService;
             ArraySet<PublicKey> signingKs;
             synchronized (mPackages) {
                 signingKs = ksms.getPublicKeysFromKeySetLPr(mSigningKeySetId);
@@ -8710,7 +8494,7 @@
         return scannedPkg;
     }
 
-    private void renameStaticSharedLibraryPackage(PackageParser.Package pkg) {
+    private static void renameStaticSharedLibraryPackage(PackageParser.Package pkg) {
         // Derive the new package synthetic package name
         pkg.setPackageName(pkg.packageName + STATIC_SHARED_LIB_DELIMITER
                 + pkg.staticSharedLibVersion);
@@ -8724,49 +8508,6 @@
         return processName;
     }
 
-    private void verifySignaturesLP(PackageSetting pkgSetting, PackageParser.Package pkg)
-            throws PackageManagerException {
-        if (pkgSetting.signatures.mSignatures != null) {
-            // Already existing package. Make sure signatures match
-            boolean match = compareSignatures(pkgSetting.signatures.mSignatures, pkg.mSignatures)
-                    == PackageManager.SIGNATURE_MATCH;
-            if (!match) {
-                match = compareSignaturesCompat(pkgSetting.signatures, pkg)
-                        == PackageManager.SIGNATURE_MATCH;
-            }
-            if (!match) {
-                match = compareSignaturesRecover(pkgSetting.signatures, pkg)
-                        == PackageManager.SIGNATURE_MATCH;
-            }
-            if (!match) {
-                throw new PackageManagerException(INSTALL_FAILED_UPDATE_INCOMPATIBLE, "Package "
-                        + pkg.packageName + " signatures do not match the "
-                        + "previously installed version; ignoring!");
-            }
-        }
-
-        // Check for shared user signatures
-        if (pkgSetting.sharedUser != null && pkgSetting.sharedUser.signatures.mSignatures != null) {
-            // Already existing package. Make sure signatures match
-            boolean match = compareSignatures(pkgSetting.sharedUser.signatures.mSignatures,
-                    pkg.mSignatures) == PackageManager.SIGNATURE_MATCH;
-            if (!match) {
-                match = compareSignaturesCompat(pkgSetting.sharedUser.signatures, pkg)
-                        == PackageManager.SIGNATURE_MATCH;
-            }
-            if (!match) {
-                match = compareSignaturesRecover(pkgSetting.sharedUser.signatures, pkg)
-                        == PackageManager.SIGNATURE_MATCH;
-            }
-            if (!match) {
-                throw new PackageManagerException(INSTALL_FAILED_SHARED_USER_INCOMPATIBLE,
-                        "Package " + pkg.packageName
-                        + " has no signatures that match those in shared user "
-                        + pkgSetting.sharedUser.name + "; ignoring!");
-            }
-        }
-    }
-
     /**
      * Enforces that only the system UID or root's UID can call a method exposed
      * via Binder.
@@ -9737,24 +9478,6 @@
         return res;
     }
 
-    /**
-     * Derive the value of the {@code cpuAbiOverride} based on the provided
-     * value and an optional stored value from the package settings.
-     */
-    private static String deriveAbiOverride(String abiOverride, PackageSetting settings) {
-        String cpuAbiOverride = null;
-
-        if (NativeLibraryHelper.CLEAR_ABI_OVERRIDE.equals(abiOverride)) {
-            cpuAbiOverride = null;
-        } else if (abiOverride != null) {
-            cpuAbiOverride = abiOverride;
-        } else if (settings != null) {
-            cpuAbiOverride = settings.cpuAbiOverrideString;
-        }
-
-        return cpuAbiOverride;
-    }
-
     private PackageParser.Package scanPackageTracedLI(PackageParser.Package pkg,
             final int policyFlags, int scanFlags, long currentTime, @Nullable UserHandle user)
                     throws PackageManagerException {
@@ -10102,8 +9825,9 @@
                 }
             }
 
-            if (shouldCheckUpgradeKeySetLP(signatureCheckPs, scanFlags)) {
-                if (checkUpgradeKeySetLP(signatureCheckPs, pkg)) {
+            final KeySetManagerService ksms = mSettings.mKeySetManagerService;
+            if (ksms.shouldCheckUpgradeKeySetLocked(signatureCheckPs, scanFlags)) {
+                if (ksms.checkUpgradeKeySetLocked(signatureCheckPs, pkg)) {
                     // We just determined the app is signed correctly, so bring
                     // over the latest parsed certs.
                     pkgSetting.signatures.mSignatures = pkg.mSignatures;
@@ -10121,8 +9845,16 @@
                 }
             } else {
                 try {
-                    // SIDE EFFECTS; compareSignaturesCompat() changes KeysetManagerService
-                    verifySignaturesLP(signatureCheckPs, pkg);
+                    final boolean compareCompat = isCompatSignatureUpdateNeeded(pkg);
+                    final boolean compareRecover = isRecoverSignatureUpdateNeeded(pkg);
+                    final boolean compatMatch = verifySignatures(signatureCheckPs, pkg.mSignatures,
+                            compareCompat, compareRecover);
+                    // The new KeySets will be re-added later in the scanning process.
+                    if (compatMatch) {
+                        synchronized (mPackages) {
+                            ksms.removeAppKeySetDataLPw(pkg.packageName);
+                        }
+                    }
                     // We just determined the app is signed correctly, so bring
                     // over the latest parsed certs.
                     pkgSetting.signatures.mSignatures = pkg.mSignatures;
@@ -10410,7 +10142,7 @@
         }
 
         // Make sure we're not adding any bogus keyset info
-        KeySetManagerService ksms = mSettings.mKeySetManagerService;
+        final KeySetManagerService ksms = mSettings.mKeySetManagerService;
         ksms.assertScannedPackageValid(pkg);
 
         synchronized (mPackages) {
@@ -15566,42 +15298,6 @@
         Trace.traceEnd(TRACE_TAG_PACKAGE_MANAGER);
     }
 
-    private boolean shouldCheckUpgradeKeySetLP(PackageSettingBase oldPs, int scanFlags) {
-        // Can't rotate keys during boot or if sharedUser.
-        if (oldPs == null || (scanFlags&SCAN_INITIAL) != 0 || oldPs.isSharedUser()
-                || !oldPs.keySetData.isUsingUpgradeKeySets()) {
-            return false;
-        }
-        // app is using upgradeKeySets; make sure all are valid
-        KeySetManagerService ksms = mSettings.mKeySetManagerService;
-        long[] upgradeKeySets = oldPs.keySetData.getUpgradeKeySets();
-        for (int i = 0; i < upgradeKeySets.length; i++) {
-            if (!ksms.isIdValidKeySetId(upgradeKeySets[i])) {
-                Slog.wtf(TAG, "Package "
-                         + (oldPs.name != null ? oldPs.name : "<null>")
-                         + " contains upgrade-key-set reference to unknown key-set: "
-                         + upgradeKeySets[i]
-                         + " reverting to signatures check.");
-                return false;
-            }
-        }
-        return true;
-    }
-
-    private boolean checkUpgradeKeySetLP(PackageSettingBase oldPS, PackageParser.Package newPkg) {
-        // Upgrade keysets are being used.  Determine if new package has a superset of the
-        // required keys.
-        long[] upgradeKeySets = oldPS.keySetData.getUpgradeKeySets();
-        KeySetManagerService ksms = mSettings.mKeySetManagerService;
-        for (int i = 0; i < upgradeKeySets.length; i++) {
-            Set<PublicKey> upgradeSet = ksms.getPublicKeysFromKeySetLPr(upgradeKeySets[i]);
-            if (upgradeSet != null && newPkg.mSigningKeys.containsAll(upgradeSet)) {
-                return true;
-            }
-        }
-        return false;
-    }
-
     private static void updateDigest(MessageDigest digest, File file) throws IOException {
         try (DigestInputStream digestStream =
                 new DigestInputStream(new FileInputStream(file), digest)) {
@@ -15640,8 +15336,9 @@
             ps = mSettings.mPackages.get(pkgName);
 
             // verify signatures are valid
-            if (shouldCheckUpgradeKeySetLP(ps, scanFlags)) {
-                if (!checkUpgradeKeySetLP(ps, pkg)) {
+            final KeySetManagerService ksms = mSettings.mKeySetManagerService;
+            if (ksms.shouldCheckUpgradeKeySetLocked(ps, scanFlags)) {
+                if (!ksms.checkUpgradeKeySetLocked(ps, pkg)) {
                     res.setError(INSTALL_FAILED_UPDATE_INCOMPATIBLE,
                             "New package not signed by keys specified by upgrade-keysets: "
                                     + pkgName);
@@ -16542,8 +16239,9 @@
                 // Quick sanity check that we're signed correctly if updating;
                 // we'll check this again later when scanning, but we want to
                 // bail early here before tripping over redefined permissions.
-                if (shouldCheckUpgradeKeySetLP(signatureCheckPs, scanFlags)) {
-                    if (!checkUpgradeKeySetLP(signatureCheckPs, pkg)) {
+                final KeySetManagerService ksms = mSettings.mKeySetManagerService;
+                if (ksms.shouldCheckUpgradeKeySetLocked(signatureCheckPs, scanFlags)) {
+                    if (!ksms.checkUpgradeKeySetLocked(signatureCheckPs, pkg)) {
                         res.setError(INSTALL_FAILED_UPDATE_INCOMPATIBLE, "Package "
                                 + pkg.packageName + " upgrade keys do not match the "
                                 + "previously installed version");
@@ -16551,7 +16249,16 @@
                     }
                 } else {
                     try {
-                        verifySignaturesLP(signatureCheckPs, pkg);
+                        final boolean compareCompat = isCompatSignatureUpdateNeeded(pkg);
+                        final boolean compareRecover = isRecoverSignatureUpdateNeeded(pkg);
+                        final boolean compatMatch = verifySignatures(
+                                signatureCheckPs, pkg.mSignatures, compareCompat, compareRecover);
+                        // The new KeySets will be re-added later in the scanning process.
+                        if (compatMatch) {
+                            synchronized (mPackages) {
+                                ksms.removeAppKeySetDataLPw(pkg.packageName);
+                            }
+                        }
                     } catch (PackageManagerException e) {
                         res.setError(e.error, e.getMessage());
                         return;
@@ -16589,10 +16296,11 @@
                     final boolean sigsOk;
                     final String sourcePackageName = bp.getSourcePackageName();
                     final PackageSettingBase sourcePackageSetting = bp.getSourcePackageSetting();
+                    final KeySetManagerService ksms = mSettings.mKeySetManagerService;
                     if (sourcePackageName.equals(pkg.packageName)
-                            && (shouldCheckUpgradeKeySetLP(sourcePackageSetting,
-                                    scanFlags))) {
-                        sigsOk = checkUpgradeKeySetLP(sourcePackageSetting, pkg);
+                            && (ksms.shouldCheckUpgradeKeySetLocked(
+                                    sourcePackageSetting, scanFlags))) {
+                        sigsOk = ksms.checkUpgradeKeySetLocked(sourcePackageSetting, pkg);
                     } else {
                         sigsOk = compareSignatures(sourcePackageSetting.signatures.mSignatures,
                                 pkg.mSignatures) == PackageManager.SIGNATURE_MATCH;
@@ -20938,34 +20646,11 @@
 
                 pw.println();
                 pw.println("Package warning messages:");
-                BufferedReader in = null;
-                String line = null;
-                try {
-                    in = new BufferedReader(new FileReader(getSettingsProblemFile()));
-                    while ((line = in.readLine()) != null) {
-                        if (line.contains("ignored: updated version")) continue;
-                        pw.println(line);
-                    }
-                } catch (IOException ignored) {
-                } finally {
-                    IoUtils.closeQuietly(in);
-                }
+                dumpCriticalInfo(pw, null);
             }
 
             if (checkin && dumpState.isDumping(DumpState.DUMP_MESSAGES)) {
-                BufferedReader in = null;
-                String line = null;
-                try {
-                    in = new BufferedReader(new FileReader(getSettingsProblemFile()));
-                    while ((line = in.readLine()) != null) {
-                        if (line.contains("ignored: updated version")) continue;
-                        pw.print("msg,");
-                        pw.println(line);
-                    }
-                } catch (IOException ignored) {
-                } finally {
-                    IoUtils.closeQuietly(in);
-                }
+                dumpCriticalInfo(pw, "msg,");
             }
         }
 
@@ -21011,26 +20696,11 @@
             dumpFeaturesProto(proto);
             mSettings.dumpPackagesProto(proto);
             mSettings.dumpSharedUsersProto(proto);
-            dumpMessagesProto(proto);
+            dumpCriticalInfo(proto);
         }
         proto.flush();
     }
 
-    private void dumpMessagesProto(ProtoOutputStream proto) {
-        BufferedReader in = null;
-        String line = null;
-        try {
-            in = new BufferedReader(new FileReader(getSettingsProblemFile()));
-            while ((line = in.readLine()) != null) {
-                if (line.contains("ignored: updated version")) continue;
-                proto.write(PackageServiceDumpProto.MESSAGES, line);
-            }
-        } catch (IOException ignored) {
-        } finally {
-            IoUtils.closeQuietly(in);
-        }
-    }
-
     private void dumpFeaturesProto(ProtoOutputStream proto) {
         synchronized (mAvailableFeatures) {
             final int count = mAvailableFeatures.size();
@@ -22439,7 +22109,7 @@
                 Slog.w(TAG, "KeySet requested for filtered package: " + packageName);
                 throw new IllegalArgumentException("Unknown package: " + packageName);
             }
-            KeySetManagerService ksms = mSettings.mKeySetManagerService;
+            final KeySetManagerService ksms = mSettings.mKeySetManagerService;
             return new KeySet(ksms.getKeySetByAliasAndPackageNameLPr(packageName, alias));
         }
     }
@@ -22468,7 +22138,7 @@
                     && Process.SYSTEM_UID != callingUid) {
                 throw new SecurityException("May not access signing KeySet of other apps.");
             }
-            KeySetManagerService ksms = mSettings.mKeySetManagerService;
+            final KeySetManagerService ksms = mSettings.mKeySetManagerService;
             return new KeySet(ksms.getSigningKeySetByPackageNameLPr(packageName));
         }
     }
@@ -22492,7 +22162,7 @@
             }
             IBinder ksh = ks.getToken();
             if (ksh instanceof KeySetHandle) {
-                KeySetManagerService ksms = mSettings.mKeySetManagerService;
+                final KeySetManagerService ksms = mSettings.mKeySetManagerService;
                 return ksms.packageIsSignedByLPr(packageName, (KeySetHandle) ksh);
             }
             return false;
@@ -22518,7 +22188,7 @@
             }
             IBinder ksh = ks.getToken();
             if (ksh instanceof KeySetHandle) {
-                KeySetManagerService ksms = mSettings.mKeySetManagerService;
+                final KeySetManagerService ksms = mSettings.mKeySetManagerService;
                 return ksms.packageIsSignedByExactlyLPr(packageName, (KeySetHandle) ksh);
             }
             return false;
diff --git a/services/core/java/com/android/server/pm/PackageManagerServiceUtils.java b/services/core/java/com/android/server/pm/PackageManagerServiceUtils.java
index 67e06dd..758abd7 100644
--- a/services/core/java/com/android/server/pm/PackageManagerServiceUtils.java
+++ b/services/core/java/com/android/server/pm/PackageManagerServiceUtils.java
@@ -16,42 +16,74 @@
 
 package com.android.server.pm;
 
+import static android.content.pm.PackageManager.INSTALL_FAILED_SHARED_USER_INCOMPATIBLE;
+import static android.content.pm.PackageManager.INSTALL_FAILED_UPDATE_INCOMPATIBLE;
+import static com.android.server.pm.PackageManagerService.COMPRESSED_EXTENSION;
+import static com.android.server.pm.PackageManagerService.DEBUG_COMPRESSION;
+import static com.android.server.pm.PackageManagerService.DEBUG_DEXOPT;
+import static com.android.server.pm.PackageManagerService.STUB_SUFFIX;
+import static com.android.server.pm.PackageManagerService.TAG;
+import static com.android.server.pm.PackageManagerServiceUtils.logCriticalInfo;
+
+import com.android.internal.content.NativeLibraryHelper;
+import com.android.internal.util.ArrayUtils;
+import com.android.internal.util.FastPrintWriter;
+import com.android.server.EventLogTags;
 import com.android.server.pm.dex.DexManager;
 import com.android.server.pm.dex.PackageDexUsage;
 
-import static com.android.server.pm.PackageManagerService.DEBUG_DEXOPT;
-import static com.android.server.pm.PackageManagerService.TAG;
-
-import com.android.internal.util.ArrayUtils;
-
 import android.annotation.NonNull;
 import android.app.AppGlobals;
 import android.content.Intent;
+import android.content.pm.PackageManager;
 import android.content.pm.PackageParser;
 import android.content.pm.ResolveInfo;
+import android.content.pm.Signature;
 import android.os.Build;
 import android.os.Debug;
+import android.os.Environment;
+import android.os.FileUtils;
 import android.os.Process;
 import android.os.RemoteException;
 import android.os.UserHandle;
+import android.service.pm.PackageServiceDumpProto;
 import android.system.ErrnoException;
 import android.system.Os;
 import android.util.ArraySet;
 import android.util.Log;
 import android.util.Slog;
 import android.util.jar.StrictJarFile;
-import dalvik.system.VMRuntime;
-import libcore.io.Libcore;
+import android.util.proto.ProtoOutputStream;
 
+import dalvik.system.VMRuntime;
+
+import libcore.io.IoUtils;
+import libcore.io.Libcore;
+import libcore.io.Streams;
+
+import java.io.BufferedReader;
 import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.FileReader;
+import java.io.FilenameFilter;
 import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.PrintWriter;
+import java.security.cert.CertificateEncodingException;
+import java.security.cert.CertificateException;
+import java.text.SimpleDateFormat;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Collection;
 import java.util.Collections;
+import java.util.Date;
 import java.util.Iterator;
 import java.util.LinkedList;
 import java.util.List;
 import java.util.function.Predicate;
+import java.util.zip.GZIPInputStream;
 import java.util.zip.ZipEntry;
 
 /**
@@ -200,7 +232,7 @@
      *
      * If it doesn't have sufficient information about the package, it return <code>false</code>.
      */
-    static boolean isUnusedSinceTimeInMillis(long firstInstallTime, long currentTimeInMillis,
+    public static boolean isUnusedSinceTimeInMillis(long firstInstallTime, long currentTimeInMillis,
             long thresholdTimeinMillis, PackageDexUsage.PackageUseInfo packageUseInfo,
             long latestPackageUseTimeInMillis, long latestForegroundPackageUseTimeInMillis) {
 
@@ -263,6 +295,21 @@
         return false;
     }
 
+    public static long getLastModifiedTime(PackageParser.Package pkg, File srcFile) {
+        if (srcFile.isDirectory()) {
+            final File baseFile = new File(pkg.baseCodePath);
+            long maxModifiedTime = baseFile.lastModified();
+            if (pkg.splitCodePaths != null) {
+                for (int i = pkg.splitCodePaths.length - 1; i >=0; --i) {
+                    final File splitFile = new File(pkg.splitCodePaths[i]);
+                    maxModifiedTime = Math.max(maxModifiedTime, splitFile.lastModified());
+                }
+            }
+            return maxModifiedTime;
+        }
+        return srcFile.lastModified();
+    }
+
     /**
      * Checks that the archive located at {@code fileName} has uncompressed dex file and so
      * files that can be direclty mapped.
@@ -318,6 +365,57 @@
         }
     }
 
+    private static File getSettingsProblemFile() {
+        File dataDir = Environment.getDataDirectory();
+        File systemDir = new File(dataDir, "system");
+        File fname = new File(systemDir, "uiderrors.txt");
+        return fname;
+    }
+
+    public static void dumpCriticalInfo(ProtoOutputStream proto) {
+        try (BufferedReader in = new BufferedReader(new FileReader(getSettingsProblemFile()))) {
+            String line = null;
+            while ((line = in.readLine()) != null) {
+                if (line.contains("ignored: updated version")) continue;
+                proto.write(PackageServiceDumpProto.MESSAGES, line);
+            }
+        } catch (IOException ignored) {
+        }
+    }
+
+    public static void dumpCriticalInfo(PrintWriter pw, String msg) {
+        try (BufferedReader in = new BufferedReader(new FileReader(getSettingsProblemFile()))) {
+            String line = null;
+            while ((line = in.readLine()) != null) {
+                if (line.contains("ignored: updated version")) continue;
+                if (msg != null) {
+                    pw.print(msg);
+                }
+                pw.println(line);
+            }
+        } catch (IOException ignored) {
+        }
+    }
+
+    public static void logCriticalInfo(int priority, String msg) {
+        Slog.println(priority, TAG, msg);
+        EventLogTags.writePmCriticalInfo(msg);
+        try {
+            File fname = getSettingsProblemFile();
+            FileOutputStream out = new FileOutputStream(fname, true);
+            PrintWriter pw = new FastPrintWriter(out);
+            SimpleDateFormat formatter = new SimpleDateFormat();
+            String dateString = formatter.format(new Date(System.currentTimeMillis()));
+            pw.println(dateString + ": " + msg);
+            pw.close();
+            FileUtils.setPermissions(
+                    fname.toString(),
+                    FileUtils.S_IRWXU|FileUtils.S_IRWXG|FileUtils.S_IROTH,
+                    -1, -1);
+        } catch (java.io.IOException e) {
+        }
+    }
+
     public static void enforceShellRestriction(String restriction, int callingUid, int userHandle) {
         if (callingUid == Process.SHELL_UID) {
             if (userHandle >= 0
@@ -331,4 +429,240 @@
             }
         }
     }
+
+    /**
+     * Derive the value of the {@code cpuAbiOverride} based on the provided
+     * value and an optional stored value from the package settings.
+     */
+    public static String deriveAbiOverride(String abiOverride, PackageSetting settings) {
+        String cpuAbiOverride = null;
+        if (NativeLibraryHelper.CLEAR_ABI_OVERRIDE.equals(abiOverride)) {
+            cpuAbiOverride = null;
+        } else if (abiOverride != null) {
+            cpuAbiOverride = abiOverride;
+        } else if (settings != null) {
+            cpuAbiOverride = settings.cpuAbiOverrideString;
+        }
+        return cpuAbiOverride;
+    }
+
+    /**
+     * Compares two sets of signatures. Returns:
+     * <br />
+     * {@link PackageManager#SIGNATURE_NEITHER_SIGNED}: if both signature sets are null,
+     * <br />
+     * {@link PackageManager#SIGNATURE_FIRST_NOT_SIGNED}: if the first signature set is null,
+     * <br />
+     * {@link PackageManager#SIGNATURE_SECOND_NOT_SIGNED}: if the second signature set is null,
+     * <br />
+     * {@link PackageManager#SIGNATURE_MATCH}: if the two signature sets are identical,
+     * <br />
+     * {@link PackageManager#SIGNATURE_NO_MATCH}: if the two signature sets differ.
+     */
+    public static int compareSignatures(Signature[] s1, Signature[] s2) {
+        if (s1 == null) {
+            return s2 == null
+                    ? PackageManager.SIGNATURE_NEITHER_SIGNED
+                    : PackageManager.SIGNATURE_FIRST_NOT_SIGNED;
+        }
+
+        if (s2 == null) {
+            return PackageManager.SIGNATURE_SECOND_NOT_SIGNED;
+        }
+
+        if (s1.length != s2.length) {
+            return PackageManager.SIGNATURE_NO_MATCH;
+        }
+
+        // Since both signature sets are of size 1, we can compare without HashSets.
+        if (s1.length == 1) {
+            return s1[0].equals(s2[0]) ?
+                    PackageManager.SIGNATURE_MATCH :
+                    PackageManager.SIGNATURE_NO_MATCH;
+        }
+
+        ArraySet<Signature> set1 = new ArraySet<Signature>();
+        for (Signature sig : s1) {
+            set1.add(sig);
+        }
+        ArraySet<Signature> set2 = new ArraySet<Signature>();
+        for (Signature sig : s2) {
+            set2.add(sig);
+        }
+        // Make sure s2 contains all signatures in s1.
+        if (set1.equals(set2)) {
+            return PackageManager.SIGNATURE_MATCH;
+        }
+        return PackageManager.SIGNATURE_NO_MATCH;
+    }
+
+    /**
+     * Used for backward compatibility to make sure any packages with
+     * certificate chains get upgraded to the new style. {@code existingSigs}
+     * will be in the old format (since they were stored on disk from before the
+     * system upgrade) and {@code scannedSigs} will be in the newer format.
+     */
+    private static boolean matchSignaturesCompat(String packageName,
+            PackageSignatures packageSignatures, Signature[] parsedSignatures) {
+        ArraySet<Signature> existingSet = new ArraySet<Signature>();
+        for (Signature sig : packageSignatures.mSignatures) {
+            existingSet.add(sig);
+        }
+        ArraySet<Signature> scannedCompatSet = new ArraySet<Signature>();
+        for (Signature sig : parsedSignatures) {
+            try {
+                Signature[] chainSignatures = sig.getChainSignatures();
+                for (Signature chainSig : chainSignatures) {
+                    scannedCompatSet.add(chainSig);
+                }
+            } catch (CertificateEncodingException e) {
+                scannedCompatSet.add(sig);
+            }
+        }
+        // make sure the expanded scanned set contains all signatures in the existing one
+        if (scannedCompatSet.equals(existingSet)) {
+            // migrate the old signatures to the new scheme
+            packageSignatures.assignSignatures(parsedSignatures);
+            return true;
+        }
+        return false;
+    }
+
+    private static boolean matchSignaturesRecover(String packageName,
+            Signature[] existingSignatures, Signature[] parsedSignatures) {
+        String msg = null;
+        try {
+            if (Signature.areEffectiveMatch(existingSignatures, parsedSignatures)) {
+                logCriticalInfo(Log.INFO,
+                        "Recovered effectively matching certificates for " + packageName);
+                return true;
+            }
+        } catch (CertificateException e) {
+            msg = e.getMessage();
+        }
+        logCriticalInfo(Log.INFO,
+                "Failed to recover certificates for " + packageName + ": " + msg);
+        return false;
+    }
+
+    /**
+     * Verifies that signatures match.
+     * @returns {@code true} if the compat signatures were matched; otherwise, {@code false}.
+     * @throws PackageManagerException if the signatures did not match.
+     */
+    public static boolean verifySignatures(PackageSetting pkgSetting,
+            Signature[] parsedSignatures, boolean compareCompat, boolean compareRecover)
+            throws PackageManagerException {
+        final String packageName = pkgSetting.name;
+        boolean compatMatch = false;
+        if (pkgSetting.signatures.mSignatures != null) {
+            // Already existing package. Make sure signatures match
+            boolean match = compareSignatures(pkgSetting.signatures.mSignatures, parsedSignatures)
+                    == PackageManager.SIGNATURE_MATCH;
+            if (!match && compareCompat) {
+                match = matchSignaturesCompat(packageName, pkgSetting.signatures, parsedSignatures);
+                compatMatch = match;
+            }
+            if (!match && compareRecover) {
+                match = matchSignaturesRecover(
+                        packageName, pkgSetting.signatures.mSignatures, parsedSignatures);
+            }
+            if (!match) {
+                throw new PackageManagerException(INSTALL_FAILED_UPDATE_INCOMPATIBLE,
+                        "Package " + packageName +
+                        " signatures don't match previously installed version; ignoring!");
+            }
+        }
+        // Check for shared user signatures
+        if (pkgSetting.sharedUser != null && pkgSetting.sharedUser.signatures.mSignatures != null) {
+            // Already existing package. Make sure signatures match
+            boolean match = compareSignatures(pkgSetting.sharedUser.signatures.mSignatures,
+                    parsedSignatures) == PackageManager.SIGNATURE_MATCH;
+            if (!match) {
+                match = matchSignaturesCompat(
+                        packageName, pkgSetting.sharedUser.signatures, parsedSignatures);
+            }
+            if (!match && compareCompat) {
+                match = matchSignaturesRecover(
+                        packageName, pkgSetting.sharedUser.signatures.mSignatures, parsedSignatures);
+                compatMatch |= match;
+            }
+            if (!match && compareRecover) {
+                throw new PackageManagerException(INSTALL_FAILED_SHARED_USER_INCOMPATIBLE,
+                        "Package " + packageName
+                        + " has no signatures that match those in shared user "
+                        + pkgSetting.sharedUser.name + "; ignoring!");
+            }
+        }
+        return compatMatch;
+    }
+
+    public static int decompressFile(File srcFile, File dstFile) throws ErrnoException {
+        if (DEBUG_COMPRESSION) {
+            Slog.i(TAG, "Decompress file"
+                    + "; src: " + srcFile.getAbsolutePath()
+                    + ", dst: " + dstFile.getAbsolutePath());
+        }
+        try (
+                InputStream fileIn = new GZIPInputStream(new FileInputStream(srcFile));
+                OutputStream fileOut = new FileOutputStream(dstFile, false /*append*/);
+        ) {
+            Streams.copy(fileIn, fileOut);
+            Os.chmod(dstFile.getAbsolutePath(), 0644);
+            return PackageManager.INSTALL_SUCCEEDED;
+        } catch (IOException e) {
+            logCriticalInfo(Log.ERROR, "Failed to decompress file"
+                    + "; src: " + srcFile.getAbsolutePath()
+                    + ", dst: " + dstFile.getAbsolutePath());
+        }
+        return PackageManager.INSTALL_FAILED_INTERNAL_ERROR;
+    }
+
+    public static File[] getCompressedFiles(String codePath) {
+        final File stubCodePath = new File(codePath);
+        final String stubName = stubCodePath.getName();
+
+        // The layout of a compressed package on a given partition is as follows :
+        //
+        // Compressed artifacts:
+        //
+        // /partition/ModuleName/foo.gz
+        // /partation/ModuleName/bar.gz
+        //
+        // Stub artifact:
+        //
+        // /partition/ModuleName-Stub/ModuleName-Stub.apk
+        //
+        // In other words, stub is on the same partition as the compressed artifacts
+        // and in a directory that's suffixed with "-Stub".
+        int idx = stubName.lastIndexOf(STUB_SUFFIX);
+        if (idx < 0 || (stubName.length() != (idx + STUB_SUFFIX.length()))) {
+            return null;
+        }
+
+        final File stubParentDir = stubCodePath.getParentFile();
+        if (stubParentDir == null) {
+            Slog.e(TAG, "Unable to determine stub parent dir for codePath: " + codePath);
+            return null;
+        }
+
+        final File compressedPath = new File(stubParentDir, stubName.substring(0, idx));
+        final File[] files = compressedPath.listFiles(new FilenameFilter() {
+            @Override
+            public boolean accept(File dir, String name) {
+                return name.toLowerCase().endsWith(COMPRESSED_EXTENSION);
+            }
+        });
+
+        if (DEBUG_COMPRESSION && files != null && files.length > 0) {
+            Slog.i(TAG, "getCompressedFiles[" + codePath + "]: " + Arrays.toString(files));
+        }
+
+        return files;
+    }
+
+    public static boolean compressedFileExists(String codePath) {
+        final File[] compressedFiles = getCompressedFiles(codePath);
+        return compressedFiles != null && compressedFiles.length > 0;
+    }
 }
diff --git a/services/core/java/com/android/server/pm/UserDataPreparer.java b/services/core/java/com/android/server/pm/UserDataPreparer.java
index bfe09b8..96c102b 100644
--- a/services/core/java/com/android/server/pm/UserDataPreparer.java
+++ b/services/core/java/com/android/server/pm/UserDataPreparer.java
@@ -16,6 +16,8 @@
 
 package com.android.server.pm;
 
+import static com.android.server.pm.PackageManagerServiceUtils.logCriticalInfo;
+
 import android.content.Context;
 import android.content.pm.UserInfo;
 import android.os.Environment;
@@ -40,8 +42,6 @@
 import java.util.Objects;
 import java.util.Set;
 
-import static com.android.server.pm.PackageManagerService.logCriticalInfo;
-
 /**
  * Helper class for preparing and destroying user storage
  */
diff --git a/services/core/java/com/android/server/pm/permission/DefaultPermissionGrantPolicy.java b/services/core/java/com/android/server/pm/permission/DefaultPermissionGrantPolicy.java
index 8014acf..c40d1fa 100644
--- a/services/core/java/com/android/server/pm/permission/DefaultPermissionGrantPolicy.java
+++ b/services/core/java/com/android/server/pm/permission/DefaultPermissionGrantPolicy.java
@@ -16,6 +16,8 @@
 
 package com.android.server.pm.permission;
 
+import static com.android.server.pm.PackageManagerServiceUtils.compareSignatures;
+
 import android.Manifest;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
@@ -1110,8 +1112,8 @@
         final String systemPackageName = mServiceInternal.getKnownPackageName(
                 PackageManagerInternal.PACKAGE_SYSTEM, UserHandle.USER_SYSTEM);
         final PackageParser.Package systemPackage = getPackage(systemPackageName);
-        return PackageManagerService.compareSignatures(systemPackage.mSignatures,
-                pkg.mSignatures) == PackageManager.SIGNATURE_MATCH;
+        return compareSignatures(systemPackage.mSignatures, pkg.mSignatures)
+                == PackageManager.SIGNATURE_MATCH;
     }
 
     private void grantDefaultPermissionExceptions(int userId) {
diff --git a/services/core/java/com/android/server/pm/permission/PermissionManagerService.java b/services/core/java/com/android/server/pm/permission/PermissionManagerService.java
index 6d2051f..7d8e206 100644
--- a/services/core/java/com/android/server/pm/permission/PermissionManagerService.java
+++ b/services/core/java/com/android/server/pm/permission/PermissionManagerService.java
@@ -1009,10 +1009,10 @@
                 PackageManagerInternal.PACKAGE_SYSTEM, UserHandle.USER_SYSTEM);
         final PackageParser.Package systemPackage =
                 mPackageManagerInt.getPackage(systemPackageName);
-        boolean allowed = (PackageManagerService.compareSignatures(
+        boolean allowed = (PackageManagerServiceUtils.compareSignatures(
                                 bp.getSourceSignatures(), pkg.mSignatures)
                         == PackageManager.SIGNATURE_MATCH)
-                || (PackageManagerService.compareSignatures(
+                || (PackageManagerServiceUtils.compareSignatures(
                                 systemPackage.mSignatures, pkg.mSignatures)
                         == PackageManager.SIGNATURE_MATCH);
         if (!allowed && (privilegedPermission || oemPermission)) {
diff --git a/services/core/java/com/android/server/policy/BurnInProtectionHelper.java b/services/core/java/com/android/server/policy/BurnInProtectionHelper.java
index 92729dc..6886985 100644
--- a/services/core/java/com/android/server/policy/BurnInProtectionHelper.java
+++ b/services/core/java/com/android/server/policy/BurnInProtectionHelper.java
@@ -253,7 +253,8 @@
     public void onDisplayChanged(int displayId) {
         if (displayId == mDisplay.getDisplayId()) {
             if (mDisplay.getState() == Display.STATE_DOZE
-                    || mDisplay.getState() == Display.STATE_DOZE_SUSPEND) {
+                    || mDisplay.getState() == Display.STATE_DOZE_SUSPEND
+                    || mDisplay.getState() == Display.STATE_ON_SUSPEND) {
                 startBurnInProtection();
             } else {
                 cancelBurnInProtection();
diff --git a/services/core/java/com/android/server/power/PowerManagerService.java b/services/core/java/com/android/server/power/PowerManagerService.java
index 8f23cf8..416a606 100644
--- a/services/core/java/com/android/server/power/PowerManagerService.java
+++ b/services/core/java/com/android/server/power/PowerManagerService.java
@@ -2364,9 +2364,13 @@
 
             if (mDisplayPowerRequest.policy == DisplayPowerRequest.POLICY_DOZE) {
                 mDisplayPowerRequest.dozeScreenState = mDozeScreenStateOverrideFromDreamManager;
-                if (mDisplayPowerRequest.dozeScreenState == Display.STATE_DOZE_SUSPEND
-                        && (mWakeLockSummary & WAKE_LOCK_DRAW) != 0) {
-                    mDisplayPowerRequest.dozeScreenState = Display.STATE_DOZE;
+                if ((mWakeLockSummary & WAKE_LOCK_DRAW) != 0) {
+                    if (mDisplayPowerRequest.dozeScreenState == Display.STATE_DOZE_SUSPEND) {
+                        mDisplayPowerRequest.dozeScreenState = Display.STATE_DOZE;
+                    }
+                    if (mDisplayPowerRequest.dozeScreenState == Display.STATE_ON_SUSPEND) {
+                        mDisplayPowerRequest.dozeScreenState = Display.STATE_ON;
+                    }
                 }
                 mDisplayPowerRequest.dozeScreenBrightness =
                         mDozeScreenBrightnessOverrideFromDreamManager;
@@ -4676,6 +4680,7 @@
                 case Display.STATE_OFF:
                 case Display.STATE_DOZE:
                 case Display.STATE_DOZE_SUSPEND:
+                case Display.STATE_ON_SUSPEND:
                 case Display.STATE_ON:
                 case Display.STATE_VR:
                     break;
diff --git a/services/core/java/com/android/server/wm/DragDropController.java b/services/core/java/com/android/server/wm/DragDropController.java
index 79d46ce..cc4e0f8 100644
--- a/services/core/java/com/android/server/wm/DragDropController.java
+++ b/services/core/java/com/android/server/wm/DragDropController.java
@@ -35,6 +35,7 @@
 import android.view.SurfaceControl;
 import android.view.SurfaceSession;
 import android.view.View;
+import com.android.server.input.InputWindowHandle;
 
 /**
  * Managing drag and drop operations initiated by View#startDragAndDrop.
@@ -49,7 +50,12 @@
     static final int MSG_TEAR_DOWN_DRAG_AND_DROP_INPUT = 2;
     static final int MSG_ANIMATION_END = 3;
 
-    DragState mDragState;
+    /**
+     * Drag state per operation.
+     * The variable is cleared by {@code #onDragStateClosedLocked} which is invoked by DragState
+     * itself, thus the variable can be null after calling DragState's methods.
+     */
+    private DragState mDragState;
 
     private WindowManagerService mService;
     private final Handler mHandler;
@@ -58,11 +64,19 @@
         return mDragState != null;
     }
 
+    InputWindowHandle getInputWindowHandleLocked() {
+        return mDragState.getInputWindowHandle();
+    }
+
     DragDropController(WindowManagerService service, Looper looper) {
         mService = service;
         mHandler = new DragHandler(service, looper);
     }
 
+    void sendDragStartedIfNeededLocked(WindowState window) {
+        mDragState.sendDragStartedIfNeededLocked(window);
+    }
+
     IBinder prepareDrag(SurfaceSession session, int callerPid,
             int callerUid, IWindow window, int flags, int width, int height, Surface outSurface) {
         if (DEBUG_DRAG) {
@@ -158,9 +172,7 @@
             if (!mService.mInputManager.transferTouchFocus(callingWin.mInputChannel,
                     mDragState.getInputChannel())) {
                 Slog.e(TAG_WM, "Unable to transfer touch focus");
-                mDragState.unregister();
-                mDragState.reset();
-                mDragState = null;
+                mDragState.closeLocked();
                 return false;
             }
 
@@ -254,6 +266,30 @@
         }
     }
 
+    /**
+     * Handles motion events.
+     * @param keepHandling Whether if the drag operation is continuing or this is the last motion
+     *          event.
+     * @param newX X coordinate value in dp in the screen coordinate
+     * @param newY Y coordinate value in dp in the screen coordinate
+     */
+    void handleMotionEvent(boolean keepHandling, float newX, float newY) {
+        synchronized (mService.mWindowMap) {
+            if (!dragDropActiveLocked()) {
+                // The drag has ended but the clean-up message has not been processed by
+                // window manager. Drop events that occur after this until window manager
+                // has a chance to clean-up the input handle.
+                return;
+            }
+
+            if (keepHandling) {
+                mDragState.notifyMoveLocked(newX, newY);
+            } else {
+                mDragState.notifyDropLocked(newX, newY);
+            }
+        }
+    }
+
     void dragRecipientEntered(IWindow window) {
         if (DEBUG_DRAG) {
             Slog.d(TAG_WM, "Drag into new candidate view @ " + window.asBinder());
@@ -282,6 +318,17 @@
         mHandler.sendMessageDelayed(msg, DRAG_TIMEOUT_MS);
     }
 
+    /**
+     * Notifies the current drag state is closed.
+     */
+    void onDragStateClosedLocked(DragState dragState) {
+        if (mDragState != dragState) {
+            Slog.wtf(TAG_WM, "Unknown drag state is closed");
+            return;
+        }
+        mDragState = null;
+    }
+
     private class DragHandler extends Handler {
         /**
          * Lock for window manager.
@@ -304,9 +351,7 @@
                     synchronized (mService.mWindowMap) {
                         // !!! TODO: ANR the app that has failed to start the drag in time
                         if (mDragState != null) {
-                            mDragState.unregister();
-                            mDragState.reset();
-                            mDragState = null;
+                            mDragState.closeLocked();
                         }
                     }
                     break;
@@ -346,7 +391,7 @@
                                     "plyaing animation");
                             return;
                         }
-                        mDragState.onAnimationEndLocked();
+                        mDragState.closeLocked();
                     }
                     break;
                 }
diff --git a/services/core/java/com/android/server/wm/DragInputEventReceiver.java b/services/core/java/com/android/server/wm/DragInputEventReceiver.java
index b4bbc90..bee2bac 100644
--- a/services/core/java/com/android/server/wm/DragInputEventReceiver.java
+++ b/services/core/java/com/android/server/wm/DragInputEventReceiver.java
@@ -37,7 +37,6 @@
  * Input receiver for drag and drop
  */
 class DragInputEventReceiver extends InputEventReceiver {
-    private final WindowManagerService mService;
     private final DragDropController mDragDropController;
 
     // Set, if stylus button was down at the start of the drag.
@@ -48,100 +47,63 @@
     // are still being dispatched.
     private boolean mMuteInput = false;
 
-    public DragInputEventReceiver(InputChannel inputChannel, Looper looper,
-            DragDropController controller, WindowManagerService service) {
+    DragInputEventReceiver(InputChannel inputChannel, Looper looper,
+            DragDropController controller) {
         super(inputChannel, looper);
         mDragDropController = controller;
-        mService = service;
     }
 
     @Override
     public void onInputEvent(InputEvent event, int displayId) {
         boolean handled = false;
         try {
-            synchronized (mService.mWindowMap) {
-                if (!mDragDropController.dragDropActiveLocked()) {
-                    // The drag has ended but the clean-up message has not been processed by
-                    // window manager. Drop events that occur after this until window manager
-                    // has a chance to clean-up the input handle.
-                    handled = true;
-                    return;
-                }
-                if (!(event instanceof MotionEvent)
-                        || (event.getSource() & SOURCE_CLASS_POINTER) == 0
-                        || mMuteInput) {
-                    return;
-                }
-                final MotionEvent motionEvent = (MotionEvent) event;
-                boolean endDrag = false;
-                final float newX = motionEvent.getRawX();
-                final float newY = motionEvent.getRawY();
-                final boolean isStylusButtonDown =
-                        (motionEvent.getButtonState() & BUTTON_STYLUS_PRIMARY) != 0;
-
-                if (mIsStartEvent) {
-                    if (isStylusButtonDown) {
-                        // First event and the button was down, check for the button being
-                        // lifted in the future, if that happens we'll drop the item.
-                        mStylusButtonDownAtStart = true;
-                    }
-                    mIsStartEvent = false;
-                }
-
-                switch (motionEvent.getAction()) {
-                    case ACTION_DOWN: {
-                        if (DEBUG_DRAG) Slog.w(TAG_WM, "Unexpected ACTION_DOWN in drag layer");
-                    }
-                    break;
-
-                    case ACTION_MOVE: {
-                        if (mStylusButtonDownAtStart && !isStylusButtonDown) {
-                            if (DEBUG_DRAG) {
-                                Slog.d(TAG_WM, "Button no longer pressed; dropping at "
-                                        + newX + "," + newY);
-                            }
-                            mMuteInput = true;
-                            endDrag = mDragDropController.mDragState
-                                    .notifyDropLocked(newX, newY);
-                        } else {
-                            // move the surface and tell the involved window(s) where we are
-                            mDragDropController.mDragState.notifyMoveLocked(newX, newY);
-                        }
-                    }
-                    break;
-
-                    case ACTION_UP: {
-                        if (DEBUG_DRAG) {
-                            Slog.d(TAG_WM, "Got UP on move channel; dropping at "
-                                    + newX + "," + newY);
-                        }
-                        mMuteInput = true;
-                        endDrag = mDragDropController.mDragState
-                                .notifyDropLocked(newX, newY);
-                    }
-                    break;
-
-                    case ACTION_CANCEL: {
-                        if (DEBUG_DRAG) Slog.d(TAG_WM, "Drag cancelled!");
-                        mMuteInput = true;
-                        endDrag = true;
-                    }
-                    break;
-                }
-
-                if (endDrag) {
-                    if (DEBUG_DRAG)
-                        Slog.d(TAG_WM, "Drag ended; tearing down state");
-                    // tell all the windows that the drag has ended
-                    // endDragLocked will post back to looper to dispose the receiver
-                    // since we still need the receiver for the last finishInputEvent.
-                    mDragDropController.mDragState.endDragLocked();
-                    mStylusButtonDownAtStart = false;
-                    mIsStartEvent = true;
-                }
-
-                handled = true;
+            if (!(event instanceof MotionEvent)
+                    || (event.getSource() & SOURCE_CLASS_POINTER) == 0
+                    || mMuteInput) {
+                return;
             }
+            final MotionEvent motionEvent = (MotionEvent) event;
+            final float newX = motionEvent.getRawX();
+            final float newY = motionEvent.getRawY();
+            final boolean isStylusButtonDown =
+                    (motionEvent.getButtonState() & BUTTON_STYLUS_PRIMARY) != 0;
+
+            if (mIsStartEvent) {
+                // First event and the button was down, check for the button being
+                // lifted in the future, if that happens we'll drop the item.
+                mStylusButtonDownAtStart = isStylusButtonDown;
+                mIsStartEvent = false;
+            }
+
+            switch (motionEvent.getAction()) {
+                case ACTION_DOWN:
+                    if (DEBUG_DRAG) Slog.w(TAG_WM, "Unexpected ACTION_DOWN in drag layer");
+                    return;
+                case ACTION_MOVE:
+                    if (mStylusButtonDownAtStart && !isStylusButtonDown) {
+                        if (DEBUG_DRAG) {
+                            Slog.d(TAG_WM, "Button no longer pressed; dropping at " + newX + ","
+                                    + newY);
+                        }
+                        mMuteInput = true;
+                    }
+                    break;
+                case ACTION_UP:
+                    if (DEBUG_DRAG) {
+                        Slog.d(TAG_WM, "Got UP on move channel; dropping at " + newX + "," + newY);
+                    }
+                    mMuteInput = true;
+                    break;
+                case ACTION_CANCEL:
+                    if (DEBUG_DRAG) Slog.d(TAG_WM, "Drag cancelled!");
+                    mMuteInput = true;
+                    break;
+                default:
+                    return;
+            }
+
+            mDragDropController.handleMotionEvent(!mMuteInput /* keepHandling */, newX, newY);
+            handled = true;
         } catch (Exception e) {
             Slog.e(TAG_WM, "Exception caught by drag handleMotion", e);
         } finally {
diff --git a/services/core/java/com/android/server/wm/DragState.java b/services/core/java/com/android/server/wm/DragState.java
index 861fb44..e81d366 100644
--- a/services/core/java/com/android/server/wm/DragState.java
+++ b/services/core/java/com/android/server/wm/DragState.java
@@ -105,6 +105,11 @@
     WindowState mTargetWindow;
     ArrayList<WindowState> mNotifiedWindows;
     boolean mDragInProgress;
+    /**
+     * Whether if animation is completed. Needs to be volatile to update from the animation thread
+     * without having a WM lock.
+     */
+    volatile boolean mAnimationCompleted = false;
     DisplayContent mDisplayContent;
 
     @Nullable private ValueAnimator mAnimator;
@@ -122,21 +127,79 @@
         mNotifiedWindows = new ArrayList<WindowState>();
     }
 
-    void reset() {
-        if (mAnimator != null) {
+    /**
+     * After calling this, DragDropController#onDragStateClosedLocked is invoked, which causes
+     * DragDropController#mDragState becomes null.
+     */
+    void closeLocked() {
+        // Unregister the input interceptor.
+        if (mInputInterceptor != null) {
+            if (DEBUG_DRAG)
+                Slog.d(TAG_WM, "unregistering drag input channel");
+
+            // Input channel should be disposed on the thread where the input is being handled.
+            mDragDropController.sendHandlerMessage(
+                    MSG_TEAR_DOWN_DRAG_AND_DROP_INPUT, mInputInterceptor);
+            mInputInterceptor = null;
+            mService.mInputMonitor.updateInputWindowsLw(true /*force*/);
+        }
+
+        // Send drag end broadcast if drag start has been sent.
+        if (mDragInProgress) {
+            final int myPid = Process.myPid();
+
+            if (DEBUG_DRAG) {
+                Slog.d(TAG_WM, "broadcasting DRAG_ENDED");
+            }
+            for (WindowState ws : mNotifiedWindows) {
+                float x = 0;
+                float y = 0;
+                if (!mDragResult && (ws.mSession.mPid == mPid)) {
+                    // Report unconsumed drop location back to the app that started the drag.
+                    x = mCurrentX;
+                    y = mCurrentY;
+                }
+                DragEvent evt = DragEvent.obtain(DragEvent.ACTION_DRAG_ENDED,
+                        x, y, null, null, null, null, mDragResult);
+                try {
+                    ws.mClient.dispatchDragEvent(evt);
+                } catch (RemoteException e) {
+                    Slog.w(TAG_WM, "Unable to drag-end window " + ws);
+                }
+                // if the current window is in the same process,
+                // the dispatch has already recycled the event
+                if (myPid != ws.mSession.mPid) {
+                    evt.recycle();
+                }
+            }
+            mNotifiedWindows.clear();
+            mDragInProgress = false;
+        }
+
+        // Take the cursor back if it has been changed.
+        if (isFromSource(InputDevice.SOURCE_MOUSE)) {
+            mService.restorePointerIconLocked(mDisplayContent, mCurrentX, mCurrentY);
+            mTouchSource = 0;
+        }
+
+        // Clear the internal variables.
+        if (mSurfaceControl != null) {
+            mSurfaceControl.destroy();
+            mSurfaceControl = null;
+        }
+        if (mAnimator != null && !mAnimationCompleted) {
             Slog.wtf(TAG_WM,
                     "Unexpectedly destroying mSurfaceControl while animation is running");
         }
-        if (mSurfaceControl != null) {
-            mSurfaceControl.destroy();
-        }
-        mSurfaceControl = null;
         mFlags = 0;
         mLocalWin = null;
         mToken = null;
         mData = null;
         mThumbOffsetX = mThumbOffsetY = 0;
         mNotifiedWindows = null;
+
+        // Notifies the controller that the drag state is closed.
+        mDragDropController.onDragStateClosedLocked(this);
     }
 
     class InputInterceptor {
@@ -151,7 +214,7 @@
             mClientChannel = channels[1];
             mService.mInputManager.registerInputChannel(mServerChannel, null);
             mInputEventReceiver = new DragInputEventReceiver(mClientChannel,
-                    mService.mH.getLooper(), mDragDropController, mService);
+                    mService.mH.getLooper(), mDragDropController);
 
             mDragApplicationHandle = new InputApplicationHandle(null);
             mDragApplicationHandle.name = "drag";
@@ -235,19 +298,6 @@
         }
     }
 
-    void unregister() {
-        if (DEBUG_DRAG) Slog.d(TAG_WM, "unregistering drag input channel");
-        if (mInputInterceptor == null) {
-            Slog.e(TAG_WM, "Unregister of nonexistent drag input channel");
-        } else {
-            // Input channel should be disposed on the thread where the input is being handled.
-            mDragDropController.sendHandlerMessage(
-                    MSG_TEAR_DOWN_DRAG_AND_DROP_INPUT, mInputInterceptor);
-            mInputInterceptor = null;
-            mService.mInputMonitor.updateInputWindowsLw(true /*force*/);
-        }
-    }
-
     int getDragLayerLocked() {
         return mService.mPolicy.getWindowLayerFromTypeLw(WindowManager.LayoutParams.TYPE_DRAG)
                 * WindowManagerService.TYPE_LAYER_MULTIPLIER
@@ -366,46 +416,15 @@
         return false;
     }
 
-    private void broadcastDragEndedLocked() {
-        final int myPid = Process.myPid();
-
-        if (DEBUG_DRAG) {
-            Slog.d(TAG_WM, "broadcasting DRAG_ENDED");
-        }
-        for (WindowState ws : mNotifiedWindows) {
-            float x = 0;
-            float y = 0;
-            if (!mDragResult && (ws.mSession.mPid == mPid)) {
-                // Report unconsumed drop location back to the app that started the drag.
-                x = mCurrentX;
-                y = mCurrentY;
-            }
-            DragEvent evt = DragEvent.obtain(DragEvent.ACTION_DRAG_ENDED,
-                    x, y, null, null, null, null, mDragResult);
-            try {
-                ws.mClient.dispatchDragEvent(evt);
-            } catch (RemoteException e) {
-                Slog.w(TAG_WM, "Unable to drag-end window " + ws);
-            }
-            // if the current window is in the same process,
-            // the dispatch has already recycled the event
-            if (myPid != ws.mSession.mPid) {
-                evt.recycle();
-            }
-        }
-        mNotifiedWindows.clear();
-        mDragInProgress = false;
-    }
-
     void endDragLocked() {
         if (mAnimator != null) {
             return;
         }
         if (!mDragResult) {
             mAnimator = createReturnAnimationLocked();
-            return;  // Will call cleanUpDragLw when the animation is done.
+            return;  // Will call closeLocked() when the animation is done.
         }
-        cleanUpDragLocked();
+        closeLocked();
     }
 
     void cancelDragLocked() {
@@ -414,33 +433,15 @@
         }
         if (!mDragInProgress) {
             // This can happen if an app invokes Session#cancelDragAndDrop before
-            // Session#performDrag. Reset the drag state:
-            // 1. without sending the end broadcast because the start broadcast has not been sent,
-            // and
-            // 2. without playing the cancel animation because H.DRAG_START_TIMEOUT may be sent to
-            //    WindowManagerService, which will cause DragState#reset() while playing the
-            //    cancel animation.
-            reset();
-            mDragDropController.mDragState = null;
+            // Session#performDrag. Reset the drag state without playing the cancel animation
+            // because H.DRAG_START_TIMEOUT may be sent to WindowManagerService, which will cause
+            // DragState#reset() while playing the cancel animation.
+            closeLocked();
             return;
         }
         mAnimator = createCancelAnimationLocked();
     }
 
-    private void cleanUpDragLocked() {
-        broadcastDragEndedLocked();
-        if (isFromSource(InputDevice.SOURCE_MOUSE)) {
-            mService.restorePointerIconLocked(mDisplayContent, mCurrentX, mCurrentY);
-        }
-
-        // stop intercepting input
-        unregister();
-
-        // free our resources and drop all the object references
-        reset();
-        mDragDropController.mDragState = null;
-    }
-
     void notifyMoveLocked(float x, float y) {
         if (mAnimator != null) {
             return;
@@ -507,34 +508,33 @@
         mTargetWindow = touchedWin;
     }
 
-    // Find the drop target and tell it about the data.  Returns 'true' if we can immediately
-    // dispatch the global drag-ended message, 'false' if we need to wait for a
-    // result from the recipient.
-    boolean notifyDropLocked(float x, float y) {
+    /**
+     * Finds the drop target and tells it about the data. If the drop event is not sent to the
+     * target, invokes {@code endDragLocked} immediately.
+     */
+    void notifyDropLocked(float x, float y) {
         if (mAnimator != null) {
-            return false;
+            return;
         }
         mCurrentX = x;
         mCurrentY = y;
 
-        WindowState touchedWin = mDisplayContent.getTouchableWinAtPointLocked(x, y);
+        final WindowState touchedWin = mDisplayContent.getTouchableWinAtPointLocked(x, y);
 
         if (!isWindowNotified(touchedWin)) {
             // "drop" outside a valid window -- no recipient to apply a
             // timeout to, and we can send the drag-ended message immediately.
             mDragResult = false;
-            return true;
+            endDragLocked();
+            return;
         }
 
-        if (DEBUG_DRAG) {
-            Slog.d(TAG_WM, "sending DROP to " + touchedWin);
-        }
+        if (DEBUG_DRAG) Slog.d(TAG_WM, "sending DROP to " + touchedWin);
 
         final int targetUserId = UserHandle.getUserId(touchedWin.getOwningUid());
 
-        DragAndDropPermissionsHandler dragAndDropPermissions = null;
-        if ((mFlags & View.DRAG_FLAG_GLOBAL) != 0 &&
-                (mFlags & DRAG_FLAGS_URI_ACCESS) != 0) {
+        final DragAndDropPermissionsHandler dragAndDropPermissions;
+        if ((mFlags & View.DRAG_FLAG_GLOBAL) != 0 && (mFlags & DRAG_FLAGS_URI_ACCESS) != 0) {
             dragAndDropPermissions = new DragAndDropPermissionsHandler(
                     mData,
                     mUid,
@@ -542,13 +542,15 @@
                     mFlags & DRAG_FLAGS_URI_PERMISSIONS,
                     mSourceUserId,
                     targetUserId);
+        } else {
+            dragAndDropPermissions = null;
         }
         if (mSourceUserId != targetUserId){
             mData.fixUris(mSourceUserId);
         }
         final int myPid = Process.myPid();
         final IBinder token = touchedWin.mClient.asBinder();
-        DragEvent evt = obtainDragEvent(touchedWin, DragEvent.ACTION_DROP, x, y,
+        final DragEvent evt = obtainDragEvent(touchedWin, DragEvent.ACTION_DROP, x, y,
                 null, null, mData, dragAndDropPermissions, false);
         try {
             touchedWin.mClient.dispatchDragEvent(evt);
@@ -557,23 +559,13 @@
             mDragDropController.sendTimeoutMessage(MSG_DRAG_END_TIMEOUT, token);
         } catch (RemoteException e) {
             Slog.w(TAG_WM, "can't send drop notification to win " + touchedWin);
-            return true;
+            endDragLocked();
         } finally {
             if (myPid != touchedWin.mSession.mPid) {
                 evt.recycle();
             }
         }
         mToken = token;
-        return false;
-    }
-
-    void onAnimationEndLocked() {
-        if (mAnimator == null) {
-            Slog.wtf(TAG_WM, "Unexpected null mAnimator");
-            return;
-        }
-        mAnimator = null;
-        cleanUpDragLocked();
     }
 
     private static DragEvent obtainDragEvent(WindowState win, int action,
@@ -677,6 +669,7 @@
 
         @Override
         public void onAnimationEnd(Animator animator) {
+            mAnimationCompleted = true;
             // Updating mDragState requires the WM lock so continues it on the out of
             // AnimationThread.
             mDragDropController.sendHandlerMessage(MSG_ANIMATION_END, null);
diff --git a/services/core/java/com/android/server/wm/InputMonitor.java b/services/core/java/com/android/server/wm/InputMonitor.java
index 40eab45..a766097 100644
--- a/services/core/java/com/android/server/wm/InputMonitor.java
+++ b/services/core/java/com/android/server/wm/InputMonitor.java
@@ -381,7 +381,7 @@
                 Log.d(TAG_WM, "Inserting drag window");
             }
             final InputWindowHandle dragWindowHandle =
-                    mService.mDragDropController.mDragState.getInputWindowHandle();
+                    mService.mDragDropController.getInputWindowHandleLocked();
             if (dragWindowHandle != null) {
                 addInputWindowHandle(dragWindowHandle);
             } else {
@@ -698,7 +698,7 @@
             // If there's a drag in progress and 'child' is a potential drop target,
             // make sure it's been told about the drag
             if (inDrag && isVisible && w.getDisplayContent().isDefaultDisplay) {
-                mService.mDragDropController.mDragState.sendDragStartedIfNeededLocked(w);
+                mService.mDragDropController.sendDragStartedIfNeededLocked(w);
             }
 
             addInputWindowHandle(
diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java
index f71520e..f313f31 100644
--- a/services/core/java/com/android/server/wm/WindowManagerService.java
+++ b/services/core/java/com/android/server/wm/WindowManagerService.java
@@ -7087,7 +7087,7 @@
         }
 
         synchronized (mWindowMap) {
-            if (mDragDropController.mDragState != null) {
+            if (mDragDropController.dragDropActiveLocked()) {
                 // Drag cursor overrides the app cursor.
                 return;
             }
diff --git a/services/tests/servicestests/src/com/android/server/BatteryServiceTest.java b/services/tests/servicestests/src/com/android/server/BatteryServiceTest.java
index daaad7a8..106f9e8 100644
--- a/services/tests/servicestests/src/com/android/server/BatteryServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/BatteryServiceTest.java
@@ -36,12 +36,14 @@
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 import org.mockito.Spy;
+import org.mockito.invocation.InvocationOnMock;
 
 
 public class BatteryServiceTest extends AndroidTestCase {
 
     @Mock IServiceManager mMockedManager;
     @Mock IHealth mMockedHal;
+    @Mock IHealth mMockedHal2;
 
     @Mock BatteryService.HealthServiceWrapper.Callback mCallback;
     @Mock BatteryService.HealthServiceWrapper.IServiceManagerSupplier mManagerSupplier;
@@ -56,6 +58,12 @@
         MockitoAnnotations.initMocks(this);
     }
 
+    @Override
+    public void tearDown() {
+        if (mWrapper != null)
+            mWrapper.getHandlerThread().quitSafely();
+    }
+
     public static <T> ArgumentMatcher<T> isOneOf(Collection<T> collection) {
         return new ArgumentMatcher<T>() {
             @Override public boolean matches(T e) {
@@ -70,42 +78,64 @@
     private void initForInstances(String... instanceNamesArr) throws Exception {
         final Collection<String> instanceNames = Arrays.asList(instanceNamesArr);
         doAnswer((invocation) -> {
-                Slog.e("BatteryServiceTest", "health: onRegistration " + invocation.getArguments()[2]);
-                ((IServiceNotification)invocation.getArguments()[2]).onRegistration(
-                        IHealth.kInterfaceName,
-                        (String)invocation.getArguments()[1],
-                        true /* preexisting */);
+                // technically, preexisting is ignored by
+                // BatteryService.HealthServiceWrapper.Notification, but still call it correctly.
+                sendNotification(invocation, true);
+                sendNotification(invocation, true);
+                sendNotification(invocation, false);
                 return null;
             }).when(mMockedManager).registerForNotifications(
                 eq(IHealth.kInterfaceName),
                 argThat(isOneOf(instanceNames)),
                 any(IServiceNotification.class));
 
-        doReturn(mMockedHal).when(mMockedManager)
-            .get(eq(IHealth.kInterfaceName), argThat(isOneOf(instanceNames)));
-
-        doReturn(IServiceManager.Transport.HWBINDER).when(mMockedManager)
-            .getTransport(eq(IHealth.kInterfaceName), argThat(isOneOf(instanceNames)));
-
         doReturn(mMockedManager).when(mManagerSupplier).get();
-        doReturn(mMockedHal).when(mHealthServiceSupplier)
-            .get(argThat(isOneOf(instanceNames)));
+        doReturn(mMockedHal)        // init calls this
+            .doReturn(mMockedHal)   // notification 1
+            .doReturn(mMockedHal)   // notification 2
+            .doReturn(mMockedHal2)  // notification 3
+            .doThrow(new RuntimeException("Should not call getService for more than 4 times"))
+            .when(mHealthServiceSupplier).get(argThat(isOneOf(instanceNames)));
 
         mWrapper = new BatteryService.HealthServiceWrapper();
     }
 
+    private void waitHandlerThreadFinish() throws Exception {
+        for (int i = 0; i < 5; i++) {
+            if (!mWrapper.getHandlerThread().getThreadHandler().hasMessagesOrCallbacks()) {
+                return;
+            }
+            Thread.sleep(300);
+        }
+        assertFalse(mWrapper.getHandlerThread().getThreadHandler().hasMessagesOrCallbacks());
+    }
+
+    private static void sendNotification(InvocationOnMock invocation, boolean preexisting)
+            throws Exception {
+        ((IServiceNotification)invocation.getArguments()[2]).onRegistration(
+                IHealth.kInterfaceName,
+                (String)invocation.getArguments()[1],
+                preexisting);
+    }
+
     @SmallTest
     public void testWrapPreferVendor() throws Exception {
         initForInstances(VENDOR, HEALTHD);
         mWrapper.init(mCallback, mManagerSupplier, mHealthServiceSupplier);
-        verify(mCallback).onRegistration(same(null), same(mMockedHal), eq(VENDOR));
+        waitHandlerThreadFinish();
+        verify(mCallback, times(1)).onRegistration(same(null), same(mMockedHal), eq(VENDOR));
+        verify(mCallback, never()).onRegistration(same(mMockedHal), same(mMockedHal), anyString());
+        verify(mCallback, times(1)).onRegistration(same(mMockedHal), same(mMockedHal2), eq(VENDOR));
     }
 
     @SmallTest
     public void testUseHealthd() throws Exception {
         initForInstances(HEALTHD);
         mWrapper.init(mCallback, mManagerSupplier, mHealthServiceSupplier);
-        verify(mCallback).onRegistration(same(null), same(mMockedHal), eq(HEALTHD));
+        waitHandlerThreadFinish();
+        verify(mCallback, times(1)).onRegistration(same(null), same(mMockedHal), eq(HEALTHD));
+        verify(mCallback, never()).onRegistration(same(mMockedHal), same(mMockedHal), anyString());
+        verify(mCallback, times(1)).onRegistration(same(mMockedHal), same(mMockedHal2), eq(HEALTHD));
     }
 
     @SmallTest
diff --git a/services/tests/servicestests/src/com/android/server/wm/ScreenDecorWindowTests.java b/services/tests/servicestests/src/com/android/server/wm/ScreenDecorWindowTests.java
index 209fdf1..9c80544 100644
--- a/services/tests/servicestests/src/com/android/server/wm/ScreenDecorWindowTests.java
+++ b/services/tests/servicestests/src/com/android/server/wm/ScreenDecorWindowTests.java
@@ -67,7 +67,8 @@
 // TODO: Test non-Activity windows.
 // TODO: Test secondary display.
 @SmallTest
-@Presubmit
+// TODO(b/68957554)
+//@Presubmit
 @RunWith(AndroidJUnit4.class)
 public class ScreenDecorWindowTests {
 
diff --git a/telephony/java/android/telephony/ims/ImsServiceProxy.java b/telephony/java/android/telephony/ims/ImsServiceProxy.java
index 038e295..31d3db4 100644
--- a/telephony/java/android/telephony/ims/ImsServiceProxy.java
+++ b/telephony/java/android/telephony/ims/ImsServiceProxy.java
@@ -305,19 +305,6 @@
         mStatusCallback = c;
     }
 
-    /**
-     * @return Returns true if the ImsService is ready to take commands, false otherwise. If this
-     * method returns false, it doesn't mean that the Binder connection is not available (use
-     * {@link #isBinderReady()} to check that), but that the ImsService is not accepting commands
-     * at this time.
-     *
-     * For example, for DSDS devices, only one slot can be {@link ImsFeature#STATE_READY} to take
-     * commands at a time, so the other slot must stay at {@link ImsFeature#STATE_NOT_AVAILABLE}.
-     */
-    public boolean isBinderReady() {
-        return isBinderAlive() && getFeatureStatus() == ImsFeature.STATE_READY;
-    }
-
     @Override
     public boolean isBinderAlive() {
         return mIsAvailable && mBinder != null && mBinder.isBinderAlive();
diff --git a/telephony/java/android/telephony/ims/ImsServiceProxyCompat.java b/telephony/java/android/telephony/ims/ImsServiceProxyCompat.java
index bbd5f02..7ec9229 100644
--- a/telephony/java/android/telephony/ims/ImsServiceProxyCompat.java
+++ b/telephony/java/android/telephony/ims/ImsServiceProxyCompat.java
@@ -171,6 +171,19 @@
         return mBinder != null && mBinder.isBinderAlive();
     }
 
+    /**
+     * @return Returns true if the ImsService is ready to take commands, false otherwise. If this
+     * method returns false, it doesn't mean that the Binder connection is not available (use
+     * {@link #isBinderReady()} to check that), but that the ImsService is not accepting commands
+     * at this time.
+     *
+     * For example, for DSDS devices, only one slot can be {@link ImsFeature#STATE_READY} to take
+     * commands at a time, so the other slot must stay at {@link ImsFeature#STATE_NOT_AVAILABLE}.
+     */
+    public boolean isBinderReady() {
+        return isBinderAlive() && getFeatureStatus() == ImsFeature.STATE_READY;
+    }
+
     private IImsService getServiceInterface(IBinder b) {
         return IImsService.Stub.asInterface(b);
     }
diff --git a/test-runner/Android.mk b/test-runner/Android.mk
index 29a95e6..3367aba 100644
--- a/test-runner/Android.mk
+++ b/test-runner/Android.mk
@@ -50,6 +50,9 @@
 
 include $(BUILD_STATIC_JAVA_LIBRARY)
 
+# For unbundled build we'll use the prebuilt jar from prebuilts/sdk.
+ifeq (,$(TARGET_BUILD_APPS)$(filter true,$(TARGET_BUILD_PDK)))
+
 # Generate the stub source files for android.test.runner.stubs
 # ============================================================
 include $(CLEAR_VARS)
@@ -149,6 +152,8 @@
 	@echo Copying removed.txt
 	$(hide) $(ACP) $(ANDROID_TEST_RUNNER_OUTPUT_REMOVED_API_FILE) $(ANDROID_TEST_RUNNER_REMOVED_API_FILE)
 
+endif  # not TARGET_BUILD_APPS not TARGET_BUILD_PDK=true
+
 # Build the android.test.mock library
 # ===================================
 include $(CLEAR_VARS)
@@ -161,6 +166,9 @@
 
 include $(BUILD_JAVA_LIBRARY)
 
+# For unbundled build we'll use the prebuilt jar from prebuilts/sdk.
+ifeq (,$(TARGET_BUILD_APPS)$(filter true,$(TARGET_BUILD_PDK)))
+
 # Generate the stub source files for android.test.mock.stubs
 # ==========================================================
 include $(CLEAR_VARS)
@@ -257,3 +265,5 @@
 
 # additionally, build unit tests in a separate .apk
 include $(call all-makefiles-under,$(LOCAL_PATH))
+
+endif  # not TARGET_BUILD_APPS not TARGET_BUILD_PDK=true
diff --git a/tests/UiBench/AndroidManifest.xml b/tests/UiBench/AndroidManifest.xml
index f2de7db..c6b4a54 100644
--- a/tests/UiBench/AndroidManifest.xml
+++ b/tests/UiBench/AndroidManifest.xml
@@ -92,6 +92,15 @@
             </intent-filter>
         </activity>
         <activity
+            android:name=".TrivialAnimationActivityWideGamut"
+            android:label="General/Trivial Animation (Wide Gamut)"
+            android:colorMode="wideColorGamut">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+                <category android:name="com.android.test.uibench.TEST" />
+            </intent-filter>
+        </activity>
+        <activity
             android:name=".TrivialListActivity"
             android:label="General/Trivial ListView" >
             <intent-filter>
diff --git a/tests/UiBench/src/com/android/test/uibench/TrivialAnimationActivityWideGamut.java b/tests/UiBench/src/com/android/test/uibench/TrivialAnimationActivityWideGamut.java
new file mode 100644
index 0000000..c492753
--- /dev/null
+++ b/tests/UiBench/src/com/android/test/uibench/TrivialAnimationActivityWideGamut.java
@@ -0,0 +1,18 @@
+/*
+ * 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.test.uibench;
+
+public class TrivialAnimationActivityWideGamut extends TrivialAnimationActivity { }
diff --git a/tools/aapt2/Android.bp b/tools/aapt2/Android.bp
index 058504d..ae67f61 100644
--- a/tools/aapt2/Android.bp
+++ b/tools/aapt2/Android.bp
@@ -16,6 +16,7 @@
 
 toolSources = [
     "cmd/Compile.cpp",
+    "cmd/Convert.cpp",
     "cmd/Diff.cpp",
     "cmd/Dump.cpp",
     "cmd/Link.cpp",
diff --git a/tools/aapt2/LoadedApk.cpp b/tools/aapt2/LoadedApk.cpp
index ae32ee9..921d853 100644
--- a/tools/aapt2/LoadedApk.cpp
+++ b/tools/aapt2/LoadedApk.cpp
@@ -21,43 +21,139 @@
 #include "format/Archive.h"
 #include "format/binary/TableFlattener.h"
 #include "format/binary/XmlFlattener.h"
+#include "format/proto/ProtoDeserialize.h"
 #include "io/BigBufferStream.h"
 #include "io/Util.h"
 #include "xml/XmlDom.h"
 
+using ::aapt::io::IFile;
+using ::aapt::io::IFileCollection;
+using ::aapt::xml::XmlResource;
+using ::android::StringPiece;
+using ::std::unique_ptr;
+
 namespace aapt {
 
-using xml::XmlResource;
-
-std::unique_ptr<LoadedApk> LoadedApk::LoadApkFromPath(IAaptContext* context,
-                                                      const android::StringPiece& path) {
+std::unique_ptr<LoadedApk> LoadedApk::LoadApkFromPath(const StringPiece& path, IDiagnostics* diag) {
   Source source(path);
   std::string error;
   std::unique_ptr<io::ZipFileCollection> apk = io::ZipFileCollection::Create(path, &error);
-  if (!apk) {
-    context->GetDiagnostics()->Error(DiagMessage(source) << error);
+  if (apk == nullptr) {
+    diag->Error(DiagMessage(path) << "failed opening zip: " << error);
     return {};
   }
 
-  io::IFile* file = apk->FindFile("resources.arsc");
-  if (!file) {
-    context->GetDiagnostics()->Error(DiagMessage(source) << "no resources.arsc found");
+  if (apk->FindFile("resources.arsc") != nullptr) {
+    return LoadBinaryApkFromFileCollection(source, std::move(apk), diag);
+  } else if (apk->FindFile("resources.pb") != nullptr) {
+    return LoadProtoApkFromFileCollection(source, std::move(apk), diag);
+  }
+  diag->Error(DiagMessage(path) << "no resource table found");
+  return {};
+}
+
+std::unique_ptr<LoadedApk> LoadedApk::LoadProtoApkFromFileCollection(
+    const Source& source, unique_ptr<io::IFileCollection> collection, IDiagnostics* diag) {
+  io::IFile* table_file = collection->FindFile(kProtoResourceTablePath);
+  if (table_file == nullptr) {
+    diag->Error(DiagMessage(source) << "failed to find " << kProtoResourceTablePath);
     return {};
   }
 
-  std::unique_ptr<io::IData> data = file->OpenAsData();
-  if (!data) {
-    context->GetDiagnostics()->Error(DiagMessage(source) << "could not open resources.arsc");
+  std::unique_ptr<io::InputStream> in = table_file->OpenInputStream();
+  if (in == nullptr) {
+    diag->Error(DiagMessage(source) << "failed to open " << kProtoResourceTablePath);
+    return {};
+  }
+
+  pb::ResourceTable pb_table;
+  io::ZeroCopyInputAdaptor adaptor(in.get());
+  if (!pb_table.ParseFromZeroCopyStream(&adaptor)) {
+    diag->Error(DiagMessage(source) << "failed to read " << kProtoResourceTablePath);
+    return {};
+  }
+
+  std::string error;
+  std::unique_ptr<ResourceTable> table = util::make_unique<ResourceTable>();
+  if (!DeserializeTableFromPb(pb_table, collection.get(), table.get(), &error)) {
+    diag->Error(DiagMessage(source)
+                << "failed to deserialize " << kProtoResourceTablePath << ": " << error);
+    return {};
+  }
+
+  io::IFile* manifest_file = collection->FindFile(kAndroidManifestPath);
+  if (manifest_file == nullptr) {
+    diag->Error(DiagMessage(source) << "failed to find " << kAndroidManifestPath);
+    return {};
+  }
+
+  std::unique_ptr<io::InputStream> manifest_in = manifest_file->OpenInputStream();
+  if (manifest_in == nullptr) {
+    diag->Error(DiagMessage(source) << "failed to open " << kAndroidManifestPath);
+    return {};
+  }
+
+  pb::XmlNode pb_node;
+  io::ZeroCopyInputAdaptor manifest_adaptor(manifest_in.get());
+  if (!pb_node.ParseFromZeroCopyStream(&manifest_adaptor)) {
+    diag->Error(DiagMessage(source) << "failed to read proto " << kAndroidManifestPath);
+    return {};
+  }
+
+  std::unique_ptr<xml::XmlResource> manifest = DeserializeXmlResourceFromPb(pb_node, &error);
+  if (manifest == nullptr) {
+    diag->Error(DiagMessage(source)
+                << "failed to deserialize proto " << kAndroidManifestPath << ": " << error);
+    return {};
+  }
+  return util::make_unique<LoadedApk>(source, std::move(collection), std::move(table),
+                                      std::move(manifest));
+}
+
+std::unique_ptr<LoadedApk> LoadedApk::LoadBinaryApkFromFileCollection(
+    const Source& source, unique_ptr<io::IFileCollection> collection, IDiagnostics* diag) {
+  io::IFile* table_file = collection->FindFile(kApkResourceTablePath);
+  if (table_file == nullptr) {
+    diag->Error(DiagMessage(source) << "failed to find " << kApkResourceTablePath);
+
+    return {};
+  }
+
+  std::unique_ptr<io::IData> data = table_file->OpenAsData();
+  if (data == nullptr) {
+    diag->Error(DiagMessage(source) << "failed to open " << kApkResourceTablePath);
     return {};
   }
 
   std::unique_ptr<ResourceTable> table = util::make_unique<ResourceTable>();
-  BinaryResourceParser parser(context, table.get(), source, data->data(), data->size(), apk.get());
+  BinaryResourceParser parser(diag, table.get(), source, data->data(), data->size(),
+                              collection.get());
   if (!parser.Parse()) {
     return {};
   }
 
-  return util::make_unique<LoadedApk>(source, std::move(apk), std::move(table));
+  io::IFile* manifest_file = collection->FindFile(kAndroidManifestPath);
+  if (manifest_file == nullptr) {
+    diag->Error(DiagMessage(source) << "failed to find " << kAndroidManifestPath);
+    return {};
+  }
+
+  std::unique_ptr<io::IData> manifest_data = manifest_file->OpenAsData();
+  if (manifest_data == nullptr) {
+    diag->Error(DiagMessage(source) << "failed to open " << kAndroidManifestPath);
+    return {};
+  }
+
+  std::string error;
+  std::unique_ptr<xml::XmlResource> manifest =
+      xml::Inflate(manifest_data->data(), manifest_data->size(), &error);
+  if (manifest == nullptr) {
+    diag->Error(DiagMessage(source)
+                << "failed to parse binary " << kAndroidManifestPath << ": " << error);
+    return {};
+  }
+  return util::make_unique<LoadedApk>(source, std::move(collection), std::move(table),
+                                      std::move(manifest));
 }
 
 bool LoadedApk::WriteToArchive(IAaptContext* context, const TableFlattenerOptions& options,
@@ -148,26 +244,4 @@
   return true;
 }
 
-std::unique_ptr<xml::XmlResource> LoadedApk::InflateManifest(IAaptContext* context) {
-  IDiagnostics* diag = context->GetDiagnostics();
-
-  io::IFile* manifest_file = GetFileCollection()->FindFile("AndroidManifest.xml");
-  if (manifest_file == nullptr) {
-    diag->Error(DiagMessage(source_) << "no AndroidManifest.xml found");
-    return {};
-  }
-
-  std::unique_ptr<io::IData> manifest_data = manifest_file->OpenAsData();
-  if (manifest_data == nullptr) {
-    diag->Error(DiagMessage(manifest_file->GetSource()) << "could not open AndroidManifest.xml");
-    return {};
-  }
-
-  std::unique_ptr<xml::XmlResource> manifest =
-      xml::Inflate(manifest_data->data(), manifest_data->size(), diag, manifest_file->GetSource());
-  if (manifest == nullptr) {
-    diag->Error(DiagMessage() << "failed to read binary AndroidManifest.xml");
-  }
-  return manifest;
-}
 }  // namespace aapt
diff --git a/tools/aapt2/LoadedApk.h b/tools/aapt2/LoadedApk.h
index d2dd5cf..ef97de3 100644
--- a/tools/aapt2/LoadedApk.h
+++ b/tools/aapt2/LoadedApk.h
@@ -29,20 +29,41 @@
 
 namespace aapt {
 
+constexpr static const char kApkResourceTablePath[] = "resources.arsc";
+constexpr static const char kProtoResourceTablePath[] = "resources.pb";
+constexpr static const char kAndroidManifestPath[] = "AndroidManifest.xml";
+
 // Info about an APK loaded in memory.
 class LoadedApk {
  public:
-  LoadedApk(
-      const Source& source,
-      std::unique_ptr<io::IFileCollection> apk,
-      std::unique_ptr<ResourceTable> table)
-      : source_(source), apk_(std::move(apk)), table_(std::move(table)) {}
-  virtual ~LoadedApk() = default;
+  // Loads both binary and proto APKs from disk.
+  static std::unique_ptr<LoadedApk> LoadApkFromPath(const ::android::StringPiece& path,
+                                                    IDiagnostics* diag);
+
+  // Loads a proto APK from the given file collection.
+  static std::unique_ptr<LoadedApk> LoadProtoApkFromFileCollection(
+      const Source& source, std::unique_ptr<io::IFileCollection> collection, IDiagnostics* diag);
+
+  // Loads a binary APK from the given file collection.
+  static std::unique_ptr<LoadedApk> LoadBinaryApkFromFileCollection(
+      const Source& source, std::unique_ptr<io::IFileCollection> collection, IDiagnostics* diag);
+
+  LoadedApk(const Source& source, std::unique_ptr<io::IFileCollection> apk,
+            std::unique_ptr<ResourceTable> table, std::unique_ptr<xml::XmlResource> manifest)
+      : source_(source),
+        apk_(std::move(apk)),
+        table_(std::move(table)),
+        manifest_(std::move(manifest)) {
+  }
 
   io::IFileCollection* GetFileCollection() {
     return apk_.get();
   }
 
+  const ResourceTable* GetResourceTable() const {
+    return table_.get();
+  }
+
   ResourceTable* GetResourceTable() {
     return table_.get();
   }
@@ -51,6 +72,10 @@
     return source_;
   }
 
+  const xml::XmlResource* GetManifest() const {
+    return manifest_.get();
+  }
+
   /**
    * Writes the APK on disk at the given path, while also removing the resource
    * files that are not referenced in the resource table.
@@ -71,11 +96,6 @@
                               const TableFlattenerOptions& options, FilterChain* filters,
                               IArchiveWriter* writer, xml::XmlResource* manifest = nullptr);
 
-  /** Inflates the AndroidManifest.xml file from the APK. */
-  std::unique_ptr<xml::XmlResource> InflateManifest(IAaptContext* context);
-
-  static std::unique_ptr<LoadedApk> LoadApkFromPath(IAaptContext* context,
-                                                    const android::StringPiece& path);
 
  private:
   DISALLOW_COPY_AND_ASSIGN(LoadedApk);
@@ -83,6 +103,7 @@
   Source source_;
   std::unique_ptr<io::IFileCollection> apk_;
   std::unique_ptr<ResourceTable> table_;
+  std::unique_ptr<xml::XmlResource> manifest_;
 };
 
 }  // namespace aapt
diff --git a/tools/aapt2/Main.cpp b/tools/aapt2/Main.cpp
index 36ab30c..808b29c 100644
--- a/tools/aapt2/Main.cpp
+++ b/tools/aapt2/Main.cpp
@@ -50,7 +50,7 @@
 }
 
 static void PrintUsage() {
-  std::cerr << "\nusage: aapt2 [compile|link|dump|diff|optimize|version] ..." << std::endl;
+  std::cerr << "\nusage: aapt2 [compile|link|dump|diff|optimize|convert|version] ..." << std::endl;
 }
 
 extern int Compile(const std::vector<StringPiece>& args, IDiagnostics* diagnostics);
@@ -58,6 +58,7 @@
 extern int Dump(const std::vector<StringPiece>& args);
 extern int Diff(const std::vector<StringPiece>& args);
 extern int Optimize(const std::vector<StringPiece>& args);
+extern int Convert(const std::vector<StringPiece>& args);
 
 static int ExecuteCommand(const StringPiece& command, const std::vector<StringPiece>& args,
                           IDiagnostics* diagnostics) {
@@ -71,6 +72,8 @@
     return Diff(args);
   } else if (command == "optimize") {
     return Optimize(args);
+  } else if (command == "convert") {
+    return Convert(args);
   } else if (command == "version") {
     PrintVersion();
     return 0;
diff --git a/tools/aapt2/cmd/Convert.cpp b/tools/aapt2/cmd/Convert.cpp
new file mode 100644
index 0000000..89ae9e8
--- /dev/null
+++ b/tools/aapt2/cmd/Convert.cpp
@@ -0,0 +1,228 @@
+/*
+ * 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.
+ */
+
+#include <vector>
+
+#include "android-base/macros.h"
+#include "androidfw/StringPiece.h"
+
+#include "Flags.h"
+#include "LoadedApk.h"
+#include "ValueVisitor.h"
+#include "cmd/Util.h"
+#include "format/binary/TableFlattener.h"
+#include "format/binary/XmlFlattener.h"
+#include "format/proto/ProtoDeserialize.h"
+#include "io/BigBufferStream.h"
+#include "io/Util.h"
+#include "process/IResourceTableConsumer.h"
+#include "process/SymbolTable.h"
+
+using ::android::StringPiece;
+using ::std::unique_ptr;
+using ::std::vector;
+
+namespace aapt {
+
+static bool FlattenXml(IAaptContext* context, const xml::XmlResource& xml,
+                       const std::string& entry_path, bool utf16, IArchiveWriter* writer) {
+  BigBuffer buffer(4096);
+  XmlFlattenerOptions options = {};
+  options.use_utf16 = utf16;
+  XmlFlattener flattener(&buffer, options);
+  if (!flattener.Consume(context, &xml)) {
+    return false;
+  }
+  io::BigBufferInputStream input_stream(&buffer);
+  return io::CopyInputStreamToArchive(context, &input_stream, entry_path, ArchiveEntry::kCompress,
+                                      writer);
+}
+
+bool ConvertProtoApkToBinaryApk(IAaptContext* context, unique_ptr<LoadedApk> apk,
+                                const TableFlattenerOptions& options, IArchiveWriter* writer) {
+  if (!FlattenXml(context, *apk->GetManifest(), kAndroidManifestPath, true /*utf16*/, writer)) {
+    return false;
+  }
+
+  BigBuffer buffer(4096);
+  TableFlattener table_flattener(options, &buffer);
+  if (!table_flattener.Consume(context, apk->GetResourceTable())) {
+    return false;
+  }
+
+  io::BigBufferInputStream input_stream(&buffer);
+  if (!io::CopyInputStreamToArchive(context, &input_stream, kApkResourceTablePath,
+                                    ArchiveEntry::kAlign, writer)) {
+    return false;
+  }
+
+  for (const auto& package : apk->GetResourceTable()->packages) {
+    for (const auto& type : package->types) {
+      for (const auto& entry : type->entries) {
+        for (const auto& config_value : entry->values) {
+          const FileReference* file = ValueCast<FileReference>(config_value->value.get());
+          if (file != nullptr) {
+            if (file->file == nullptr) {
+              context->GetDiagnostics()->Warn(DiagMessage(apk->GetSource())
+                                              << "no file associated with " << *file);
+              return false;
+            }
+
+            if (file->type == ResourceFile::Type::kProtoXml) {
+              unique_ptr<io::InputStream> in = file->file->OpenInputStream();
+              if (in == nullptr) {
+                context->GetDiagnostics()->Error(DiagMessage(apk->GetSource())
+                                                 << "failed to open file " << *file->path);
+                return false;
+              }
+
+              pb::XmlNode pb_node;
+              io::ZeroCopyInputAdaptor adaptor(in.get());
+              if (!pb_node.ParseFromZeroCopyStream(&adaptor)) {
+                context->GetDiagnostics()->Error(DiagMessage(apk->GetSource())
+                                                 << "failed to parse proto XML " << *file->path);
+                return false;
+              }
+
+              std::string error;
+              unique_ptr<xml::XmlResource> xml = DeserializeXmlResourceFromPb(pb_node, &error);
+              if (xml == nullptr) {
+                context->GetDiagnostics()->Error(DiagMessage(apk->GetSource())
+                                                 << "failed to deserialize proto XML "
+                                                 << *file->path << ": " << error);
+                return false;
+              }
+
+              if (!FlattenXml(context, *xml, *file->path, false /*utf16*/, writer)) {
+                context->GetDiagnostics()->Error(DiagMessage(apk->GetSource())
+                                                 << "failed to serialize XML " << *file->path);
+                return false;
+              }
+            } else {
+              if (!io::CopyFileToArchive(context, file->file, *file->path,
+                                         file->file->WasCompressed() ? ArchiveEntry::kCompress : 0u,
+                                         writer)) {
+                context->GetDiagnostics()->Error(DiagMessage(apk->GetSource())
+                                                 << "failed to copy file " << *file->path);
+                return false;
+              }
+            }
+
+          } // file
+        } // config_value
+      } // entry
+    } // type
+  } // package
+  return true;
+}
+
+class Context : public IAaptContext {
+ public:
+  Context() : mangler_({}), symbols_(&mangler_) {
+  }
+
+  PackageType GetPackageType() override {
+    return PackageType::kApp;
+  }
+
+  SymbolTable* GetExternalSymbols() override {
+    return &symbols_;
+  }
+
+  IDiagnostics* GetDiagnostics() override {
+    return &diag_;
+  }
+
+  const std::string& GetCompilationPackage() override {
+    return package_;
+  }
+
+  uint8_t GetPackageId() override {
+    // Nothing should call this.
+    UNIMPLEMENTED(FATAL) << "PackageID should not be necessary";
+    return 0;
+  }
+
+  NameMangler* GetNameMangler() override {
+    UNIMPLEMENTED(FATAL);
+    return nullptr;
+  }
+
+  bool IsVerbose() override {
+    return verbose_;
+  }
+
+  int GetMinSdkVersion() override {
+    return 0u;
+  }
+
+  bool verbose_ = false;
+  std::string package_;
+
+ private:
+  DISALLOW_COPY_AND_ASSIGN(Context);
+
+  NameMangler mangler_;
+  SymbolTable symbols_;
+  StdErrDiagnostics diag_;
+};
+
+int Convert(const vector<StringPiece>& args) {
+  Context context;
+  std::string output_path;
+  TableFlattenerOptions options;
+  Flags flags =
+      Flags()
+          .RequiredFlag("-o", "Output path", &output_path)
+          .OptionalSwitch("--enable-sparse-encoding",
+                          "Enables encoding sparse entries using a binary search tree.\n"
+                          "This decreases APK size at the cost of resource retrieval performance.",
+                          &options.use_sparse_entries)
+          .OptionalSwitch("-v", "Enables verbose logging", &context.verbose_);
+  if (!flags.Parse("aapt2 convert", args, &std::cerr)) {
+    return 1;
+  }
+
+  if (flags.GetArgs().size() != 1) {
+    std::cerr << "must supply a single proto APK\n";
+    flags.Usage("aapt2 convert", &std::cerr);
+    return 1;
+  }
+
+  const StringPiece& path = flags.GetArgs()[0];
+  unique_ptr<LoadedApk> apk = LoadedApk::LoadApkFromPath(path, context.GetDiagnostics());
+  if (apk == nullptr) {
+    context.GetDiagnostics()->Error(DiagMessage(path) << "failed to load APK");
+    return 1;
+  }
+
+  Maybe<AppInfo> app_info =
+      ExtractAppInfoFromBinaryManifest(*apk->GetManifest(), context.GetDiagnostics());
+  if (!app_info) {
+    return 1;
+  }
+
+  context.package_ = app_info.value().package;
+
+  unique_ptr<IArchiveWriter> writer =
+      CreateZipFileArchiveWriter(context.GetDiagnostics(), output_path);
+  if (writer == nullptr) {
+    return 1;
+  }
+  return ConvertProtoApkToBinaryApk(&context, std::move(apk), options, writer.get()) ? 0 : 1;
+}
+
+}  // namespace aapt
diff --git a/tools/aapt2/cmd/Diff.cpp b/tools/aapt2/cmd/Diff.cpp
index 625c47c..fc1f1d6 100644
--- a/tools/aapt2/cmd/Diff.cpp
+++ b/tools/aapt2/cmd/Diff.cpp
@@ -357,8 +357,9 @@
     return 1;
   }
 
-  std::unique_ptr<LoadedApk> apk_a = LoadedApk::LoadApkFromPath(&context, flags.GetArgs()[0]);
-  std::unique_ptr<LoadedApk> apk_b = LoadedApk::LoadApkFromPath(&context, flags.GetArgs()[1]);
+  IDiagnostics* diag = context.GetDiagnostics();
+  std::unique_ptr<LoadedApk> apk_a = LoadedApk::LoadApkFromPath(flags.GetArgs()[0], diag);
+  std::unique_ptr<LoadedApk> apk_b = LoadedApk::LoadApkFromPath(flags.GetArgs()[1], diag);
   if (!apk_a || !apk_b) {
     return 1;
   }
diff --git a/tools/aapt2/cmd/Dump.cpp b/tools/aapt2/cmd/Dump.cpp
index 090c3fb..bc8f1dc 100644
--- a/tools/aapt2/cmd/Dump.cpp
+++ b/tools/aapt2/cmd/Dump.cpp
@@ -78,7 +78,7 @@
         return false;
       }
 
-      if (!DeserializeTableFromPb(pb_table, &table, &err)) {
+      if (!DeserializeTableFromPb(pb_table, zip.get(), &table, &err)) {
         context->GetDiagnostics()->Error(DiagMessage(file_path)
                                          << "failed to parse table: " << err);
         return false;
@@ -90,7 +90,8 @@
         return false;
       }
 
-      BinaryResourceParser parser(context, &table, Source(file_path), data->data(), data->size());
+      BinaryResourceParser parser(context->GetDiagnostics(), &table, Source(file_path),
+                                  data->data(), data->size());
       if (!parser.Parse()) {
         return false;
       }
@@ -129,7 +130,7 @@
 
       ResourceTable table;
       err.clear();
-      if (!DeserializeTableFromPb(pb_table, &table, &err)) {
+      if (!DeserializeTableFromPb(pb_table, nullptr /*files*/, &table, &err)) {
         context->GetDiagnostics()->Error(DiagMessage(file_path)
                                          << "failed to parse table: " << err);
         continue;
diff --git a/tools/aapt2/cmd/Link.cpp b/tools/aapt2/cmd/Link.cpp
index e0dae1b..b372923 100644
--- a/tools/aapt2/cmd/Link.cpp
+++ b/tools/aapt2/cmd/Link.cpp
@@ -29,6 +29,7 @@
 #include "AppInfo.h"
 #include "Debug.h"
 #include "Flags.h"
+#include "LoadedApk.h"
 #include "Locale.h"
 #include "NameMangler.h"
 #include "ResourceUtils.h"
@@ -39,7 +40,6 @@
 #include "filter/ConfigFilter.h"
 #include "format/Archive.h"
 #include "format/Container.h"
-#include "format/binary/BinaryResourceParser.h"
 #include "format/binary/TableFlattener.h"
 #include "format/binary/XmlFlattener.h"
 #include "format/proto/ProtoDeserialize.h"
@@ -71,9 +71,6 @@
 
 namespace aapt {
 
-constexpr static const char kApkResourceTablePath[] = "resources.arsc";
-constexpr static const char kProtoResourceTablePath[] = "resources.pb";
-
 enum class OutputFormat {
   kApk,
   kProto,
@@ -298,23 +295,6 @@
   return false;
 }
 
-static std::unique_ptr<ResourceTable> LoadTableFromPb(const Source& source, const void* data,
-                                                      size_t len, IDiagnostics* diag) {
-  pb::ResourceTable pb_table;
-  if (!pb_table.ParseFromArray(data, len)) {
-    diag->Error(DiagMessage(source) << "invalid compiled table");
-    return {};
-  }
-
-  std::unique_ptr<ResourceTable> table = util::make_unique<ResourceTable>();
-  std::string error;
-  if (!DeserializeTableFromPb(pb_table, table.get(), &error)) {
-    diag->Error(DiagMessage(source) << "invalid compiled table: " << error);
-    return {};
-  }
-  return table;
-}
-
 // Inflates an XML file from the source path.
 static std::unique_ptr<xml::XmlResource> LoadXml(const std::string& path, IDiagnostics* diag) {
   FileInputStream fin(path);
@@ -587,7 +567,7 @@
               pb::XmlNode pb_xml_node;
               if (!pb_xml_node.ParseFromArray(data->data(), static_cast<int>(data->size()))) {
                 context_->GetDiagnostics()->Error(DiagMessage(file->GetSource())
-                                                  << "failed to parse proto xml");
+                                                  << "failed to parse proto XML");
                 return false;
               }
 
@@ -595,13 +575,15 @@
               file_op.xml_to_flatten = DeserializeXmlResourceFromPb(pb_xml_node, &error);
               if (file_op.xml_to_flatten == nullptr) {
                 context_->GetDiagnostics()->Error(DiagMessage(file->GetSource())
-                                                  << "failed to deserialize proto xml: " << error);
+                                                  << "failed to deserialize proto XML: " << error);
                 return false;
               }
             } else {
-              file_op.xml_to_flatten = xml::Inflate(data->data(), data->size(),
-                                                    context_->GetDiagnostics(), file->GetSource());
+              std::string error_str;
+              file_op.xml_to_flatten = xml::Inflate(data->data(), data->size(), &error_str);
               if (file_op.xml_to_flatten == nullptr) {
+                context_->GetDiagnostics()->Error(DiagMessage(file->GetSource())
+                                                  << "failed to parse binary XML: " << error_str);
                 return false;
               }
             }
@@ -748,22 +730,29 @@
         file_collection_(util::make_unique<io::FileCollection>()) {
   }
 
-  /**
-   * Creates a SymbolTable that loads symbols from the various APKs and caches
-   * the results for faster lookup.
-   */
+  // Creates a SymbolTable that loads symbols from the various APKs.
   bool LoadSymbolsFromIncludePaths() {
-    std::unique_ptr<AssetManagerSymbolSource> asset_source =
-        util::make_unique<AssetManagerSymbolSource>();
+    auto asset_source = util::make_unique<AssetManagerSymbolSource>();
     for (const std::string& path : options_.include_paths) {
       if (context_->IsVerbose()) {
         context_->GetDiagnostics()->Note(DiagMessage() << "including " << path);
       }
 
-      // First try to load the file as a static lib.
-      std::string error_str;
-      std::unique_ptr<ResourceTable> include_static = LoadStaticLibrary(path, &error_str);
-      if (include_static) {
+      std::string error;
+      auto zip_collection = io::ZipFileCollection::Create(path, &error);
+      if (zip_collection == nullptr) {
+        context_->GetDiagnostics()->Error(DiagMessage() << "failed to open APK: " << error);
+        return false;
+      }
+
+      if (zip_collection->FindFile(kProtoResourceTablePath) != nullptr) {
+        // Load this as a static library include.
+        std::unique_ptr<LoadedApk> static_apk = LoadedApk::LoadProtoApkFromFileCollection(
+            Source(path), std::move(zip_collection), context_->GetDiagnostics());
+        if (static_apk == nullptr) {
+          return false;
+        }
+
         if (context_->GetPackageType() != PackageType::kStaticLib) {
           // Can't include static libraries when not building a static library (they have no IDs
           // assigned).
@@ -772,13 +761,15 @@
           return false;
         }
 
-        // If we are using --no-static-lib-packages, we need to rename the
-        // package of this table to our compilation package.
+        ResourceTable* table = static_apk->GetResourceTable();
+
+        // If we are using --no-static-lib-packages, we need to rename the package of this table to
+        // our compilation package.
         if (options_.no_static_lib_packages) {
           // Since package names can differ, and multiple packages can exist in a ResourceTable,
           // we place the requirement that all static libraries are built with the package
           // ID 0x7f. So if one is not found, this is an error.
-          if (ResourceTablePackage* pkg = include_static->FindPackageById(kAppPackageId)) {
+          if (ResourceTablePackage* pkg = table->FindPackageById(kAppPackageId)) {
             pkg->name = context_->GetCompilationPackage();
           } else {
             context_->GetDiagnostics()->Error(DiagMessage(path)
@@ -788,19 +779,14 @@
         }
 
         context_->GetExternalSymbols()->AppendSource(
-            util::make_unique<ResourceTableSymbolSource>(include_static.get()));
-
-        static_table_includes_.push_back(std::move(include_static));
-
-      } else if (!error_str.empty()) {
-        // We had an error with reading, so fail.
-        context_->GetDiagnostics()->Error(DiagMessage(path) << error_str);
-        return false;
-      }
-
-      if (!asset_source->AddAssetPath(path)) {
-        context_->GetDiagnostics()->Error(DiagMessage(path) << "failed to load include path");
-        return false;
+            util::make_unique<ResourceTableSymbolSource>(table));
+        static_library_includes_.push_back(std::move(static_apk));
+      } else {
+        if (!asset_source->AddAssetPath(path)) {
+          context_->GetDiagnostics()->Error(DiagMessage()
+                                            << "failed to load include path " << path);
+          return false;
+        }
       }
     }
 
@@ -1194,46 +1180,18 @@
     return true;
   }
 
-  std::unique_ptr<ResourceTable> LoadStaticLibrary(const std::string& input,
-                                                   std::string* out_error) {
-    std::unique_ptr<io::ZipFileCollection> collection =
-        io::ZipFileCollection::Create(input, out_error);
-    if (!collection) {
-      return {};
-    }
-    return LoadTablePbFromCollection(collection.get());
-  }
-
-  std::unique_ptr<ResourceTable> LoadTablePbFromCollection(io::IFileCollection* collection) {
-    io::IFile* file = collection->FindFile(kProtoResourceTablePath);
-    if (!file) {
-      return {};
-    }
-
-    std::unique_ptr<io::IData> data = file->OpenAsData();
-    return LoadTableFromPb(file->GetSource(), data->data(), data->size(),
-                           context_->GetDiagnostics());
-  }
-
   bool MergeStaticLibrary(const std::string& input, bool override) {
     if (context_->IsVerbose()) {
       context_->GetDiagnostics()->Note(DiagMessage() << "merging static library " << input);
     }
 
-    std::string error_str;
-    std::unique_ptr<io::ZipFileCollection> collection =
-        io::ZipFileCollection::Create(input, &error_str);
-    if (!collection) {
-      context_->GetDiagnostics()->Error(DiagMessage(input) << error_str);
-      return false;
-    }
-
-    std::unique_ptr<ResourceTable> table = LoadTablePbFromCollection(collection.get());
-    if (!table) {
+    std::unique_ptr<LoadedApk> apk = LoadedApk::LoadApkFromPath(input, context_->GetDiagnostics());
+    if (apk == nullptr) {
       context_->GetDiagnostics()->Error(DiagMessage(input) << "invalid static library");
       return false;
     }
 
+    ResourceTable* table = apk->GetResourceTable();
     ResourceTablePackage* pkg = table->FindPackageById(kAppPackageId);
     if (!pkg) {
       context_->GetDiagnostics()->Error(DiagMessage(input) << "static library has no package");
@@ -1254,13 +1212,12 @@
       // Clear the package name, so as to make the resources look like they are coming from the
       // local package.
       pkg->name = "";
-      result = table_merger_->Merge(Source(input), table.get(), override, collection.get());
+      result = table_merger_->Merge(Source(input), table, override);
 
     } else {
       // This is the proper way to merge libraries, where the package name is
       // preserved and resource names are mangled.
-      result =
-          table_merger_->MergeAndMangle(Source(input), pkg->name, table.get(), collection.get());
+      result = table_merger_->MergeAndMangle(Source(input), pkg->name, table);
     }
 
     if (!result) {
@@ -1268,31 +1225,10 @@
     }
 
     // Make sure to move the collection into the set of IFileCollections.
-    collections_.push_back(std::move(collection));
+    merged_apks_.push_back(std::move(apk));
     return true;
   }
 
-  bool MergeResourceTable(io::IFile* file, bool override) {
-    if (context_->IsVerbose()) {
-      context_->GetDiagnostics()->Note(DiagMessage() << "merging resource table "
-                                                     << file->GetSource());
-    }
-
-    std::unique_ptr<io::IData> data = file->OpenAsData();
-    if (!data) {
-      context_->GetDiagnostics()->Error(DiagMessage(file->GetSource()) << "failed to open file");
-      return false;
-    }
-
-    std::unique_ptr<ResourceTable> table =
-        LoadTableFromPb(file->GetSource(), data->data(), data->size(), context_->GetDiagnostics());
-    if (!table) {
-      return false;
-    }
-
-    return table_merger_->Merge(file->GetSource(), table.get(), override);
-  }
-
   bool MergeCompiledFile(const ResourceFile& compiled_file, io::IFile* file, bool override) {
     if (context_->IsVerbose()) {
       context_->GetDiagnostics()->Note(DiagMessage()
@@ -1423,7 +1359,7 @@
 
         ResourceTable table;
         std::string error;
-        if (!DeserializeTableFromPb(pb_table, &table, &error)) {
+        if (!DeserializeTableFromPb(pb_table, nullptr /*files*/, &table, &error)) {
           context_->GetDiagnostics()->Error(DiagMessage(src)
                                             << "failed to deserialize resource table: " << error);
           return false;
@@ -1868,13 +1804,15 @@
   // A pointer to the FileCollection representing the filesystem (not archives).
   std::unique_ptr<io::FileCollection> file_collection_;
 
-  // A vector of IFileCollections. This is mainly here to keep ownership of the
+  // A vector of IFileCollections. This is mainly here to retain ownership of the
   // collections.
   std::vector<std::unique_ptr<io::IFileCollection>> collections_;
 
-  // A vector of ResourceTables. This is here to retain ownership, so that the
-  // SymbolTable can use these.
-  std::vector<std::unique_ptr<ResourceTable>> static_table_includes_;
+  // The set of merged APKs. This is mainly here to retain ownership of the APKs.
+  std::vector<std::unique_ptr<LoadedApk>> merged_apks_;
+
+  // The set of included APKs (not merged). This is mainly here to retain ownership of the APKs.
+  std::vector<std::unique_ptr<LoadedApk>> static_library_includes_;
 
   // The set of shared libraries being used, mapping their assigned package ID to package name.
   std::map<size_t, std::string> shared_libs_;
diff --git a/tools/aapt2/cmd/Optimize.cpp b/tools/aapt2/cmd/Optimize.cpp
index 44e148e..688b6a7 100644
--- a/tools/aapt2/cmd/Optimize.cpp
+++ b/tools/aapt2/cmd/Optimize.cpp
@@ -281,15 +281,14 @@
   OptimizeContext* context_;
 };
 
-bool ExtractAppDataFromManifest(OptimizeContext* context, LoadedApk* apk,
+bool ExtractAppDataFromManifest(OptimizeContext* context, const LoadedApk* apk,
                                 OptimizeOptions* out_options) {
-  std::unique_ptr<xml::XmlResource> manifest = apk->InflateManifest(context);
+  const xml::XmlResource* manifest = apk->GetManifest();
   if (manifest == nullptr) {
     return false;
   }
 
-  Maybe<AppInfo> app_info =
-      ExtractAppInfoFromBinaryManifest(manifest.get(), context->GetDiagnostics());
+  Maybe<AppInfo> app_info = ExtractAppInfoFromBinaryManifest(*manifest, context->GetDiagnostics());
   if (!app_info) {
     context->GetDiagnostics()->Error(DiagMessage()
                                      << "failed to extract data from AndroidManifest.xml");
@@ -355,7 +354,7 @@
   }
 
   const std::string& apk_path = flags.GetArgs()[0];
-  std::unique_ptr<LoadedApk> apk = LoadedApk::LoadApkFromPath(&context, apk_path);
+  std::unique_ptr<LoadedApk> apk = LoadedApk::LoadApkFromPath(apk_path, context.GetDiagnostics());
   if (!apk) {
     return 1;
   }
diff --git a/tools/aapt2/cmd/Util.cpp b/tools/aapt2/cmd/Util.cpp
index 708bed8..d39f43e 100644
--- a/tools/aapt2/cmd/Util.cpp
+++ b/tools/aapt2/cmd/Util.cpp
@@ -214,9 +214,10 @@
   return doc;
 }
 
-static Maybe<std::string> ExtractCompiledString(xml::Attribute* attr, std::string* out_error) {
-  if (attr->compiled_value != nullptr) {
-    String* compiled_str = ValueCast<String>(attr->compiled_value.get());
+static Maybe<std::string> ExtractCompiledString(const xml::Attribute& attr,
+                                                std::string* out_error) {
+  if (attr.compiled_value != nullptr) {
+    const String* compiled_str = ValueCast<String>(attr.compiled_value.get());
     if (compiled_str != nullptr) {
       if (!compiled_str->value->empty()) {
         return *compiled_str->value;
@@ -230,16 +231,16 @@
   }
 
   // Fallback to the plain text value if there is one.
-  if (!attr->value.empty()) {
-    return attr->value;
+  if (!attr.value.empty()) {
+    return attr.value;
   }
   *out_error = "value is an empty string";
   return {};
 }
 
-static Maybe<uint32_t> ExtractCompiledInt(xml::Attribute* attr, std::string* out_error) {
-  if (attr->compiled_value != nullptr) {
-    BinaryPrimitive* compiled_prim = ValueCast<BinaryPrimitive>(attr->compiled_value.get());
+static Maybe<uint32_t> ExtractCompiledInt(const xml::Attribute& attr, std::string* out_error) {
+  if (attr.compiled_value != nullptr) {
+    const BinaryPrimitive* compiled_prim = ValueCast<BinaryPrimitive>(attr.compiled_value.get());
     if (compiled_prim != nullptr) {
       if (compiled_prim->value.dataType >= android::Res_value::TYPE_FIRST_INT &&
           compiled_prim->value.dataType <= android::Res_value::TYPE_LAST_INT) {
@@ -251,19 +252,19 @@
   }
 
   // Fallback to the plain text value if there is one.
-  Maybe<uint32_t> integer = ResourceUtils::ParseInt(attr->value);
+  Maybe<uint32_t> integer = ResourceUtils::ParseInt(attr.value);
   if (integer) {
     return integer;
   }
   std::stringstream error_msg;
-  error_msg << "'" << attr->value << "' is not a valid integer";
+  error_msg << "'" << attr.value << "' is not a valid integer";
   *out_error = error_msg.str();
   return {};
 }
 
-static Maybe<int> ExtractSdkVersion(xml::Attribute* attr, std::string* out_error) {
-  if (attr->compiled_value != nullptr) {
-    BinaryPrimitive* compiled_prim = ValueCast<BinaryPrimitive>(attr->compiled_value.get());
+static Maybe<int> ExtractSdkVersion(const xml::Attribute& attr, std::string* out_error) {
+  if (attr.compiled_value != nullptr) {
+    const BinaryPrimitive* compiled_prim = ValueCast<BinaryPrimitive>(attr.compiled_value.get());
     if (compiled_prim != nullptr) {
       if (compiled_prim->value.dataType >= android::Res_value::TYPE_FIRST_INT &&
           compiled_prim->value.dataType <= android::Res_value::TYPE_LAST_INT) {
@@ -273,7 +274,7 @@
       return {};
     }
 
-    String* compiled_str = ValueCast<String>(attr->compiled_value.get());
+    const String* compiled_str = ValueCast<String>(attr.compiled_value.get());
     if (compiled_str != nullptr) {
       Maybe<int> sdk_version = ResourceUtils::ParseSdkVersion(*compiled_str->value);
       if (sdk_version) {
@@ -288,19 +289,20 @@
   }
 
   // Fallback to the plain text value if there is one.
-  Maybe<int> sdk_version = ResourceUtils::ParseSdkVersion(attr->value);
+  Maybe<int> sdk_version = ResourceUtils::ParseSdkVersion(attr.value);
   if (sdk_version) {
     return sdk_version;
   }
   std::stringstream error_msg;
-  error_msg << "'" << attr->value << "' is not a valid SDK version";
+  error_msg << "'" << attr.value << "' is not a valid SDK version";
   *out_error = error_msg.str();
   return {};
 }
 
-Maybe<AppInfo> ExtractAppInfoFromBinaryManifest(xml::XmlResource* xml_res, IDiagnostics* diag) {
+Maybe<AppInfo> ExtractAppInfoFromBinaryManifest(const xml::XmlResource& xml_res,
+                                                IDiagnostics* diag) {
   // Make sure the first element is <manifest> with package attribute.
-  xml::Element* manifest_el = xml_res->root.get();
+  const xml::Element* manifest_el = xml_res.root.get();
   if (manifest_el == nullptr) {
     return {};
   }
@@ -308,63 +310,63 @@
   AppInfo app_info;
 
   if (!manifest_el->namespace_uri.empty() || manifest_el->name != "manifest") {
-    diag->Error(DiagMessage(xml_res->file.source) << "root tag must be <manifest>");
+    diag->Error(DiagMessage(xml_res.file.source) << "root tag must be <manifest>");
     return {};
   }
 
-  xml::Attribute* package_attr = manifest_el->FindAttribute({}, "package");
+  const xml::Attribute* package_attr = manifest_el->FindAttribute({}, "package");
   if (!package_attr) {
-    diag->Error(DiagMessage(xml_res->file.source) << "<manifest> must have a 'package' attribute");
+    diag->Error(DiagMessage(xml_res.file.source) << "<manifest> must have a 'package' attribute");
     return {};
   }
 
   std::string error_msg;
-  Maybe<std::string> maybe_package = ExtractCompiledString(package_attr, &error_msg);
+  Maybe<std::string> maybe_package = ExtractCompiledString(*package_attr, &error_msg);
   if (!maybe_package) {
-    diag->Error(DiagMessage(xml_res->file.source.WithLine(manifest_el->line_number))
+    diag->Error(DiagMessage(xml_res.file.source.WithLine(manifest_el->line_number))
                 << "invalid package name: " << error_msg);
     return {};
   }
   app_info.package = maybe_package.value();
 
-  if (xml::Attribute* version_code_attr =
+  if (const xml::Attribute* version_code_attr =
           manifest_el->FindAttribute(xml::kSchemaAndroid, "versionCode")) {
-    Maybe<uint32_t> maybe_code = ExtractCompiledInt(version_code_attr, &error_msg);
+    Maybe<uint32_t> maybe_code = ExtractCompiledInt(*version_code_attr, &error_msg);
     if (!maybe_code) {
-      diag->Error(DiagMessage(xml_res->file.source.WithLine(manifest_el->line_number))
+      diag->Error(DiagMessage(xml_res.file.source.WithLine(manifest_el->line_number))
                   << "invalid android:versionCode: " << error_msg);
       return {};
     }
     app_info.version_code = maybe_code.value();
   }
 
-  if (xml::Attribute* revision_code_attr =
+  if (const xml::Attribute* revision_code_attr =
           manifest_el->FindAttribute(xml::kSchemaAndroid, "revisionCode")) {
-    Maybe<uint32_t> maybe_code = ExtractCompiledInt(revision_code_attr, &error_msg);
+    Maybe<uint32_t> maybe_code = ExtractCompiledInt(*revision_code_attr, &error_msg);
     if (!maybe_code) {
-      diag->Error(DiagMessage(xml_res->file.source.WithLine(manifest_el->line_number))
+      diag->Error(DiagMessage(xml_res.file.source.WithLine(manifest_el->line_number))
                   << "invalid android:revisionCode: " << error_msg);
       return {};
     }
     app_info.revision_code = maybe_code.value();
   }
 
-  if (xml::Attribute* split_name_attr = manifest_el->FindAttribute({}, "split")) {
-    Maybe<std::string> maybe_split_name = ExtractCompiledString(split_name_attr, &error_msg);
+  if (const xml::Attribute* split_name_attr = manifest_el->FindAttribute({}, "split")) {
+    Maybe<std::string> maybe_split_name = ExtractCompiledString(*split_name_attr, &error_msg);
     if (!maybe_split_name) {
-      diag->Error(DiagMessage(xml_res->file.source.WithLine(manifest_el->line_number))
+      diag->Error(DiagMessage(xml_res.file.source.WithLine(manifest_el->line_number))
                   << "invalid split name: " << error_msg);
       return {};
     }
     app_info.split_name = maybe_split_name.value();
   }
 
-  if (xml::Element* uses_sdk_el = manifest_el->FindChild({}, "uses-sdk")) {
-    if (xml::Attribute* min_sdk =
+  if (const xml::Element* uses_sdk_el = manifest_el->FindChild({}, "uses-sdk")) {
+    if (const xml::Attribute* min_sdk =
             uses_sdk_el->FindAttribute(xml::kSchemaAndroid, "minSdkVersion")) {
-      Maybe<int> maybe_sdk = ExtractSdkVersion(min_sdk, &error_msg);
+      Maybe<int> maybe_sdk = ExtractSdkVersion(*min_sdk, &error_msg);
       if (!maybe_sdk) {
-        diag->Error(DiagMessage(xml_res->file.source.WithLine(uses_sdk_el->line_number))
+        diag->Error(DiagMessage(xml_res.file.source.WithLine(uses_sdk_el->line_number))
                     << "invalid android:minSdkVersion: " << error_msg);
         return {};
       }
diff --git a/tools/aapt2/cmd/Util.h b/tools/aapt2/cmd/Util.h
index fd9b39c..7611c15 100644
--- a/tools/aapt2/cmd/Util.h
+++ b/tools/aapt2/cmd/Util.h
@@ -57,7 +57,8 @@
                                                         const SplitConstraints& constraints);
 
 // Extracts relevant info from the AndroidManifest.xml.
-Maybe<AppInfo> ExtractAppInfoFromBinaryManifest(xml::XmlResource* xml_res, IDiagnostics* diag);
+Maybe<AppInfo> ExtractAppInfoFromBinaryManifest(const xml::XmlResource& xml_res,
+                                                IDiagnostics* diag);
 
 }  // namespace aapt
 
diff --git a/tools/aapt2/format/binary/BinaryResourceParser.cpp b/tools/aapt2/format/binary/BinaryResourceParser.cpp
index 95eec4a..66510b0 100644
--- a/tools/aapt2/format/binary/BinaryResourceParser.cpp
+++ b/tools/aapt2/format/binary/BinaryResourceParser.cpp
@@ -73,29 +73,22 @@
 
 }  // namespace
 
-BinaryResourceParser::BinaryResourceParser(IAaptContext* context, ResourceTable* table,
+BinaryResourceParser::BinaryResourceParser(IDiagnostics* diag, ResourceTable* table,
                                            const Source& source, const void* data, size_t len,
                                            io::IFileCollection* files)
-    : context_(context),
-      table_(table),
-      source_(source),
-      data_(data),
-      data_len_(len),
-      files_(files) {
+    : diag_(diag), table_(table), source_(source), data_(data), data_len_(len), files_(files) {
 }
 
 bool BinaryResourceParser::Parse() {
   ResChunkPullParser parser(data_, data_len_);
 
   if (!ResChunkPullParser::IsGoodEvent(parser.Next())) {
-    context_->GetDiagnostics()->Error(DiagMessage(source_)
-                                      << "corrupt resources.arsc: " << parser.error());
+    diag_->Error(DiagMessage(source_) << "corrupt resources.arsc: " << parser.error());
     return false;
   }
 
   if (parser.chunk()->type != android::RES_TABLE_TYPE) {
-    context_->GetDiagnostics()->Error(DiagMessage(source_)
-                                      << StringPrintf("unknown chunk of type 0x%02x",
+    diag_->Error(DiagMessage(source_) << StringPrintf("unknown chunk of type 0x%02x",
                                                       static_cast<int>(parser.chunk()->type)));
     return false;
   }
@@ -106,13 +99,12 @@
 
   if (parser.Next() != ResChunkPullParser::Event::kEndDocument) {
     if (parser.event() == ResChunkPullParser::Event::kBadDocument) {
-      context_->GetDiagnostics()->Warn(
-          DiagMessage(source_) << "invalid chunk trailing RES_TABLE_TYPE: " << parser.error());
+      diag_->Warn(DiagMessage(source_)
+                  << "invalid chunk trailing RES_TABLE_TYPE: " << parser.error());
     } else {
-      context_->GetDiagnostics()->Warn(
-          DiagMessage(source_) << StringPrintf(
-              "unexpected chunk of type 0x%02x trailing RES_TABLE_TYPE",
-              static_cast<int>(parser.chunk()->type)));
+      diag_->Warn(DiagMessage(source_)
+                  << StringPrintf("unexpected chunk of type 0x%02x trailing RES_TABLE_TYPE",
+                                  static_cast<int>(parser.chunk()->type)));
     }
   }
   return true;
@@ -122,7 +114,7 @@
 bool BinaryResourceParser::ParseTable(const ResChunk_header* chunk) {
   const ResTable_header* table_header = ConvertTo<ResTable_header>(chunk);
   if (!table_header) {
-    context_->GetDiagnostics()->Error(DiagMessage(source_) << "corrupt ResTable_header chunk");
+    diag_->Error(DiagMessage(source_) << "corrupt ResTable_header chunk");
     return false;
   }
 
@@ -135,17 +127,15 @@
           status_t err =
               value_pool_.setTo(parser.chunk(), util::DeviceToHost32(parser.chunk()->size));
           if (err != NO_ERROR) {
-            context_->GetDiagnostics()->Error(DiagMessage(source_)
-                                              << "corrupt string pool in ResTable: "
-                                              << value_pool_.getError());
+            diag_->Error(DiagMessage(source_)
+                         << "corrupt string pool in ResTable: " << value_pool_.getError());
             return false;
           }
 
           // Reserve some space for the strings we are going to add.
           table_->string_pool.HintWillAdd(value_pool_.size(), value_pool_.styleCount());
         } else {
-          context_->GetDiagnostics()->Warn(DiagMessage(source_)
-                                           << "unexpected string pool in ResTable");
+          diag_->Warn(DiagMessage(source_) << "unexpected string pool in ResTable");
         }
         break;
 
@@ -156,16 +146,15 @@
         break;
 
       default:
-        context_->GetDiagnostics()->Warn(
-            DiagMessage(source_) << "unexpected chunk type "
-                                 << static_cast<int>(util::DeviceToHost16(parser.chunk()->type)));
+        diag_->Warn(DiagMessage(source_)
+                    << "unexpected chunk type "
+                    << static_cast<int>(util::DeviceToHost16(parser.chunk()->type)));
         break;
     }
   }
 
   if (parser.event() == ResChunkPullParser::Event::kBadDocument) {
-    context_->GetDiagnostics()->Error(DiagMessage(source_)
-                                      << "corrupt resource table: " << parser.error());
+    diag_->Error(DiagMessage(source_) << "corrupt resource table: " << parser.error());
     return false;
   }
   return true;
@@ -176,14 +165,13 @@
       sizeof(ResTable_package) - sizeof(ResTable_package::typeIdOffset);
   const ResTable_package* package_header = ConvertTo<ResTable_package, kMinPackageSize>(chunk);
   if (!package_header) {
-    context_->GetDiagnostics()->Error(DiagMessage(source_) << "corrupt ResTable_package chunk");
+    diag_->Error(DiagMessage(source_) << "corrupt ResTable_package chunk");
     return false;
   }
 
   uint32_t package_id = util::DeviceToHost32(package_header->id);
   if (package_id > std::numeric_limits<uint8_t>::max()) {
-    context_->GetDiagnostics()->Error(DiagMessage(source_)
-                                      << "package ID is too big (" << package_id << ")");
+    diag_->Error(DiagMessage(source_) << "package ID is too big (" << package_id << ")");
     return false;
   }
 
@@ -198,9 +186,8 @@
   ResourceTablePackage* package =
       table_->CreatePackage(util::Utf16ToUtf8(package_name), static_cast<uint8_t>(package_id));
   if (!package) {
-    context_->GetDiagnostics()->Error(DiagMessage(source_)
-                                      << "incompatible package '" << package_name << "' with ID "
-                                      << package_id);
+    diag_->Error(DiagMessage(source_)
+                 << "incompatible package '" << package_name << "' with ID " << package_id);
     return false;
   }
 
@@ -218,8 +205,7 @@
           status_t err =
               type_pool_.setTo(parser.chunk(), util::DeviceToHost32(parser.chunk()->size));
           if (err != NO_ERROR) {
-            context_->GetDiagnostics()->Error(DiagMessage(source_)
-                                              << "corrupt type string pool in "
+            diag_->Error(DiagMessage(source_) << "corrupt type string pool in "
                                               << "ResTable_package: " << type_pool_.getError());
             return false;
           }
@@ -227,13 +213,12 @@
           status_t err =
               key_pool_.setTo(parser.chunk(), util::DeviceToHost32(parser.chunk()->size));
           if (err != NO_ERROR) {
-            context_->GetDiagnostics()->Error(DiagMessage(source_)
-                                              << "corrupt key string pool in "
+            diag_->Error(DiagMessage(source_) << "corrupt key string pool in "
                                               << "ResTable_package: " << key_pool_.getError());
             return false;
           }
         } else {
-          context_->GetDiagnostics()->Warn(DiagMessage(source_) << "unexpected string pool");
+          diag_->Warn(DiagMessage(source_) << "unexpected string pool");
         }
         break;
 
@@ -256,16 +241,15 @@
         break;
 
       default:
-        context_->GetDiagnostics()->Warn(
-            DiagMessage(source_) << "unexpected chunk type "
-                                 << static_cast<int>(util::DeviceToHost16(parser.chunk()->type)));
+        diag_->Warn(DiagMessage(source_)
+                    << "unexpected chunk type "
+                    << static_cast<int>(util::DeviceToHost16(parser.chunk()->type)));
         break;
     }
   }
 
   if (parser.event() == ResChunkPullParser::Event::kBadDocument) {
-    context_->GetDiagnostics()->Error(DiagMessage(source_)
-                                      << "corrupt ResTable_package: " << parser.error());
+    diag_->Error(DiagMessage(source_) << "corrupt ResTable_package: " << parser.error());
     return false;
   }
 
@@ -278,19 +262,18 @@
 
 bool BinaryResourceParser::ParseTypeSpec(const ResChunk_header* chunk) {
   if (type_pool_.getError() != NO_ERROR) {
-    context_->GetDiagnostics()->Error(DiagMessage(source_) << "missing type string pool");
+    diag_->Error(DiagMessage(source_) << "missing type string pool");
     return false;
   }
 
   const ResTable_typeSpec* type_spec = ConvertTo<ResTable_typeSpec>(chunk);
   if (!type_spec) {
-    context_->GetDiagnostics()->Error(DiagMessage(source_) << "corrupt ResTable_typeSpec chunk");
+    diag_->Error(DiagMessage(source_) << "corrupt ResTable_typeSpec chunk");
     return false;
   }
 
   if (type_spec->id == 0) {
-    context_->GetDiagnostics()->Error(DiagMessage(source_)
-                                      << "ResTable_typeSpec has invalid id: " << type_spec->id);
+    diag_->Error(DiagMessage(source_) << "ResTable_typeSpec has invalid id: " << type_spec->id);
     return false;
   }
   return true;
@@ -299,12 +282,12 @@
 bool BinaryResourceParser::ParseType(const ResourceTablePackage* package,
                                      const ResChunk_header* chunk) {
   if (type_pool_.getError() != NO_ERROR) {
-    context_->GetDiagnostics()->Error(DiagMessage(source_) << "missing type string pool");
+    diag_->Error(DiagMessage(source_) << "missing type string pool");
     return false;
   }
 
   if (key_pool_.getError() != NO_ERROR) {
-    context_->GetDiagnostics()->Error(DiagMessage(source_) << "missing key string pool");
+    diag_->Error(DiagMessage(source_) << "missing key string pool");
     return false;
   }
 
@@ -312,13 +295,12 @@
   // a lot and has its own code to handle variable size.
   const ResTable_type* type = ConvertTo<ResTable_type, kResTableTypeMinSize>(chunk);
   if (!type) {
-    context_->GetDiagnostics()->Error(DiagMessage(source_) << "corrupt ResTable_type chunk");
+    diag_->Error(DiagMessage(source_) << "corrupt ResTable_type chunk");
     return false;
   }
 
   if (type->id == 0) {
-    context_->GetDiagnostics()->Error(DiagMessage(source_)
-                                      << "ResTable_type has invalid id: " << (int)type->id);
+    diag_->Error(DiagMessage(source_) << "ResTable_type has invalid id: " << (int)type->id);
     return false;
   }
 
@@ -329,9 +311,8 @@
 
   const ResourceType* parsed_type = ParseResourceType(type_str);
   if (!parsed_type) {
-    context_->GetDiagnostics()->Error(DiagMessage(source_)
-                                      << "invalid type name '" << type_str << "' for type with ID "
-                                      << (int)type->id);
+    diag_->Error(DiagMessage(source_)
+                 << "invalid type name '" << type_str << "' for type with ID " << (int)type->id);
     return false;
   }
 
@@ -360,14 +341,13 @@
     }
 
     if (!resource_value) {
-      context_->GetDiagnostics()->Error(DiagMessage(source_)
-                                        << "failed to parse value for resource " << name << " ("
+      diag_->Error(DiagMessage(source_) << "failed to parse value for resource " << name << " ("
                                         << res_id << ") with configuration '" << config << "'");
       return false;
     }
 
     if (!table_->AddResourceAllowMangled(name, res_id, config, {}, std::move(resource_value),
-                                         context_->GetDiagnostics())) {
+                                         diag_)) {
       return false;
     }
 
@@ -375,7 +355,7 @@
       Symbol symbol;
       symbol.state = SymbolState::kPublic;
       symbol.source = source_.WithLine(0);
-      if (!table_->SetSymbolStateAllowMangled(name, res_id, symbol, context_->GetDiagnostics())) {
+      if (!table_->SetSymbolStateAllowMangled(name, res_id, symbol, diag_)) {
         return false;
       }
     }
@@ -410,15 +390,14 @@
                                                        const android::Res_value& value) {
   std::unique_ptr<Item> item = ResourceUtils::ParseBinaryResValue(name.type, config, value_pool_,
                                                                   value, &table_->string_pool);
-  if (files_ != nullptr && item != nullptr) {
+  if (files_ != nullptr) {
     FileReference* file_ref = ValueCast<FileReference>(item.get());
     if (file_ref != nullptr) {
       file_ref->file = files_->FindFile(*file_ref->path);
       if (file_ref->file == nullptr) {
-        context_->GetDiagnostics()->Warn(DiagMessage()
-                                         << "resource " << name << " for config '" << config
-                                         << "' is a file reference to '" << *file_ref->path
-                                         << "' but no such path exists");
+        diag_->Warn(DiagMessage() << "resource " << name << " for config '" << config
+                                  << "' is a file reference to '" << *file_ref->path
+                                  << "' but no such path exists");
       }
     }
   }
@@ -432,7 +411,7 @@
     case ResourceType::kStyle:
       return ParseStyle(name, config, map);
     case ResourceType::kAttrPrivate:
-    // fallthrough
+      // fallthrough
     case ResourceType::kAttr:
       return ParseAttr(name, config, map);
     case ResourceType::kArray:
@@ -445,8 +424,8 @@
       // We can ignore the value here.
       return util::make_unique<Id>();
     default:
-      context_->GetDiagnostics()->Error(DiagMessage() << "illegal map type '" << ToString(name.type)
-                                                      << "' (" << (int)name.type << ")");
+      diag_->Error(DiagMessage() << "illegal map type '" << ToString(name.type) << "' ("
+                                 << (int)name.type << ")");
       break;
   }
   return {};
diff --git a/tools/aapt2/format/binary/BinaryResourceParser.h b/tools/aapt2/format/binary/BinaryResourceParser.h
index dc9a384..052f806 100644
--- a/tools/aapt2/format/binary/BinaryResourceParser.h
+++ b/tools/aapt2/format/binary/BinaryResourceParser.h
@@ -39,7 +39,7 @@
  public:
   // Creates a parser, which will read `len` bytes from `data`, and add any resources parsed to
   // `table`. `source` is for logging purposes.
-  BinaryResourceParser(IAaptContext* context, ResourceTable* table, const Source& source,
+  BinaryResourceParser(IDiagnostics* diag, ResourceTable* table, const Source& source,
                        const void* data, size_t data_len, io::IFileCollection* files = nullptr);
 
   // Parses the binary resource table and returns true if successful.
@@ -80,7 +80,7 @@
    */
   bool CollectMetaData(const android::ResTable_map& map_entry, Value* value);
 
-  IAaptContext* context_;
+  IDiagnostics* diag_;
   ResourceTable* table_;
 
   const Source source_;
diff --git a/tools/aapt2/format/binary/TableFlattener.cpp b/tools/aapt2/format/binary/TableFlattener.cpp
index 57565a5..259f2e9 100644
--- a/tools/aapt2/format/binary/TableFlattener.cpp
+++ b/tools/aapt2/format/binary/TableFlattener.cpp
@@ -568,14 +568,6 @@
   ResTable_header* table_header = table_writer.StartChunk<ResTable_header>(RES_TABLE_TYPE);
   table_header->packageCount = util::HostToDevice32(table->packages.size());
 
-  // Write a self mapping entry for this package if the ID is non-standard (0x7f).
-  if (context->GetPackageType() == PackageType::kApp) {
-    const uint8_t package_id = context->GetPackageId();
-    if (package_id != kFrameworkPackageId && package_id != kAppPackageId) {
-      table->included_packages_[package_id] = context->GetCompilationPackage();
-    }
-  }
-
   // Flatten the values string pool.
   StringPool::FlattenUtf8(table_writer.buffer(), table->string_pool);
 
@@ -583,6 +575,14 @@
 
   // Flatten each package.
   for (auto& package : table->packages) {
+    if (context->GetPackageType() == PackageType::kApp) {
+      // Write a self mapping entry for this package if the ID is non-standard (0x7f).
+      const uint8_t package_id = package->id.value();
+      if (package_id != kFrameworkPackageId && package_id != kAppPackageId) {
+        table->included_packages_[package_id] = package->name;
+      }
+    }
+
     PackageFlattener flattener(context, package.get(), &table->included_packages_,
                                options_.use_sparse_entries);
     if (!flattener.FlattenPackage(&package_buffer)) {
diff --git a/tools/aapt2/format/binary/TableFlattener_test.cpp b/tools/aapt2/format/binary/TableFlattener_test.cpp
index 6d75973..e11890b 100644
--- a/tools/aapt2/format/binary/TableFlattener_test.cpp
+++ b/tools/aapt2/format/binary/TableFlattener_test.cpp
@@ -71,7 +71,8 @@
       return result;
     }
 
-    BinaryResourceParser parser(context, out_table, {}, content.data(), content.size());
+    BinaryResourceParser parser(context->GetDiagnostics(), out_table, {}, content.data(),
+                                content.size());
     if (!parser.Parse()) {
       return ::testing::AssertionFailure() << "flattened ResTable is corrupt";
     }
@@ -278,7 +279,7 @@
   // Attempt to parse the sparse contents.
 
   ResourceTable sparse_table;
-  BinaryResourceParser parser(context.get(), &sparse_table, Source("test.arsc"),
+  BinaryResourceParser parser(context->GetDiagnostics(), &sparse_table, Source("test.arsc"),
                               sparse_contents.data(), sparse_contents.size());
   ASSERT_TRUE(parser.Parse());
 
diff --git a/tools/aapt2/format/proto/ProtoDeserialize.cpp b/tools/aapt2/format/proto/ProtoDeserialize.cpp
index 86bd865..0f0bce8 100644
--- a/tools/aapt2/format/proto/ProtoDeserialize.cpp
+++ b/tools/aapt2/format/proto/ProtoDeserialize.cpp
@@ -371,7 +371,8 @@
 }
 
 static bool DeserializePackageFromPb(const pb::Package& pb_package, const ResStringPool& src_pool,
-                                     ResourceTable* out_table, std::string* out_error) {
+                                     io::IFileCollection* files, ResourceTable* out_table,
+                                     std::string* out_error) {
   Maybe<uint8_t> id;
   if (pb_package.has_package_id()) {
     id = static_cast<uint8_t>(pb_package.package_id().id());
@@ -444,7 +445,7 @@
         }
 
         config_value->value = DeserializeValueFromPb(pb_config_value.value(), src_pool, config,
-                                                     &out_table->string_pool, out_error);
+                                                     &out_table->string_pool, files, out_error);
         if (config_value->value == nullptr) {
           return false;
         }
@@ -457,8 +458,8 @@
   return true;
 }
 
-bool DeserializeTableFromPb(const pb::ResourceTable& pb_table, ResourceTable* out_table,
-                            std::string* out_error) {
+bool DeserializeTableFromPb(const pb::ResourceTable& pb_table, io::IFileCollection* files,
+                            ResourceTable* out_table, std::string* out_error) {
   // We import the android namespace because on Windows NO_ERROR is a macro, not an enum, which
   // causes errors when qualifying it with android::
   using namespace android;
@@ -474,7 +475,7 @@
   }
 
   for (const pb::Package& pb_package : pb_table.package()) {
-    if (!DeserializePackageFromPb(pb_package, source_pool, out_table, out_error)) {
+    if (!DeserializePackageFromPb(pb_package, source_pool, files, out_table, out_error)) {
       return false;
     }
   }
@@ -600,10 +601,11 @@
 std::unique_ptr<Value> DeserializeValueFromPb(const pb::Value& pb_value,
                                               const android::ResStringPool& src_pool,
                                               const ConfigDescription& config,
-                                              StringPool* value_pool, std::string* out_error) {
+                                              StringPool* value_pool, io::IFileCollection* files,
+                                              std::string* out_error) {
   std::unique_ptr<Value> value;
   if (pb_value.has_item()) {
-    value = DeserializeItemFromPb(pb_value.item(), src_pool, config, value_pool, out_error);
+    value = DeserializeItemFromPb(pb_value.item(), src_pool, config, value_pool, files, out_error);
     if (value == nullptr) {
       return {};
     }
@@ -651,8 +653,8 @@
             return {};
           }
           DeserializeItemMetaDataFromPb(pb_entry, src_pool, &entry.key);
-          entry.value =
-              DeserializeItemFromPb(pb_entry.item(), src_pool, config, value_pool, out_error);
+          entry.value = DeserializeItemFromPb(pb_entry.item(), src_pool, config, value_pool, files,
+                                              out_error);
           if (entry.value == nullptr) {
             return {};
           }
@@ -680,8 +682,8 @@
         const pb::Array& pb_array = pb_compound_value.array();
         std::unique_ptr<Array> array = util::make_unique<Array>();
         for (const pb::Array_Element& pb_entry : pb_array.element()) {
-          std::unique_ptr<Item> item =
-              DeserializeItemFromPb(pb_entry.item(), src_pool, config, value_pool, out_error);
+          std::unique_ptr<Item> item = DeserializeItemFromPb(pb_entry.item(), src_pool, config,
+                                                             value_pool, files, out_error);
           if (item == nullptr) {
             return {};
           }
@@ -697,8 +699,8 @@
         std::unique_ptr<Plural> plural = util::make_unique<Plural>();
         for (const pb::Plural_Entry& pb_entry : pb_plural.entry()) {
           size_t plural_idx = DeserializePluralEnumFromPb(pb_entry.arity());
-          plural->values[plural_idx] =
-              DeserializeItemFromPb(pb_entry.item(), src_pool, config, value_pool, out_error);
+          plural->values[plural_idx] = DeserializeItemFromPb(pb_entry.item(), src_pool, config,
+                                                             value_pool, files, out_error);
           if (!plural->values[plural_idx]) {
             return {};
           }
@@ -727,7 +729,7 @@
 std::unique_ptr<Item> DeserializeItemFromPb(const pb::Item& pb_item,
                                             const android::ResStringPool& src_pool,
                                             const ConfigDescription& config, StringPool* value_pool,
-                                            std::string* out_error) {
+                                            io::IFileCollection* files, std::string* out_error) {
   switch (pb_item.value_case()) {
     case pb::Item::kRef: {
       const pb::Reference& pb_ref = pb_item.ref();
@@ -774,6 +776,9 @@
           util::make_unique<FileReference>(value_pool->MakeRef(
               pb_file.path(), StringPool::Context(StringPool::Context::kHighPriority, config)));
       file_ref->type = DeserializeFileReferenceTypeFromPb(pb_file.type());
+      if (files != nullptr) {
+        file_ref->file = files->FindFile(*file_ref->path);
+      }
       return std::move(file_ref);
     } break;
 
@@ -825,7 +830,7 @@
     }
     if (pb_attr.has_compiled_item()) {
       attr.compiled_value =
-          DeserializeItemFromPb(pb_attr.compiled_item(), {}, {}, value_pool, out_error);
+          DeserializeItemFromPb(pb_attr.compiled_item(), {}, {}, value_pool, nullptr, out_error);
       if (attr.compiled_value == nullptr) {
         return {};
       }
diff --git a/tools/aapt2/format/proto/ProtoDeserialize.h b/tools/aapt2/format/proto/ProtoDeserialize.h
index 7dc54f2..0c581a1 100644
--- a/tools/aapt2/format/proto/ProtoDeserialize.h
+++ b/tools/aapt2/format/proto/ProtoDeserialize.h
@@ -27,6 +27,7 @@
 #include "Resources.pb.h"
 #include "ResourcesInternal.pb.h"
 #include "StringPool.h"
+#include "io/File.h"
 #include "xml/XmlDom.h"
 
 namespace aapt {
@@ -34,12 +35,13 @@
 std::unique_ptr<Value> DeserializeValueFromPb(const pb::Value& pb_value,
                                               const android::ResStringPool& src_pool,
                                               const ConfigDescription& config,
-                                              StringPool* value_pool, std::string* out_error);
+                                              StringPool* value_pool, io::IFileCollection* files,
+                                              std::string* out_error);
 
 std::unique_ptr<Item> DeserializeItemFromPb(const pb::Item& pb_item,
                                             const android::ResStringPool& src_pool,
                                             const ConfigDescription& config, StringPool* value_pool,
-                                            std::string* out_error);
+                                            io::IFileCollection* files, std::string* out_error);
 
 std::unique_ptr<xml::XmlResource> DeserializeXmlResourceFromPb(const pb::XmlNode& pb_node,
                                                                std::string* out_error);
@@ -50,8 +52,9 @@
 bool DeserializeConfigFromPb(const pb::Configuration& pb_config, ConfigDescription* out_config,
                              std::string* out_error);
 
-bool DeserializeTableFromPb(const pb::ResourceTable& pb_table, ResourceTable* out_table,
-                            std::string* out_error);
+// Optional io::IFileCollection used to lookup references to files in the ResourceTable.
+bool DeserializeTableFromPb(const pb::ResourceTable& pb_table, io::IFileCollection* files,
+                            ResourceTable* out_table, std::string* out_error);
 
 bool DeserializeCompiledFileFromPb(const pb::internal::CompiledFile& pb_file,
                                    ResourceFile* out_file, std::string* out_error);
diff --git a/tools/aapt2/format/proto/ProtoSerialize_test.cpp b/tools/aapt2/format/proto/ProtoSerialize_test.cpp
index 8efac8a..9649a4d 100644
--- a/tools/aapt2/format/proto/ProtoSerialize_test.cpp
+++ b/tools/aapt2/format/proto/ProtoSerialize_test.cpp
@@ -29,6 +29,12 @@
 
 namespace aapt {
 
+class MockFileCollection : public io::IFileCollection {
+ public:
+  MOCK_METHOD1(FindFile, io::IFile*(const StringPiece& path));
+  MOCK_METHOD0(Iterator, std::unique_ptr<io::IFileCollectionIterator>());
+};
+
 TEST(ProtoSerializeTest, SerializeSinglePackage) {
   std::unique_ptr<IAaptContext> context = test::ContextBuilder().Build();
   std::unique_ptr<ResourceTable> table =
@@ -86,9 +92,14 @@
   pb::ResourceTable pb_table;
   SerializeTableToPb(*table, &pb_table);
 
+  test::TestFile file_a("res/layout/main.xml");
+  MockFileCollection files;
+  EXPECT_CALL(files, FindFile(Eq("res/layout/main.xml")))
+      .WillRepeatedly(::testing::Return(&file_a));
+
   ResourceTable new_table;
   std::string error;
-  ASSERT_TRUE(DeserializeTableFromPb(pb_table, &new_table, &error));
+  ASSERT_TRUE(DeserializeTableFromPb(pb_table, &files, &new_table, &error));
   EXPECT_THAT(error, IsEmpty());
 
   Id* new_id = test::GetValue<Id>(&new_table, "com.app.a:id/foo");
@@ -124,6 +135,11 @@
   ASSERT_TRUE(actual_ref->id);
   EXPECT_THAT(*actual_ref, Eq(expected_ref));
 
+  FileReference* actual_file_ref =
+      test::GetValue<FileReference>(&new_table, "com.app.a:layout/main");
+  ASSERT_THAT(actual_file_ref, NotNull());
+  EXPECT_THAT(actual_file_ref->file, Eq(&file_a));
+
   StyledString* actual_styled_str =
       test::GetValue<StyledString>(&new_table, "com.app.a:string/styled");
   ASSERT_THAT(actual_styled_str, NotNull());
diff --git a/tools/aapt2/integration-tests/NamespaceTest/App/Android.mk b/tools/aapt2/integration-tests/NamespaceTest/App/Android.mk
index 6ed07b0..94686c0 100644
--- a/tools/aapt2/integration-tests/NamespaceTest/App/Android.mk
+++ b/tools/aapt2/integration-tests/NamespaceTest/App/Android.mk
@@ -25,5 +25,4 @@
 LOCAL_STATIC_ANDROID_LIBRARIES := \
     AaptTestNamespace_LibOne \
     AaptTestNamespace_LibTwo
-LOCAL_AAPT_FLAGS := -v
 include $(BUILD_PACKAGE)
diff --git a/tools/aapt2/link/TableMerger.cpp b/tools/aapt2/link/TableMerger.cpp
index 21c6b11..58d0607 100644
--- a/tools/aapt2/link/TableMerger.cpp
+++ b/tools/aapt2/link/TableMerger.cpp
@@ -37,17 +37,14 @@
   CHECK(master_package_ != nullptr) << "package name or ID already taken";
 }
 
-bool TableMerger::Merge(const Source& src, ResourceTable* table, bool overlay,
-                        io::IFileCollection* collection) {
+bool TableMerger::Merge(const Source& src, ResourceTable* table, bool overlay) {
   // We allow adding new resources if this is not an overlay, or if the options allow overlays
   // to add new resources.
-  return MergeImpl(src, table, collection, overlay,
-                   options_.auto_add_overlay || !overlay /*allow_new*/);
+  return MergeImpl(src, table, overlay, options_.auto_add_overlay || !overlay /*allow_new*/);
 }
 
 // This will merge packages with the same package name (or no package name).
-bool TableMerger::MergeImpl(const Source& src, ResourceTable* table,
-                            io::IFileCollection* collection, bool overlay, bool allow_new) {
+bool TableMerger::MergeImpl(const Source& src, ResourceTable* table, bool overlay, bool allow_new) {
   bool error = false;
   for (auto& package : table->packages) {
     // Only merge an empty package or the package we're building.
@@ -55,37 +52,20 @@
     // This is because at compile time it is unknown if the attributes are
     // simply uses of the attribute or definitions.
     if (package->name.empty() || context_->GetCompilationPackage() == package->name) {
-      FileMergeCallback callback;
-      if (collection) {
-        callback = [&](const ResourceNameRef& name, const ConfigDescription& config,
-                       FileReference* new_file, FileReference* old_file) -> bool {
-          // The old file's path points inside the APK, so we can use it as is.
-          io::IFile* f = collection->FindFile(*old_file->path);
-          if (!f) {
-            context_->GetDiagnostics()->Error(DiagMessage(src)
-                                              << "file '" << *old_file->path << "' not found");
-            return false;
-          }
-
-          new_file->file = f;
-          return true;
-        };
-      }
-
       // Merge here. Once the entries are merged and mangled, any references to them are still
       // valid. This is because un-mangled references are mangled, then looked up at resolution
       // time. Also, when linking, we convert references with no package name to use the compilation
       // package name.
-      error |=
-          !DoMerge(src, table, package.get(), false /* mangle */, overlay, allow_new, callback);
+      error |= !DoMerge(src, table, package.get(), false /*mangle*/, overlay, allow_new);
     }
   }
   return !error;
 }
 
-// This will merge and mangle resources from a static library.
+// This will merge and mangle resources from a static library. It is assumed that all FileReferences
+// have correctly set their io::IFile*.
 bool TableMerger::MergeAndMangle(const Source& src, const StringPiece& package_name,
-                                 ResourceTable* table, io::IFileCollection* collection) {
+                                 ResourceTable* table) {
   bool error = false;
   for (auto& package : table->packages) {
     // Warn of packages with an unrelated ID.
@@ -96,23 +76,7 @@
 
     bool mangle = package_name != context_->GetCompilationPackage();
     merged_packages_.insert(package->name);
-
-    auto callback = [&](const ResourceNameRef& name, const ConfigDescription& config,
-                        FileReference* new_file, FileReference* old_file) -> bool {
-      // The old file's path points inside the APK, so we can use it as is.
-      io::IFile* f = collection->FindFile(*old_file->path);
-      if (!f) {
-        context_->GetDiagnostics()->Error(DiagMessage(src)
-                                          << "file '" << *old_file->path << "' not found");
-        return false;
-      }
-
-      new_file->file = f;
-      return true;
-    };
-
-    error |= !DoMerge(src, table, package.get(), mangle, false /*overlay*/, true /*allow_new*/,
-                      callback);
+    error |= !DoMerge(src, table, package.get(), mangle, false /*overlay*/, true /*allow_new*/);
   }
   return !error;
 }
@@ -187,7 +151,7 @@
 
 static ResourceTable::CollisionResult MergeConfigValue(IAaptContext* context,
                                                        const ResourceNameRef& res_name,
-                                                       const bool overlay,
+                                                       bool overlay,
                                                        ResourceConfigValue* dst_config_value,
                                                        ResourceConfigValue* src_config_value,
                                                        StringPool* pool) {
@@ -220,10 +184,8 @@
 }
 
 bool TableMerger::DoMerge(const Source& src, ResourceTable* src_table,
-                          ResourceTablePackage* src_package,
-                          const bool mangle_package, const bool overlay,
-                          const bool allow_new_resources,
-                          const FileMergeCallback& callback) {
+                          ResourceTablePackage* src_package, bool mangle_package, bool overlay,
+                          bool allow_new_resources) {
   bool error = false;
 
   for (auto& src_type : src_package->types) {
@@ -292,13 +254,6 @@
           } else {
             new_file_ref = std::unique_ptr<FileReference>(f->Clone(&master_table_->string_pool));
           }
-
-          if (callback) {
-            if (!callback(res_name, src_config_value->config, new_file_ref.get(), f)) {
-              error = true;
-              continue;
-            }
-          }
           dst_config_value->value = std::move(new_file_ref);
 
         } else {
@@ -343,8 +298,8 @@
       ->FindOrCreateValue(file_desc.config, {})
       ->value = std::move(file_ref);
 
-  return DoMerge(file->GetSource(), &table, pkg, false /* mangle */, overlay /* overlay */,
-                 true /* allow_new */, {});
+  return DoMerge(file->GetSource(), &table, pkg, false /*mangle*/, overlay /*overlay*/,
+                 true /*allow_new*/);
 }
 
 }  // namespace aapt
diff --git a/tools/aapt2/link/TableMerger.h b/tools/aapt2/link/TableMerger.h
index d024aa4..47e23dd 100644
--- a/tools/aapt2/link/TableMerger.h
+++ b/tools/aapt2/link/TableMerger.h
@@ -40,6 +40,9 @@
 // TableMerger takes resource tables and merges all packages within the tables that have the same
 // package ID.
 //
+// It is assumed that any FileReference values have their io::IFile pointer set to point to the
+// file they represent.
+//
 // If a package has a different name, all the entries in that table have their names mangled
 // to include the package name. This way there are no collisions. In order to do this correctly,
 // the TableMerger needs to also mangle any FileReference paths. Once these are mangled, the
@@ -60,14 +63,11 @@
 
   // Merges resources from the same or empty package. This is for local sources.
   // If overlay is true, the resources are treated as overlays.
-  // An io::IFileCollection is optional and used to find the referenced Files and process them.
-  bool Merge(const Source& src, ResourceTable* table, bool overlay,
-             io::IFileCollection* collection = nullptr);
+  bool Merge(const Source& src, ResourceTable* table, bool overlay);
 
   // Merges resources from the given package, mangling the name. This is for static libraries.
-  // An io::IFileCollection is needed in order to find the referenced Files and process them.
-  bool MergeAndMangle(const Source& src, const android::StringPiece& package, ResourceTable* table,
-                      io::IFileCollection* collection);
+  // All FileReference values must have their io::IFile set.
+  bool MergeAndMangle(const Source& src, const android::StringPiece& package, ResourceTable* table);
 
   // Merges a compiled file that belongs to this same or empty package.
   bool MergeFile(const ResourceFile& fileDesc, bool overlay, io::IFile* file);
@@ -75,23 +75,16 @@
  private:
   DISALLOW_COPY_AND_ASSIGN(TableMerger);
 
-  using FileMergeCallback = std::function<bool(const ResourceNameRef&,
-                                               const ConfigDescription& config,
-                                               FileReference*, FileReference*)>;
-
   IAaptContext* context_;
   ResourceTable* master_table_;
   TableMergerOptions options_;
   ResourceTablePackage* master_package_;
   std::set<std::string> merged_packages_;
 
-  bool MergeImpl(const Source& src, ResourceTable* src_table,
-                 io::IFileCollection* collection, bool overlay, bool allow_new);
+  bool MergeImpl(const Source& src, ResourceTable* src_table, bool overlay, bool allow_new);
 
-  bool DoMerge(const Source& src, ResourceTable* src_table,
-               ResourceTablePackage* src_package, const bool mangle_package,
-               const bool overlay, const bool allow_new_resources,
-               const FileMergeCallback& callback);
+  bool DoMerge(const Source& src, ResourceTable* src_table, ResourceTablePackage* src_package,
+               bool mangle_package, bool overlay, bool allow_new_resources);
 
   std::unique_ptr<FileReference> CloneAndMangleFile(const std::string& package,
                                                     const FileReference& value);
diff --git a/tools/aapt2/link/TableMerger_test.cpp b/tools/aapt2/link/TableMerger_test.cpp
index 3499809..6aab8de 100644
--- a/tools/aapt2/link/TableMerger_test.cpp
+++ b/tools/aapt2/link/TableMerger_test.cpp
@@ -22,11 +22,12 @@
 
 using ::aapt::test::ValueEq;
 using ::testing::Contains;
-using ::testing::NotNull;
-using ::testing::UnorderedElementsAreArray;
-using ::testing::Pointee;
-using ::testing::Field;
 using ::testing::Eq;
+using ::testing::Field;
+using ::testing::NotNull;
+using ::testing::Pointee;
+using ::testing::StrEq;
+using ::testing::UnorderedElementsAreArray;
 
 namespace aapt {
 
@@ -67,10 +68,9 @@
 
   ResourceTable final_table;
   TableMerger merger(context_.get(), &final_table, TableMergerOptions{});
-  io::FileCollection collection;
 
   ASSERT_TRUE(merger.Merge({}, table_a.get(), false /*overlay*/));
-  ASSERT_TRUE(merger.MergeAndMangle({}, "com.app.b", table_b.get(), &collection));
+  ASSERT_TRUE(merger.MergeAndMangle({}, "com.app.b", table_b.get()));
 
   EXPECT_TRUE(merger.merged_packages().count("com.app.b") != 0);
 
@@ -122,32 +122,35 @@
 }
 
 TEST_F(TableMergerTest, MergeFileReferences) {
+  test::TestFile file_a("res/xml/file.xml");
+  test::TestFile file_b("res/xml/file.xml");
+
   std::unique_ptr<ResourceTable> table_a =
       test::ResourceTableBuilder()
           .SetPackageId("com.app.a", 0x7f)
-          .AddFileReference("com.app.a:xml/file", "res/xml/file.xml")
+          .AddFileReference("com.app.a:xml/file", "res/xml/file.xml", &file_a)
           .Build();
   std::unique_ptr<ResourceTable> table_b =
       test::ResourceTableBuilder()
           .SetPackageId("com.app.b", 0x7f)
-          .AddFileReference("com.app.b:xml/file", "res/xml/file.xml")
+          .AddFileReference("com.app.b:xml/file", "res/xml/file.xml", &file_b)
           .Build();
 
   ResourceTable final_table;
   TableMerger merger(context_.get(), &final_table, TableMergerOptions{});
-  io::FileCollection collection;
-  collection.InsertFile("res/xml/file.xml");
 
   ASSERT_TRUE(merger.Merge({}, table_a.get(), false /*overlay*/));
-  ASSERT_TRUE(merger.MergeAndMangle({}, "com.app.b", table_b.get(), &collection));
+  ASSERT_TRUE(merger.MergeAndMangle({}, "com.app.b", table_b.get()));
 
   FileReference* f = test::GetValue<FileReference>(&final_table, "com.app.a:xml/file");
   ASSERT_THAT(f, NotNull());
-  EXPECT_EQ(std::string("res/xml/file.xml"), *f->path);
+  EXPECT_THAT(*f->path, StrEq("res/xml/file.xml"));
+  EXPECT_THAT(f->file, Eq(&file_a));
 
   f = test::GetValue<FileReference>(&final_table, "com.app.a:xml/com.app.b$file");
   ASSERT_THAT(f, NotNull());
-  EXPECT_EQ(std::string("res/xml/com.app.b$file.xml"), *f->path);
+  EXPECT_THAT(*f->path, StrEq("res/xml/com.app.b$file.xml"));
+  EXPECT_THAT(f->file, Eq(&file_b));
 }
 
 TEST_F(TableMergerTest, OverrideResourceWithOverlay) {
diff --git a/tools/aapt2/optimize/MultiApkGenerator.cpp b/tools/aapt2/optimize/MultiApkGenerator.cpp
index 6803088..473693c 100644
--- a/tools/aapt2/optimize/MultiApkGenerator.cpp
+++ b/tools/aapt2/optimize/MultiApkGenerator.cpp
@@ -265,12 +265,14 @@
                                        const PostProcessingConfiguration& config,
                                        std::unique_ptr<XmlResource>* updated_manifest,
                                        IDiagnostics* diag) {
-  *updated_manifest = apk_->InflateManifest(context_);
-  XmlResource* manifest = updated_manifest->get();
-  if (manifest == nullptr) {
+  const xml::XmlResource* apk_manifest = apk_->GetManifest();
+  if (apk_manifest == nullptr) {
     return false;
   }
 
+  *updated_manifest = apk_manifest->Clone();
+  XmlResource* manifest = updated_manifest->get();
+
   // Make sure the first element is <manifest> with package attribute.
   xml::Element* manifest_el = manifest->root.get();
   if (manifest_el == nullptr) {
diff --git a/tools/aapt2/optimize/MultiApkGenerator_test.cpp b/tools/aapt2/optimize/MultiApkGenerator_test.cpp
index c8f3524..30c9146 100644
--- a/tools/aapt2/optimize/MultiApkGenerator_test.cpp
+++ b/tools/aapt2/optimize/MultiApkGenerator_test.cpp
@@ -106,7 +106,7 @@
 TEST_F(MultiApkGeneratorTest, VersionFilterNewerVersion) {
   std::unique_ptr<ResourceTable> table = BuildTable();
 
-  LoadedApk apk = {{"test.apk"}, {}, std::move(table)};
+  LoadedApk apk = {{"test.apk"}, {}, std::move(table), {}};
   std::unique_ptr<IAaptContext> ctx = test::ContextBuilder().SetMinSdkVersion(19).Build();
   PostProcessingConfiguration empty_config;
   TableFlattenerOptions table_flattener_options;
@@ -147,7 +147,7 @@
 TEST_F(MultiApkGeneratorTest, VersionFilterOlderVersion) {
   std::unique_ptr<ResourceTable> table = BuildTable();
 
-  LoadedApk apk = {{"test.apk"}, {}, std::move(table)};
+  LoadedApk apk = {{"test.apk"}, {}, std::move(table), {}};
   std::unique_ptr<IAaptContext> ctx = test::ContextBuilder().SetMinSdkVersion(1).Build();
   PostProcessingConfiguration empty_config;
   TableFlattenerOptions table_flattener_options;
@@ -186,7 +186,7 @@
 TEST_F(MultiApkGeneratorTest, VersionFilterNoVersion) {
   std::unique_ptr<ResourceTable> table = BuildTable();
 
-  LoadedApk apk = {{"test.apk"}, {}, std::move(table)};
+  LoadedApk apk = {{"test.apk"}, {}, std::move(table), {}};
   std::unique_ptr<IAaptContext> ctx = test::ContextBuilder().SetMinSdkVersion(1).Build();
   PostProcessingConfiguration empty_config;
   TableFlattenerOptions table_flattener_options;
diff --git a/tools/aapt2/test/Builders.cpp b/tools/aapt2/test/Builders.cpp
index 5a62e97..ecec63f 100644
--- a/tools/aapt2/test/Builders.cpp
+++ b/tools/aapt2/test/Builders.cpp
@@ -78,21 +78,27 @@
 }
 
 ResourceTableBuilder& ResourceTableBuilder::AddFileReference(const StringPiece& name,
-                                                             const StringPiece& path) {
-  return AddFileReference(name, {}, path);
+                                                             const StringPiece& path,
+                                                             io::IFile* file) {
+  return AddFileReference(name, {}, path, file);
 }
 
 ResourceTableBuilder& ResourceTableBuilder::AddFileReference(const StringPiece& name,
                                                              const ResourceId& id,
-                                                             const StringPiece& path) {
-  return AddValue(name, id, util::make_unique<FileReference>(table_->string_pool.MakeRef(path)));
+                                                             const StringPiece& path,
+                                                             io::IFile* file) {
+  auto file_ref = util::make_unique<FileReference>(table_->string_pool.MakeRef(path));
+  file_ref->file = file;
+  return AddValue(name, id, std::move(file_ref));
 }
 
 ResourceTableBuilder& ResourceTableBuilder::AddFileReference(const StringPiece& name,
                                                              const StringPiece& path,
-                                                             const ConfigDescription& config) {
-  return AddValue(name, config, {},
-                  util::make_unique<FileReference>(table_->string_pool.MakeRef(path)));
+                                                             const ConfigDescription& config,
+                                                             io::IFile* file) {
+  auto file_ref = util::make_unique<FileReference>(table_->string_pool.MakeRef(path));
+  file_ref->file = file;
+  return AddValue(name, config, {}, std::move(file_ref));
 }
 
 ResourceTableBuilder& ResourceTableBuilder::AddValue(const StringPiece& name,
diff --git a/tools/aapt2/test/Builders.h b/tools/aapt2/test/Builders.h
index 263fb55..4cdfc33 100644
--- a/tools/aapt2/test/Builders.h
+++ b/tools/aapt2/test/Builders.h
@@ -52,12 +52,15 @@
   ResourceTableBuilder& AddString(const android::StringPiece& name, const ResourceId& id,
                                   const ConfigDescription& config, const android::StringPiece& str);
   ResourceTableBuilder& AddFileReference(const android::StringPiece& name,
-                                         const android::StringPiece& path);
+                                         const android::StringPiece& path,
+                                         io::IFile* file = nullptr);
   ResourceTableBuilder& AddFileReference(const android::StringPiece& name, const ResourceId& id,
-                                         const android::StringPiece& path);
+                                         const android::StringPiece& path,
+                                         io::IFile* file = nullptr);
   ResourceTableBuilder& AddFileReference(const android::StringPiece& name,
                                          const android::StringPiece& path,
-                                         const ConfigDescription& config);
+                                         const ConfigDescription& config,
+                                         io::IFile* file = nullptr);
   ResourceTableBuilder& AddValue(const android::StringPiece& name, std::unique_ptr<Value> value);
   ResourceTableBuilder& AddValue(const android::StringPiece& name, const ResourceId& id,
                                  std::unique_ptr<Value> value);
diff --git a/tools/aapt2/xml/XmlDom.cpp b/tools/aapt2/xml/XmlDom.cpp
index 3522506..b0cf44a 100644
--- a/tools/aapt2/xml/XmlDom.cpp
+++ b/tools/aapt2/xml/XmlDom.cpp
@@ -258,8 +258,7 @@
   }
 }
 
-std::unique_ptr<XmlResource> Inflate(const void* data, size_t data_len, IDiagnostics* diag,
-                                     const Source& source) {
+std::unique_ptr<XmlResource> Inflate(const void* data, size_t len, std::string* out_error) {
   // We import the android namespace because on Windows NO_ERROR is a macro, not
   // an enum, which causes errors when qualifying it with android::
   using namespace android;
@@ -270,7 +269,10 @@
   std::unique_ptr<Element> pending_element;
 
   ResXMLTree tree;
-  if (tree.setTo(data, data_len) != NO_ERROR) {
+  if (tree.setTo(data, len) != NO_ERROR) {
+    if (out_error != nullptr) {
+      *out_error = "failed to initialize ResXMLTree";
+    }
     return {};
   }
 
@@ -361,6 +363,27 @@
   return util::make_unique<XmlResource>(ResourceFile{}, std::move(string_pool), std::move(root));
 }
 
+std::unique_ptr<XmlResource> XmlResource::Clone() const {
+  std::unique_ptr<XmlResource> cloned = util::make_unique<XmlResource>(file);
+  if (root != nullptr) {
+    cloned->root = root->CloneElement([&](const xml::Element& src, xml::Element* dst) {
+      dst->attributes.reserve(src.attributes.size());
+      for (const xml::Attribute& attr : src.attributes) {
+        xml::Attribute cloned_attr;
+        cloned_attr.name = attr.name;
+        cloned_attr.namespace_uri = attr.namespace_uri;
+        cloned_attr.value = attr.value;
+        cloned_attr.compiled_attribute = attr.compiled_attribute;
+        if (attr.compiled_value != nullptr) {
+          cloned_attr.compiled_value.reset(attr.compiled_value->Clone(&cloned->string_pool));
+        }
+        dst->attributes.push_back(std::move(cloned_attr));
+      }
+    });
+  }
+  return cloned;
+}
+
 Element* FindRootElement(Node* node) {
   if (node == nullptr) {
     return nullptr;
@@ -383,12 +406,7 @@
 }
 
 Attribute* Element::FindAttribute(const StringPiece& ns, const StringPiece& name) {
-  for (auto& attr : attributes) {
-    if (ns == attr.namespace_uri && name == attr.name) {
-      return &attr;
-    }
-  }
-  return nullptr;
+  return const_cast<Attribute*>(static_cast<const Element*>(this)->FindAttribute(ns, name));
 }
 
 const Attribute* Element::FindAttribute(const StringPiece& ns, const StringPiece& name) const {
@@ -404,17 +422,29 @@
   return FindChildWithAttribute(ns, name, {}, {}, {});
 }
 
+const Element* Element::FindChild(const StringPiece& ns, const StringPiece& name) const {
+  return FindChildWithAttribute(ns, name, {}, {}, {});
+}
+
 Element* Element::FindChildWithAttribute(const StringPiece& ns, const StringPiece& name,
                                          const StringPiece& attr_ns, const StringPiece& attr_name,
                                          const StringPiece& attr_value) {
-  for (auto& child : children) {
-    if (Element* el = NodeCast<Element>(child.get())) {
+  return const_cast<Element*>(static_cast<const Element*>(this)->FindChildWithAttribute(
+      ns, name, attr_ns, attr_name, attr_value));
+}
+
+const Element* Element::FindChildWithAttribute(const StringPiece& ns, const StringPiece& name,
+                                               const StringPiece& attr_ns,
+                                               const StringPiece& attr_name,
+                                               const StringPiece& attr_value) const {
+  for (const auto& child : children) {
+    if (const Element* el = NodeCast<Element>(child.get())) {
       if (ns == el->namespace_uri && name == el->name) {
         if (attr_ns.empty() && attr_name.empty()) {
           return el;
         }
 
-        Attribute* attr = el->FindAttribute(attr_ns, attr_name);
+        const Attribute* attr = el->FindAttribute(attr_ns, attr_name);
         if (attr && attr_value == attr->value) {
           return el;
         }
diff --git a/tools/aapt2/xml/XmlDom.h b/tools/aapt2/xml/XmlDom.h
index 063d7b9..cf06ba5 100644
--- a/tools/aapt2/xml/XmlDom.h
+++ b/tools/aapt2/xml/XmlDom.h
@@ -100,11 +100,21 @@
   Attribute* FindAttribute(const android::StringPiece& ns, const android::StringPiece& name);
   const Attribute* FindAttribute(const android::StringPiece& ns,
                                  const android::StringPiece& name) const;
+
   Element* FindChild(const android::StringPiece& ns, const android::StringPiece& name);
+  const Element* FindChild(const android::StringPiece& ns, const android::StringPiece& name) const;
+
   Element* FindChildWithAttribute(const android::StringPiece& ns, const android::StringPiece& name,
                                   const android::StringPiece& attr_ns,
                                   const android::StringPiece& attr_name,
                                   const android::StringPiece& attr_value);
+
+  const Element* FindChildWithAttribute(const android::StringPiece& ns,
+                                        const android::StringPiece& name,
+                                        const android::StringPiece& attr_ns,
+                                        const android::StringPiece& attr_name,
+                                        const android::StringPiece& attr_value) const;
+
   std::vector<Element*> GetChildElements();
 
   // Due to overriding of subtypes not working with unique_ptr, define a convenience Clone method
@@ -139,16 +149,16 @@
   StringPool string_pool;
 
   std::unique_ptr<xml::Element> root;
+
+  std::unique_ptr<XmlResource> Clone() const;
 };
 
 // Inflates an XML DOM from an InputStream, logging errors to the logger.
-// Returns the root node on success, or nullptr on failure.
 std::unique_ptr<XmlResource> Inflate(io::InputStream* in, IDiagnostics* diag, const Source& source);
 
-// Inflates an XML DOM from a binary ResXMLTree, logging errors to the logger.
-// Returns the root node on success, or nullptr on failure.
-std::unique_ptr<XmlResource> Inflate(const void* data, size_t data_len, IDiagnostics* diag,
-                                     const Source& source);
+// Inflates an XML DOM from a binary ResXMLTree.
+std::unique_ptr<XmlResource> Inflate(const void* data, size_t len,
+                                     std::string* out_error = nullptr);
 
 Element* FindRootElement(Node* node);
 
diff --git a/tools/aapt2/xml/XmlDom_test.cpp b/tools/aapt2/xml/XmlDom_test.cpp
index 34e6d3f..e5012d6 100644
--- a/tools/aapt2/xml/XmlDom_test.cpp
+++ b/tools/aapt2/xml/XmlDom_test.cpp
@@ -70,8 +70,7 @@
   ASSERT_TRUE(flattener.Consume(context.get(), doc.get()));
 
   auto block = util::Copy(buffer);
-  std::unique_ptr<XmlResource> new_doc =
-      Inflate(block.get(), buffer.size(), context->GetDiagnostics(), Source("test.xml"));
+  std::unique_ptr<XmlResource> new_doc = Inflate(block.get(), buffer.size(), nullptr);
   ASSERT_THAT(new_doc, NotNull());
 
   EXPECT_THAT(new_doc->root->name, StrEq("Layout"));