Merge "Prevents NPE after stack got removed from display"
diff --git a/api/current.txt b/api/current.txt
index 9a3c681..de5a0b1 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -10238,6 +10238,7 @@
     field public static final java.lang.String EXTRA_ASSIST_INPUT_HINT_KEYBOARD = "android.intent.extra.ASSIST_INPUT_HINT_KEYBOARD";
     field public static final java.lang.String EXTRA_ASSIST_PACKAGE = "android.intent.extra.ASSIST_PACKAGE";
     field public static final java.lang.String EXTRA_ASSIST_UID = "android.intent.extra.ASSIST_UID";
+    field public static final java.lang.String EXTRA_AUTO_LAUNCH_SINGLE_CHOICE = "android.intent.extra.AUTO_LAUNCH_SINGLE_CHOICE";
     field public static final java.lang.String EXTRA_BCC = "android.intent.extra.BCC";
     field public static final java.lang.String EXTRA_BUG_REPORT = "android.intent.extra.BUG_REPORT";
     field public static final java.lang.String EXTRA_CC = "android.intent.extra.CC";
@@ -23396,6 +23397,7 @@
     method public int getStreamType();
     method public boolean getTimestamp(android.media.AudioTimestamp);
     method public int getUnderrunCount();
+    method public static boolean isDirectPlaybackSupported(android.media.AudioFormat, android.media.AudioAttributes);
     method public void pause() throws java.lang.IllegalStateException;
     method public void play() throws java.lang.IllegalStateException;
     method public void registerStreamEventCallback(java.util.concurrent.Executor, android.media.AudioTrack.StreamEventCallback);
@@ -24753,6 +24755,7 @@
     field public static final java.lang.String MIMETYPE_AUDIO_AMR_NB = "audio/3gpp";
     field public static final java.lang.String MIMETYPE_AUDIO_AMR_WB = "audio/amr-wb";
     field public static final java.lang.String MIMETYPE_AUDIO_EAC3 = "audio/eac3";
+    field public static final java.lang.String MIMETYPE_AUDIO_EAC3_JOC = "audio/eac3-joc";
     field public static final java.lang.String MIMETYPE_AUDIO_FLAC = "audio/flac";
     field public static final java.lang.String MIMETYPE_AUDIO_G711_ALAW = "audio/g711-alaw";
     field public static final java.lang.String MIMETYPE_AUDIO_G711_MLAW = "audio/g711-mlaw";
diff --git a/config/hiddenapi-greylist.txt b/config/hiddenapi-greylist.txt
index 56dc4c1..bacb991 100644
--- a/config/hiddenapi-greylist.txt
+++ b/config/hiddenapi-greylist.txt
@@ -1639,10 +1639,6 @@
 Landroid/widget/QuickContactBadge$QueryHandler;-><init>(Landroid/widget/QuickContactBadge;Landroid/content/ContentResolver;)V
 Landroid/widget/RelativeLayout$DependencyGraph$Node;-><init>()V
 Landroid/widget/ScrollBarDrawable;-><init>()V
-Lcom/android/i18n/phonenumbers/Phonenumber$PhoneNumber$CountryCodeSource;->values()[Lcom/android/i18n/phonenumbers/Phonenumber$PhoneNumber$CountryCodeSource;
-Lcom/android/i18n/phonenumbers/PhoneNumberUtil$MatchType;->values()[Lcom/android/i18n/phonenumbers/PhoneNumberUtil$MatchType;
-Lcom/android/i18n/phonenumbers/PhoneNumberUtil$PhoneNumberFormat;->values()[Lcom/android/i18n/phonenumbers/PhoneNumberUtil$PhoneNumberFormat;
-Lcom/android/i18n/phonenumbers/PhoneNumberUtil$PhoneNumberType;->values()[Lcom/android/i18n/phonenumbers/PhoneNumberUtil$PhoneNumberType;
 Lcom/android/ims/ImsCall;->deflect(Ljava/lang/String;)V
 Lcom/android/ims/ImsCall;->isMultiparty()Z
 Lcom/android/ims/ImsCall;->reject(I)V
diff --git a/core/java/android/annotation/UnsupportedAppUsage.java b/core/java/android/annotation/UnsupportedAppUsage.java
index 65e3f25..ac3daaf 100644
--- a/core/java/android/annotation/UnsupportedAppUsage.java
+++ b/core/java/android/annotation/UnsupportedAppUsage.java
@@ -26,16 +26,32 @@
 import java.lang.annotation.Target;
 
 /**
- * Indicates that a class member, that is not part of the SDK, is used by apps.
- * Since the member is not part of the SDK, such use is not supported.
+ * Indicates that this non-SDK interface is used by apps. A non-SDK interface is a
+ * class member (field or method) that is not part of the public SDK. Since the
+ * member is not part of the SDK, usage by apps is not supported.
  *
- * <p>This annotation acts as a heads up that changing a given method or field
+ * <h2>If you are an Android App developer</h2>
+ *
+ * This annotation indicates that you may be able to access the member, but that
+ * this access is discouraged and not supported by Android. If there is a value
+ * for {@link #maxTargetSdk()} on the annotation, access will be restricted based
+ * on the {@code targetSdkVersion} value set in your manifest.
+ *
+ * <p>Fields and methods annotated with this are likely to be restricted, changed
+ * or removed in future Android releases. If you rely on these members for
+ * functionality that is not otherwise supported by Android, consider filing a
+ * <a href="http://g.co/dev/appcompat">feature request</a>.
+ *
+ * <h2>If you are an Android OS developer</h2>
+ *
+ * This annotation acts as a heads up that changing a given method or field
  * may affect apps, potentially breaking them when the next Android version is
  * released. In some cases, for members that are heavily used, this annotation
  * may imply restrictions on changes to the member.
  *
  * <p>This annotation also results in access to the member being permitted by the
- * runtime, with a warning being generated in debug builds.
+ * runtime, with a warning being generated in debug builds. Which apps can access
+ * the member is determined by the value of {@link #maxTargetSdk()}.
  *
  * <p>For more details, see go/UnsupportedAppUsage.
  *
diff --git a/core/java/android/content/Intent.java b/core/java/android/content/Intent.java
index b6f9d15..2f0618c 100644
--- a/core/java/android/content/Intent.java
+++ b/core/java/android/content/Intent.java
@@ -5267,8 +5267,6 @@
      * Used as a boolean extra field in {@link #ACTION_CHOOSER} intents to specify
      * whether to show the chooser or not when there is only one application available
      * to choose from.
-     *
-     * @hide
      */
     public static final String EXTRA_AUTO_LAUNCH_SINGLE_CHOICE =
             "android.intent.extra.AUTO_LAUNCH_SINGLE_CHOICE";
diff --git a/core/java/android/content/pm/SharedLibraryNames.java b/core/java/android/content/pm/SharedLibraryNames.java
index 5afc8a9..a607a9f 100644
--- a/core/java/android/content/pm/SharedLibraryNames.java
+++ b/core/java/android/content/pm/SharedLibraryNames.java
@@ -22,15 +22,15 @@
  */
 public class SharedLibraryNames {
 
-    public static final String ANDROID_HIDL_BASE = "android.hidl.base-V1.0-java";
+    static final String ANDROID_HIDL_BASE = "android.hidl.base-V1.0-java";
 
-    public static final String ANDROID_HIDL_MANAGER = "android.hidl.manager-V1.0-java";
+    static final String ANDROID_HIDL_MANAGER = "android.hidl.manager-V1.0-java";
 
-    public static final String ANDROID_TEST_BASE = "android.test.base";
+    static final String ANDROID_TEST_BASE = "android.test.base";
 
-    public static final String ANDROID_TEST_MOCK = "android.test.mock";
+    static final String ANDROID_TEST_MOCK = "android.test.mock";
 
-    public static final String ANDROID_TEST_RUNNER = "android.test.runner";
+    static final String ANDROID_TEST_RUNNER = "android.test.runner";
 
     public static final String ORG_APACHE_HTTP_LEGACY = "org.apache.http.legacy";
 }
diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java
index de6afcb..9380695 100644
--- a/core/java/android/provider/Settings.java
+++ b/core/java/android/provider/Settings.java
@@ -12852,6 +12852,13 @@
         public static final String HIDDEN_API_POLICY = "hidden_api_policy";
 
         /**
+         * Current version of signed configuration applied.
+         *
+         * @hide
+         */
+        public static final String SIGNED_CONFIG_VERSION = "signed_config_version";
+
+        /**
          * Timeout for a single {@link android.media.soundtrigger.SoundTriggerDetectionService}
          * operation (in ms).
          *
diff --git a/core/java/com/android/server/SystemConfig.java b/core/java/com/android/server/SystemConfig.java
index b97a9fa..b00e6fd 100644
--- a/core/java/com/android/server/SystemConfig.java
+++ b/core/java/com/android/server/SystemConfig.java
@@ -25,7 +25,6 @@
 import android.os.Build;
 import android.os.Environment;
 import android.os.Process;
-import android.os.SystemProperties;
 import android.os.storage.StorageManager;
 import android.permission.PermissionManager.SplitPermissionInfo;
 import android.text.TextUtils;
@@ -78,10 +77,23 @@
 
     final ArrayList<SplitPermissionInfo> mSplitPermissions = new ArrayList<>();
 
+    public static final class SharedLibraryEntry {
+        public final String name;
+        public final String filename;
+        public final String[] dependencies;
+
+        SharedLibraryEntry(String name, String filename, String[] dependencies) {
+            this.name = name;
+            this.filename = filename;
+            this.dependencies = dependencies;
+        }
+    }
+
     // These are the built-in shared libraries that were read from the
-    // system configuration files.  Keys are the library names; strings are the
-    // paths to the libraries.
-    final ArrayMap<String, String> mSharedLibraries  = new ArrayMap<>();
+    // system configuration files. Keys are the library names; values are
+    // the individual entries that contain information such as filename
+    // and dependencies.
+    final ArrayMap<String, SharedLibraryEntry> mSharedLibraries = new ArrayMap<>();
 
     // These are the features this devices supports that were read from the
     // system configuration files.
@@ -200,7 +212,7 @@
         return mSplitPermissions;
     }
 
-    public ArrayMap<String, String> getSharedLibraries() {
+    public ArrayMap<String, SharedLibraryEntry> getSharedLibraries() {
         return mSharedLibraries;
     }
 
@@ -497,6 +509,7 @@
                 } else if ("library".equals(name) && allowLibs) {
                     String lname = parser.getAttributeValue(null, "name");
                     String lfile = parser.getAttributeValue(null, "file");
+                    String ldependency = parser.getAttributeValue(null, "dependency");
                     if (lname == null) {
                         Slog.w(TAG, "<library> without name in " + permFile + " at "
                                 + parser.getPositionDescription());
@@ -505,11 +518,12 @@
                                 + parser.getPositionDescription());
                     } else {
                         //Log.i(TAG, "Got library " + lname + " in " + lfile);
-                        mSharedLibraries.put(lname, lfile);
+                        SharedLibraryEntry entry = new SharedLibraryEntry(lname, lfile,
+                                ldependency == null ? new String[0] : ldependency.split(":"));
+                        mSharedLibraries.put(lname, entry);
                     }
                     XmlUtils.skipCurrentTag(parser);
                     continue;
-
                 } else if ("feature".equals(name) && allowFeatures) {
                     String fname = parser.getAttributeValue(null, "name");
                     int fversion = XmlUtils.readIntAttribute(parser, "version", 0);
diff --git a/core/jni/android_media_AudioTrack.cpp b/core/jni/android_media_AudioTrack.cpp
index 7ebb2b2..516093e 100644
--- a/core/jni/android_media_AudioTrack.cpp
+++ b/core/jni/android_media_AudioTrack.cpp
@@ -479,6 +479,24 @@
 }
 
 // ----------------------------------------------------------------------------
+static jboolean
+android_media_AudioTrack_is_direct_output_supported(JNIEnv *env, jobject thiz,
+                                             jint encoding, jint sampleRate,
+                                             jint channelMask, jint channelIndexMask,
+                                             jint contentType, jint usage, jint flags) {
+    audio_config_base_t config = {};
+    audio_attributes_t attributes = {};
+    config.format = static_cast<audio_format_t>(audioFormatToNative(encoding));
+    config.sample_rate = static_cast<uint32_t>(sampleRate);
+    config.channel_mask = nativeChannelMaskFromJavaChannelMasks(channelMask, channelIndexMask);
+    attributes.content_type = static_cast<audio_content_type_t>(contentType);
+    attributes.usage = static_cast<audio_usage_t>(usage);
+    attributes.flags = static_cast<audio_flags_mask_t>(flags);
+    // ignore source and tags attributes as they don't affect querying whether output is supported
+    return AudioTrack::isDirectOutputSupported(config, attributes);
+}
+
+// ----------------------------------------------------------------------------
 static void
 android_media_AudioTrack_start(JNIEnv *env, jobject thiz)
 {
@@ -1297,6 +1315,9 @@
 // ----------------------------------------------------------------------------
 static const JNINativeMethod gMethods[] = {
     // name,              signature,     funcPtr
+    {"native_is_direct_output_supported",
+                             "(IIIIIII)Z",
+                                         (void *)android_media_AudioTrack_is_direct_output_supported},
     {"native_start",         "()V",      (void *)android_media_AudioTrack_start},
     {"native_stop",          "()V",      (void *)android_media_AudioTrack_stop},
     {"native_pause",         "()V",      (void *)android_media_AudioTrack_pause},
diff --git a/core/proto/android/stats/devicepolicy/device_policy_enums.proto b/core/proto/android/stats/devicepolicy/device_policy_enums.proto
index 5726d9a..c7a6b68 100644
--- a/core/proto/android/stats/devicepolicy/device_policy_enums.proto
+++ b/core/proto/android/stats/devicepolicy/device_policy_enums.proto
@@ -51,7 +51,7 @@
   SET_ALWAYS_ON_VPN_PACKAGE = 26;
   SET_PERMITTED_INPUT_METHODS = 27;
   SET_PERMITTED_ACCESSIBILITY_SERVICES = 28;
-  SET_SCREEN_CAPTURE_DISABLE = 29;
+  SET_SCREEN_CAPTURE_DISABLED = 29;
   SET_CAMERA_DISABLED = 30;
   QUERY_SUMMARY_FOR_USER = 31;
   QUERY_SUMMARY = 32;
@@ -64,7 +64,7 @@
   SET_ORGANIZATION_COLOR = 39;
   SET_PROFILE_NAME = 40;
   SET_USER_ICON = 41;
-  SET_DEVICE_OWNER_LOCKSCREEN_INFO = 42;
+  SET_DEVICE_OWNER_LOCK_SCREEN_INFO = 42;
   SET_SHORT_SUPPORT_MESSAGE = 43;
   SET_LONG_SUPPORT_MESSAGE = 44;
   SET_CROSS_PROFILE_CONTACTS_SEARCH_DISABLED = 45;
@@ -139,4 +139,6 @@
   SET_GLOBAL_SETTING = 111;
   PM_IS_INSTALLER_DEVICE_OWNER_OR_AFFILIATED_PROFILE_OWNER = 112;
   PM_UNINSTALL = 113;
+  WIFI_SERVICE_ADD_NETWORK_SUGGESTIONS = 114;
+  WIFI_SERVICE_ADD_OR_UPDATE_NETWORK = 115;
 }
diff --git a/core/tests/coretests/src/android/provider/SettingsBackupTest.java b/core/tests/coretests/src/android/provider/SettingsBackupTest.java
index 49a7555..3a37fb6 100644
--- a/core/tests/coretests/src/android/provider/SettingsBackupTest.java
+++ b/core/tests/coretests/src/android/provider/SettingsBackupTest.java
@@ -415,6 +415,7 @@
                     Settings.Global.SHOW_NOTIFICATION_CHANNEL_WARNINGS,
                     Settings.Global.SHOW_RESTART_IN_CRASH_DIALOG,
                     Settings.Global.SHOW_TEMPERATURE_WARNING,
+                    Settings.Global.SIGNED_CONFIG_VERSION,
                     Settings.Global.SMART_SELECTION_UPDATE_CONTENT_URL,
                     Settings.Global.SMART_SELECTION_UPDATE_METADATA_URL,
                     Settings.Global.SMS_ACCESS_RESTRICTION_ENABLED,
diff --git a/core/tests/coretests/src/android/security/keystore/recovery/KeyChainSnapshotTest.java b/core/tests/coretests/src/android/security/keystore/recovery/KeyChainSnapshotTest.java
index 8d6fbd5..61ab152 100644
--- a/core/tests/coretests/src/android/security/keystore/recovery/KeyChainSnapshotTest.java
+++ b/core/tests/coretests/src/android/security/keystore/recovery/KeyChainSnapshotTest.java
@@ -28,7 +28,8 @@
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
-// TODO(b/73862682): Add tests for RecoveryCertPath
+import java.security.cert.CertPath;
+
 @RunWith(AndroidJUnit4.class)
 @SmallTest
 public class KeyChainSnapshotTest {
@@ -43,35 +44,41 @@
     private static final int USER_SECRET_TYPE = KeyChainProtectionParams.TYPE_LOCKSCREEN;
     private static final String KEY_ALIAS = "steph";
     private static final byte[] KEY_MATERIAL = new byte[] { 3, 5, 7, 9, 1 };
+    private static final CertPath CERT_PATH = TestData.getThmCertPath();
 
     @Test
-    public void build_setsCounterId() {
+    public void build_setsCounterId() throws Exception {
         assertEquals(COUNTER_ID, createKeyChainSnapshot().getCounterId());
     }
 
     @Test
-    public void build_setsSnapshotVersion() {
+    public void build_setsSnapshotVersion() throws Exception {
         assertEquals(SNAPSHOT_VERSION, createKeyChainSnapshot().getSnapshotVersion());
     }
 
     @Test
-    public void build_setsMaxAttempts() {
+    public void build_setsMaxAttempts() throws Exception {
         assertEquals(MAX_ATTEMPTS, createKeyChainSnapshot().getMaxAttempts());
     }
 
     @Test
-    public void build_setsServerParams() {
+    public void build_setsServerParams() throws Exception {
         assertArrayEquals(SERVER_PARAMS, createKeyChainSnapshot().getServerParams());
     }
 
     @Test
-    public void build_setsRecoveryKeyBlob() {
+    public void build_setsRecoveryKeyBlob() throws Exception {
         assertArrayEquals(RECOVERY_KEY_BLOB,
                 createKeyChainSnapshot().getEncryptedRecoveryKeyBlob());
     }
 
     @Test
-    public void build_setsKeyChainProtectionParams() {
+    public void build_setsCertPath() throws Exception {
+        assertEquals(CERT_PATH, createKeyChainSnapshot().getTrustedHardwareCertPath());
+    }
+
+    @Test
+    public void build_setsKeyChainProtectionParams() throws Exception {
         KeyChainSnapshot snapshot = createKeyChainSnapshot();
 
         assertEquals(1, snapshot.getKeyChainProtectionParams().size());
@@ -85,7 +92,7 @@
     }
 
     @Test
-    public void build_setsWrappedApplicationKeys() {
+    public void build_setsWrappedApplicationKeys() throws Exception {
         KeyChainSnapshot snapshot = createKeyChainSnapshot();
 
         assertEquals(1, snapshot.getWrappedApplicationKeys().size());
@@ -95,42 +102,49 @@
     }
 
     @Test
-    public void writeToParcel_writesCounterId() {
+    public void writeToParcel_writesCounterId() throws Exception {
         KeyChainSnapshot snapshot = writeToThenReadFromParcel(createKeyChainSnapshot());
 
         assertEquals(COUNTER_ID, snapshot.getCounterId());
     }
 
     @Test
-    public void writeToParcel_writesSnapshotVersion() {
+    public void writeToParcel_writesSnapshotVersion() throws Exception {
         KeyChainSnapshot snapshot = writeToThenReadFromParcel(createKeyChainSnapshot());
 
         assertEquals(SNAPSHOT_VERSION, snapshot.getSnapshotVersion());
     }
 
     @Test
-    public void writeToParcel_writesMaxAttempts() {
+    public void writeToParcel_writesMaxAttempts() throws Exception {
         KeyChainSnapshot snapshot = writeToThenReadFromParcel(createKeyChainSnapshot());
 
         assertEquals(MAX_ATTEMPTS, snapshot.getMaxAttempts());
     }
 
     @Test
-    public void writeToParcel_writesServerParams() {
+    public void writeToParcel_writesServerParams() throws Exception {
         KeyChainSnapshot snapshot = writeToThenReadFromParcel(createKeyChainSnapshot());
 
         assertArrayEquals(SERVER_PARAMS, snapshot.getServerParams());
     }
 
     @Test
-    public void writeToParcel_writesKeyRecoveryBlob() {
+    public void writeToParcel_writesKeyRecoveryBlob() throws Exception {
         KeyChainSnapshot snapshot = writeToThenReadFromParcel(createKeyChainSnapshot());
 
         assertArrayEquals(RECOVERY_KEY_BLOB, snapshot.getEncryptedRecoveryKeyBlob());
     }
 
     @Test
-    public void writeToParcel_writesKeyChainProtectionParams() {
+    public void writeToParcel_writesCertPath() throws Exception {
+        KeyChainSnapshot snapshot = writeToThenReadFromParcel(createKeyChainSnapshot());
+
+        assertEquals(CERT_PATH, snapshot.getTrustedHardwareCertPath());
+    }
+
+    @Test
+    public void writeToParcel_writesKeyChainProtectionParams() throws Exception {
         KeyChainSnapshot snapshot = writeToThenReadFromParcel(createKeyChainSnapshot());
 
         assertEquals(1, snapshot.getKeyChainProtectionParams().size());
@@ -144,7 +158,7 @@
     }
 
     @Test
-    public void writeToParcel_writesWrappedApplicationKeys() {
+    public void writeToParcel_writesWrappedApplicationKeys() throws Exception {
         KeyChainSnapshot snapshot = writeToThenReadFromParcel(createKeyChainSnapshot());
 
         assertEquals(1, snapshot.getWrappedApplicationKeys().size());
@@ -153,7 +167,7 @@
         assertArrayEquals(KEY_MATERIAL, wrappedApplicationKey.getEncryptedKeyMaterial());
     }
 
-    private static KeyChainSnapshot createKeyChainSnapshot() {
+    private static KeyChainSnapshot createKeyChainSnapshot() throws Exception {
         return new KeyChainSnapshot.Builder()
                 .setCounterId(COUNTER_ID)
                 .setSnapshotVersion(SNAPSHOT_VERSION)
@@ -162,6 +176,7 @@
                 .setEncryptedRecoveryKeyBlob(RECOVERY_KEY_BLOB)
                 .setKeyChainProtectionParams(Lists.newArrayList(createKeyChainProtectionParams()))
                 .setWrappedApplicationKeys(Lists.newArrayList(createWrappedApplicationKey()))
+                .setTrustedHardwareCertPath(CERT_PATH)
                 .build();
     }
 
diff --git a/core/tests/coretests/src/android/security/keystore/recovery/RecoveryCertPathTest.java b/core/tests/coretests/src/android/security/keystore/recovery/RecoveryCertPathTest.java
new file mode 100644
index 0000000..dd8cd8d
--- /dev/null
+++ b/core/tests/coretests/src/android/security/keystore/recovery/RecoveryCertPathTest.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.security.keystore.recovery;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
+
+import android.os.Parcel;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.security.cert.CertificateException;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class RecoveryCertPathTest {
+
+    @Test
+    public void createRecoveryCertPath_getCertPath_succeeds() throws Exception {
+        RecoveryCertPath recoveryCertPath = RecoveryCertPath.createRecoveryCertPath(
+                TestData.getThmCertPath());
+        assertEquals(TestData.getThmCertPath(), recoveryCertPath.getCertPath());
+    }
+
+    @Test
+    public void getCertPath_throwsIfCannnotDecode() {
+        Parcel parcel = Parcel.obtain();
+        parcel.writeByteArray(new byte[]{0, 1, 2, 3});
+        parcel.setDataPosition(0);
+        RecoveryCertPath recoveryCertPath = RecoveryCertPath.CREATOR.createFromParcel(parcel);
+        parcel.recycle();
+
+        try {
+            recoveryCertPath.getCertPath();
+            fail("Did not throw when attempting to decode invalid cert path");
+        } catch (CertificateException e) {
+            // Expected
+        }
+    }
+
+    @Test
+    public void writeToParcel_writesCertPath() throws Exception {
+        RecoveryCertPath recoveryCertPath =
+                writeToThenReadFromParcel(
+                        RecoveryCertPath.createRecoveryCertPath(TestData.getThmCertPath()));
+        assertEquals(TestData.getThmCertPath(), recoveryCertPath.getCertPath());
+    }
+
+    private RecoveryCertPath writeToThenReadFromParcel(RecoveryCertPath recoveryCertPath) {
+        Parcel parcel = Parcel.obtain();
+        recoveryCertPath.writeToParcel(parcel, /*flags=*/ 0);
+        parcel.setDataPosition(0);
+        RecoveryCertPath fromParcel = RecoveryCertPath.CREATOR.createFromParcel(parcel);
+        parcel.recycle();
+        return fromParcel;
+    }
+}
diff --git a/core/tests/coretests/src/android/security/keystore/recovery/TestData.java b/core/tests/coretests/src/android/security/keystore/recovery/TestData.java
new file mode 100644
index 0000000..829a92a
--- /dev/null
+++ b/core/tests/coretests/src/android/security/keystore/recovery/TestData.java
@@ -0,0 +1,88 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.security.keystore.recovery;
+
+import java.io.ByteArrayInputStream;
+import java.security.cert.CertPath;
+import java.security.cert.CertificateFactory;
+import java.util.Base64;
+
+/** This class provides data for testing purposes. */
+class TestData {
+
+    private static final String THM_CERT_PATH_BASE64 = ""
+            + "MIIIXTCCBRowggMCoAMCAQICEB35ZwzVpI9ssXg9SAehnU0wDQYJKoZIhvcNAQEL"
+            + "BQAwMTEvMC0GA1UEAxMmR29vZ2xlIENsb3VkIEtleSBWYXVsdCBTZXJ2aWNlIFJv"
+            + "b3QgQ0EwHhcNMTgwNTA3MTg1ODEwWhcNMjgwNTA4MTg1ODEwWjA5MTcwNQYDVQQD"
+            + "Ey5Hb29nbGUgQ2xvdWQgS2V5IFZhdWx0IFNlcnZpY2UgSW50ZXJtZWRpYXRlIENB"
+            + "MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA73TrvH3j6zEimpcc32tx"
+            + "2iupWwfyzdE5l4Ejc5EBYzx0aZH6b/KDuutwustk0IoyjlGySMBz/21YgWejIm+n"
+            + "duAlpk7WY5kYHp0XWtzdmxZknmWTqugPeNZeiKEjoDmpyIbY6N+f13hQ2RVh+WDT"
+            + "EowQ/i04WBL75chshlIG+3A42g5Qr7DZEKdT9oJQqkntzj0cGyJ5X8BwjeTiJrvY"
+            + "k2Kn/0555/Kpp65G3Rf29VPPU3i67kthAT3SavLBpH03S4WZ+QlfrAiGQziydtz9"
+            + "t7mSk1xefjax5ZWAuJAfCbKfI3VWAcaUr4P57BzmDcSi0jgs1aM3t2BrPfAMRxWv"
+            + "35yDZnrC+HipzkjyDGBfHmFgoglyhc9e/Kj3mSusO0Rq1wguVXKs2hKXRoaGJuHt"
+            + "e3YIwTC1pLznqvolhD1nPoXf8rMzgHRzlc9H8iXsgB1p7975nh5WCPrMDX2eAmYd"
+            + "a0xTMccTeBzIM2ohxQsxlh5rsjXVNU3ihbWkHquzIiwFcAtldP3dMksj0dn/DnYD"
+            + "yokjEgU/z2I216E93x9hmKkEk6Pp7o8t/z6lwMT9FJIuzp7NREnWCSi+e5s2E7FD"
+            + "j6S7xY2zEIUHrmwuuJc0jzJnwdZ+0myinaTmBDvBXR5cU1cmEAZoheCAoRv9Z/6o"
+            + "ASczLF0C4uuVfA5GXcAm14cCAwEAAaMmMCQwDgYDVR0PAQH/BAQDAgGGMBIGA1Ud"
+            + "EwEB/wQIMAYBAf8CAQEwDQYJKoZIhvcNAQELBQADggIBAEPht79yQm8woQbPB1Bs"
+            + "eotkzJtTWTO9fnIWwNiRfQ3vJFXf69ghE77wUS13Ez3FlgNPj0Qxmg5ouE0d2yYV"
+            + "4AUrXnEGZELcyN2XHRXyNK0zXgnr3x6eZyY7QfgGKJgkyja5TS6ZPWGyaLKhClS0"
+            + "AYZSzWJtz0+AkGCdTbmyy7ShdXJ+GfnmssbndZA62VhcjeQmHsDq7V3PKAsp4/B9"
+            + "PzcnTrgkUFNnP1F1pr7JpUUX3xyRFy6gjIrUx1fcOFRxFYPWGLLMZ6P41rafm+M/"
+            + "CbBNr5CY7NrZjr34jLqWycfYes49o9OK44X/wPrxj0Sjg+VrW21+AJ9vrM7DS5hE"
+            + "QX1lDbDtQGkk3N1vgCTo6xt9LXsEu4xUT5bk7YAfpJqM0ltDFPwYAGCbjSkVT/M5"
+            + "JVZkKiUW668Us67x8yZc/5bxbvTA+5xrYhak/VYIBY6qub4J+bKwadw6uBgxnq4P"
+            + "hwgwjfaoJy9YAXCswjCtaE9GwkVmRnJE9vFjJ33IGf37hFTYEHBFy4FomVmQwRFZ"
+            + "TIe7tkKDq9i18F7lzBPJPO6wEG8bxi4csatrjcVHR9erpY5u6ebtkKG8qsan9qzh"
+            + "iWAgSytiT++HejZeoQ+RRgQWjupjdDo5/0oSdQqvaN8Ah6C2J+ecCZ12Lu0FwF+t"
+            + "t9Ie3pF6W8TzxzuMdFWq+afvMIIDOzCCASOgAwIBAgIRAOTj/iNQb6/Qit7zAW9n"
+            + "cL0wDQYJKoZIhvcNAQELBQAwOTE3MDUGA1UEAxMuR29vZ2xlIENsb3VkIEtleSBW"
+            + "YXVsdCBTZXJ2aWNlIEludGVybWVkaWF0ZSBDQTAeFw0xODA1MDcyMjE4MTFaFw0y"
+            + "MzA1MDgyMjE4MTFaMDIxMDAuBgNVBAMTJ0dvb2dsZSBDbG91ZCBLZXkgVmF1bHQg"
+            + "U2VydmljZSBFbmRwb2ludDBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABI4MEUp5"
+            + "IHwATNfpBuJYIUX6JMsHZt798YO0JlWYy6nVVa1lxf9c+xxONJh+T5aio370RlIE"
+            + "uiq5R7vCHt0VGsCjEDAOMAwGA1UdEwEB/wQCMAAwDQYJKoZIhvcNAQELBQADggIB"
+            + "AGf6QU58lU+gGzy8hnp0suR/ixC++CExtf39pDHkdfU/e3ui4ROR+pjQ5F7okDFW"
+            + "eKSCNcJZ7tyXMJ9g7/I0qVY8Bj/gRnlVokdl/wD5PiL9GIzqWfnHNe3T+xrAAAgO"
+            + "D0bEmjgwNYmekfUIYQczd04d7ZMGnmAkpVH/0O2mf9q5x9fMlbKuAygUqQ/gmnlg"
+            + "xKfl9DSRWi4oMBOqlKlCRP1XAh3anu92+M/EhsFbyc07CWZY0SByX5M/cHVMLhUX"
+            + "jZHvcYLyOmJWJmXznidgyNeIR6t9yDB55iCt7WSn3qMY+9vA9ELzt8jYpBNaKc0G"
+            + "bWQkRzYWegkf4kMis98eQ3SnAKbRz6669nmuAdxKs9/LK6BlFOFw1xvsTRQ96dBa"
+            + "oiX2XGhou+Im0Td/AMs0Aigz2N+Ujq/yW//35GZQfdGGIYtFbkcltStygjIJyAM1"
+            + "pBhyBBkJhOhRpO4fXh98aq8H5J7R9i5A9WpnDstAxPxcNCDWn0O/WxhPvVZkFTpi"
+            + "NXh9dnlJ/kZe+j+z5ZMaxW435drLPx2AQKjXA9GgGrFPltTUyGycmEGtuxLvSnm/"
+            + "zPlmk5FUk7x2wEr0+bZ3cx0JHHgAtgXpe0jkDi8Bw8O3X7mUOjxVhYU6auiYJezW"
+            + "9LGmweaKwYvS04UCWOReolUVexob9LI/VX1JrrwD3s7k";
+
+    static CertPath getThmCertPath() {
+        try {
+            return decodeCertPath(THM_CERT_PATH_BASE64);
+        } catch (Exception e) {
+            // Should never happen
+            throw new RuntimeException(e);
+        }
+    }
+
+    private static CertPath decodeCertPath(String base64CertPath) throws Exception {
+        byte[] certPathBytes = Base64.getDecoder().decode(base64CertPath);
+        CertificateFactory certFactory = CertificateFactory.getInstance("X.509");
+        return certFactory.generateCertPath(new ByteArrayInputStream(certPathBytes), "PkiPath");
+    }
+}
diff --git a/data/etc/platform.xml b/data/etc/platform.xml
index 141948f..4a2db0a 100644
--- a/data/etc/platform.xml
+++ b/data/etc/platform.xml
@@ -225,15 +225,18 @@
     <library name="android.test.base"
             file="/system/framework/android.test.base.impl.jar" />
     <library name="android.test.mock"
-            file="/system/framework/android.test.mock.impl.jar" />
+            file="/system/framework/android.test.mock.impl.jar"
+            dependency="android.test.base" />
     <library name="android.test.runner"
-            file="/system/framework/android.test.runner.impl.jar" />
+            file="/system/framework/android.test.runner.impl.jar"
+            dependency="android.test.base:android.test.mock" />
 
     <!-- In BOOT_JARS historically, and now added to legacy applications. -->
     <library name="android.hidl.base-V1.0-java"
             file="/system/framework/android.hidl.base-V1.0-java.jar" />
     <library name="android.hidl.manager-V1.0-java"
-            file="/system/framework/android.hidl.manager-V1.0-java.jar" />
+            file="/system/framework/android.hidl.manager-V1.0-java.jar"
+            dependency="android.hidl.base-V1.0-java" />
 
     <!-- These are the standard packages that are white-listed to always have internet
          access while in power save mode, even if they aren't in the foreground. -->
diff --git a/libs/input/Android.bp b/libs/input/Android.bp
index f1d9397..a2e8672 100644
--- a/libs/input/Android.bp
+++ b/libs/input/Android.bp
@@ -14,7 +14,7 @@
 
 cc_library_shared {
     name: "libinputservice",
-
+    cpp_std: "c++17",
     srcs: [
         "PointerController.cpp",
         "SpriteController.cpp",
diff --git a/libs/input/PointerController.cpp b/libs/input/PointerController.cpp
index 0a90f85..80d8e72 100644
--- a/libs/input/PointerController.cpp
+++ b/libs/input/PointerController.cpp
@@ -89,10 +89,6 @@
 
     mLocked.animationPending = false;
 
-    mLocked.displayWidth = -1;
-    mLocked.displayHeight = -1;
-    mLocked.displayOrientation = DISPLAY_ORIENTATION_0;
-
     mLocked.presentation = PRESENTATION_POINTER;
     mLocked.presentationChanged = false;
 
@@ -110,15 +106,6 @@
     mLocked.lastFrameUpdatedTime = 0;
 
     mLocked.buttonState = 0;
-
-    mPolicy->loadPointerIcon(&mLocked.pointerIcon);
-
-    loadResources();
-
-    if (mLocked.pointerIcon.isValid()) {
-        mLocked.pointerIconChanged = true;
-        updatePointerLocked();
-    }
 }
 
 PointerController::~PointerController() {
@@ -144,23 +131,15 @@
 
 bool PointerController::getBoundsLocked(float* outMinX, float* outMinY,
         float* outMaxX, float* outMaxY) const {
-    if (mLocked.displayWidth <= 0 || mLocked.displayHeight <= 0) {
+
+    if (!mLocked.viewport.isValid()) {
         return false;
     }
 
-    *outMinX = 0;
-    *outMinY = 0;
-    switch (mLocked.displayOrientation) {
-    case DISPLAY_ORIENTATION_90:
-    case DISPLAY_ORIENTATION_270:
-        *outMaxX = mLocked.displayHeight - 1;
-        *outMaxY = mLocked.displayWidth - 1;
-        break;
-    default:
-        *outMaxX = mLocked.displayWidth - 1;
-        *outMaxY = mLocked.displayHeight - 1;
-        break;
-    }
+    *outMinX = mLocked.viewport.logicalLeft;
+    *outMinY = mLocked.viewport.logicalTop;
+    *outMaxX = mLocked.viewport.logicalRight - 1;
+    *outMaxY = mLocked.viewport.logicalBottom - 1;
     return true;
 }
 
@@ -231,6 +210,12 @@
     *outY = mLocked.pointerY;
 }
 
+int32_t PointerController::getDisplayId() const {
+    AutoMutex _l(mLock);
+
+    return mLocked.viewport.displayId;
+}
+
 void PointerController::fade(Transition transition) {
     AutoMutex _l(mLock);
 
@@ -355,48 +340,57 @@
 void PointerController::reloadPointerResources() {
     AutoMutex _l(mLock);
 
-    loadResources();
-
-    if (mLocked.presentation == PRESENTATION_POINTER) {
-        mLocked.additionalMouseResources.clear();
-        mLocked.animationResources.clear();
-        mPolicy->loadPointerIcon(&mLocked.pointerIcon);
-        mPolicy->loadAdditionalMouseResources(&mLocked.additionalMouseResources,
-                                              &mLocked.animationResources);
-    }
-
-    mLocked.presentationChanged = true;
+    loadResourcesLocked();
     updatePointerLocked();
 }
 
-void PointerController::setDisplayViewport(int32_t width, int32_t height, int32_t orientation) {
-    AutoMutex _l(mLock);
+/**
+ * The viewport values for deviceHeight and deviceWidth have already been adjusted for rotation,
+ * so here we are getting the dimensions in the original, unrotated orientation (orientation 0).
+ */
+static void getNonRotatedSize(const DisplayViewport& viewport, int32_t& width, int32_t& height) {
+    if (viewport.orientation == DISPLAY_ORIENTATION_90
+            || viewport.orientation == DISPLAY_ORIENTATION_270) {
+        width = viewport.deviceHeight;
+        height = viewport.deviceWidth;
+    } else {
+        width = viewport.deviceWidth;
+        height = viewport.deviceHeight;
+    }
+}
 
-    // Adjust to use the display's unrotated coordinate frame.
-    if (orientation == DISPLAY_ORIENTATION_90
-            || orientation == DISPLAY_ORIENTATION_270) {
-        int32_t temp = height;
-        height = width;
-        width = temp;
+void PointerController::setDisplayViewport(const DisplayViewport& viewport) {
+    AutoMutex _l(mLock);
+    if (viewport == mLocked.viewport) {
+        return;
     }
 
-    if (mLocked.displayWidth != width || mLocked.displayHeight != height) {
-        mLocked.displayWidth = width;
-        mLocked.displayHeight = height;
+    const DisplayViewport oldViewport = mLocked.viewport;
+    mLocked.viewport = viewport;
+
+    int32_t oldDisplayWidth, oldDisplayHeight;
+    getNonRotatedSize(oldViewport, oldDisplayWidth, oldDisplayHeight);
+    int32_t newDisplayWidth, newDisplayHeight;
+    getNonRotatedSize(viewport, newDisplayWidth, newDisplayHeight);
+
+    // Reset cursor position to center if size or display changed.
+    if (oldViewport.displayId != viewport.displayId
+            || oldDisplayWidth != newDisplayWidth
+            || oldDisplayHeight != newDisplayHeight) {
 
         float minX, minY, maxX, maxY;
         if (getBoundsLocked(&minX, &minY, &maxX, &maxY)) {
             mLocked.pointerX = (minX + maxX) * 0.5f;
             mLocked.pointerY = (minY + maxY) * 0.5f;
+            // Reload icon resources for density may be changed.
+            loadResourcesLocked();
         } else {
             mLocked.pointerX = 0;
             mLocked.pointerY = 0;
         }
 
         fadeOutAndReleaseAllSpotsLocked();
-    }
-
-    if (mLocked.displayOrientation != orientation) {
+    } else if (oldViewport.orientation != viewport.orientation) {
         // Apply offsets to convert from the pixel top-left corner position to the pixel center.
         // This creates an invariant frame of reference that we can easily rotate when
         // taking into account that the pointer may be located at fractional pixel offsets.
@@ -405,37 +399,37 @@
         float temp;
 
         // Undo the previous rotation.
-        switch (mLocked.displayOrientation) {
+        switch (oldViewport.orientation) {
         case DISPLAY_ORIENTATION_90:
             temp = x;
-            x = mLocked.displayWidth - y;
+            x =  oldViewport.deviceHeight - y;
             y = temp;
             break;
         case DISPLAY_ORIENTATION_180:
-            x = mLocked.displayWidth - x;
-            y = mLocked.displayHeight - y;
+            x = oldViewport.deviceWidth - x;
+            y = oldViewport.deviceHeight - y;
             break;
         case DISPLAY_ORIENTATION_270:
             temp = x;
             x = y;
-            y = mLocked.displayHeight - temp;
+            y = oldViewport.deviceWidth - temp;
             break;
         }
 
         // Perform the new rotation.
-        switch (orientation) {
+        switch (viewport.orientation) {
         case DISPLAY_ORIENTATION_90:
             temp = x;
             x = y;
-            y = mLocked.displayWidth - temp;
+            y = viewport.deviceHeight - temp;
             break;
         case DISPLAY_ORIENTATION_180:
-            x = mLocked.displayWidth - x;
-            y = mLocked.displayHeight - y;
+            x = viewport.deviceWidth - x;
+            y = viewport.deviceHeight - y;
             break;
         case DISPLAY_ORIENTATION_270:
             temp = x;
-            x = mLocked.displayHeight - y;
+            x = viewport.deviceWidth - y;
             y = temp;
             break;
         }
@@ -444,7 +438,6 @@
         // and save the results.
         mLocked.pointerX = x - 0.5f;
         mLocked.pointerY = y - 0.5f;
-        mLocked.displayOrientation = orientation;
     }
 
     updatePointerLocked();
@@ -614,11 +607,16 @@
     mLooper->removeMessages(mHandler, MSG_INACTIVITY_TIMEOUT);
 }
 
-void PointerController::updatePointerLocked() {
+void PointerController::updatePointerLocked() REQUIRES(mLock) {
+    if (!mLocked.viewport.isValid()) {
+        return;
+    }
+
     mSpriteController->openTransaction();
 
     mLocked.pointerSprite->setLayer(Sprite::BASE_LAYER_POINTER);
     mLocked.pointerSprite->setPosition(mLocked.pointerX, mLocked.pointerY);
+    mLocked.pointerSprite->setDisplayId(mLocked.viewport.displayId);
 
     if (mLocked.pointerAlpha > 0) {
         mLocked.pointerSprite->setAlpha(mLocked.pointerAlpha);
@@ -729,8 +727,18 @@
     }
 }
 
-void PointerController::loadResources() {
+void PointerController::loadResourcesLocked() REQUIRES(mLock) {
     mPolicy->loadPointerResources(&mResources);
+
+    if (mLocked.presentation == PRESENTATION_POINTER) {
+        mLocked.additionalMouseResources.clear();
+        mLocked.animationResources.clear();
+        mPolicy->loadPointerIcon(&mLocked.pointerIcon);
+        mPolicy->loadAdditionalMouseResources(&mLocked.additionalMouseResources,
+                                              &mLocked.animationResources);
+    }
+
+    mLocked.pointerIconChanged = true;
 }
 
 
diff --git a/libs/input/PointerController.h b/libs/input/PointerController.h
index 7f4e5a5..a32cc42 100644
--- a/libs/input/PointerController.h
+++ b/libs/input/PointerController.h
@@ -23,6 +23,7 @@
 #include <vector>
 
 #include <ui/DisplayInfo.h>
+#include <input/DisplayViewport.h>
 #include <input/Input.h>
 #include <PointerControllerInterface.h>
 #include <utils/BitSet.h>
@@ -96,6 +97,7 @@
     virtual int32_t getButtonState() const;
     virtual void setPosition(float x, float y);
     virtual void getPosition(float* outX, float* outY) const;
+    virtual int32_t getDisplayId() const;
     virtual void fade(Transition transition);
     virtual void unfade(Transition transition);
 
@@ -106,7 +108,7 @@
 
     void updatePointerIcon(int32_t iconId);
     void setCustomPointerIcon(const SpriteIcon& icon);
-    void setDisplayViewport(int32_t width, int32_t height, int32_t orientation);
+    void setDisplayViewport(const DisplayViewport& viewport);
     void setInactivityTimeout(InactivityTimeout inactivityTimeout);
     void reloadPointerResources();
 
@@ -156,9 +158,7 @@
         size_t animationFrameIndex;
         nsecs_t lastFrameUpdatedTime;
 
-        int32_t displayWidth;
-        int32_t displayHeight;
-        int32_t displayOrientation;
+        DisplayViewport viewport;
 
         InactivityTimeout inactivityTimeout;
 
@@ -182,7 +182,7 @@
 
         Vector<Spot*> spots;
         Vector<sp<Sprite> > recycledSprites;
-    } mLocked;
+    } mLocked GUARDED_BY(mLock);
 
     bool getBoundsLocked(float* outMinX, float* outMinY, float* outMaxX, float* outMaxY) const;
     void setPositionLocked(float x, float y);
@@ -207,7 +207,7 @@
     void fadeOutAndReleaseSpotLocked(Spot* spot);
     void fadeOutAndReleaseAllSpotsLocked();
 
-    void loadResources();
+    void loadResourcesLocked();
 };
 
 } // namespace android
diff --git a/libs/input/SpriteController.cpp b/libs/input/SpriteController.cpp
index eb2bc98..c1868d3 100644
--- a/libs/input/SpriteController.cpp
+++ b/libs/input/SpriteController.cpp
@@ -144,13 +144,16 @@
         }
     }
 
-    // Resize sprites if needed.
+    // Resize and/or reparent sprites if needed.
     SurfaceComposerClient::Transaction t;
     bool needApplyTransaction = false;
     for (size_t i = 0; i < numSprites; i++) {
         SpriteUpdate& update = updates.editItemAt(i);
+        if (update.state.surfaceControl == nullptr) {
+            continue;
+        }
 
-        if (update.state.surfaceControl != NULL && update.state.wantSurfaceVisible()) {
+        if (update.state.wantSurfaceVisible()) {
             int32_t desiredWidth = update.state.icon.bitmap.width();
             int32_t desiredHeight = update.state.icon.bitmap.height();
             if (update.state.surfaceWidth < desiredWidth
@@ -170,6 +173,12 @@
                 }
             }
         }
+
+        // If surface is a new one, we have to set right layer stack.
+        if (update.surfaceChanged || update.state.dirty & DIRTY_DISPLAY_ID) {
+            t.setLayerStack(update.state.surfaceControl, update.state.displayId);
+            needApplyTransaction = true;
+        }
     }
     if (needApplyTransaction) {
         t.apply();
@@ -236,7 +245,7 @@
         if (update.state.surfaceControl != NULL && (becomingVisible || becomingHidden
                 || (wantSurfaceVisibleAndDrawn && (update.state.dirty & (DIRTY_ALPHA
                         | DIRTY_POSITION | DIRTY_TRANSFORMATION_MATRIX | DIRTY_LAYER
-                        | DIRTY_VISIBILITY | DIRTY_HOTSPOT))))) {
+                        | DIRTY_VISIBILITY | DIRTY_HOTSPOT | DIRTY_DISPLAY_ID))))) {
             needApplyTransaction = true;
 
             if (wantSurfaceVisibleAndDrawn
@@ -445,6 +454,15 @@
     }
 }
 
+void SpriteController::SpriteImpl::setDisplayId(int32_t displayId) {
+    AutoMutex _l(mController->mLock);
+
+    if (mLocked.state.displayId != displayId) {
+        mLocked.state.displayId = displayId;
+        invalidateLocked(DIRTY_DISPLAY_ID);
+    }
+}
+
 void SpriteController::SpriteImpl::invalidateLocked(uint32_t dirty) {
     bool wasDirty = mLocked.state.dirty;
     mLocked.state.dirty |= dirty;
diff --git a/libs/input/SpriteController.h b/libs/input/SpriteController.h
index 31e43e9..5b216f5 100644
--- a/libs/input/SpriteController.h
+++ b/libs/input/SpriteController.h
@@ -125,6 +125,9 @@
 
     /* Sets the sprite transformation matrix. */
     virtual void setTransformationMatrix(const SpriteTransformationMatrix& matrix) = 0;
+
+    /* Sets the id of the display where the sprite should be shown. */
+    virtual void setDisplayId(int32_t displayId) = 0;
 };
 
 /*
@@ -170,6 +173,7 @@
         DIRTY_LAYER = 1 << 4,
         DIRTY_VISIBILITY = 1 << 5,
         DIRTY_HOTSPOT = 1 << 6,
+        DIRTY_DISPLAY_ID = 1 << 7,
     };
 
     /* Describes the state of a sprite.
@@ -180,7 +184,7 @@
     struct SpriteState {
         inline SpriteState() :
                 dirty(0), visible(false),
-                positionX(0), positionY(0), layer(0), alpha(1.0f),
+                positionX(0), positionY(0), layer(0), alpha(1.0f), displayId(ADISPLAY_ID_DEFAULT),
                 surfaceWidth(0), surfaceHeight(0), surfaceDrawn(false), surfaceVisible(false) {
         }
 
@@ -193,6 +197,7 @@
         int32_t layer;
         float alpha;
         SpriteTransformationMatrix transformationMatrix;
+        int32_t displayId;
 
         sp<SurfaceControl> surfaceControl;
         int32_t surfaceWidth;
@@ -225,6 +230,7 @@
         virtual void setLayer(int32_t layer);
         virtual void setAlpha(float alpha);
         virtual void setTransformationMatrix(const SpriteTransformationMatrix& matrix);
+        virtual void setDisplayId(int32_t displayId);
 
         inline const SpriteState& getStateLocked() const {
             return mLocked.state;
diff --git a/media/java/android/media/AudioTrack.java b/media/java/android/media/AudioTrack.java
index e4d4795..5e77fdf 100644
--- a/media/java/android/media/AudioTrack.java
+++ b/media/java/android/media/AudioTrack.java
@@ -995,6 +995,35 @@
         }
     }
 
+    /**
+     * Returns whether direct playback of an audio format with the provided attributes is
+     * currently supported on the system.
+     * <p>Direct playback means that the audio stream is not resampled or downmixed
+     * by the framework. Checking for direct support can help the app select the representation
+     * of audio content that most closely matches the capabilities of the device and peripherials
+     * (e.g. A/V receiver) connected to it. Note that the provided stream can still be re-encoded
+     * or mixed with other streams, if needed.
+     * <p>Also note that this query only provides information about the support of an audio format.
+     * It does not indicate whether the resources necessary for the playback are available
+     * at that instant.
+     * @param format a non-null {@link AudioFormat} instance describing the format of
+     *   the audio data.
+     * @param attributes a non-null {@link AudioAttributes} instance.
+     * @return true if the given audio format can be played directly.
+     */
+    public static boolean isDirectPlaybackSupported(@NonNull AudioFormat format,
+            @NonNull AudioAttributes attributes) {
+        if (format == null) {
+            throw new IllegalArgumentException("Illegal null AudioFormat argument");
+        }
+        if (attributes == null) {
+            throw new IllegalArgumentException("Illegal null AudioAttributes argument");
+        }
+        return native_is_direct_output_supported(format.getEncoding(), format.getSampleRate(),
+                format.getChannelMask(), format.getChannelIndexMask(),
+                attributes.getContentType(), attributes.getUsage(), attributes.getFlags());
+    }
+
     // mask of all the positional channels supported, however the allowed combinations
     // are further restricted by the matching left/right rule and
     // AudioSystem.OUT_CHANNEL_COUNT_MAX
@@ -3328,6 +3357,9 @@
     // Native methods called from the Java side
     //--------------------
 
+    private static native boolean native_is_direct_output_supported(int encoding, int sampleRate,
+            int channelMask, int channelIndexMask, int contentType, int usage, int flags);
+
     // post-condition: mStreamType is overwritten with a value
     //     that reflects the audio attributes (e.g. an AudioAttributes object with a usage of
     //     AudioAttributes.USAGE_MEDIA will map to AudioManager.STREAM_MUSIC
diff --git a/media/java/android/media/MediaCodecInfo.java b/media/java/android/media/MediaCodecInfo.java
index 95e3df2..9025829 100644
--- a/media/java/android/media/MediaCodecInfo.java
+++ b/media/java/android/media/MediaCodecInfo.java
@@ -1135,6 +1135,10 @@
                 maxChannels = 6;
             } else if (mime.equalsIgnoreCase(MediaFormat.MIMETYPE_AUDIO_EAC3)) {
                 maxChannels = 16;
+            } else if (mime.equalsIgnoreCase(MediaFormat.MIMETYPE_AUDIO_EAC3_JOC)) {
+                sampleRates = new int[] { 48000 };
+                bitRates = Range.create(32000, 6144000);
+                maxChannels = 16;
             } else if (mime.equalsIgnoreCase(MediaFormat.MIMETYPE_AUDIO_AC4)) {
                 sampleRates = new int[] { 44100, 48000, 96000, 192000 };
                 bitRates = Range.create(16000, 2688000);
diff --git a/media/java/android/media/MediaFormat.java b/media/java/android/media/MediaFormat.java
index b62108f..594a224 100644
--- a/media/java/android/media/MediaFormat.java
+++ b/media/java/android/media/MediaFormat.java
@@ -147,6 +147,7 @@
     public static final String MIMETYPE_AUDIO_MSGSM = "audio/gsm";
     public static final String MIMETYPE_AUDIO_AC3 = "audio/ac3";
     public static final String MIMETYPE_AUDIO_EAC3 = "audio/eac3";
+    public static final String MIMETYPE_AUDIO_EAC3_JOC = "audio/eac3-joc";
     public static final String MIMETYPE_AUDIO_AC4 = "audio/ac4";
     public static final String MIMETYPE_AUDIO_SCRAMBLED = "audio/scrambled";
 
diff --git a/packages/ExtServices/src/android/ext/services/notification/Assistant.java b/packages/ExtServices/src/android/ext/services/notification/Assistant.java
index 0cad5af..133d8ba 100644
--- a/packages/ExtServices/src/android/ext/services/notification/Assistant.java
+++ b/packages/ExtServices/src/android/ext/services/notification/Assistant.java
@@ -27,20 +27,15 @@
 import android.app.INotificationManager;
 import android.app.Notification;
 import android.app.NotificationChannel;
-import android.content.ContentResolver;
 import android.content.Context;
 import android.content.pm.IPackageManager;
-import android.database.ContentObserver;
 import android.ext.services.notification.AgingHelper.Callback;
-import android.net.Uri;
 import android.os.AsyncTask;
 import android.os.Bundle;
 import android.os.Environment;
-import android.os.Handler;
 import android.os.SystemProperties;
 import android.os.UserHandle;
 import android.os.storage.StorageManager;
-import android.provider.Settings;
 import android.service.notification.Adjustment;
 import android.service.notification.NotificationAssistantService;
 import android.service.notification.NotificationStats;
@@ -92,8 +87,6 @@
         PREJUDICAL_DISMISSALS.add(REASON_LISTENER_CANCEL);
     }
 
-    private float mDismissToViewRatioLimit;
-    private int mStreakLimit;
     private SmartActionsHelper mSmartActionsHelper;
     private NotificationCategorizer mNotificationCategorizer;
     private AgingHelper mAgingHelper;
@@ -107,7 +100,11 @@
     private Ranking mFakeRanking = null;
     private AtomicFile mFile = null;
     private IPackageManager mPackageManager;
-    protected SettingsObserver mSettingsObserver;
+
+    @VisibleForTesting
+    protected AssistantSettings.Factory mSettingsFactory = AssistantSettings.FACTORY;
+    @VisibleForTesting
+    protected AssistantSettings mSettings;
 
     public Assistant() {
     }
@@ -118,7 +115,8 @@
         // Contexts are correctly hooked up by the creation step, which is required for the observer
         // to be hooked up/initialized.
         mPackageManager = ActivityThread.getPackageManager();
-        mSettingsObserver = new SettingsObserver(mHandler);
+        mSettings = mSettingsFactory.createAndRegister(mHandler,
+                getApplicationContext().getContentResolver(), getUserId(), this::updateThresholds);
         mSmartActionsHelper = new SmartActionsHelper();
         mNotificationCategorizer = new NotificationCategorizer();
         mAgingHelper = new AgingHelper(getContext(),
@@ -216,11 +214,11 @@
         if (!isForCurrentUser(sbn)) {
             return null;
         }
-        NotificationEntry entry = new NotificationEntry(
-                ActivityThread.getPackageManager(), sbn, channel);
+        NotificationEntry entry = new NotificationEntry(mPackageManager, sbn, channel);
         ArrayList<Notification.Action> actions =
-                mSmartActionsHelper.suggestActions(this, entry);
-        ArrayList<CharSequence> replies = mSmartActionsHelper.suggestReplies(this, entry);
+                mSmartActionsHelper.suggestActions(this, entry, mSettings);
+        ArrayList<CharSequence> replies =
+                mSmartActionsHelper.suggestReplies(this, entry, mSettings);
         return createEnqueuedNotificationAdjustment(entry, actions, replies);
     }
 
@@ -239,8 +237,7 @@
         if (!smartReplies.isEmpty()) {
             signals.putCharSequenceArrayList(Adjustment.KEY_SMART_REPLIES, smartReplies);
         }
-        if (Settings.Secure.getInt(getContentResolver(),
-                Settings.Secure.NOTIFICATION_NEW_INTERRUPTION_MODEL, 1) == 1) {
+        if (mSettings.mNewInterruptionModel) {
             if (mNotificationCategorizer.shouldSilence(entry)) {
                 final int importance = entry.getImportance() < IMPORTANCE_LOW
                         ? entry.getImportance() : IMPORTANCE_LOW;
@@ -460,6 +457,11 @@
     }
 
     @VisibleForTesting
+    public void setSmartActionsHelper(SmartActionsHelper smartActionsHelper) {
+        mSmartActionsHelper = smartActionsHelper;
+    }
+
+    @VisibleForTesting
     public ChannelImpressions getImpressions(String key) {
         synchronized (mkeyToImpressions) {
             return mkeyToImpressions.get(key);
@@ -475,10 +477,20 @@
 
     private ChannelImpressions createChannelImpressionsWithThresholds() {
         ChannelImpressions impressions = new ChannelImpressions();
-        impressions.updateThresholds(mDismissToViewRatioLimit, mStreakLimit);
+        impressions.updateThresholds(mSettings.mDismissToViewRatioLimit, mSettings.mStreakLimit);
         return impressions;
     }
 
+    private void updateThresholds() {
+        // Update all existing channel impression objects with any new limits/thresholds.
+        synchronized (mkeyToImpressions) {
+            for (ChannelImpressions channelImpressions: mkeyToImpressions.values()) {
+                channelImpressions.updateThresholds(
+                        mSettings.mDismissToViewRatioLimit, mSettings.mStreakLimit);
+            }
+        }
+    }
+
     protected final class AgingCallback implements Callback {
         @Override
         public void sendAdjustment(String key, int newImportance) {
@@ -495,51 +507,4 @@
         }
     }
 
-    /**
-     * Observer for updates on blocking helper threshold values.
-     */
-    protected final class SettingsObserver extends ContentObserver {
-        private final Uri STREAK_LIMIT_URI =
-                Settings.Global.getUriFor(Settings.Global.BLOCKING_HELPER_STREAK_LIMIT);
-        private final Uri DISMISS_TO_VIEW_RATIO_LIMIT_URI =
-                Settings.Global.getUriFor(
-                        Settings.Global.BLOCKING_HELPER_DISMISS_TO_VIEW_RATIO_LIMIT);
-
-        public SettingsObserver(Handler handler) {
-            super(handler);
-            ContentResolver resolver = getApplicationContext().getContentResolver();
-            resolver.registerContentObserver(
-                    DISMISS_TO_VIEW_RATIO_LIMIT_URI, false, this, getUserId());
-            resolver.registerContentObserver(STREAK_LIMIT_URI, false, this, getUserId());
-
-            // Update all uris on creation.
-            update(null);
-        }
-
-        @Override
-        public void onChange(boolean selfChange, Uri uri) {
-            update(uri);
-        }
-
-        private void update(Uri uri) {
-            ContentResolver resolver = getApplicationContext().getContentResolver();
-            if (uri == null || DISMISS_TO_VIEW_RATIO_LIMIT_URI.equals(uri)) {
-                mDismissToViewRatioLimit = Settings.Global.getFloat(
-                        resolver, Settings.Global.BLOCKING_HELPER_DISMISS_TO_VIEW_RATIO_LIMIT,
-                        ChannelImpressions.DEFAULT_DISMISS_TO_VIEW_RATIO_LIMIT);
-            }
-            if (uri == null || STREAK_LIMIT_URI.equals(uri)) {
-                mStreakLimit = Settings.Global.getInt(
-                        resolver, Settings.Global.BLOCKING_HELPER_STREAK_LIMIT,
-                        ChannelImpressions.DEFAULT_STREAK_LIMIT);
-            }
-
-            // Update all existing channel impression objects with any new limits/thresholds.
-            synchronized (mkeyToImpressions) {
-                for (ChannelImpressions channelImpressions: mkeyToImpressions.values()) {
-                    channelImpressions.updateThresholds(mDismissToViewRatioLimit, mStreakLimit);
-                }
-            }
-        }
-    }
 }
diff --git a/packages/ExtServices/src/android/ext/services/notification/AssistantSettings.java b/packages/ExtServices/src/android/ext/services/notification/AssistantSettings.java
new file mode 100644
index 0000000..39a1676
--- /dev/null
+++ b/packages/ExtServices/src/android/ext/services/notification/AssistantSettings.java
@@ -0,0 +1,140 @@
+/**
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.ext.services.notification;
+
+import android.content.ContentResolver;
+import android.database.ContentObserver;
+import android.net.Uri;
+import android.os.Handler;
+import android.provider.Settings;
+import android.util.KeyValueListParser;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+/**
+ * Observes the settings for {@link Assistant}.
+ */
+final class AssistantSettings extends ContentObserver {
+    public static Factory FACTORY = AssistantSettings::createAndRegister;
+    private static final boolean DEFAULT_GENERATE_REPLIES = true;
+    private static final boolean DEFAULT_GENERATE_ACTIONS = true;
+    private static final int DEFAULT_NEW_INTERRUPTION_MODEL_INT = 1;
+
+    private static final Uri STREAK_LIMIT_URI =
+            Settings.Global.getUriFor(Settings.Global.BLOCKING_HELPER_STREAK_LIMIT);
+    private static final Uri DISMISS_TO_VIEW_RATIO_LIMIT_URI =
+            Settings.Global.getUriFor(
+                    Settings.Global.BLOCKING_HELPER_DISMISS_TO_VIEW_RATIO_LIMIT);
+    private static final Uri SMART_SUGGESTIONS_IN_NOTIFICATIONS_FLAGS_URI =
+            Settings.Global.getUriFor(
+                    Settings.Global.SMART_SUGGESTIONS_IN_NOTIFICATIONS_FLAGS);
+    private static final Uri NOTIFICATION_NEW_INTERRUPTION_MODEL_URI =
+            Settings.Secure.getUriFor(Settings.Secure.NOTIFICATION_NEW_INTERRUPTION_MODEL);
+
+    private static final String KEY_GENERATE_REPLIES = "generate_replies";
+    private static final String KEY_GENERATE_ACTIONS = "generate_actions";
+
+    private final KeyValueListParser mParser = new KeyValueListParser(',');
+    private final ContentResolver mResolver;
+    private final int mUserId;
+
+    @VisibleForTesting
+    protected final Runnable mOnUpdateRunnable;
+
+    // Actuall configuration settings.
+    float mDismissToViewRatioLimit;
+    int mStreakLimit;
+    boolean mGenerateReplies = DEFAULT_GENERATE_REPLIES;
+    boolean mGenerateActions = DEFAULT_GENERATE_ACTIONS;
+    boolean mNewInterruptionModel;
+
+    private AssistantSettings(Handler handler, ContentResolver resolver, int userId,
+            Runnable onUpdateRunnable) {
+        super(handler);
+        mResolver = resolver;
+        mUserId = userId;
+        mOnUpdateRunnable = onUpdateRunnable;
+    }
+
+    private static AssistantSettings createAndRegister(
+            Handler handler, ContentResolver resolver, int userId, Runnable onUpdateRunnable) {
+        AssistantSettings assistantSettings =
+                new AssistantSettings(handler, resolver, userId, onUpdateRunnable);
+        assistantSettings.register();
+        return assistantSettings;
+    }
+
+    /**
+     * Creates an instance but doesn't register it as an observer.
+     */
+    @VisibleForTesting
+    protected static AssistantSettings createForTesting(
+            Handler handler, ContentResolver resolver, int userId, Runnable onUpdateRunnable) {
+        return new AssistantSettings(handler, resolver, userId, onUpdateRunnable);
+    }
+
+    private void register() {
+        mResolver.registerContentObserver(
+                DISMISS_TO_VIEW_RATIO_LIMIT_URI, false, this, mUserId);
+        mResolver.registerContentObserver(STREAK_LIMIT_URI, false, this, mUserId);
+        mResolver.registerContentObserver(
+                SMART_SUGGESTIONS_IN_NOTIFICATIONS_FLAGS_URI, false, this, mUserId);
+
+        // Update all uris on creation.
+        update(null);
+    }
+
+    @Override
+    public void onChange(boolean selfChange, Uri uri) {
+        update(uri);
+    }
+
+    private void update(Uri uri) {
+        if (uri == null || DISMISS_TO_VIEW_RATIO_LIMIT_URI.equals(uri)) {
+            mDismissToViewRatioLimit = Settings.Global.getFloat(
+                    mResolver, Settings.Global.BLOCKING_HELPER_DISMISS_TO_VIEW_RATIO_LIMIT,
+                    ChannelImpressions.DEFAULT_DISMISS_TO_VIEW_RATIO_LIMIT);
+        }
+        if (uri == null || STREAK_LIMIT_URI.equals(uri)) {
+            mStreakLimit = Settings.Global.getInt(
+                    mResolver, Settings.Global.BLOCKING_HELPER_STREAK_LIMIT,
+                    ChannelImpressions.DEFAULT_STREAK_LIMIT);
+        }
+        if (uri == null || SMART_SUGGESTIONS_IN_NOTIFICATIONS_FLAGS_URI.equals(uri)) {
+            mParser.setString(
+                    Settings.Global.getString(mResolver,
+                            Settings.Global.SMART_SUGGESTIONS_IN_NOTIFICATIONS_FLAGS));
+            mGenerateReplies =
+                    mParser.getBoolean(KEY_GENERATE_REPLIES, DEFAULT_GENERATE_REPLIES);
+            mGenerateActions =
+                    mParser.getBoolean(KEY_GENERATE_ACTIONS, DEFAULT_GENERATE_ACTIONS);
+        }
+        if (uri == null || NOTIFICATION_NEW_INTERRUPTION_MODEL_URI.equals(uri)) {
+            int mNewInterruptionModelInt = Settings.Secure.getInt(
+                    mResolver, Settings.Secure.NOTIFICATION_NEW_INTERRUPTION_MODEL,
+                    DEFAULT_NEW_INTERRUPTION_MODEL_INT);
+            mNewInterruptionModel = mNewInterruptionModelInt == 1;
+        }
+
+        mOnUpdateRunnable.run();
+    }
+
+    public interface Factory {
+        AssistantSettings createAndRegister(Handler handler, ContentResolver resolver, int userId,
+                Runnable onUpdateRunnable);
+    }
+}
diff --git a/packages/ExtServices/src/android/ext/services/notification/SmartActionsHelper.java b/packages/ExtServices/src/android/ext/services/notification/SmartActionsHelper.java
index 892267b..6f2b6c9 100644
--- a/packages/ExtServices/src/android/ext/services/notification/SmartActionsHelper.java
+++ b/packages/ExtServices/src/android/ext/services/notification/SmartActionsHelper.java
@@ -69,8 +69,11 @@
      * from notification text / message, we can replace most of the code here by consuming that API.
      */
     @NonNull
-    ArrayList<Notification.Action> suggestActions(
-            @Nullable Context context, @NonNull NotificationEntry entry) {
+    ArrayList<Notification.Action> suggestActions(@Nullable Context context,
+            @NonNull NotificationEntry entry, @NonNull AssistantSettings settings) {
+        if (!settings.mGenerateActions) {
+            return EMPTY_ACTION_LIST;
+        }
         if (!isEligibleForActionAdjustment(entry)) {
             return EMPTY_ACTION_LIST;
         }
@@ -86,8 +89,11 @@
                 getMostSalientActionText(entry.getNotification()), MAX_SMART_ACTIONS);
     }
 
-    ArrayList<CharSequence> suggestReplies(
-            @Nullable Context context, @NonNull NotificationEntry entry) {
+    ArrayList<CharSequence> suggestReplies(@Nullable Context context,
+            @NonNull NotificationEntry entry, @NonNull AssistantSettings settings) {
+        if (!settings.mGenerateReplies) {
+            return EMPTY_REPLY_LIST;
+        }
         if (!isEligibleForReplyAdjustment(entry)) {
             return EMPTY_REPLY_LIST;
         }
diff --git a/packages/ExtServices/tests/src/android/ext/services/notification/AssistantSettingsTest.java b/packages/ExtServices/tests/src/android/ext/services/notification/AssistantSettingsTest.java
new file mode 100644
index 0000000..fd23f2b
--- /dev/null
+++ b/packages/ExtServices/tests/src/android/ext/services/notification/AssistantSettingsTest.java
@@ -0,0 +1,162 @@
+/**
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.ext.services.notification;
+
+import static junit.framework.Assert.assertFalse;
+import static junit.framework.Assert.assertTrue;
+
+import static org.junit.Assert.assertEquals;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+
+import android.content.ContentResolver;
+import android.os.Handler;
+import android.os.Looper;
+import android.provider.Settings;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.runner.AndroidJUnit4;
+import android.testing.TestableContext;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+@RunWith(AndroidJUnit4.class)
+public class AssistantSettingsTest {
+    private static final int USER_ID = 5;
+
+    @Rule
+    public final TestableContext mContext =
+            new TestableContext(InstrumentationRegistry.getContext(), null);
+
+    @Mock Runnable mOnUpdateRunnable;
+
+    private ContentResolver mResolver;
+    private AssistantSettings mAssistantSettings;
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+
+        mResolver = mContext.getContentResolver();
+        Handler handler = new Handler(Looper.getMainLooper());
+
+        // To bypass real calls to global settings values, set the Settings values here.
+        Settings.Global.putFloat(mResolver,
+                Settings.Global.BLOCKING_HELPER_DISMISS_TO_VIEW_RATIO_LIMIT, 0.8f);
+        Settings.Global.putInt(mResolver, Settings.Global.BLOCKING_HELPER_STREAK_LIMIT, 2);
+        Settings.Global.putString(mResolver,
+                Settings.Global.SMART_SUGGESTIONS_IN_NOTIFICATIONS_FLAGS,
+                "generate_replies=true,generate_actions=true");
+        Settings.Secure.putInt(mResolver, Settings.Secure.NOTIFICATION_NEW_INTERRUPTION_MODEL, 1);
+
+        mAssistantSettings = AssistantSettings.createForTesting(
+                handler, mResolver, USER_ID, mOnUpdateRunnable);
+    }
+
+    @Test
+    public void testGenerateRepliesDisabled() {
+        Settings.Global.putString(mResolver,
+                Settings.Global.SMART_SUGGESTIONS_IN_NOTIFICATIONS_FLAGS,
+                "generate_replies=false");
+
+        // Notify for the settings values we updated.
+        mAssistantSettings.onChange(false,
+                Settings.Global.getUriFor(
+                        Settings.Global.SMART_SUGGESTIONS_IN_NOTIFICATIONS_FLAGS));
+
+
+        assertFalse(mAssistantSettings.mGenerateReplies);
+    }
+
+    @Test
+    public void testGenerateRepliesEnabled() {
+        Settings.Global.putString(mResolver,
+                Settings.Global.SMART_SUGGESTIONS_IN_NOTIFICATIONS_FLAGS, "generate_replies=true");
+
+        // Notify for the settings values we updated.
+        mAssistantSettings.onChange(false,
+                Settings.Global.getUriFor(
+                        Settings.Global.SMART_SUGGESTIONS_IN_NOTIFICATIONS_FLAGS));
+
+        assertTrue(mAssistantSettings.mGenerateReplies);
+    }
+
+    @Test
+    public void testGenerateActionsDisabled() {
+        Settings.Global.putString(mResolver,
+                Settings.Global.SMART_SUGGESTIONS_IN_NOTIFICATIONS_FLAGS, "generate_actions=false");
+
+        // Notify for the settings values we updated.
+        mAssistantSettings.onChange(false,
+                Settings.Global.getUriFor(
+                        Settings.Global.SMART_SUGGESTIONS_IN_NOTIFICATIONS_FLAGS));
+
+        assertTrue(mAssistantSettings.mGenerateReplies);
+    }
+
+    @Test
+    public void testGenerateActionsEnabled() {
+        Settings.Global.putString(mResolver,
+                Settings.Global.SMART_SUGGESTIONS_IN_NOTIFICATIONS_FLAGS, "generate_actions=true");
+
+        // Notify for the settings values we updated.
+        mAssistantSettings.onChange(false,
+                Settings.Global.getUriFor(
+                        Settings.Global.SMART_SUGGESTIONS_IN_NOTIFICATIONS_FLAGS));
+
+        assertTrue(mAssistantSettings.mGenerateReplies);
+    }
+
+    @Test
+    public void testStreakLimit() {
+        verify(mOnUpdateRunnable, never()).run();
+
+        // Update settings value.
+        int newStreakLimit = 4;
+        Settings.Global.putInt(mResolver,
+                Settings.Global.BLOCKING_HELPER_STREAK_LIMIT, newStreakLimit);
+
+        // Notify for the settings value we updated.
+        mAssistantSettings.onChange(false, Settings.Global.getUriFor(
+                Settings.Global.BLOCKING_HELPER_STREAK_LIMIT));
+
+        assertEquals(newStreakLimit, mAssistantSettings.mStreakLimit);
+        verify(mOnUpdateRunnable).run();
+    }
+
+    @Test
+    public void testDismissToViewRatioLimit() {
+        verify(mOnUpdateRunnable, never()).run();
+
+        // Update settings value.
+        float newDismissToViewRatioLimit = 3f;
+        Settings.Global.putFloat(mResolver,
+                Settings.Global.BLOCKING_HELPER_DISMISS_TO_VIEW_RATIO_LIMIT,
+                newDismissToViewRatioLimit);
+
+        // Notify for the settings value we updated.
+        mAssistantSettings.onChange(false, Settings.Global.getUriFor(
+                Settings.Global.BLOCKING_HELPER_DISMISS_TO_VIEW_RATIO_LIMIT));
+
+        assertEquals(newDismissToViewRatioLimit, mAssistantSettings.mDismissToViewRatioLimit, 1e-6);
+        verify(mOnUpdateRunnable).run();
+    }
+}
diff --git a/packages/ExtServices/tests/src/android/ext/services/notification/AssistantTest.java b/packages/ExtServices/tests/src/android/ext/services/notification/AssistantTest.java
index 2eb005a..0a95b83 100644
--- a/packages/ExtServices/tests/src/android/ext/services/notification/AssistantTest.java
+++ b/packages/ExtServices/tests/src/android/ext/services/notification/AssistantTest.java
@@ -33,13 +33,11 @@
 import android.app.INotificationManager;
 import android.app.Notification;
 import android.app.NotificationChannel;
-import android.content.ContentResolver;
 import android.content.Intent;
 import android.content.pm.ApplicationInfo;
 import android.content.pm.IPackageManager;
 import android.os.Build;
 import android.os.UserHandle;
-import android.provider.Settings;
 import android.service.notification.Adjustment;
 import android.service.notification.NotificationListenerService;
 import android.service.notification.NotificationListenerService.Ranking;
@@ -86,8 +84,7 @@
 
     @Mock INotificationManager mNoMan;
     @Mock AtomicFile mFile;
-    @Mock
-    IPackageManager mPackageManager;
+    @Mock IPackageManager mPackageManager;
 
     Assistant mAssistant;
     Application mApplication;
@@ -108,20 +105,26 @@
                 new Intent("android.service.notification.NotificationAssistantService");
         startIntent.setPackage("android.ext.services");
 
-        // To bypass real calls to global settings values, set the Settings values here.
-        Settings.Global.putFloat(mContext.getContentResolver(),
-                Settings.Global.BLOCKING_HELPER_DISMISS_TO_VIEW_RATIO_LIMIT, 0.8f);
-        Settings.Global.putInt(mContext.getContentResolver(),
-                Settings.Global.BLOCKING_HELPER_STREAK_LIMIT, 2);
         mApplication = (Application) InstrumentationRegistry.getInstrumentation().
                 getTargetContext().getApplicationContext();
         // Force the test to use the correct application instead of trying to use a mock application
         setApplication(mApplication);
-        bindService(startIntent);
+
+        setupService();
         mAssistant = getService();
+
+        // Override the AssistantSettings factory.
+        mAssistant.mSettingsFactory = AssistantSettings::createForTesting;
+
+        bindService(startIntent);
+
+        mAssistant.mSettings.mDismissToViewRatioLimit = 0.8f;
+        mAssistant.mSettings.mStreakLimit = 2;
+        mAssistant.mSettings.mNewInterruptionModel = true;
         mAssistant.setNoMan(mNoMan);
         mAssistant.setFile(mFile);
         mAssistant.setPackageManager(mPackageManager);
+
         ApplicationInfo info = mock(ApplicationInfo.class);
         when(mPackageManager.getApplicationInfo(anyString(), anyInt(), anyInt()))
                 .thenReturn(info);
@@ -408,6 +411,8 @@
         mAssistant.writeXml(serializer);
 
         Assistant assistant = new Assistant();
+        // onCreate is not invoked, so settings won't be initialised, unless we do it here.
+        assistant.mSettings = mAssistant.mSettings;
         assistant.readXml(new BufferedInputStream(new ByteArrayInputStream(baos.toByteArray())));
 
         assertEquals(ci1, assistant.getImpressions(key1));
@@ -417,8 +422,6 @@
 
     @Test
     public void testSettingsProviderUpdate() {
-        ContentResolver resolver = mApplication.getContentResolver();
-
         // Set up channels
         String key = mAssistant.getKey("pkg1", 1, "channel1");
         ChannelImpressions ci = new ChannelImpressions();
@@ -435,19 +438,11 @@
         assertEquals(false, ci.shouldTriggerBlock());
 
         // Update settings values.
-        float newDismissToViewRatioLimit = 0f;
-        int newStreakLimit = 0;
-        Settings.Global.putFloat(resolver,
-                Settings.Global.BLOCKING_HELPER_DISMISS_TO_VIEW_RATIO_LIMIT,
-                newDismissToViewRatioLimit);
-        Settings.Global.putInt(resolver,
-                Settings.Global.BLOCKING_HELPER_STREAK_LIMIT, newStreakLimit);
+        mAssistant.mSettings.mDismissToViewRatioLimit = 0f;
+        mAssistant.mSettings.mStreakLimit = 0;
 
         // Notify for the settings values we updated.
-        mAssistant.mSettingsObserver.onChange(false, Settings.Global.getUriFor(
-                Settings.Global.BLOCKING_HELPER_STREAK_LIMIT));
-        mAssistant.mSettingsObserver.onChange(false, Settings.Global.getUriFor(
-                Settings.Global.BLOCKING_HELPER_DISMISS_TO_VIEW_RATIO_LIMIT));
+        mAssistant.mSettings.mOnUpdateRunnable.run();
 
         // With the new threshold, the blocking helper should be triggered.
         assertEquals(true, ci.shouldTriggerBlock());
diff --git a/packages/SettingsLib/Android.bp b/packages/SettingsLib/Android.bp
index cc17b25..0126e7e5 100644
--- a/packages/SettingsLib/Android.bp
+++ b/packages/SettingsLib/Android.bp
@@ -18,6 +18,7 @@
         "SettingsLibSettingsSpinner",
         "SettingsLayoutPreference",
         "ActionButtonsPreference",
+        "SettingsLibEntityHeaderWidgets",
     ],
 
     // ANDROIDMK TRANSLATION ERROR: unsupported assignment to LOCAL_SHARED_JAVA_LIBRARIES
diff --git a/packages/SettingsLib/EntityHeaderWidgets/Android.bp b/packages/SettingsLib/EntityHeaderWidgets/Android.bp
new file mode 100644
index 0000000..3ca4ecd
--- /dev/null
+++ b/packages/SettingsLib/EntityHeaderWidgets/Android.bp
@@ -0,0 +1,14 @@
+android_library {
+    name: "SettingsLibEntityHeaderWidgets",
+
+    srcs: ["src/**/*.java"],
+    resource_dirs: ["res"],
+
+    static_libs: [
+          "androidx.annotation_annotation",
+          "SettingsLibAppPreference"
+    ],
+
+    sdk_version: "system_current",
+    min_sdk_version: "21",
+}
diff --git a/packages/SettingsLib/EntityHeaderWidgets/AndroidManifest.xml b/packages/SettingsLib/EntityHeaderWidgets/AndroidManifest.xml
new file mode 100644
index 0000000..4b9f1ab
--- /dev/null
+++ b/packages/SettingsLib/EntityHeaderWidgets/AndroidManifest.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Copyright (C) 2018 The Android Open Source Project
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+  -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+          package="com.android.settingslib.widget">
+
+    <uses-sdk android:minSdkVersion="21" />
+
+</manifest>
diff --git a/packages/SettingsLib/EntityHeaderWidgets/res/layout/app_entities_header.xml b/packages/SettingsLib/EntityHeaderWidgets/res/layout/app_entities_header.xml
new file mode 100644
index 0000000..9f30eda
--- /dev/null
+++ b/packages/SettingsLib/EntityHeaderWidgets/res/layout/app_entities_header.xml
@@ -0,0 +1,64 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Copyright (C) 2018 The Android Open Source Project
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+  -->
+
+<LinearLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/app_entities_header"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:paddingStart="24dp"
+    android:paddingEnd="8dp"
+    android:gravity="center"
+    android:orientation="vertical">
+
+    <TextView
+        android:id="@+id/header_title"
+        android:layout_width="wrap_content"
+        android:layout_height="48dp"
+        android:gravity="center"
+        android:textAppearance="@style/AppEntitiesHeader.Text.HeaderTitle"/>
+
+    <LinearLayout
+        android:id="@+id/all_apps_view"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:paddingTop="16dp"
+        android:paddingBottom="16dp"
+        android:gravity="center">
+
+        <include
+            android:id="@+id/app1_view"
+            layout="@layout/app_view"/>
+
+        <include
+            android:id="@+id/app2_view"
+            layout="@layout/app_view"/>
+
+        <include
+            android:id="@+id/app3_view"
+            layout="@layout/app_view"/>
+
+    </LinearLayout>
+
+    <Button
+        android:id="@+id/header_details"
+        style="@*android:style/Widget.DeviceDefault.Button.Borderless.Colored"
+        android:layout_width="wrap_content"
+        android:layout_height="48dp"
+        android:gravity="center"/>
+
+</LinearLayout>
diff --git a/packages/SettingsLib/EntityHeaderWidgets/res/layout/app_view.xml b/packages/SettingsLib/EntityHeaderWidgets/res/layout/app_view.xml
new file mode 100644
index 0000000..fcafa31
--- /dev/null
+++ b/packages/SettingsLib/EntityHeaderWidgets/res/layout/app_view.xml
@@ -0,0 +1,50 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Copyright (C) 2018 The Android Open Source Project
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+  -->
+
+<LinearLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="0dp"
+    android:layout_height="wrap_content"
+    android:layout_weight="1"
+    android:layout_marginEnd="16dp"
+    android:gravity="center"
+    android:orientation="vertical">
+
+    <ImageView
+        android:id="@+id/app_icon"
+        android:layout_width="@dimen/secondary_app_icon_size"
+        android:layout_height="@dimen/secondary_app_icon_size"
+        android:layout_marginBottom="12dp"/>
+
+    <TextView
+        android:id="@+id/app_title"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_marginBottom="2dp"
+        android:singleLine="true"
+        android:ellipsize="marquee"
+        android:textAppearance="@style/AppEntitiesHeader.Text.Title"/>
+
+    <TextView
+        android:id="@+id/app_summary"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:singleLine="true"
+        android:ellipsize="marquee"
+        android:textAppearance="@style/AppEntitiesHeader.Text.Summary"/>
+
+</LinearLayout>
\ No newline at end of file
diff --git a/packages/SettingsLib/EntityHeaderWidgets/res/values/styles.xml b/packages/SettingsLib/EntityHeaderWidgets/res/values/styles.xml
new file mode 100644
index 0000000..0eefd4b
--- /dev/null
+++ b/packages/SettingsLib/EntityHeaderWidgets/res/values/styles.xml
@@ -0,0 +1,38 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Copyright (C) 2018 The Android Open Source Project
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+  -->
+
+<resources>
+    <style name="AppEntitiesHeader.Text"
+           parent="@android:style/TextAppearance.Material.Subhead">
+        <item name="android:fontFamily">@*android:string/config_headlineFontFamilyMedium</item>
+        <item name="android:textColor">?android:attr/textColorPrimary</item>
+    </style>
+
+    <style name="AppEntitiesHeader.Text.HeaderTitle">
+        <item name="android:textSize">14sp</item>
+    </style>
+
+    <style name="AppEntitiesHeader.Text.Title">
+        <item name="android:textSize">16sp</item>
+    </style>
+
+    <style name="AppEntitiesHeader.Text.Summary"
+           parent="@android:style/TextAppearance.Material.Body1">
+        <item name="android:textColor">?android:attr/textColorSecondary</item>
+        <item name="android:textSize">14sp</item>
+    </style>
+</resources>
\ No newline at end of file
diff --git a/packages/SettingsLib/EntityHeaderWidgets/src/com/android/settingslib/widget/AppEntitiesHeaderController.java b/packages/SettingsLib/EntityHeaderWidgets/src/com/android/settingslib/widget/AppEntitiesHeaderController.java
new file mode 100644
index 0000000..8ccf89f
--- /dev/null
+++ b/packages/SettingsLib/EntityHeaderWidgets/src/com/android/settingslib/widget/AppEntitiesHeaderController.java
@@ -0,0 +1,267 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.widget;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.drawable.Drawable;
+import android.text.TextUtils;
+import android.util.Log;
+import android.view.View;
+import android.widget.Button;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.StringRes;
+import androidx.annotation.VisibleForTesting;
+
+/**
+ * This is used to initialize view which was inflated
+ * from {@link R.xml.app_entities_header.xml}.
+ *
+ * <p>The view looks like below.
+ *
+ * <pre>
+ * --------------------------------------------------------------
+ * |                     Header title                           |
+ * --------------------------------------------------------------
+ * |    App1 icon       |   App2 icon        |   App3 icon      |
+ * |    App1 title      |   App2 title       |   App3 title     |
+ * |    App1 summary    |   App2 summary     |   App3 summary   |
+ * |-------------------------------------------------------------
+ * |                     Header details                         |
+ * --------------------------------------------------------------
+ * </pre>
+ *
+ * <p>How to use AppEntitiesHeaderController?
+ *
+ * <p>1. Add a {@link LayoutPreference} in layout XML file.
+ * <pre>
+ * &lt;com.android.settingslib.widget.LayoutPreference
+ *        android:key="app_entities_header"
+ *        android:layout="@layout/app_entities_header"/&gt;
+ * </pre>
+ *
+ * <p>2. Use AppEntitiesHeaderController to call below methods, then you can initialize
+ * view of <code>app_entities_header</code>.
+ *
+ * <pre>
+ *
+ * View headerView = ((LayoutPreference) screen.findPreference("app_entities_header"))
+ *         .findViewById(R.id.app_entities_header);
+ *
+ * AppEntitiesHeaderController.newInstance(context, headerView)
+ *         .setHeaderTitleRes(R.string.xxxxx)
+ *         .setHeaderDetailsRes(R.string.xxxxx)
+ *         .setHeaderDetailsClickListener(onClickListener)
+ *         .setAppEntity(0, icon, "app title", "app summary")
+ *         .setAppEntity(1, icon, "app title", "app summary")
+ *         .setAppEntity(2, icon, "app title", "app summary")
+ *         .apply();
+ * </pre>
+ */
+public class AppEntitiesHeaderController {
+
+    private static final String TAG = "AppEntitiesHeaderCtl";
+
+    @VisibleForTesting
+    static final int MAXIMUM_APPS = 3;
+
+    private final Context mContext;
+    private final TextView mHeaderTitleView;
+    private final Button mHeaderDetailsView;
+
+    private final AppEntity[] mAppEntities;
+    private final View[] mAppEntityViews;
+    private final ImageView[] mAppIconViews;
+    private final TextView[] mAppTitleViews;
+    private final TextView[] mAppSummaryViews;
+
+    private int mHeaderTitleRes;
+    private int mHeaderDetailsRes;
+    private View.OnClickListener mDetailsOnClickListener;
+
+    /**
+     * Creates a new instance of the controller.
+     *
+     * @param context the Context the view is running in
+     * @param appEntitiesHeaderView view was inflated from <code>app_entities_header</code>
+     */
+    public static AppEntitiesHeaderController newInstance(@NonNull Context context,
+            @NonNull View appEntitiesHeaderView) {
+        return new AppEntitiesHeaderController(context, appEntitiesHeaderView);
+    }
+
+    private AppEntitiesHeaderController(Context context, View appEntitiesHeaderView) {
+        mContext = context;
+        mHeaderTitleView = appEntitiesHeaderView.findViewById(R.id.header_title);
+        mHeaderDetailsView = appEntitiesHeaderView.findViewById(R.id.header_details);
+
+        mAppEntities = new AppEntity[MAXIMUM_APPS];
+        mAppIconViews = new ImageView[MAXIMUM_APPS];
+        mAppTitleViews = new TextView[MAXIMUM_APPS];
+        mAppSummaryViews = new TextView[MAXIMUM_APPS];
+
+        mAppEntityViews = new View[]{
+                appEntitiesHeaderView.findViewById(R.id.app1_view),
+                appEntitiesHeaderView.findViewById(R.id.app2_view),
+                appEntitiesHeaderView.findViewById(R.id.app3_view)
+        };
+
+        // Initialize view in advance, so we won't take too much time to do it when controller is
+        // binding view.
+        for (int index = 0; index < MAXIMUM_APPS; index++) {
+            final View appView = mAppEntityViews[index];
+            mAppIconViews[index] = (ImageView) appView.findViewById(R.id.app_icon);
+            mAppTitleViews[index] = (TextView) appView.findViewById(R.id.app_title);
+            mAppSummaryViews[index] = (TextView) appView.findViewById(R.id.app_summary);
+        }
+    }
+
+    /**
+     * Set the text resource for app entities header title.
+     */
+    public AppEntitiesHeaderController setHeaderTitleRes(@StringRes int titleRes) {
+        mHeaderTitleRes = titleRes;
+        return this;
+    }
+
+    /**
+     * Set the text resource for app entities header details.
+     */
+    public AppEntitiesHeaderController setHeaderDetailsRes(@StringRes int detailsRes) {
+        mHeaderDetailsRes = detailsRes;
+        return this;
+    }
+
+    /**
+     * Register a callback to be invoked when header details view is clicked.
+     */
+    public AppEntitiesHeaderController setHeaderDetailsClickListener(
+            @Nullable View.OnClickListener clickListener) {
+        mDetailsOnClickListener = clickListener;
+        return this;
+    }
+
+    /**
+     * Set an app entity at a specified position view.
+     *
+     * @param index the index at which the specified view is to be inserted
+     * @param icon the icon of app entity
+     * @param titleRes the title of app entity
+     * @param summaryRes the summary of app entity
+     * @return this {@code AppEntitiesHeaderController} object
+     */
+    public AppEntitiesHeaderController setAppEntity(int index, @NonNull Drawable icon,
+            @Nullable CharSequence titleRes, @Nullable CharSequence summaryRes) {
+        final AppEntity appEntity = new AppEntity(icon, titleRes, summaryRes);
+        mAppEntities[index] = appEntity;
+        return this;
+    }
+
+    /**
+     * Remove an app entity at a specified position view.
+     *
+     * @param index the index at which the specified view is to be removed
+     * @return this {@code AppEntitiesHeaderController} object
+     */
+    public AppEntitiesHeaderController removeAppEntity(int index) {
+        mAppEntities[index] = null;
+        return this;
+    }
+
+    /**
+     * Clear all app entities in app entities header.
+     *
+     * @return this {@code AppEntitiesHeaderController} object
+     */
+    public AppEntitiesHeaderController clearAllAppEntities() {
+        for (int index = 0; index < MAXIMUM_APPS; index++) {
+            removeAppEntity(index);
+        }
+        return this;
+    }
+
+    /**
+     * Done mutating app entities header, rebinds everything.
+     */
+    public void apply() {
+        bindHeaderTitleView();
+        bindHeaderDetailsView();
+
+        // Rebind all apps view
+        for (int index = 0; index < MAXIMUM_APPS; index++) {
+            bindAppEntityView(index);
+        }
+    }
+
+    private void bindHeaderTitleView() {
+        CharSequence titleText = "";
+        try {
+            titleText = mContext.getText(mHeaderTitleRes);
+        } catch (Resources.NotFoundException e) {
+            Log.e(TAG, "Resource of header title can't not be found!", e);
+        }
+        mHeaderTitleView.setText(titleText);
+        mHeaderTitleView.setVisibility(
+                TextUtils.isEmpty(titleText) ? View.GONE : View.VISIBLE);
+    }
+
+    private void bindHeaderDetailsView() {
+        CharSequence detailsText = "";
+        try {
+            detailsText = mContext.getText(mHeaderDetailsRes);
+        } catch (Resources.NotFoundException e) {
+            Log.e(TAG, "Resource of header details can't not be found!", e);
+        }
+        mHeaderDetailsView.setText(detailsText);
+        mHeaderDetailsView.setVisibility(
+                TextUtils.isEmpty(detailsText) ? View.GONE : View.VISIBLE);
+        mHeaderDetailsView.setOnClickListener(mDetailsOnClickListener);
+    }
+
+    private void bindAppEntityView(int index) {
+        final AppEntity appEntity = mAppEntities[index];
+        mAppEntityViews[index].setVisibility(appEntity != null ? View.VISIBLE : View.GONE);
+
+        if (appEntity != null) {
+            mAppIconViews[index].setImageDrawable(appEntity.icon);
+
+            mAppTitleViews[index].setVisibility(
+                    TextUtils.isEmpty(appEntity.title) ? View.INVISIBLE : View.VISIBLE);
+            mAppTitleViews[index].setText(appEntity.title);
+
+            mAppSummaryViews[index].setVisibility(
+                    TextUtils.isEmpty(appEntity.summary) ? View.INVISIBLE : View.VISIBLE);
+            mAppSummaryViews[index].setText(appEntity.summary);
+        }
+    }
+
+    private static class AppEntity {
+        public final Drawable icon;
+        public final CharSequence title;
+        public final CharSequence summary;
+
+        AppEntity(Drawable appIcon, CharSequence appTitle, CharSequence appSummary) {
+            icon = appIcon;
+            title = appTitle;
+            summary = appSummary;
+        }
+    }
+}
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/widget/AppEntitiesHeaderControllerTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/widget/AppEntitiesHeaderControllerTest.java
new file mode 100644
index 0000000..c3bc8da
--- /dev/null
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/widget/AppEntitiesHeaderControllerTest.java
@@ -0,0 +1,181 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.widget;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.content.Context;
+import android.graphics.drawable.Drawable;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.RuntimeEnvironment;
+
+@RunWith(RobolectricTestRunner.class)
+public class AppEntitiesHeaderControllerTest {
+
+    private static final CharSequence TITLE = "APP_TITLE";
+    private static final CharSequence SUMMARY = "APP_SUMMARY";
+
+    @Rule
+    public final ExpectedException thrown = ExpectedException.none();
+
+    private Context mContext;
+    private Drawable mIcon;
+    private View mAppEntitiesHeaderView;
+    private AppEntitiesHeaderController mController;
+
+    @Before
+    public void setUp() {
+        mContext = RuntimeEnvironment.application;
+        mAppEntitiesHeaderView = LayoutInflater.from(mContext).inflate(
+                R.layout.app_entities_header, null /* root */);
+        mIcon = mContext.getDrawable(R.drawable.ic_menu);
+        mController = AppEntitiesHeaderController.newInstance(mContext,
+                mAppEntitiesHeaderView);
+    }
+
+    @Test
+    public void assert_amountOfMaximumAppsAreThree() {
+        assertThat(AppEntitiesHeaderController.MAXIMUM_APPS).isEqualTo(3);
+    }
+
+    @Test
+    public void setHeaderTitleRes_setTextRes_shouldSetToTitleView() {
+        mController.setHeaderTitleRes(R.string.expand_button_title).apply();
+        final TextView view = mAppEntitiesHeaderView.findViewById(R.id.header_title);
+
+        assertThat(view.getText()).isEqualTo(mContext.getText(R.string.expand_button_title));
+    }
+
+    @Test
+    public void setHeaderDetailsRes_setTextRes_shouldSetToDetailsView() {
+        mController.setHeaderDetailsRes(R.string.expand_button_title).apply();
+        final TextView view = mAppEntitiesHeaderView.findViewById(R.id.header_details);
+
+        assertThat(view.getText()).isEqualTo(mContext.getText(R.string.expand_button_title));
+    }
+
+    @Test
+    public void setHeaderDetailsClickListener_setClickListener_detailsViewAttachClickListener() {
+        mController.setHeaderDetailsClickListener(v -> {
+        }).apply();
+        final TextView view = mAppEntitiesHeaderView.findViewById(R.id.header_details);
+
+        assertThat(view.hasOnClickListeners()).isTrue();
+    }
+
+    @Test
+    public void setAppEntity_indexLessThanZero_shouldThrowArrayIndexOutOfBoundsException() {
+        thrown.expect(ArrayIndexOutOfBoundsException.class);
+
+        mController.setAppEntity(-1, mIcon, TITLE, SUMMARY);
+    }
+
+    @Test
+    public void asetAppEntity_indexGreaterThanMaximum_shouldThrowArrayIndexOutOfBoundsException() {
+        thrown.expect(ArrayIndexOutOfBoundsException.class);
+
+        mController.setAppEntity(AppEntitiesHeaderController.MAXIMUM_APPS + 1, mIcon, TITLE,
+                SUMMARY);
+    }
+
+    @Test
+    public void setAppEntity_addAppToIndex0_shouldShowAppView1() {
+        mController.setAppEntity(0, mIcon, TITLE, SUMMARY).apply();
+        final View app1View = mAppEntitiesHeaderView.findViewById(R.id.app1_view);
+        final ImageView appIconView = app1View.findViewById(R.id.app_icon);
+        final TextView appTitle = app1View.findViewById(R.id.app_title);
+        final TextView appSummary = app1View.findViewById(R.id.app_summary);
+
+        assertThat(app1View.getVisibility()).isEqualTo(View.VISIBLE);
+        assertThat(appIconView.getDrawable()).isNotNull();
+        assertThat(appTitle.getText()).isEqualTo(TITLE);
+        assertThat(appSummary.getText()).isEqualTo(SUMMARY);
+    }
+
+    @Test
+    public void setAppEntity_addAppToIndex1_shouldShowAppView2() {
+        mController.setAppEntity(1, mIcon, TITLE, SUMMARY).apply();
+        final View app2View = mAppEntitiesHeaderView.findViewById(R.id.app2_view);
+        final ImageView appIconView = app2View.findViewById(R.id.app_icon);
+        final TextView appTitle = app2View.findViewById(R.id.app_title);
+        final TextView appSummary = app2View.findViewById(R.id.app_summary);
+
+        assertThat(app2View.getVisibility()).isEqualTo(View.VISIBLE);
+        assertThat(appIconView.getDrawable()).isNotNull();
+        assertThat(appTitle.getText()).isEqualTo(TITLE);
+        assertThat(appSummary.getText()).isEqualTo(SUMMARY);
+    }
+
+    @Test
+    public void setAppEntity_addAppToIndex2_shouldShowAppView3() {
+        mController.setAppEntity(2, mIcon, TITLE, SUMMARY).apply();
+        final View app3View = mAppEntitiesHeaderView.findViewById(R.id.app3_view);
+        final ImageView appIconView = app3View.findViewById(R.id.app_icon);
+        final TextView appTitle = app3View.findViewById(R.id.app_title);
+        final TextView appSummary = app3View.findViewById(R.id.app_summary);
+
+        assertThat(app3View.getVisibility()).isEqualTo(View.VISIBLE);
+        assertThat(appIconView.getDrawable()).isNotNull();
+        assertThat(appTitle.getText()).isEqualTo(TITLE);
+        assertThat(appSummary.getText()).isEqualTo(SUMMARY);
+    }
+
+    @Test
+    public void removeAppEntity_removeIndex0_shouldNotShowAppView1() {
+        mController.setAppEntity(0, mIcon, TITLE, SUMMARY)
+                .setAppEntity(1, mIcon, TITLE, SUMMARY).apply();
+        final View app1View = mAppEntitiesHeaderView.findViewById(R.id.app1_view);
+        final View app2View = mAppEntitiesHeaderView.findViewById(R.id.app2_view);
+
+        assertThat(app1View.getVisibility()).isEqualTo(View.VISIBLE);
+        assertThat(app2View.getVisibility()).isEqualTo(View.VISIBLE);
+
+        mController.removeAppEntity(0).apply();
+
+        assertThat(app1View.getVisibility()).isEqualTo(View.GONE);
+        assertThat(app2View.getVisibility()).isEqualTo(View.VISIBLE);
+    }
+
+    @Test
+    public void clearAllAppEntities_shouldNotShowAllAppViews() {
+        mController.setAppEntity(0, mIcon, TITLE, SUMMARY)
+                .setAppEntity(1, mIcon, TITLE, SUMMARY)
+                .setAppEntity(2, mIcon, TITLE, SUMMARY).apply();
+        final View app1View = mAppEntitiesHeaderView.findViewById(R.id.app1_view);
+        final View app2View = mAppEntitiesHeaderView.findViewById(R.id.app2_view);
+        final View app3View = mAppEntitiesHeaderView.findViewById(R.id.app3_view);
+
+        assertThat(app1View.getVisibility()).isEqualTo(View.VISIBLE);
+        assertThat(app2View.getVisibility()).isEqualTo(View.VISIBLE);
+        assertThat(app3View.getVisibility()).isEqualTo(View.VISIBLE);
+
+        mController.clearAllAppEntities().apply();
+        assertThat(app1View.getVisibility()).isEqualTo(View.GONE);
+        assertThat(app2View.getVisibility()).isEqualTo(View.GONE);
+        assertThat(app3View.getVisibility()).isEqualTo(View.GONE);
+    }
+}
diff --git a/services/core/java/com/android/server/ConnectivityService.java b/services/core/java/com/android/server/ConnectivityService.java
index 14503f9..eda9fe1 100644
--- a/services/core/java/com/android/server/ConnectivityService.java
+++ b/services/core/java/com/android/server/ConnectivityService.java
@@ -902,6 +902,7 @@
         // Listen to package add and removal events for all users.
         intentFilter = new IntentFilter();
         intentFilter.addAction(Intent.ACTION_PACKAGE_ADDED);
+        intentFilter.addAction(Intent.ACTION_PACKAGE_REPLACED);
         intentFilter.addAction(Intent.ACTION_PACKAGE_REMOVED);
         intentFilter.addDataScheme("package");
         mContext.registerReceiverAsUser(
@@ -4203,12 +4204,46 @@
         mPermissionMonitor.onPackageAdded(packageName, uid);
     }
 
-    private void onPackageRemoved(String packageName, int uid) {
+    private void onPackageReplaced(String packageName, int uid) {
+        if (TextUtils.isEmpty(packageName) || uid < 0) {
+            Slog.wtf(TAG, "Invalid package in onPackageReplaced: " + packageName + " | " + uid);
+            return;
+        }
+        final int userId = UserHandle.getUserId(uid);
+        synchronized (mVpns) {
+            final Vpn vpn = mVpns.get(userId);
+            if (vpn == null) {
+                return;
+            }
+            // Legacy always-on VPN won't be affected since the package name is not set.
+            if (TextUtils.equals(vpn.getAlwaysOnPackage(), packageName)) {
+                Slog.d(TAG, "Restarting always-on VPN package " + packageName + " for user "
+                        + userId);
+                vpn.startAlwaysOnVpn();
+            }
+        }
+    }
+
+    private void onPackageRemoved(String packageName, int uid, boolean isReplacing) {
         if (TextUtils.isEmpty(packageName) || uid < 0) {
             Slog.wtf(TAG, "Invalid package in onPackageRemoved: " + packageName + " | " + uid);
             return;
         }
         mPermissionMonitor.onPackageRemoved(uid);
+
+        final int userId = UserHandle.getUserId(uid);
+        synchronized (mVpns) {
+            final Vpn vpn = mVpns.get(userId);
+            if (vpn == null) {
+                return;
+            }
+            // Legacy always-on VPN won't be affected since the package name is not set.
+            if (TextUtils.equals(vpn.getAlwaysOnPackage(), packageName) && !isReplacing) {
+                Slog.d(TAG, "Removing always-on VPN package " + packageName + " for user "
+                        + userId);
+                vpn.setAlwaysOnPackage(null, false);
+            }
+        }
     }
 
     private void onUserUnlocked(int userId) {
@@ -4245,8 +4280,12 @@
                 onUserUnlocked(userId);
             } else if (Intent.ACTION_PACKAGE_ADDED.equals(action)) {
                 onPackageAdded(packageName, uid);
+            } else if (Intent.ACTION_PACKAGE_REPLACED.equals(action)) {
+                onPackageReplaced(packageName, uid);
             } else if (Intent.ACTION_PACKAGE_REMOVED.equals(action)) {
-                onPackageRemoved(packageName, uid);
+                final boolean isReplacing = intent.getBooleanExtra(
+                        Intent.EXTRA_REPLACING, false);
+                onPackageRemoved(packageName, uid, isReplacing);
             }
         }
     };
diff --git a/services/core/java/com/android/server/connectivity/Tethering.java b/services/core/java/com/android/server/connectivity/Tethering.java
index 3c14393..d75601b 100644
--- a/services/core/java/com/android/server/connectivity/Tethering.java
+++ b/services/core/java/com/android/server/connectivity/Tethering.java
@@ -944,10 +944,11 @@
     public boolean hasTetherableConfiguration() {
         final TetheringConfiguration cfg = mConfig;
         final boolean hasDownstreamConfiguration =
-                (cfg.tetherableUsbRegexs.length != 0) ||
-                (cfg.tetherableWifiRegexs.length != 0) ||
-                (cfg.tetherableBluetoothRegexs.length != 0);
-        final boolean hasUpstreamConfiguration = !cfg.preferredUpstreamIfaceTypes.isEmpty();
+                (cfg.tetherableUsbRegexs.length != 0)
+                || (cfg.tetherableWifiRegexs.length != 0)
+                || (cfg.tetherableBluetoothRegexs.length != 0);
+        final boolean hasUpstreamConfiguration = !cfg.preferredUpstreamIfaceTypes.isEmpty()
+                || cfg.chooseUpstreamAutomatically;
 
         return hasDownstreamConfiguration && hasUpstreamConfiguration;
     }
diff --git a/services/core/java/com/android/server/connectivity/Vpn.java b/services/core/java/com/android/server/connectivity/Vpn.java
index b7ed2f9..602aedb 100644
--- a/services/core/java/com/android/server/connectivity/Vpn.java
+++ b/services/core/java/com/android/server/connectivity/Vpn.java
@@ -206,45 +206,6 @@
     // Handle of the user initiating VPN.
     private final int mUserHandle;
 
-    // Listen to package removal and change events (update/uninstall) for this user
-    private final BroadcastReceiver mPackageIntentReceiver = new BroadcastReceiver() {
-        @Override
-        public void onReceive(Context context, Intent intent) {
-            final Uri data = intent.getData();
-            final String packageName = data == null ? null : data.getSchemeSpecificPart();
-            if (packageName == null) {
-                return;
-            }
-
-            synchronized (Vpn.this) {
-                // Avoid race where always-on package has been unset
-                if (!packageName.equals(getAlwaysOnPackage())) {
-                    return;
-                }
-
-                final String action = intent.getAction();
-                Log.i(TAG, "Received broadcast " + action + " for always-on VPN package "
-                        + packageName + " in user " + mUserHandle);
-
-                switch(action) {
-                    case Intent.ACTION_PACKAGE_REPLACED:
-                        // Start vpn after app upgrade
-                        startAlwaysOnVpn();
-                        break;
-                    case Intent.ACTION_PACKAGE_REMOVED:
-                        final boolean isPackageRemoved = !intent.getBooleanExtra(
-                                Intent.EXTRA_REPLACING, false);
-                        if (isPackageRemoved) {
-                            setAlwaysOnPackage(null, false);
-                        }
-                        break;
-                }
-            }
-        }
-    };
-
-    private boolean mIsPackageIntentReceiverRegistered = false;
-
     public Vpn(Looper looper, Context context, INetworkManagementService netService,
             @UserIdInt int userHandle) {
         this(looper, context, netService, userHandle, new SystemServices(context));
@@ -500,7 +461,6 @@
             // Prepare this app. The notification will update as a side-effect of updateState().
             prepareInternal(packageName);
         }
-        maybeRegisterPackageChangeReceiverLocked(packageName);
         setVpnForcedLocked(mLockdown);
         return true;
     }
@@ -509,31 +469,6 @@
         return packageName == null || VpnConfig.LEGACY_VPN.equals(packageName);
     }
 
-    private void unregisterPackageChangeReceiverLocked() {
-        if (mIsPackageIntentReceiverRegistered) {
-            mContext.unregisterReceiver(mPackageIntentReceiver);
-            mIsPackageIntentReceiverRegistered = false;
-        }
-    }
-
-    private void maybeRegisterPackageChangeReceiverLocked(String packageName) {
-        // Unregister IntentFilter listening for previous always-on package change
-        unregisterPackageChangeReceiverLocked();
-
-        if (!isNullOrLegacyVpn(packageName)) {
-            mIsPackageIntentReceiverRegistered = true;
-
-            IntentFilter intentFilter = new IntentFilter();
-            // Protected intent can only be sent by system. No permission required in register.
-            intentFilter.addAction(Intent.ACTION_PACKAGE_REPLACED);
-            intentFilter.addAction(Intent.ACTION_PACKAGE_REMOVED);
-            intentFilter.addDataScheme("package");
-            intentFilter.addDataSchemeSpecificPart(packageName, PatternMatcher.PATTERN_LITERAL);
-            mContext.registerReceiverAsUser(
-                    mPackageIntentReceiver, UserHandle.of(mUserHandle), intentFilter, null, null);
-        }
-    }
-
     /**
      * @return the package name of the VPN controller responsible for always-on VPN,
      *         or {@code null} if none is set or always-on VPN is controlled through
@@ -1302,7 +1237,6 @@
         setLockdown(false);
         mAlwaysOn = false;
 
-        unregisterPackageChangeReceiverLocked();
         // Quit any active connections
         agentDisconnect();
     }
diff --git a/services/core/java/com/android/server/input/InputManagerService.java b/services/core/java/com/android/server/input/InputManagerService.java
index d96b6cb..e7c3c7b 100644
--- a/services/core/java/com/android/server/input/InputManagerService.java
+++ b/services/core/java/com/android/server/input/InputManagerService.java
@@ -1951,6 +1951,11 @@
     }
 
     // Native callback.
+    private int getPointerDisplayId() {
+        return mWindowManagerCallbacks.getPointerDisplayId();
+    }
+
+    // Native callback.
     private String[] getKeyboardLayoutOverlay(InputDeviceIdentifier identifier) {
         if (!mSystemReady) {
             return null;
@@ -2017,6 +2022,8 @@
                 KeyEvent event, int policyFlags);
 
         public int getPointerLayer();
+
+        public int getPointerDisplayId();
     }
 
     /**
diff --git a/services/core/java/com/android/server/locksettings/recoverablekeystore/certificate/CertUtils.java b/services/core/java/com/android/server/locksettings/recoverablekeystore/certificate/CertUtils.java
index 6e08949..26e8270 100644
--- a/services/core/java/com/android/server/locksettings/recoverablekeystore/certificate/CertUtils.java
+++ b/services/core/java/com/android/server/locksettings/recoverablekeystore/certificate/CertUtils.java
@@ -16,13 +16,17 @@
 
 package com.android.server.locksettings.recoverablekeystore.certificate;
 
-import static javax.xml.xpath.XPathConstants.NODESET;
-
 import android.annotation.IntDef;
 import android.annotation.Nullable;
 
 import com.android.internal.annotations.VisibleForTesting;
 
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
+import org.xml.sax.SAXException;
+
 import java.io.ByteArrayInputStream;
 import java.io.IOException;
 import java.io.InputStream;
@@ -40,7 +44,6 @@
 import java.security.cert.CertPathValidator;
 import java.security.cert.CertPathValidatorException;
 import java.security.cert.CertStore;
-import java.security.cert.Certificate;
 import java.security.cert.CertificateException;
 import java.security.cert.CertificateFactory;
 import java.security.cert.CollectionCertStoreParameters;
@@ -58,15 +61,6 @@
 
 import javax.xml.parsers.DocumentBuilderFactory;
 import javax.xml.parsers.ParserConfigurationException;
-import javax.xml.xpath.XPath;
-import javax.xml.xpath.XPathExpressionException;
-import javax.xml.xpath.XPathFactory;
-
-import org.w3c.dom.Document;
-import org.w3c.dom.Element;
-import org.w3c.dom.Node;
-import org.w3c.dom.NodeList;
-import org.xml.sax.SAXException;
 
 /** Utility functions related to parsing and validating public-key certificates. */
 public final class CertUtils {
@@ -167,50 +161,63 @@
     static List<String> getXmlNodeContents(@MustExist int mustExist, Element rootNode,
             String... nodeTags)
             throws CertParsingException {
-        String expression = String.join("/", nodeTags);
-
-        XPath xPath = XPathFactory.newInstance().newXPath();
-        NodeList nodeList;
-        try {
-            nodeList = (NodeList) xPath.compile(expression).evaluate(rootNode, NODESET);
-        } catch (XPathExpressionException e) {
-            throw new CertParsingException(e);
+        if (nodeTags.length == 0) {
+            throw new CertParsingException("The tag list must not be empty");
         }
 
-        switch (mustExist) {
-            case MUST_EXIST_UNENFORCED:
-                break;
-
-            case MUST_EXIST_EXACTLY_ONE:
-                if (nodeList.getLength() != 1) {
-                    throw new CertParsingException(
-                            "The XML file must contain exactly one node with the path "
-                                    + expression);
-                }
-                break;
-
-            case MUST_EXIST_AT_LEAST_ONE:
-                if (nodeList.getLength() == 0) {
-                    throw new CertParsingException(
-                            "The XML file must contain at least one node with the path "
-                                    + expression);
-                }
-                break;
-
-            default:
-                throw new UnsupportedOperationException(
-                        "This value of MustExist is not supported: " + mustExist);
+        // Go down through all the intermediate node tags (except the last tag for the leaf nodes).
+        // Note that this implementation requires that at most one path exists for the given
+        // intermediate node tags.
+        Element parent = rootNode;
+        for (int i = 0; i < nodeTags.length - 1; i++) {
+            String tag = nodeTags[i];
+            List<Element> children = getXmlDirectChildren(parent, tag);
+            if ((children.size() == 0 && mustExist != MUST_EXIST_UNENFORCED)
+                    || children.size() > 1) {
+                throw new CertParsingException(
+                        "The XML file must contain exactly one path with the tag " + tag);
+            }
+            if (children.size() == 0) {
+                return new ArrayList<>();
+            }
+            parent = children.get(0);
         }
 
+        // Then collect the contents of the leaf nodes.
+        List<Element> leafs = getXmlDirectChildren(parent, nodeTags[nodeTags.length - 1]);
+        if (mustExist == MUST_EXIST_EXACTLY_ONE && leafs.size() != 1) {
+            throw new CertParsingException(
+                    "The XML file must contain exactly one node with the path "
+                            + String.join("/", nodeTags));
+        }
+        if (mustExist == MUST_EXIST_AT_LEAST_ONE && leafs.size() == 0) {
+            throw new CertParsingException(
+                    "The XML file must contain at least one node with the path "
+                            + String.join("/", nodeTags));
+        }
         List<String> result = new ArrayList<>();
-        for (int i = 0; i < nodeList.getLength(); i++) {
-            Node node = nodeList.item(i);
+        for (Element leaf : leafs) {
             // Remove whitespaces and newlines.
-            result.add(node.getTextContent().replaceAll("\\s", ""));
+            result.add(leaf.getTextContent().replaceAll("\\s", ""));
         }
         return result;
     }
 
+    /** Get the direct child nodes with a given tag. */
+    private static List<Element> getXmlDirectChildren(Element parent, String tag) {
+        // Cannot use Element.getElementsByTagName because it will return all descendant elements
+        // with the tag name, i.e. not only the direct child nodes.
+        List<Element> children = new ArrayList<>();
+        NodeList childNodes = parent.getChildNodes();
+        for (int i = 0; i < childNodes.getLength(); i++) {
+            Node node = childNodes.item(i);
+            if (node.getNodeType() == Node.ELEMENT_NODE && node.getNodeName().equals(tag)) {
+                children.add((Element) node);
+            }
+        }
+        return children;
+    }
+
     /**
      * Decodes a base64-encoded string.
      *
diff --git a/services/core/java/com/android/server/pm/PackageManagerService.java b/services/core/java/com/android/server/pm/PackageManagerService.java
index 888675c..b36ac98 100644
--- a/services/core/java/com/android/server/pm/PackageManagerService.java
+++ b/services/core/java/com/android/server/pm/PackageManagerService.java
@@ -86,11 +86,6 @@
 import static android.content.pm.PackageManager.PERMISSION_DENIED;
 import static android.content.pm.PackageManager.PERMISSION_GRANTED;
 import static android.content.pm.PackageParser.isApkFile;
-import static android.content.pm.SharedLibraryNames.ANDROID_HIDL_BASE;
-import static android.content.pm.SharedLibraryNames.ANDROID_HIDL_MANAGER;
-import static android.content.pm.SharedLibraryNames.ANDROID_TEST_BASE;
-import static android.content.pm.SharedLibraryNames.ANDROID_TEST_MOCK;
-import static android.content.pm.SharedLibraryNames.ANDROID_TEST_RUNNER;
 import static android.os.Trace.TRACE_TAG_PACKAGE_MANAGER;
 import static android.os.storage.StorageManager.FLAG_STORAGE_CE;
 import static android.os.storage.StorageManager.FLAG_STORAGE_DE;
@@ -2094,28 +2089,6 @@
         }
     }
 
-    @GuardedBy("mPackages")
-    private void setupBuiltinSharedLibraryDependenciesLocked() {
-        // Builtin libraries don't have versions.
-        long version = SharedLibraryInfo.VERSION_UNDEFINED;
-
-        SharedLibraryInfo libraryInfo = getSharedLibraryInfoLPr(ANDROID_HIDL_MANAGER, version);
-        if (libraryInfo != null) {
-            libraryInfo.addDependency(getSharedLibraryInfoLPr(ANDROID_HIDL_BASE, version));
-        }
-
-        libraryInfo = getSharedLibraryInfoLPr(ANDROID_TEST_RUNNER, version);
-        if (libraryInfo != null) {
-            libraryInfo.addDependency(getSharedLibraryInfoLPr(ANDROID_TEST_MOCK, version));
-            libraryInfo.addDependency(getSharedLibraryInfoLPr(ANDROID_TEST_BASE, version));
-        }
-
-        libraryInfo = getSharedLibraryInfoLPr(ANDROID_TEST_MOCK, version);
-        if (libraryInfo != null) {
-            libraryInfo.addDependency(getSharedLibraryInfoLPr(ANDROID_TEST_BASE, version));
-        }
-    }
-
     public PackageManagerService(Context context, Installer installer,
             boolean factoryTest, boolean onlyCore) {
         LockGuard.installLock(mPackages, LockGuard.INDEX_PACKAGES);
@@ -2223,17 +2196,32 @@
             Watchdog.getInstance().addThread(mHandler, WATCHDOG_TIMEOUT);
             mInstantAppRegistry = new InstantAppRegistry(this);
 
-            ArrayMap<String, String> libConfig = systemConfig.getSharedLibraries();
+            ArrayMap<String, SystemConfig.SharedLibraryEntry> libConfig
+                    = systemConfig.getSharedLibraries();
             final int builtInLibCount = libConfig.size();
             for (int i = 0; i < builtInLibCount; i++) {
                 String name = libConfig.keyAt(i);
-                String path = libConfig.valueAt(i);
-                addSharedLibraryLPw(path, null, null, name, SharedLibraryInfo.VERSION_UNDEFINED,
-                        SharedLibraryInfo.TYPE_BUILTIN, PLATFORM_PACKAGE_NAME, 0);
+                SystemConfig.SharedLibraryEntry entry = libConfig.valueAt(i);
+                addSharedLibraryLPw(entry.filename, null, null, name,
+                        SharedLibraryInfo.VERSION_UNDEFINED, SharedLibraryInfo.TYPE_BUILTIN,
+                        PLATFORM_PACKAGE_NAME, 0);
             }
-            // Builtin libraries cannot encode their dependency where they are
-            // defined, so fix that now.
-            setupBuiltinSharedLibraryDependenciesLocked();
+
+            // Now that we have added all the libraries, iterate again to add dependency
+            // information IFF their dependencies are added.
+            long undefinedVersion = SharedLibraryInfo.VERSION_UNDEFINED;
+            for (int i = 0; i < builtInLibCount; i++) {
+                String name = libConfig.keyAt(i);
+                SystemConfig.SharedLibraryEntry entry = libConfig.valueAt(i);
+                final int dependencyCount = entry.dependencies.length;
+                for (int j = 0; j < dependencyCount; j++) {
+                    final SharedLibraryInfo dependency =
+                        getSharedLibraryInfoLPr(entry.dependencies[j], undefinedVersion);
+                    if (dependency != null) {
+                        getSharedLibraryInfoLPr(name, undefinedVersion).addDependency(dependency);
+                    }
+                }
+            }
 
             SELinuxMMAC.readInstallPolicy();
 
diff --git a/services/core/java/com/android/server/pm/dex/DexManager.java b/services/core/java/com/android/server/pm/dex/DexManager.java
index 3a74ab5..36b7269 100644
--- a/services/core/java/com/android/server/pm/dex/DexManager.java
+++ b/services/core/java/com/android/server/pm/dex/DexManager.java
@@ -16,6 +16,11 @@
 
 package com.android.server.pm.dex;
 
+import static com.android.server.pm.InstructionSets.getAppDexInstructionSets;
+import static com.android.server.pm.dex.PackageDexUsage.DexUseInfo;
+import static com.android.server.pm.dex.PackageDexUsage.PackageUseInfo;
+import static com.android.server.pm.dex.PackageDynamicCodeLoading.PackageDynamicCode;
+
 import android.content.ContentResolver;
 import android.content.Context;
 import android.content.pm.ApplicationInfo;
@@ -26,9 +31,9 @@
 import android.os.Build;
 import android.os.FileUtils;
 import android.os.RemoteException;
-import android.os.storage.StorageManager;
 import android.os.SystemProperties;
 import android.os.UserHandle;
+import android.os.storage.StorageManager;
 import android.provider.Settings.Global;
 import android.util.Log;
 import android.util.Slog;
@@ -48,18 +53,14 @@
 import java.util.Arrays;
 import java.util.Collection;
 import java.util.Collections;
-import java.util.Iterator;
-import java.util.List;
 import java.util.HashMap;
 import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
 import java.util.Map;
 import java.util.Set;
 import java.util.zip.ZipEntry;
 
-import static com.android.server.pm.InstructionSets.getAppDexInstructionSets;
-import static com.android.server.pm.dex.PackageDexUsage.PackageUseInfo;
-import static com.android.server.pm.dex.PackageDexUsage.DexUseInfo;
-
 /**
  * This class keeps track of how dex files are used.
  * Every time it gets a notification about a dex file being loaded it tracks
@@ -89,6 +90,12 @@
     // encode and save the dex usage data.
     private final PackageDexUsage mPackageDexUsage;
 
+    // PackageDynamicCodeLoading handles recording of dynamic code loading -
+    // which is similar to PackageDexUsage but records a different aspect of the data.
+    // (It additionally includes DEX files loaded with unsupported class loaders, and doesn't
+    // record class loaders or ISAs.)
+    private final PackageDynamicCodeLoading mPackageDynamicCodeLoading;
+
     private final IPackageManager mPackageManager;
     private final PackageDexOptimizer mPackageDexOptimizer;
     private final Object mInstallLock;
@@ -126,14 +133,15 @@
 
     public DexManager(Context context, IPackageManager pms, PackageDexOptimizer pdo,
             Installer installer, Object installLock, Listener listener) {
-      mContext = context;
-      mPackageCodeLocationsCache = new HashMap<>();
-      mPackageDexUsage = new PackageDexUsage();
-      mPackageManager = pms;
-      mPackageDexOptimizer = pdo;
-      mInstaller = installer;
-      mInstallLock = installLock;
-      mListener = listener;
+        mContext = context;
+        mPackageCodeLocationsCache = new HashMap<>();
+        mPackageDexUsage = new PackageDexUsage();
+        mPackageDynamicCodeLoading = new PackageDynamicCodeLoading();
+        mPackageManager = pms;
+        mPackageDexOptimizer = pdo;
+        mInstaller = installer;
+        mInstallLock = installLock;
+        mListener = listener;
     }
 
     public void systemReady() {
@@ -207,7 +215,6 @@
                 Slog.i(TAG, loadingAppInfo.packageName +
                         " uses unsupported class loader in " + classLoaderNames);
             }
-            return;
         }
 
         int dexPathIndex = 0;
@@ -236,15 +243,24 @@
                     continue;
                 }
 
-                // Record dex file usage. If the current usage is a new pattern (e.g. new secondary,
-                // or UsedByOtherApps), record will return true and we trigger an async write
-                // to disk to make sure we don't loose the data in case of a reboot.
+                if (mPackageDynamicCodeLoading.record(searchResult.mOwningPackageName, dexPath,
+                        PackageDynamicCodeLoading.FILE_TYPE_DEX, loaderUserId,
+                        loadingAppInfo.packageName)) {
+                    mPackageDynamicCodeLoading.maybeWriteAsync();
+                }
 
-                String classLoaderContext = classLoaderContexts[dexPathIndex];
-                if (mPackageDexUsage.record(searchResult.mOwningPackageName,
-                        dexPath, loaderUserId, loaderIsa, isUsedByOtherApps, primaryOrSplit,
-                        loadingAppInfo.packageName, classLoaderContext)) {
-                    mPackageDexUsage.maybeWriteAsync();
+                if (classLoaderContexts != null) {
+
+                    // Record dex file usage. If the current usage is a new pattern (e.g. new
+                    // secondary, or UsedByOtherApps), record will return true and we trigger an
+                    // async write to disk to make sure we don't loose the data in case of a reboot.
+
+                    String classLoaderContext = classLoaderContexts[dexPathIndex];
+                    if (mPackageDexUsage.record(searchResult.mOwningPackageName,
+                            dexPath, loaderUserId, loaderIsa, isUsedByOtherApps, primaryOrSplit,
+                            loadingAppInfo.packageName, classLoaderContext)) {
+                        mPackageDexUsage.maybeWriteAsync();
+                    }
                 }
             } else {
                 // If we can't find the owner of the dex we simply do not track it. The impact is
@@ -268,8 +284,8 @@
             loadInternal(existingPackages);
         } catch (Exception e) {
             mPackageDexUsage.clear();
-            Slog.w(TAG, "Exception while loading package dex usage. " +
-                    "Starting with a fresh state.", e);
+            mPackageDynamicCodeLoading.clear();
+            Slog.w(TAG, "Exception while loading. Starting with a fresh state.", e);
         }
     }
 
@@ -311,15 +327,24 @@
      * all usage information for the package will be removed.
      */
     public void notifyPackageDataDestroyed(String packageName, int userId) {
-        boolean updated = userId == UserHandle.USER_ALL
-            ? mPackageDexUsage.removePackage(packageName)
-            : mPackageDexUsage.removeUserPackage(packageName, userId);
         // In case there was an update, write the package use info to disk async.
-        // Note that we do the writing here and not in PackageDexUsage in order to be
+        // Note that we do the writing here and not in the lower level classes in order to be
         // consistent with other methods in DexManager (e.g. reconcileSecondaryDexFiles performs
         // multiple updates in PackageDexUsage before writing it).
-        if (updated) {
-            mPackageDexUsage.maybeWriteAsync();
+        if (userId == UserHandle.USER_ALL) {
+            if (mPackageDexUsage.removePackage(packageName)) {
+                mPackageDexUsage.maybeWriteAsync();
+            }
+            if (mPackageDynamicCodeLoading.removePackage(packageName)) {
+                mPackageDynamicCodeLoading.maybeWriteAsync();
+            }
+        } else {
+            if (mPackageDexUsage.removeUserPackage(packageName, userId)) {
+                mPackageDexUsage.maybeWriteAsync();
+            }
+            if (mPackageDynamicCodeLoading.removeUserPackage(packageName, userId)) {
+                mPackageDynamicCodeLoading.maybeWriteAsync();
+            }
         }
     }
 
@@ -388,8 +413,23 @@
             }
         }
 
-        mPackageDexUsage.read();
-        mPackageDexUsage.syncData(packageToUsersMap, packageToCodePaths);
+        try {
+            mPackageDexUsage.read();
+            mPackageDexUsage.syncData(packageToUsersMap, packageToCodePaths);
+        } catch (Exception e) {
+            mPackageDexUsage.clear();
+            Slog.w(TAG, "Exception while loading package dex usage. "
+                    + "Starting with a fresh state.", e);
+        }
+
+        try {
+            mPackageDynamicCodeLoading.read();
+            mPackageDynamicCodeLoading.syncData(packageToUsersMap);
+        } catch (Exception e) {
+            mPackageDynamicCodeLoading.clear();
+            Slog.w(TAG, "Exception while loading package dynamic code usage. "
+                    + "Starting with a fresh state.", e);
+        }
     }
 
     /**
@@ -415,10 +455,16 @@
      * TODO(calin): maybe we should not (prune) so we can have an accurate view when we try
      * to access the package use.
      */
+    @VisibleForTesting
     /*package*/ boolean hasInfoOnPackage(String packageName) {
         return mPackageDexUsage.getPackageUseInfo(packageName) != null;
     }
 
+    @VisibleForTesting
+    /*package*/ PackageDynamicCode getPackageDynamicCodeInfo(String packageName) {
+        return mPackageDynamicCodeLoading.getPackageDynamicCodeInfo(packageName);
+    }
+
     /**
      * Perform dexopt on with the given {@code options} on the secondary dex files.
      * @return true if all secondary dex files were processed successfully (compiled or skipped
@@ -652,7 +698,7 @@
             // to load dex files through it.
             try {
                 String dexPathReal = PackageManagerServiceUtils.realpath(new File(dexPath));
-                if (dexPathReal != dexPath) {
+                if (!dexPath.equals(dexPathReal)) {
                     Slog.d(TAG, "Dex loaded with symlink. dexPath=" +
                             dexPath + " dexPathReal=" + dexPathReal);
                 }
@@ -675,6 +721,7 @@
      */
     public void writePackageDexUsageNow() {
         mPackageDexUsage.writeNow();
+        mPackageDynamicCodeLoading.writeNow();
     }
 
     private void registerSettingObserver() {
diff --git a/services/core/java/com/android/server/pm/dex/PackageDynamicCodeLoading.java b/services/core/java/com/android/server/pm/dex/PackageDynamicCodeLoading.java
new file mode 100644
index 0000000..f74aa1d
--- /dev/null
+++ b/services/core/java/com/android/server/pm/dex/PackageDynamicCodeLoading.java
@@ -0,0 +1,612 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.pm.dex;
+
+import android.util.AtomicFile;
+import android.util.Slog;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.util.FastPrintWriter;
+import com.android.server.pm.AbstractStatsBase;
+
+import libcore.io.IoUtils;
+
+import java.io.BufferedReader;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.io.PrintWriter;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Stats file which stores information about secondary code files that are dynamically loaded.
+ */
+class PackageDynamicCodeLoading extends AbstractStatsBase<Void> {
+    // Type code to indicate a secondary file containing DEX code. (The char value is how it
+    // is represented in the text file format.)
+    static final int FILE_TYPE_DEX = 'D';
+
+    private static final String TAG = "PackageDynamicCodeLoading";
+
+    private static final String FILE_VERSION_HEADER = "DCL1";
+    private static final String PACKAGE_PREFIX = "P:";
+
+    private static final char FIELD_SEPARATOR = ':';
+    private static final String PACKAGE_SEPARATOR = ",";
+
+    /**
+     * Regular expression to match the expected format of an input line describing one file.
+     * <p>Example: {@code D:10:package.name1,package.name2:/escaped/path}
+     * <p>The capturing groups are the file type, user ID, loading packages and escaped file path
+     * (in that order).
+     * <p>See {@link #write(OutputStream, Map)} below for more details of the format.
+     */
+    private static final Pattern PACKAGE_LINE_PATTERN =
+            Pattern.compile("([A-Z]):([0-9]+):([^:]*):(.*)");
+
+    private final Object mLock = new Object();
+
+    // Map from package name to data about loading of dynamic code files owned by that package.
+    // (Apps may load code files owned by other packages, subject to various access
+    // constraints.)
+    // Any PackageDynamicCode in this map will be non-empty.
+    @GuardedBy("mLock")
+    private Map<String, PackageDynamicCode> mPackageMap = new HashMap<>();
+
+    PackageDynamicCodeLoading() {
+        super("package-dcl.list", "PackageDynamicCodeLoading_DiskWriter", false);
+    }
+
+    /**
+     * Record dynamic code loading from a file.
+     *
+     * Note this is called when an app loads dex files and as such it should return
+     * as fast as possible.
+     *
+     * @param owningPackageName the package owning the file path
+     * @param filePath the path of the dex files being loaded
+     * @param fileType the type of code loading
+     * @param ownerUserId the user id which runs the code loading the file
+     * @param loadingPackageName the package performing the load
+     * @return whether new information has been recorded
+     * @throws IllegalArgumentException if clearly invalid information is detected
+     */
+    boolean record(String owningPackageName, String filePath, int fileType, int ownerUserId,
+            String loadingPackageName) {
+        if (fileType != FILE_TYPE_DEX) {
+            throw new IllegalArgumentException("Bad file type: " + fileType);
+        }
+        synchronized (mLock) {
+            PackageDynamicCode packageInfo = mPackageMap.get(owningPackageName);
+            if (packageInfo == null) {
+                packageInfo = new PackageDynamicCode();
+                mPackageMap.put(owningPackageName, packageInfo);
+            }
+            return packageInfo.add(filePath, (char) fileType, ownerUserId, loadingPackageName);
+        }
+    }
+
+    /**
+     * Return all packages that contain records of secondary dex files. (Note that data updates
+     * asynchronously, so {@link #getPackageDynamicCodeInfo} may still return null if passed
+     * one of these package names.)
+     */
+    Set<String> getAllPackagesWithDynamicCodeLoading() {
+        synchronized (mLock) {
+            return new HashSet<>(mPackageMap.keySet());
+        }
+    }
+
+    /**
+     * Return information about the dynamic code file usage of the specified package,
+     * or null if there is currently no usage information. The object returned is a copy of the
+     * live information that is not updated.
+     */
+    PackageDynamicCode getPackageDynamicCodeInfo(String packageName) {
+        synchronized (mLock) {
+            PackageDynamicCode info = mPackageMap.get(packageName);
+            return info == null ? null : new PackageDynamicCode(info);
+        }
+    }
+
+    /**
+     * Remove all information about all packages.
+     */
+    void clear() {
+        synchronized (mLock) {
+            mPackageMap.clear();
+        }
+    }
+
+    /**
+     * Remove the data associated with package {@code packageName}. Affects all users.
+     * @return true if the package usage was found and removed successfully
+     */
+    boolean removePackage(String packageName) {
+        synchronized (mLock) {
+            return mPackageMap.remove(packageName) != null;
+        }
+    }
+
+    /**
+     * Remove all the records about package {@code packageName} belonging to user {@code userId}.
+     * @return whether any data was actually removed
+     */
+    boolean removeUserPackage(String packageName, int userId) {
+        synchronized (mLock) {
+            PackageDynamicCode packageDynamicCode = mPackageMap.get(packageName);
+            if (packageDynamicCode == null) {
+                return false;
+            }
+            if (packageDynamicCode.removeUser(userId)) {
+                if (packageDynamicCode.mFileUsageMap.isEmpty()) {
+                    mPackageMap.remove(packageName);
+                }
+                return true;
+            } else {
+                return false;
+            }
+        }
+    }
+
+    /**
+     * Remove the specified dynamic code file record belonging to the package {@code packageName}
+     * and user {@code userId}.
+     * @return whether data was actually removed
+     */
+    boolean removeFile(String packageName, String filePath, int userId) {
+        synchronized (mLock) {
+            PackageDynamicCode packageDynamicCode = mPackageMap.get(packageName);
+            if (packageDynamicCode == null) {
+                return false;
+            }
+            if (packageDynamicCode.removeFile(filePath, userId)) {
+                if (packageDynamicCode.mFileUsageMap.isEmpty()) {
+                    mPackageMap.remove(packageName);
+                }
+                return true;
+            } else {
+                return false;
+            }
+        }
+    }
+
+    /**
+     * Syncs data with the set of installed packages. Data about packages that are no longer
+     * installed is removed.
+     * @param packageToUsersMap a map from all existing package names to the users who have the
+     *                          package installed
+     */
+    void syncData(Map<String, Set<Integer>> packageToUsersMap) {
+        synchronized (mLock) {
+            Iterator<Entry<String, PackageDynamicCode>> it = mPackageMap.entrySet().iterator();
+            while (it.hasNext()) {
+                Entry<String, PackageDynamicCode> entry = it.next();
+                Set<Integer> packageUsers = packageToUsersMap.get(entry.getKey());
+                if (packageUsers == null) {
+                    it.remove();
+                } else {
+                    PackageDynamicCode packageDynamicCode = entry.getValue();
+                    packageDynamicCode.syncData(packageToUsersMap, packageUsers);
+                    if (packageDynamicCode.mFileUsageMap.isEmpty()) {
+                        it.remove();
+                    }
+                }
+            }
+        }
+    }
+
+    /**
+     * Request that data be written to persistent file at the next time allowed by write-limiting.
+     */
+    void maybeWriteAsync() {
+        super.maybeWriteAsync(null);
+    }
+
+    /**
+     * Writes data to persistent file immediately.
+     */
+    void writeNow() {
+        super.writeNow(null);
+    }
+
+    @Override
+    protected final void writeInternal(Void data) {
+        AtomicFile file = getFile();
+        FileOutputStream output = null;
+        try {
+            output = file.startWrite();
+            write(output);
+            file.finishWrite(output);
+        } catch (IOException e) {
+            file.failWrite(output);
+            Slog.e(TAG, "Failed to write dynamic usage for secondary code files.", e);
+        }
+    }
+
+    @VisibleForTesting
+    void write(OutputStream output) throws IOException {
+        // Make a deep copy to avoid holding the lock while writing to disk.
+        Map<String, PackageDynamicCode> copiedMap;
+        synchronized (mLock) {
+            copiedMap = new HashMap<>(mPackageMap.size());
+            for (Entry<String, PackageDynamicCode> entry : mPackageMap.entrySet()) {
+                PackageDynamicCode copiedValue = new PackageDynamicCode(entry.getValue());
+                copiedMap.put(entry.getKey(), copiedValue);
+            }
+        }
+
+        write(output, copiedMap);
+    }
+
+    /**
+     * Write the dynamic code loading data as a text file to {@code output}. The file format begins
+     * with a line indicating the file type and version - {@link #FILE_VERSION_HEADER}.
+     * <p>There is then one section for each owning package, introduced by a line beginning "P:".
+     * This is followed by a line for each file owned by the package this is dynamically loaded,
+     * containing the file type, user ID, loading package names and full path (with newlines and
+     * backslashes escaped - see {@link #escape}).
+     * <p>For example:
+     * <pre>{@code
+     * DCL1
+     * P:first.owning.package
+     * D:0:loading.package_1,loading.package_2:/path/to/file
+     * D:10:loading.package_1:/another/file
+     * P:second.owning.package
+     * D:0:loading.package:/third/file
+     * }</pre>
+     */
+    private static void write(OutputStream output, Map<String, PackageDynamicCode> packageMap)
+            throws IOException {
+        PrintWriter writer = new FastPrintWriter(output);
+
+        writer.println(FILE_VERSION_HEADER);
+        for (Entry<String, PackageDynamicCode> packageEntry : packageMap.entrySet()) {
+            writer.print(PACKAGE_PREFIX);
+            writer.println(packageEntry.getKey());
+
+            Map<String, DynamicCodeFile> mFileUsageMap = packageEntry.getValue().mFileUsageMap;
+            for (Entry<String, DynamicCodeFile> fileEntry : mFileUsageMap.entrySet()) {
+                String path = fileEntry.getKey();
+                DynamicCodeFile dynamicCodeFile = fileEntry.getValue();
+
+                writer.print(dynamicCodeFile.mFileType);
+                writer.print(FIELD_SEPARATOR);
+                writer.print(dynamicCodeFile.mUserId);
+                writer.print(FIELD_SEPARATOR);
+
+                String prefix = "";
+                for (String packageName : dynamicCodeFile.mLoadingPackages) {
+                    writer.print(prefix);
+                    writer.print(packageName);
+                    prefix = PACKAGE_SEPARATOR;
+                }
+
+                writer.print(FIELD_SEPARATOR);
+                writer.println(escape(path));
+            }
+        }
+
+        writer.flush();
+        if (writer.checkError()) {
+            throw new IOException("Writer failed");
+        }
+    }
+
+    /**
+     * Read data from the persistent file. Replaces existing data completely if successful.
+     */
+    void read() {
+        super.read(null);
+    }
+
+    @Override
+    protected final void readInternal(Void data) {
+        AtomicFile file = getFile();
+
+        FileInputStream stream = null;
+        try {
+            stream = file.openRead();
+            read(stream);
+        } catch (FileNotFoundException expected) {
+            // The file may not be there. E.g. When we first take the OTA with this feature.
+        } catch (IOException e) {
+            Slog.w(TAG, "Failed to parse dynamic usage for secondary code files.", e);
+        } finally {
+            IoUtils.closeQuietly(stream);
+        }
+    }
+
+    @VisibleForTesting
+    void read(InputStream stream) throws IOException {
+        Map<String, PackageDynamicCode> newPackageMap = new HashMap<>();
+        read(stream, newPackageMap);
+        synchronized (mLock) {
+            mPackageMap = newPackageMap;
+        }
+    }
+
+    private static void read(InputStream stream, Map<String, PackageDynamicCode> packageMap)
+            throws IOException {
+        BufferedReader reader = new BufferedReader(new InputStreamReader(stream));
+
+        String versionLine = reader.readLine();
+        if (!FILE_VERSION_HEADER.equals(versionLine)) {
+            throw new IOException("Incorrect version line: " + versionLine);
+        }
+
+        String line = reader.readLine();
+        if (line != null && !line.startsWith(PACKAGE_PREFIX)) {
+            throw new IOException("Malformed line: " + line);
+        }
+
+        while (line != null) {
+            String packageName = line.substring(PACKAGE_PREFIX.length());
+
+            PackageDynamicCode packageInfo = new PackageDynamicCode();
+            while (true) {
+                line = reader.readLine();
+                if (line == null || line.startsWith(PACKAGE_PREFIX)) {
+                    break;
+                }
+                readFileInfo(line, packageInfo);
+            }
+
+            if (!packageInfo.mFileUsageMap.isEmpty()) {
+                packageMap.put(packageName, packageInfo);
+            }
+        }
+    }
+
+    private static void readFileInfo(String line, PackageDynamicCode output) throws IOException {
+        try {
+            Matcher matcher = PACKAGE_LINE_PATTERN.matcher(line);
+            if (!matcher.matches()) {
+                throw new IOException("Malformed line: " + line);
+            }
+
+            char type = matcher.group(1).charAt(0);
+            int user = Integer.parseInt(matcher.group(2));
+            String[] packages = matcher.group(3).split(PACKAGE_SEPARATOR);
+            String path = unescape(matcher.group(4));
+
+            if (packages.length == 0) {
+                throw new IOException("Malformed line: " + line);
+            }
+            if (type != FILE_TYPE_DEX) {
+                throw new IOException("Unknown file type: " + line);
+            }
+
+            output.mFileUsageMap.put(path, new DynamicCodeFile(type, user, packages));
+        } catch (RuntimeException e) {
+            // Just in case we get NumberFormatException, or various
+            // impossible out of bounds errors happen.
+            throw new IOException("Unable to parse line: " + line, e);
+        }
+    }
+
+    /**
+     * Escape any newline and backslash characters in path. A newline in a path is legal if unusual,
+     * and it would break our line-based file parsing.
+     */
+    @VisibleForTesting
+    static String escape(String path) {
+        if (path.indexOf('\\') == -1 && path.indexOf('\n') == -1 && path.indexOf('\r') == -1) {
+            return path;
+        }
+
+        StringBuilder result = new StringBuilder(path.length() + 10);
+        for (int i = 0; i < path.length(); i++) {
+            // Surrogates will never match the characters we care about, so it's ok to use chars
+            // not code points here.
+            char c = path.charAt(i);
+            switch (c) {
+                case '\\':
+                    result.append("\\\\");
+                    break;
+                case '\n':
+                    result.append("\\n");
+                    break;
+                case '\r':
+                    result.append("\\r");
+                    break;
+                default:
+                    result.append(c);
+                    break;
+            }
+        }
+        return result.toString();
+    }
+
+    /**
+     * Reverse the effect of {@link #escape}.
+     * @throws IOException if the input string is malformed
+     */
+    @VisibleForTesting
+    static String unescape(String escaped) throws IOException {
+        // As we move through the input string, start is the position of the first character
+        // after the previous escape sequence and finish is the position of the following backslash.
+        int start = 0;
+        int finish = escaped.indexOf('\\');
+        if (finish == -1) {
+            return escaped;
+        }
+
+        StringBuilder result = new StringBuilder(escaped.length());
+        while (true) {
+            if (finish >= escaped.length() - 1) {
+                // Backslash mustn't be the last character
+                throw new IOException("Unexpected \\ in: " + escaped);
+            }
+            result.append(escaped, start, finish);
+            switch (escaped.charAt(finish + 1)) {
+                case '\\':
+                    result.append('\\');
+                    break;
+                case 'r':
+                    result.append('\r');
+                    break;
+                case 'n':
+                    result.append('\n');
+                    break;
+                default:
+                    throw new IOException("Bad escape in: " + escaped);
+            }
+
+            start = finish + 2;
+            finish = escaped.indexOf('\\', start);
+            if (finish == -1) {
+                result.append(escaped, start, escaped.length());
+                break;
+            }
+        }
+        return result.toString();
+    }
+
+    /**
+     * Represents the dynamic code usage of a single package.
+     */
+    static class PackageDynamicCode {
+        /**
+         * Map from secondary code file path to information about which packages dynamically load
+         * that file.
+         */
+        final Map<String, DynamicCodeFile> mFileUsageMap;
+
+        private PackageDynamicCode() {
+            mFileUsageMap = new HashMap<>();
+        }
+
+        private PackageDynamicCode(PackageDynamicCode original) {
+            mFileUsageMap = new HashMap<>(original.mFileUsageMap.size());
+            for (Entry<String, DynamicCodeFile> entry : original.mFileUsageMap.entrySet()) {
+                DynamicCodeFile newValue = new DynamicCodeFile(entry.getValue());
+                mFileUsageMap.put(entry.getKey(), newValue);
+            }
+        }
+
+        private boolean add(String path, char fileType, int userId, String loadingPackage) {
+            DynamicCodeFile fileInfo = mFileUsageMap.get(path);
+            if (fileInfo == null) {
+                fileInfo = new DynamicCodeFile(fileType, userId, loadingPackage);
+                mFileUsageMap.put(path, fileInfo);
+                return true;
+            } else {
+                if (fileInfo.mUserId != userId) {
+                    // This should be impossible: private app files are always user-specific and
+                    // can't be accessed from different users.
+                    throw new IllegalArgumentException("Cannot change userId for '" + path
+                            + "' from " + fileInfo.mUserId + " to " + userId);
+                }
+                // Changing file type (i.e. loading the same file in different ways is possible if
+                // unlikely. We allow it but ignore it.
+                return fileInfo.mLoadingPackages.add(loadingPackage);
+            }
+        }
+
+        private boolean removeUser(int userId) {
+            boolean updated = false;
+            Iterator<DynamicCodeFile> it = mFileUsageMap.values().iterator();
+            while (it.hasNext()) {
+                DynamicCodeFile fileInfo = it.next();
+                if (fileInfo.mUserId == userId) {
+                    it.remove();
+                    updated = true;
+                }
+            }
+            return updated;
+        }
+
+        private boolean removeFile(String filePath, int userId) {
+            DynamicCodeFile fileInfo = mFileUsageMap.get(filePath);
+            if (fileInfo == null || fileInfo.mUserId != userId) {
+                return false;
+            } else {
+                mFileUsageMap.remove(filePath);
+                return true;
+            }
+        }
+
+        private void syncData(Map<String, Set<Integer>> packageToUsersMap,
+                Set<Integer> owningPackageUsers) {
+            Iterator<DynamicCodeFile> fileIt = mFileUsageMap.values().iterator();
+            while (fileIt.hasNext()) {
+                DynamicCodeFile fileInfo = fileIt.next();
+                int fileUserId = fileInfo.mUserId;
+                if (!owningPackageUsers.contains(fileUserId)) {
+                    fileIt.remove();
+                } else {
+                    // Also remove information about any loading packages that are no longer
+                    // installed for this user.
+                    Iterator<String> loaderIt = fileInfo.mLoadingPackages.iterator();
+                    while (loaderIt.hasNext()) {
+                        String loader = loaderIt.next();
+                        Set<Integer> loadingPackageUsers = packageToUsersMap.get(loader);
+                        if (loadingPackageUsers == null
+                                || !loadingPackageUsers.contains(fileUserId)) {
+                            loaderIt.remove();
+                        }
+                    }
+                    if (fileInfo.mLoadingPackages.isEmpty()) {
+                        fileIt.remove();
+                    }
+                }
+            }
+        }
+    }
+
+    /**
+     * Represents a single dynamic code file loaded by one or more packages. Note that it is
+     * possible for one app to dynamically load code from a different app's home dir, if the
+     * owning app:
+     * <ul>
+     *     <li>Targets API 27 or lower and has shared its home dir.
+     *     <li>Is a system app.
+     *     <li>Has a shared UID with the loading app.
+     * </ul>
+     */
+    static class DynamicCodeFile {
+        final char mFileType;
+        final int mUserId;
+        final Set<String> mLoadingPackages;
+
+        private DynamicCodeFile(char type, int user, String... packages) {
+            mFileType = type;
+            mUserId = user;
+            mLoadingPackages = new HashSet<>(Arrays.asList(packages));
+        }
+
+        private DynamicCodeFile(DynamicCodeFile original) {
+            mFileType = original.mFileType;
+            mUserId = original.mUserId;
+            mLoadingPackages = new HashSet<>(original.mLoadingPackages);
+        }
+    }
+}
diff --git a/services/core/java/com/android/server/signedconfig/SignedConfigApplicator.java b/services/core/java/com/android/server/signedconfig/SignedConfigApplicator.java
index 7ce071f..2312f5f 100644
--- a/services/core/java/com/android/server/signedconfig/SignedConfigApplicator.java
+++ b/services/core/java/com/android/server/signedconfig/SignedConfigApplicator.java
@@ -17,12 +17,86 @@
 package com.android.server.signedconfig;
 
 import android.content.Context;
+import android.os.Build;
+import android.provider.Settings;
+import android.util.ArraySet;
+import android.util.Slog;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Map;
+import java.util.Set;
 
 class SignedConfigApplicator {
 
-    static void applyConfig(Context context, String config, String signature) {
-        //TODO verify signature
-        //TODO parse & apply config
+    private static final String TAG = "SignedConfig";
+
+    private static final Set<String> ALLOWED_KEYS = Collections.unmodifiableSet(new ArraySet<>(
+            Arrays.asList(
+                    Settings.Global.HIDDEN_API_POLICY,
+                    Settings.Global.HIDDEN_API_BLACKLIST_EXEMPTIONS
+            )));
+
+    private final Context mContext;
+    private final String mSourcePackage;
+
+    SignedConfigApplicator(Context context, String sourcePackage) {
+        mContext = context;
+        mSourcePackage = sourcePackage;
     }
 
+    private boolean checkSignature(String data, String signature) {
+        Slog.w(TAG, "SIGNATURE CHECK NOT IMPLEMENTED YET!");
+        return false;
+    }
+
+    private int getCurrentConfigVersion() {
+        return Settings.Global.getInt(mContext.getContentResolver(),
+                Settings.Global.SIGNED_CONFIG_VERSION, 0);
+    }
+
+    private void updateCurrentConfig(int version, Map<String, String> values) {
+        for (Map.Entry<String, String> e: values.entrySet()) {
+            Settings.Global.putString(
+                    mContext.getContentResolver(),
+                    e.getKey(),
+                    e.getValue());
+        }
+        Settings.Global.putInt(
+                mContext.getContentResolver(), Settings.Global.SIGNED_CONFIG_VERSION, version);
+    }
+
+
+    void applyConfig(String configStr, String signature) {
+        if (!checkSignature(configStr, signature)) {
+            Slog.e(TAG, "Signature check on signed configuration in package " + mSourcePackage
+                    + " failed; ignoring");
+            return;
+        }
+        SignedConfig config;
+        try {
+            config = SignedConfig.parse(configStr, ALLOWED_KEYS);
+        } catch (InvalidConfigException e) {
+            Slog.e(TAG, "Failed to parse config from package " + mSourcePackage, e);
+            return;
+        }
+        int currentVersion = getCurrentConfigVersion();
+        if (currentVersion >= config.version) {
+            Slog.i(TAG, "Config from package " + mSourcePackage + " is older than existing: "
+                    + config.version + "<=" + currentVersion);
+            return;
+        }
+        // We have new config!
+        Slog.i(TAG, "Got new signed config from package " + mSourcePackage + ": version "
+                + config.version + " replacing existing version " + currentVersion);
+        SignedConfig.PerSdkConfig matchedConfig =
+                config.getMatchingConfig(Build.VERSION.SDK_INT);
+        if (matchedConfig == null) {
+            Slog.i(TAG, "Config is not applicable to current SDK version; ignoring");
+            return;
+        }
+
+        Slog.i(TAG, "Updating signed config to version " + config.version);
+        updateCurrentConfig(config.version, matchedConfig.values);
+    }
 }
diff --git a/services/core/java/com/android/server/signedconfig/SignedConfigService.java b/services/core/java/com/android/server/signedconfig/SignedConfigService.java
index 1485686..be1d41d 100644
--- a/services/core/java/com/android/server/signedconfig/SignedConfigService.java
+++ b/services/core/java/com/android/server/signedconfig/SignedConfigService.java
@@ -85,7 +85,8 @@
                 Slog.d(TAG, "Got signed config: " + config);
                 Slog.d(TAG, "Got config signature: " + signature);
             }
-            SignedConfigApplicator.applyConfig(mContext, config, signature);
+            new SignedConfigApplicator(mContext, packageName).applyConfig(
+                    config, signature);
         } else {
             if (DBG) Slog.d(TAG, "Package has no config/signature.");
         }
diff --git a/services/core/java/com/android/server/wm/BarController.java b/services/core/java/com/android/server/wm/BarController.java
index a335fa2..5b20af3 100644
--- a/services/core/java/com/android/server/wm/BarController.java
+++ b/services/core/java/com/android/server/wm/BarController.java
@@ -219,7 +219,7 @@
     }
 
     private boolean updateStateLw(final int state) {
-        if (state != mState) {
+        if (mWin != null && state != mState) {
             mState = state;
             if (DEBUG) Slog.d(mTag, "mState: " + StatusBarManager.windowStateToString(state));
             mHandler.post(new Runnable() {
diff --git a/services/core/java/com/android/server/wm/InputManagerCallback.java b/services/core/java/com/android/server/wm/InputManagerCallback.java
index 639ed02..f9c9d33 100644
--- a/services/core/java/com/android/server/wm/InputManagerCallback.java
+++ b/services/core/java/com/android/server/wm/InputManagerCallback.java
@@ -1,5 +1,6 @@
 package com.android.server.wm;
 
+import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
 import static android.view.Display.DEFAULT_DISPLAY;
 import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
 
@@ -9,7 +10,6 @@
 import android.os.Debug;
 import android.os.IBinder;
 import android.util.Slog;
-import android.view.InputApplicationHandle;
 import android.view.KeyEvent;
 import android.view.WindowManager;
 
@@ -204,6 +204,37 @@
                 + WindowManagerService.TYPE_LAYER_OFFSET;
     }
 
+    /** Callback to get pointer display id. */
+    @Override
+    public int getPointerDisplayId() {
+        synchronized (mService.mGlobalLock) {
+            // If desktop mode is not enabled, show on the default display.
+            if (!mService.mForceDesktopModeOnExternalDisplays) {
+                return DEFAULT_DISPLAY;
+            }
+
+            // Look for the topmost freeform display.
+            int firstExternalDisplayId = DEFAULT_DISPLAY;
+            for (int i = mService.mRoot.mChildren.size() - 1; i >= 0; --i) {
+                final DisplayContent displayContent = mService.mRoot.mChildren.get(i);
+                // Heuristic solution here. Currently when "Freeform windows" developer option is
+                // enabled we automatically put secondary displays in freeform mode and emulating
+                // "desktop mode". It also makes sense to show the pointer on the same display.
+                if (displayContent.getWindowingMode() == WINDOWING_MODE_FREEFORM) {
+                    return displayContent.getDisplayId();
+                }
+
+                if (firstExternalDisplayId == DEFAULT_DISPLAY
+                        && displayContent.getDisplayId() != DEFAULT_DISPLAY) {
+                    firstExternalDisplayId = displayContent.getDisplayId();
+                }
+            }
+
+            // Look for the topmost non-default display
+            return firstExternalDisplayId;
+        }
+    }
+
     /** Waits until the built-in input devices have been configured. */
     public boolean waitForInputDevicesReady(long timeoutMillis) {
         synchronized (mInputDevicesReadyMonitor) {
diff --git a/services/core/jni/com_android_server_input_InputManagerService.cpp b/services/core/jni/com_android_server_input_InputManagerService.cpp
index 43d2dcf..bf83ca9 100644
--- a/services/core/jni/com_android_server_input_InputManagerService.cpp
+++ b/services/core/jni/com_android_server_input_InputManagerService.cpp
@@ -107,6 +107,7 @@
     jmethodID getLongPressTimeout;
     jmethodID getPointerLayer;
     jmethodID getPointerIcon;
+    jmethodID getPointerDisplayId;
     jmethodID getKeyboardLayoutOverlay;
     jmethodID getDeviceAlias;
     jmethodID getTouchCalibrationForInputDevice;
@@ -174,15 +175,6 @@
     loadSystemIconAsSpriteWithPointerIcon(env, contextObj, style, &pointerIcon, outSpriteIcon);
 }
 
-static void updatePointerControllerFromViewport(
-        sp<PointerController> controller, const DisplayViewport* const viewport) {
-    if (controller != nullptr && viewport != nullptr) {
-        const int32_t width = viewport->logicalRight - viewport->logicalLeft;
-        const int32_t height = viewport->logicalBottom - viewport->logicalTop;
-        controller->setDisplayViewport(width, height, viewport->orientation);
-    }
-}
-
 enum {
     WM_ACTION_PASS_TO_USER = 1,
 };
@@ -242,6 +234,7 @@
             jfloatArray matrixArr);
     virtual TouchAffineTransformation getTouchAffineTransformation(
             const std::string& inputDeviceDescriptor, int32_t surfaceRotation);
+    virtual void updatePointerDisplay();
 
     /* --- InputDispatcherPolicyInterface implementation --- */
 
@@ -314,10 +307,11 @@
 
     std::atomic<bool> mInteractive;
 
-    void updateInactivityTimeoutLocked(const sp<PointerController>& controller);
+    void updateInactivityTimeoutLocked();
     void handleInterceptActions(jint wmActions, nsecs_t when, uint32_t& policyFlags);
     void ensureSpriteControllerLocked();
-
+    const DisplayViewport* findDisplayViewportLocked(int32_t displayId);
+    int32_t getPointerDisplayId();
     static bool checkAndClearExceptionFromCallback(JNIEnv* env, const char* methodName);
 
     static inline JNIEnv* jniEnv() {
@@ -391,9 +385,10 @@
     return false;
 }
 
-static const DisplayViewport* findInternalViewport(const std::vector<DisplayViewport>& viewports) {
-    for (const DisplayViewport& v : viewports) {
-        if (v.type == ViewportType::VIEWPORT_INTERNAL) {
+const DisplayViewport* NativeInputManager::findDisplayViewportLocked(int32_t displayId)
+        REQUIRES(mLock) {
+    for (const DisplayViewport& v : mLocked.viewports) {
+        if (v.displayId == displayId) {
             return &v;
         }
     }
@@ -420,20 +415,10 @@
         }
     }
 
-    const DisplayViewport* newInternalViewport = findInternalViewport(viewports);
-    {
+    { // acquire lock
         AutoMutex _l(mLock);
-        const DisplayViewport* oldInternalViewport = findInternalViewport(mLocked.viewports);
-        // Internal viewport has changed if there wasn't one earlier, and there is one now, or,
-        // if they are different.
-        const bool internalViewportChanged = (newInternalViewport != nullptr) &&
-                (oldInternalViewport == nullptr || (*oldInternalViewport != *newInternalViewport));
-        if (internalViewportChanged) {
-            sp<PointerController> controller = mLocked.pointerController.promote();
-            updatePointerControllerFromViewport(controller, newInternalViewport);
-        }
         mLocked.viewports = viewports;
-    }
+    } // release lock
 
     mInputManager->getReader()->requestRefreshConfiguration(
             InputReaderConfiguration::CHANGE_DISPLAY_INFO);
@@ -556,15 +541,43 @@
 
         controller = new PointerController(this, mLooper, mLocked.spriteController);
         mLocked.pointerController = controller;
-
-        const DisplayViewport* internalViewport = findInternalViewport(mLocked.viewports);
-        updatePointerControllerFromViewport(controller, internalViewport);
-
-        updateInactivityTimeoutLocked(controller);
+        updateInactivityTimeoutLocked();
     }
+
     return controller;
 }
 
+int32_t NativeInputManager::getPointerDisplayId() {
+    JNIEnv* env = jniEnv();
+    jint pointerDisplayId = env->CallIntMethod(mServiceObj,
+            gServiceClassInfo.getPointerDisplayId);
+    if (checkAndClearExceptionFromCallback(env, "getPointerDisplayId")) {
+        pointerDisplayId = ADISPLAY_ID_DEFAULT;
+    }
+
+    return pointerDisplayId;
+}
+
+void NativeInputManager::updatePointerDisplay() {
+    ATRACE_CALL();
+
+    jint pointerDisplayId = getPointerDisplayId();
+
+    AutoMutex _l(mLock);
+    sp<PointerController> controller = mLocked.pointerController.promote();
+    if (controller != nullptr) {
+        const DisplayViewport* viewport = findDisplayViewportLocked(pointerDisplayId);
+        if (viewport == nullptr) {
+            ALOGW("Can't find pointer display viewport, fallback to default display.");
+            viewport = findDisplayViewportLocked(ADISPLAY_ID_DEFAULT);
+        }
+
+        if (viewport != nullptr) {
+            controller->setDisplayViewport(*viewport);
+        }
+    }
+}
+
 void NativeInputManager::ensureSpriteControllerLocked() REQUIRES(mLock) {
     if (mLocked.spriteController == nullptr) {
         JNIEnv* env = jniEnv();
@@ -821,16 +834,16 @@
 
     if (mLocked.systemUiVisibility != visibility) {
         mLocked.systemUiVisibility = visibility;
-
-        sp<PointerController> controller = mLocked.pointerController.promote();
-        if (controller != nullptr) {
-            updateInactivityTimeoutLocked(controller);
-        }
+        updateInactivityTimeoutLocked();
     }
 }
 
-void NativeInputManager::updateInactivityTimeoutLocked(const sp<PointerController>& controller)
-        REQUIRES(mLock) {
+void NativeInputManager::updateInactivityTimeoutLocked() REQUIRES(mLock) {
+    sp<PointerController> controller = mLocked.pointerController.promote();
+    if (controller == nullptr) {
+        return;
+    }
+
     bool lightsOut = mLocked.systemUiVisibility & ASYSTEM_UI_VISIBILITY_STATUS_BAR_HIDDEN;
     controller->setInactivityTimeout(lightsOut
             ? PointerController::INACTIVITY_TIMEOUT_SHORT
@@ -1824,6 +1837,9 @@
     GET_METHOD_ID(gServiceClassInfo.getPointerIcon, clazz,
             "getPointerIcon", "()Landroid/view/PointerIcon;");
 
+    GET_METHOD_ID(gServiceClassInfo.getPointerDisplayId, clazz,
+            "getPointerDisplayId", "()I");
+
     GET_METHOD_ID(gServiceClassInfo.getKeyboardLayoutOverlay, clazz,
             "getKeyboardLayoutOverlay",
             "(Landroid/hardware/input/InputDeviceIdentifier;)[Ljava/lang/String;");
diff --git a/services/tests/servicestests/src/com/android/server/pm/dex/DexManagerTests.java b/services/tests/servicestests/src/com/android/server/pm/dex/DexManagerTests.java
index dad7b93..fd07cb0 100644
--- a/services/tests/servicestests/src/com/android/server/pm/dex/DexManagerTests.java
+++ b/services/tests/servicestests/src/com/android/server/pm/dex/DexManagerTests.java
@@ -18,10 +18,14 @@
 
 import static com.android.server.pm.dex.PackageDexUsage.DexUseInfo;
 import static com.android.server.pm.dex.PackageDexUsage.PackageUseInfo;
+import static com.android.server.pm.dex.PackageDynamicCodeLoading.DynamicCodeFile;
+import static com.android.server.pm.dex.PackageDynamicCodeLoading.PackageDynamicCode;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotEquals;
 import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyInt;
@@ -41,6 +45,10 @@
 
 import com.android.server.pm.Installer;
 
+import dalvik.system.DelegateLastClassLoader;
+import dalvik.system.PathClassLoader;
+import dalvik.system.VMRuntime;
+
 import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
@@ -50,10 +58,6 @@
 import org.mockito.junit.MockitoRule;
 import org.mockito.quality.Strictness;
 
-import dalvik.system.DelegateLastClassLoader;
-import dalvik.system.PathClassLoader;
-import dalvik.system.VMRuntime;
-
 import java.io.File;
 import java.util.ArrayList;
 import java.util.Arrays;
@@ -129,6 +133,9 @@
 
         // Package is not used by others, so we should get nothing back.
         assertNoUseInfo(mFooUser0);
+
+        // A package loading its own code is not stored as DCL.
+        assertNoDclInfo(mFooUser0);
     }
 
     @Test
@@ -140,6 +147,8 @@
         PackageUseInfo pui = getPackageUseInfo(mBarUser0);
         assertIsUsedByOtherApps(mBarUser0, pui, true);
         assertTrue(pui.getDexUseInfoMap().isEmpty());
+
+        assertHasDclInfo(mBarUser0, mFooUser0, mBarUser0.getBaseAndSplitDexPaths());
     }
 
     @Test
@@ -152,6 +161,8 @@
         assertIsUsedByOtherApps(mFooUser0, pui, false);
         assertEquals(fooSecondaries.size(), pui.getDexUseInfoMap().size());
         assertSecondaryUse(mFooUser0, pui, fooSecondaries, /*isUsedByOtherApps*/false, mUser0);
+
+        assertHasDclInfo(mFooUser0, mFooUser0, fooSecondaries);
     }
 
     @Test
@@ -164,6 +175,8 @@
         assertIsUsedByOtherApps(mBarUser0, pui, false);
         assertEquals(barSecondaries.size(), pui.getDexUseInfoMap().size());
         assertSecondaryUse(mFooUser0, pui, barSecondaries, /*isUsedByOtherApps*/true, mUser0);
+
+        assertHasDclInfo(mBarUser0, mFooUser0, barSecondaries);
     }
 
     @Test
@@ -200,9 +213,10 @@
     }
 
     @Test
-    public void testPackageUseInfoNotFound() {
+    public void testNoNotify() {
         // Assert we don't get back data we did not previously record.
         assertNoUseInfo(mFooUser0);
+        assertNoDclInfo(mFooUser0);
     }
 
     @Test
@@ -210,6 +224,7 @@
         // Notifying with an invalid ISA should be ignored.
         notifyDexLoad(mInvalidIsa, mInvalidIsa.getSecondaryDexPaths(), mUser0);
         assertNoUseInfo(mInvalidIsa);
+        assertNoDclInfo(mInvalidIsa);
     }
 
     @Test
@@ -218,6 +233,7 @@
         // register in DexManager#load should be ignored.
         notifyDexLoad(mDoesNotExist, mDoesNotExist.getBaseAndSplitDexPaths(), mUser0);
         assertNoUseInfo(mDoesNotExist);
+        assertNoDclInfo(mDoesNotExist);
     }
 
     @Test
@@ -226,6 +242,8 @@
         // Request should be ignored.
         notifyDexLoad(mBarUser1, mBarUser0.getSecondaryDexPaths(), mUser1);
         assertNoUseInfo(mBarUser1);
+
+        assertNoDclInfo(mBarUser1);
     }
 
     @Test
@@ -235,6 +253,10 @@
         // still check that nothing goes unexpected in DexManager.
         notifyDexLoad(mBarUser0, mFooUser0.getBaseAndSplitDexPaths(), mUser1);
         assertNoUseInfo(mBarUser1);
+        assertNoUseInfo(mFooUser0);
+
+        assertNoDclInfo(mBarUser1);
+        assertNoDclInfo(mFooUser0);
     }
 
     @Test
@@ -247,6 +269,7 @@
         // is trying to load something from it we should not find it.
         notifyDexLoad(mFooUser0, newSecondaries, mUser0);
         assertNoUseInfo(newPackage);
+        assertNoDclInfo(newPackage);
 
         // Notify about newPackage install and let mFoo load its dexes.
         mDexManager.notifyPackageInstalled(newPackage.mPackageInfo, mUser0);
@@ -257,6 +280,7 @@
         assertIsUsedByOtherApps(newPackage, pui, false);
         assertEquals(newSecondaries.size(), pui.getDexUseInfoMap().size());
         assertSecondaryUse(newPackage, pui, newSecondaries, /*isUsedByOtherApps*/true, mUser0);
+        assertHasDclInfo(newPackage, mFooUser0, newSecondaries);
     }
 
     @Test
@@ -273,6 +297,7 @@
         assertIsUsedByOtherApps(newPackage, pui, false);
         assertEquals(newSecondaries.size(), pui.getDexUseInfoMap().size());
         assertSecondaryUse(newPackage, pui, newSecondaries, /*isUsedByOtherApps*/false, mUser0);
+        assertHasDclInfo(newPackage, newPackage, newSecondaries);
     }
 
     @Test
@@ -305,6 +330,7 @@
         // We shouldn't find yet the new split as we didn't notify the package update.
         notifyDexLoad(mFooUser0, newSplits, mUser0);
         assertNoUseInfo(mBarUser0);
+        assertNoDclInfo(mBarUser0);
 
         // Notify that bar is updated. splitSourceDirs will contain the updated path.
         mDexManager.notifyPackageUpdated(mBarUser0.getPackageName(),
@@ -314,8 +340,8 @@
         // Now, when the split is loaded we will find it and we should mark Bar as usedByOthers.
         notifyDexLoad(mFooUser0, newSplits, mUser0);
         PackageUseInfo pui = getPackageUseInfo(mBarUser0);
-        assertNotNull(pui);
         assertIsUsedByOtherApps(newSplits, pui, true);
+        assertHasDclInfo(mBarUser0, mFooUser0, newSplits);
     }
 
     @Test
@@ -326,11 +352,15 @@
 
         mDexManager.notifyPackageDataDestroyed(mBarUser0.getPackageName(), mUser0);
 
-        // Bar should not be around since it was removed for all users.
+        // Data for user 1 should still be present
         PackageUseInfo pui = getPackageUseInfo(mBarUser1);
-        assertNotNull(pui);
         assertSecondaryUse(mBarUser1, pui, mBarUser1.getSecondaryDexPaths(),
                 /*isUsedByOtherApps*/false, mUser1);
+        assertHasDclInfo(mBarUser1, mBarUser1, mBarUser1.getSecondaryDexPaths());
+
+        // But not user 0
+        assertNoUseInfo(mBarUser0, mUser0);
+        assertNoDclInfo(mBarUser0, mUser0);
     }
 
     @Test
@@ -349,6 +379,8 @@
         PackageUseInfo pui = getPackageUseInfo(mFooUser0);
         assertIsUsedByOtherApps(mFooUser0, pui, true);
         assertTrue(pui.getDexUseInfoMap().isEmpty());
+
+        assertNoDclInfo(mFooUser0);
     }
 
     @Test
@@ -362,6 +394,7 @@
         // Foo should not be around since all its secondary dex info were deleted
         // and it is not used by other apps.
         assertNoUseInfo(mFooUser0);
+        assertNoDclInfo(mFooUser0);
     }
 
     @Test
@@ -374,6 +407,7 @@
 
         // Bar should not be around since it was removed for all users.
         assertNoUseInfo(mBarUser0);
+        assertNoDclInfo(mBarUser0);
     }
 
     @Test
@@ -383,6 +417,7 @@
         notifyDexLoad(mFooUser0, Arrays.asList(frameworkDex), mUser0);
         // The dex file should not be recognized as a package.
         assertFalse(mDexManager.hasInfoOnPackage(frameworkDex));
+        assertNull(mDexManager.getPackageDynamicCodeInfo(frameworkDex));
     }
 
     @Test
@@ -395,6 +430,8 @@
         assertIsUsedByOtherApps(mFooUser0, pui, false);
         assertEquals(fooSecondaries.size(), pui.getDexUseInfoMap().size());
         assertSecondaryUse(mFooUser0, pui, fooSecondaries, /*isUsedByOtherApps*/false, mUser0);
+
+        assertHasDclInfo(mFooUser0, mFooUser0, fooSecondaries);
     }
 
     @Test
@@ -402,7 +439,12 @@
         List<String> secondaries = mBarUser0UnsupportedClassLoader.getSecondaryDexPaths();
         notifyDexLoad(mBarUser0UnsupportedClassLoader, secondaries, mUser0);
 
+        // We don't record the dex usage
         assertNoUseInfo(mBarUser0UnsupportedClassLoader);
+
+        // But we do record this as an intance of dynamic code loading
+        assertHasDclInfo(
+                mBarUser0UnsupportedClassLoader, mBarUser0UnsupportedClassLoader, secondaries);
     }
 
     @Test
@@ -414,6 +456,8 @@
         notifyDexLoad(mBarUser0, classLoaders, classPaths, mUser0);
 
         assertNoUseInfo(mBarUser0);
+
+        assertHasDclInfo(mBarUser0, mBarUser0, mBarUser0.getSecondaryDexPaths());
     }
 
     @Test
@@ -421,6 +465,7 @@
         notifyDexLoad(mBarUser0, null, mUser0);
 
         assertNoUseInfo(mBarUser0);
+        assertNoDclInfo(mBarUser0);
     }
 
     @Test
@@ -455,12 +500,14 @@
         notifyDexLoad(mBarUser0, secondaries, mUser0);
         PackageUseInfo pui = getPackageUseInfo(mBarUser0);
         assertSecondaryUse(mBarUser0, pui, secondaries, /*isUsedByOtherApps*/false, mUser0);
+        assertHasDclInfo(mBarUser0, mBarUser0, secondaries);
 
         // Record bar secondaries again with an unsupported class loader. This should not change the
         // context.
         notifyDexLoad(mBarUser0UnsupportedClassLoader, secondaries, mUser0);
         pui = getPackageUseInfo(mBarUser0);
         assertSecondaryUse(mBarUser0, pui, secondaries, /*isUsedByOtherApps*/false, mUser0);
+        assertHasDclInfo(mBarUser0, mBarUser0, secondaries);
     }
 
     @Test
@@ -533,13 +580,53 @@
 
     private PackageUseInfo getPackageUseInfo(TestData testData) {
         assertTrue(mDexManager.hasInfoOnPackage(testData.getPackageName()));
-        return mDexManager.getPackageUseInfoOrDefault(testData.getPackageName());
+        PackageUseInfo pui = mDexManager.getPackageUseInfoOrDefault(testData.getPackageName());
+        assertNotNull(pui);
+        return pui;
     }
 
     private void assertNoUseInfo(TestData testData) {
         assertFalse(mDexManager.hasInfoOnPackage(testData.getPackageName()));
     }
 
+    private void assertNoUseInfo(TestData testData, int userId) {
+        if (!mDexManager.hasInfoOnPackage(testData.getPackageName())) {
+            return;
+        }
+        PackageUseInfo pui = getPackageUseInfo(testData);
+        for (DexUseInfo dexUseInfo : pui.getDexUseInfoMap().values()) {
+            assertNotEquals(userId, dexUseInfo.getOwnerUserId());
+        }
+    }
+
+    private void assertNoDclInfo(TestData testData) {
+        assertNull(mDexManager.getPackageDynamicCodeInfo(testData.getPackageName()));
+    }
+
+    private void assertNoDclInfo(TestData testData, int userId) {
+        PackageDynamicCode info = mDexManager.getPackageDynamicCodeInfo(testData.getPackageName());
+        if (info == null) {
+            return;
+        }
+
+        for (DynamicCodeFile fileInfo : info.mFileUsageMap.values()) {
+            assertNotEquals(userId, fileInfo.mUserId);
+        }
+    }
+
+    private void assertHasDclInfo(TestData owner, TestData loader, List<String> paths) {
+        PackageDynamicCode info = mDexManager.getPackageDynamicCodeInfo(owner.getPackageName());
+        assertNotNull("No DCL data for owner " + owner.getPackageName(), info);
+        for (String path : paths) {
+            DynamicCodeFile fileInfo = info.mFileUsageMap.get(path);
+            assertNotNull("No DCL data for path " + path, fileInfo);
+            assertEquals(PackageDynamicCodeLoading.FILE_TYPE_DEX, fileInfo.mFileType);
+            assertEquals(owner.mUserId, fileInfo.mUserId);
+            assertTrue("No DCL data for loader " + loader.getPackageName(),
+                    fileInfo.mLoadingPackages.contains(loader.getPackageName()));
+        }
+    }
+
     private static PackageInfo getMockPackageInfo(String packageName, int userId) {
         PackageInfo pi = new PackageInfo();
         pi.packageName = packageName;
@@ -563,11 +650,13 @@
         private final PackageInfo mPackageInfo;
         private final String mLoaderIsa;
         private final String mClassLoader;
+        private final int mUserId;
 
         private TestData(String packageName, String loaderIsa, int userId, String classLoader) {
             mPackageInfo = getMockPackageInfo(packageName, userId);
             mLoaderIsa = loaderIsa;
             mClassLoader = classLoader;
+            mUserId = userId;
         }
 
         private TestData(String packageName, String loaderIsa, int userId) {
@@ -603,9 +692,7 @@
         List<String> getBaseAndSplitDexPaths() {
             List<String> paths = new ArrayList<>();
             paths.add(mPackageInfo.applicationInfo.sourceDir);
-            for (String split : mPackageInfo.applicationInfo.splitSourceDirs) {
-                paths.add(split);
-            }
+            Collections.addAll(paths, mPackageInfo.applicationInfo.splitSourceDirs);
             return paths;
         }
 
diff --git a/services/tests/servicestests/src/com/android/server/pm/dex/PackageDynamicCodeLoadingTests.java b/services/tests/servicestests/src/com/android/server/pm/dex/PackageDynamicCodeLoadingTests.java
new file mode 100644
index 0000000..eb4cc4e
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/pm/dex/PackageDynamicCodeLoadingTests.java
@@ -0,0 +1,560 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.pm.dex;
+
+import static com.android.server.pm.dex.PackageDynamicCodeLoading.escape;
+import static com.android.server.pm.dex.PackageDynamicCodeLoading.unescape;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
+import static org.testng.Assert.assertFalse;
+import static org.testng.Assert.assertThrows;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.server.pm.dex.PackageDynamicCodeLoading.DynamicCodeFile;
+import com.android.server.pm.dex.PackageDynamicCodeLoading.PackageDynamicCode;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class PackageDynamicCodeLoadingTests {
+
+    // Deliberately making a copy here since we're testing identity and
+    // string literals have a tendency to be identical.
+    private static final String TRIVIAL_STRING = new String("hello/world");
+    private static final Entry[] NO_ENTRIES = {};
+    private static final String[] NO_PACKAGES = {};
+
+    @Test
+    public void testRecord() {
+        Entry[] entries = {
+                new Entry("owning.package1", "/path/file1", 'D', 10, "loading.package1"),
+                new Entry("owning.package1", "/path/file1", 'D', 10, "loading.package2"),
+                new Entry("owning.package1", "/path/file2", 'D', 5, "loading.package1"),
+                new Entry("owning.package2", "/path/file3", 'D', 0, "loading.package2"),
+        };
+
+        PackageDynamicCodeLoading info = makePackageDcl(entries);
+        assertHasEntries(info, entries);
+    }
+
+    @Test
+    public void testRecord_returnsHasChanged() {
+        Entry owner1Path1Loader1 =
+                new Entry("owning.package1", "/path/file1", 'D', 10, "loading.package1");
+        Entry owner1Path1Loader2 =
+                new Entry("owning.package1", "/path/file1", 'D', 10, "loading.package2");
+        Entry owner1Path2Loader1 =
+                new Entry("owning.package1", "/path/file2", 'D', 10, "loading.package1");
+        Entry owner2Path1Loader1 =
+                new Entry("owning.package2", "/path/file1", 'D', 10, "loading.package2");
+
+        PackageDynamicCodeLoading info = new PackageDynamicCodeLoading();
+
+        assertTrue(record(info, owner1Path1Loader1));
+        assertFalse(record(info, owner1Path1Loader1));
+
+        assertTrue(record(info, owner1Path1Loader2));
+        assertFalse(record(info, owner1Path1Loader2));
+
+        assertTrue(record(info, owner1Path2Loader1));
+        assertFalse(record(info, owner1Path2Loader1));
+
+        assertTrue(record(info, owner2Path1Loader1));
+        assertFalse(record(info, owner2Path1Loader1));
+
+        assertHasEntries(info,
+                owner1Path1Loader1, owner1Path1Loader2, owner1Path2Loader1, owner2Path1Loader1);
+    }
+
+    @Test
+    public void testRecord_changeUserForFile_throws() {
+        Entry entry1 = new Entry("owning.package1", "/path/file1", 'D', 10, "loading.package1");
+        Entry entry2 = new Entry("owning.package1", "/path/file1", 'D', 20, "loading.package1");
+
+        PackageDynamicCodeLoading info = makePackageDcl(entry1);
+
+        assertThrows(() -> record(info, entry2));
+        assertHasEntries(info, entry1);
+    }
+
+    @Test
+    public void testRecord_badFileType_throws() {
+        Entry entry = new Entry("owning.package", "/path/file", 'Z', 10, "loading.package");
+        PackageDynamicCodeLoading info = new PackageDynamicCodeLoading();
+
+        assertThrows(() -> record(info, entry));
+    }
+
+    @Test
+    public void testClear() {
+        Entry[] entries = {
+                new Entry("owner1", "file1", 'D', 10, "loader1"),
+                new Entry("owner2", "file2", 'D', 20, "loader2"),
+        };
+        PackageDynamicCodeLoading info = makePackageDcl(entries);
+
+        info.clear();
+        assertHasEntries(info, NO_ENTRIES);
+    }
+
+    @Test
+    public void testRemovePackage_present() {
+        Entry other = new Entry("other", "file", 'D', 0, "loader");
+        Entry[] entries = {
+                new Entry("owner", "file1", 'D', 10, "loader1"),
+                new Entry("owner", "file2", 'D', 20, "loader2"),
+                other
+        };
+        PackageDynamicCodeLoading info = makePackageDcl(entries);
+
+        assertTrue(info.removePackage("owner"));
+        assertHasEntries(info, other);
+        assertHasPackages(info, "other");
+    }
+
+    @Test
+    public void testRemovePackage_notPresent() {
+        Entry[] entries = { new Entry("owner", "file", 'D', 0, "loader") };
+        PackageDynamicCodeLoading info = makePackageDcl(entries);
+
+        assertFalse(info.removePackage("other"));
+        assertHasEntries(info, entries);
+    }
+
+    @Test
+    public void testRemoveUserPackage_notPresent() {
+        Entry[] entries = { new Entry("owner", "file", 'D', 0, "loader") };
+        PackageDynamicCodeLoading info = makePackageDcl(entries);
+
+        assertFalse(info.removeUserPackage("other", 0));
+        assertHasEntries(info, entries);
+    }
+
+    @Test
+    public void testRemoveUserPackage_presentWithNoOtherUsers() {
+        Entry other = new Entry("other", "file", 'D', 0, "loader");
+        Entry[] entries = {
+                new Entry("owner", "file1", 'D', 0, "loader1"),
+                new Entry("owner", "file2", 'D', 0, "loader2"),
+                other
+        };
+        PackageDynamicCodeLoading info = makePackageDcl(entries);
+
+        assertTrue(info.removeUserPackage("owner", 0));
+        assertHasEntries(info, other);
+        assertHasPackages(info, "other");
+    }
+
+    @Test
+    public void testRemoveUserPackage_presentWithUsers() {
+        Entry other = new Entry("owner", "file", 'D', 1, "loader");
+        Entry[] entries = {
+                new Entry("owner", "file1", 'D', 0, "loader1"),
+                new Entry("owner", "file2", 'D', 0, "loader2"),
+                other
+        };
+        PackageDynamicCodeLoading info = makePackageDcl(entries);
+
+        assertTrue(info.removeUserPackage("owner", 0));
+        assertHasEntries(info, other);
+    }
+
+    @Test
+    public void testRemoveFile_present() {
+        Entry[] entries = {
+                new Entry("package1", "file1", 'D', 0, "loader1"),
+                new Entry("package1", "file2", 'D', 0, "loader1"),
+                new Entry("package2", "file1", 'D', 0, "loader2"),
+        };
+        Entry[] expectedSurvivors = {
+                new Entry("package1", "file2", 'D', 0, "loader1"),
+                new Entry("package2", "file1", 'D', 0, "loader2"),
+        };
+        PackageDynamicCodeLoading info = makePackageDcl(entries);
+
+        assertTrue(info.removeFile("package1", "file1", 0));
+        assertHasEntries(info, expectedSurvivors);
+    }
+
+    @Test
+    public void testRemoveFile_onlyEntry() {
+        Entry[] entries = {
+                new Entry("package1", "file1", 'D', 0, "loader1"),
+        };
+        PackageDynamicCodeLoading info = makePackageDcl(entries);
+
+        assertTrue(info.removeFile("package1", "file1", 0));
+        assertHasEntries(info, NO_ENTRIES);
+        assertHasPackages(info, NO_PACKAGES);
+    }
+
+    @Test
+    public void testRemoveFile_notPresent() {
+        Entry[] entries = {
+                new Entry("package1", "file2", 'D', 0, "loader1"),
+        };
+        PackageDynamicCodeLoading info = makePackageDcl(entries);
+
+        assertFalse(info.removeFile("package1", "file1", 0));
+        assertHasEntries(info, entries);
+    }
+
+    @Test
+    public void testRemoveFile_wrongUser() {
+        Entry[] entries = {
+                new Entry("package1", "file1", 'D', 10, "loader1"),
+        };
+        PackageDynamicCodeLoading info = makePackageDcl(entries);
+
+        assertFalse(info.removeFile("package1", "file1", 0));
+        assertHasEntries(info, entries);
+    }
+
+    @Test
+    public void testSyncData() {
+        Map<String, Set<Integer>> packageToUsersMap = ImmutableMap.of(
+                "package1", ImmutableSet.of(10, 20),
+                "package2", ImmutableSet.of(20));
+
+        Entry[] entries = {
+                new Entry("deleted.packaged", "file1", 'D', 10, "package1"),
+                new Entry("package1", "file2", 'D', 20, "package2"),
+                new Entry("package1", "file3", 'D', 10, "package2"),
+                new Entry("package1", "file3", 'D', 10, "deleted.package"),
+                new Entry("package2", "file4", 'D', 20, "deleted.package"),
+        };
+
+        Entry[] expectedSurvivors = {
+                new Entry("package1", "file2", 'D', 20, "package2"),
+        };
+
+        PackageDynamicCodeLoading info = makePackageDcl(entries);
+        info.syncData(packageToUsersMap);
+        assertHasEntries(info, expectedSurvivors);
+        assertHasPackages(info, "package1");
+    }
+
+    @Test
+    public void testRead_onlyHeader_emptyResult() throws Exception {
+        assertHasEntries(read("DCL1"), NO_ENTRIES);
+    }
+
+    @Test
+    public void testRead_noHeader_throws() {
+        assertThrows(IOException.class, () -> read(""));
+    }
+
+    @Test
+    public void testRead_wrongHeader_throws() {
+        assertThrows(IOException.class, () -> read("DCL2"));
+    }
+
+    @Test
+    public void testRead_oneEntry() throws Exception {
+        String inputText = ""
+                + "DCL1\n"
+                + "P:owning.package\n"
+                + "D:10:loading.package:/path/fi\\\\le\n";
+        assertHasEntries(read(inputText),
+                new Entry("owning.package", "/path/fi\\le", 'D', 10, "loading.package"));
+    }
+
+    @Test
+    public void testRead_emptyPackage() throws Exception {
+        String inputText = ""
+                + "DCL1\n"
+                + "P:owning.package\n";
+        PackageDynamicCodeLoading info = read(inputText);
+        assertHasEntries(info, NO_ENTRIES);
+        assertHasPackages(info, NO_PACKAGES);
+    }
+
+    @Test
+    public void testRead_complex() throws Exception {
+        String inputText = ""
+                + "DCL1\n"
+                + "P:owning.package1\n"
+                + "D:10:loading.package1,loading.package2:/path/file1\n"
+                + "D:5:loading.package1:/path/file2\n"
+                + "P:owning.package2\n"
+                + "D:0:loading.package2:/path/file3";
+        assertHasEntries(read(inputText),
+                new Entry("owning.package1", "/path/file1", 'D', 10, "loading.package1"),
+                new Entry("owning.package1", "/path/file1", 'D', 10, "loading.package2"),
+                new Entry("owning.package1", "/path/file2", 'D', 5, "loading.package1"),
+                new Entry("owning.package2", "/path/file3", 'D', 0, "loading.package2"));
+    }
+
+    @Test
+    public void testRead_missingPackageLine_throws() {
+        String inputText = ""
+                + "DCL1\n"
+                + "D:10:loading.package:/path/file\n";
+        assertThrows(IOException.class, () -> read(inputText));
+    }
+
+    @Test
+    public void testRead_malformedFile_throws() {
+        String inputText = ""
+                + "DCL1\n"
+                + "P:owning.package\n"
+                + "Hello world!\n";
+        assertThrows(IOException.class, () -> read(inputText));
+    }
+
+    @Test
+    public void testRead_badFileType_throws() {
+        String inputText = ""
+                + "DCL1\n"
+                + "P:owning.package\n"
+                + "X:10:loading.package:/path/file\n";
+        assertThrows(IOException.class, () -> read(inputText));
+    }
+
+    @Test
+    public void testRead_badUserId_throws() {
+        String inputText = ""
+                + "DCL1\n"
+                + "P:owning.package\n"
+                + "D:999999999999999999:loading.package:/path/file\n";
+        assertThrows(IOException.class, () -> read(inputText));
+    }
+
+    @Test
+    public void testRead_missingPackages_throws() {
+        String inputText = ""
+                + "DCL1\n"
+                + "P:owning.package\n"
+                + "D:1:,:/path/file\n";
+        assertThrows(IOException.class, () -> read(inputText));
+    }
+
+    @Test
+    public void testWrite_empty() throws Exception {
+        assertEquals("DCL1\n", write(NO_ENTRIES));
+    }
+
+    @Test
+    public void testWrite_oneEntry() throws Exception {
+        String expected = ""
+                + "DCL1\n"
+                + "P:owning.package\n"
+                + "D:10:loading.package:/path/fi\\\\le\n";
+        String actual = write(
+                new Entry("owning.package", "/path/fi\\le", 'D', 10, "loading.package"));
+        assertEquals(expected, actual);
+    }
+
+    @Test
+    public void testWrite_complex_roundTrips() throws Exception {
+        // There isn't a canonical order for the output in the presence of multiple items.
+        // So we just check that if we read back what we write we end up where we started.
+        Entry[] entries = {
+            new Entry("owning.package1", "/path/file1", 'D', 10, "loading.package1"),
+            new Entry("owning.package1", "/path/file1", 'D', 10, "loading.package2"),
+            new Entry("owning.package1", "/path/file2", 'D', 5, "loading.package1"),
+            new Entry("owning.package2", "/path/fi\\le3", 'D', 0, "loading.package2")
+        };
+        assertHasEntries(read(write(entries)), entries);
+    }
+
+    @Test
+    public void testWrite_failure_throws() {
+        PackageDynamicCodeLoading info = makePackageDcl(
+                new Entry("owning.package", "/path/fi\\le", 'D', 10, "loading.package"));
+        assertThrows(IOException.class, () -> info.write(new ThrowingOutputStream()));
+    }
+
+    @Test
+    public void testEscape_trivialCase_returnsSameString() {
+        assertSame(TRIVIAL_STRING, escape(TRIVIAL_STRING));
+    }
+
+    @Test
+    public void testEscape() {
+        String input = "backslash\\newline\nreturn\r";
+        String expected = "backslash\\\\newline\\nreturn\\r";
+        assertEquals(expected, escape(input));
+    }
+
+    @Test
+    public void testUnescape_trivialCase_returnsSameString() throws Exception {
+        assertSame(TRIVIAL_STRING, unescape(TRIVIAL_STRING));
+    }
+
+    @Test
+    public void testUnescape() throws Exception {
+        String input = "backslash\\\\newline\\nreturn\\r";
+        String expected = "backslash\\newline\nreturn\r";
+        assertEquals(expected, unescape(input));
+    }
+
+    @Test
+    public void testUnescape_badEscape_throws() {
+        assertThrows(IOException.class, () -> unescape("this is \\bad"));
+    }
+
+    @Test
+    public void testUnescape_trailingBackslash_throws() {
+        assertThrows(IOException.class, () -> unescape("don't do this\\"));
+    }
+
+    @Test
+    public void testEscapeUnescape_roundTrips() throws Exception {
+        assertRoundTripsWithEscape("foo");
+        assertRoundTripsWithEscape("\\\\\n\n\r");
+        assertRoundTripsWithEscape("\\a\\b\\");
+        assertRoundTripsWithEscape("\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\");
+    }
+
+    private void assertRoundTripsWithEscape(String original) throws Exception {
+        assertEquals(original, unescape(escape(original)));
+    }
+
+    private boolean record(PackageDynamicCodeLoading info, Entry entry) {
+        return info.record(entry.mOwningPackage, entry.mPath, entry.mFileType, entry.mUserId,
+                entry.mLoadingPackage);
+    }
+
+    private PackageDynamicCodeLoading read(String inputText) throws Exception {
+        ByteArrayInputStream inputStream =
+                new ByteArrayInputStream(inputText.getBytes(UTF_8));
+
+        PackageDynamicCodeLoading info = new PackageDynamicCodeLoading();
+        info.read(inputStream);
+
+        return info;
+    }
+
+    private String write(Entry... entries) throws Exception {
+        ByteArrayOutputStream output = new ByteArrayOutputStream();
+        makePackageDcl(entries).write(output);
+        return new String(output.toByteArray(), UTF_8);
+    }
+
+    private Set<Entry> entriesFrom(PackageDynamicCodeLoading info) {
+        ImmutableSet.Builder<Entry> entries = ImmutableSet.builder();
+        for (String owningPackage : info.getAllPackagesWithDynamicCodeLoading()) {
+            PackageDynamicCode packageInfo = info.getPackageDynamicCodeInfo(owningPackage);
+            Map<String, DynamicCodeFile> usageMap = packageInfo.mFileUsageMap;
+            for (Map.Entry<String, DynamicCodeFile> fileEntry : usageMap.entrySet()) {
+                String path = fileEntry.getKey();
+                DynamicCodeFile fileInfo = fileEntry.getValue();
+                for (String loadingPackage : fileInfo.mLoadingPackages) {
+                    entries.add(new Entry(owningPackage, path, fileInfo.mFileType, fileInfo.mUserId,
+                            loadingPackage));
+                }
+            }
+        }
+
+        return entries.build();
+    }
+
+    private PackageDynamicCodeLoading makePackageDcl(Entry... entries) {
+        PackageDynamicCodeLoading result = new PackageDynamicCodeLoading();
+        for (Entry entry : entries) {
+            result.record(entry.mOwningPackage, entry.mPath, entry.mFileType, entry.mUserId,
+                    entry.mLoadingPackage);
+        }
+        return result;
+
+    }
+
+    private void assertHasEntries(PackageDynamicCodeLoading info, Entry... expected) {
+        assertEquals(ImmutableSet.copyOf(expected), entriesFrom(info));
+    }
+
+    private void assertHasPackages(PackageDynamicCodeLoading info, String... expected) {
+        assertEquals(ImmutableSet.copyOf(expected), info.getAllPackagesWithDynamicCodeLoading());
+    }
+
+    /**
+     * Immutable representation of one entry in the dynamic code loading data (one package
+     * owning one file loaded by one package). Has well-behaved equality, hash and toString
+     * for ease of use in assertions.
+     */
+    private static class Entry {
+        private final String mOwningPackage;
+        private final String mPath;
+        private final char mFileType;
+        private final int mUserId;
+        private final String mLoadingPackage;
+
+        private Entry(String owningPackage, String path, char fileType, int userId,
+                String loadingPackage) {
+            mOwningPackage = owningPackage;
+            mPath = path;
+            mFileType = fileType;
+            mUserId = userId;
+            mLoadingPackage = loadingPackage;
+        }
+
+        @Override
+        public boolean equals(Object o) {
+            if (this == o) return true;
+            if (o == null || getClass() != o.getClass()) return false;
+            Entry that = (Entry) o;
+            return mFileType == that.mFileType
+                    && mUserId == that.mUserId
+                    && Objects.equals(mOwningPackage, that.mOwningPackage)
+                    && Objects.equals(mPath, that.mPath)
+                    && Objects.equals(mLoadingPackage, that.mLoadingPackage);
+        }
+
+        @Override
+        public int hashCode() {
+            return Objects.hash(mOwningPackage, mPath, mFileType, mUserId, mLoadingPackage);
+        }
+
+        @Override
+        public String toString() {
+            return "Entry("
+                    + "\"" + mOwningPackage + '"'
+                    + ", \"" + mPath + '"'
+                    + ", '" + mFileType + '\''
+                    + ", " + mUserId
+                    + ", \"" + mLoadingPackage + '\"'
+                    + ')';
+        }
+    }
+
+    private static class ThrowingOutputStream extends OutputStream {
+        @Override
+        public void write(int b) throws IOException {
+            throw new IOException("Intentional failure");
+        }
+    }
+}
diff --git a/tools/hiddenapi/exclude.sh b/tools/hiddenapi/exclude.sh
index 2291e5a..4ffcf68 100755
--- a/tools/hiddenapi/exclude.sh
+++ b/tools/hiddenapi/exclude.sh
@@ -11,6 +11,7 @@
   android.system \
   com.android.bouncycastle \
   com.android.conscrypt \
+  com.android.i18n.phonenumbers \
   com.android.okhttp \
   com.sun \
   dalvik \