Merge "Add IntRange and FloatRange annotations"
diff --git a/api/current.txt b/api/current.txt
index f44b370..695756e 100755
--- a/api/current.txt
+++ b/api/current.txt
@@ -42217,6 +42217,9 @@
     field public static final String KEY_MONTHLY_DATA_CYCLE_DAY_INT = "monthly_data_cycle_day_int";
     field public static final String KEY_ONLY_SINGLE_DC_ALLOWED_INT_ARRAY = "only_single_dc_allowed_int_array";
     field public static final String KEY_OPERATOR_SELECTION_EXPAND_BOOL = "operator_selection_expand_bool";
+    field public static final String KEY_OPPORTUNISTIC_NETWORK_DATA_SWITCH_HYSTERESIS_TIME_LONG = "opportunistic_network_data_switch_hysteresis_time_long";
+    field public static final String KEY_OPPORTUNISTIC_NETWORK_ENTRY_OR_EXIT_HYSTERESIS_TIME_LONG = "opportunistic_network_entry_or_exit_hysteresis_time_long";
+    field public static final String KEY_OPPORTUNISTIC_NETWORK_ENTRY_THRESHOLD_BANDWIDTH_INT = "opportunistic_network_entry_threshold_bandwidth_int";
     field public static final String KEY_OPPORTUNISTIC_NETWORK_ENTRY_THRESHOLD_RSRP_INT = "opportunistic_network_entry_threshold_rsrp_int";
     field public static final String KEY_OPPORTUNISTIC_NETWORK_ENTRY_THRESHOLD_RSSNR_INT = "opportunistic_network_entry_threshold_rssnr_int";
     field public static final String KEY_OPPORTUNISTIC_NETWORK_EXIT_THRESHOLD_RSRP_INT = "opportunistic_network_exit_threshold_rsrp_int";
@@ -42878,6 +42881,7 @@
   public class SubscriptionInfo implements android.os.Parcelable {
     method public android.graphics.Bitmap createIconBitmap(android.content.Context);
     method public int describeContents();
+    method public int getCardId();
     method public int getCarrierId();
     method public CharSequence getCarrierName();
     method public String getCountryIso();
@@ -42994,6 +42998,7 @@
     method public android.telephony.TelephonyManager createForSubscriptionId(int);
     method @RequiresPermission(android.Manifest.permission.ACCESS_COARSE_LOCATION) public java.util.List<android.telephony.CellInfo> getAllCellInfo();
     method public int getCallState();
+    method public int getCardIdForDefaultEuicc();
     method @RequiresPermission(android.Manifest.permission.READ_PHONE_STATE) @WorkerThread public android.os.PersistableBundle getCarrierConfig();
     method public int getCarrierIdFromSimMccMnc();
     method @Deprecated @RequiresPermission(anyOf={android.Manifest.permission.ACCESS_COARSE_LOCATION, android.Manifest.permission.ACCESS_FINE_LOCATION}) public android.telephony.CellLocation getCellLocation();
@@ -43041,6 +43046,7 @@
     method @RequiresPermission(android.Manifest.permission.READ_PHONE_STATE) public String getSubscriberId();
     method public String getTypeAllocationCode();
     method public String getTypeAllocationCode(int);
+    method @RequiresPermission("android.permission.READ_PRIVILEGED_PHONE_STATE") public java.util.List<android.telephony.UiccCardInfo> getUiccCardsInfo();
     method @Nullable @RequiresPermission(android.Manifest.permission.READ_PHONE_STATE) public String getVisualVoicemailPackageName();
     method @RequiresPermission(android.Manifest.permission.READ_PHONE_STATE) public String getVoiceMailAlphaTag();
     method @RequiresPermission(android.Manifest.permission.READ_PHONE_STATE) public String getVoiceMailNumber();
@@ -43132,6 +43138,7 @@
     field public static final String EXTRA_STATE_RINGING;
     field public static final String EXTRA_SUBSCRIPTION_ID = "android.telephony.extra.SUBSCRIPTION_ID";
     field public static final String EXTRA_VOICEMAIL_NUMBER = "android.telephony.extra.VOICEMAIL_NUMBER";
+    field public static final int INVALID_CARD_ID = -1; // 0xffffffff
     field public static final String METADATA_HIDE_VOICEMAIL_SETTINGS_MENU = "android.telephony.HIDE_VOICEMAIL_SETTINGS_MENU";
     field public static final int NETWORK_TYPE_1xRTT = 7; // 0x7
     field public static final int NETWORK_TYPE_CDMA = 4; // 0x4
@@ -43199,6 +43206,18 @@
     method public void onResults(java.util.List<android.telephony.CellInfo>);
   }
 
+  public final class UiccCardInfo implements android.os.Parcelable {
+    ctor public UiccCardInfo(boolean, int, String, String, int);
+    method public int describeContents();
+    method public int getCardId();
+    method public String getEid();
+    method public String getIccId();
+    method public int getSlotIndex();
+    method public boolean isEuicc();
+    method public void writeToParcel(android.os.Parcel, int);
+    field public static final android.os.Parcelable.Creator<android.telephony.UiccCardInfo> CREATOR;
+  }
+
   public abstract class VisualVoicemailService extends android.app.Service {
     ctor public VisualVoicemailService();
     method public android.os.IBinder onBind(android.content.Intent);
diff --git a/api/system-current.txt b/api/system-current.txt
index e6fb33e..97094c7 100644
--- a/api/system-current.txt
+++ b/api/system-current.txt
@@ -3088,12 +3088,14 @@
 
   public final class IpPrefix implements android.os.Parcelable {
     ctor public IpPrefix(java.net.InetAddress, int);
+    ctor public IpPrefix(String);
   }
 
   public class LinkAddress implements android.os.Parcelable {
     ctor public LinkAddress(java.net.InetAddress, int, int, int);
     ctor public LinkAddress(java.net.InetAddress, int);
     ctor public LinkAddress(String);
+    ctor public LinkAddress(String, int, int);
     method public boolean isGlobalPreferred();
     method public boolean isIPv4();
     method public boolean isIPv6();
@@ -3104,6 +3106,7 @@
     ctor public LinkProperties();
     ctor public LinkProperties(android.net.LinkProperties);
     method public boolean addDnsServer(java.net.InetAddress);
+    method public boolean addLinkAddress(android.net.LinkAddress);
     method public boolean addRoute(android.net.RouteInfo);
     method public void clear();
     method @Nullable public android.net.IpPrefix getNat64Prefix();
@@ -3118,6 +3121,7 @@
     method public boolean isProvisioned();
     method public boolean isReachable(java.net.InetAddress);
     method public boolean removeDnsServer(java.net.InetAddress);
+    method public boolean removeLinkAddress(android.net.LinkAddress);
     method public boolean removeRoute(android.net.RouteInfo);
     method public void setDnsServers(java.util.Collection<java.net.InetAddress>);
     method public void setDomains(String);
@@ -3134,6 +3138,7 @@
   }
 
   public class Network implements android.os.Parcelable {
+    ctor public Network(android.net.Network);
     method public android.net.Network getPrivateDnsBypassingCopy();
   }
 
@@ -3269,8 +3274,8 @@
 
   public class ApfCapabilities {
     ctor public ApfCapabilities(int, int, int);
-    method public boolean getApfDrop8023Frames(android.content.Context);
-    method public int[] getApfEthTypeBlackList(android.content.Context);
+    method public static boolean getApfDrop8023Frames(android.content.Context);
+    method public static int[] getApfEthTypeBlackList(android.content.Context);
     method public boolean hasDataAccess();
     field public final int apfPacketFormat;
     field public final int apfVersionSupported;
@@ -3467,11 +3472,19 @@
 package android.net.util {
 
   public class SocketUtils {
+    method public static void addArpEntry(java.net.Inet4Address, android.net.MacAddress, String, java.io.FileDescriptor) throws java.io.IOException;
+    method public static void attachControlPacketFilter(java.io.FileDescriptor, int) throws java.net.SocketException;
+    method public static void attachDhcpFilter(java.io.FileDescriptor) throws java.net.SocketException;
+    method public static void attachRaFilter(java.io.FileDescriptor, int) throws java.net.SocketException;
+    method public static void bindSocket(java.io.FileDescriptor, java.net.SocketAddress) throws android.system.ErrnoException, java.net.SocketException;
     method public static void bindSocketToInterface(java.io.FileDescriptor, String) throws android.system.ErrnoException;
     method public static void closeSocket(java.io.FileDescriptor) throws java.io.IOException;
+    method public static void connectSocket(java.io.FileDescriptor, java.net.SocketAddress) throws android.system.ErrnoException, java.net.SocketException;
     method public static java.net.SocketAddress makeNetlinkSocketAddress(int, int);
     method public static java.net.SocketAddress makePacketSocketAddress(short, int);
     method public static java.net.SocketAddress makePacketSocketAddress(int, byte[]);
+    method public static void sendTo(java.io.FileDescriptor, byte[], int, int, int, java.net.SocketAddress) throws android.system.ErrnoException, java.net.SocketException;
+    method public static void setSocketTimeValueOption(java.io.FileDescriptor, int, int, long) throws android.system.ErrnoException;
   }
 
 }
@@ -6213,7 +6226,6 @@
 
   public class SubscriptionInfo implements android.os.Parcelable {
     method @Nullable public java.util.List<android.telephony.UiccAccessRule> getAccessRules();
-    method public int getCardId();
     method public int getProfileClass();
   }
 
@@ -6273,7 +6285,6 @@
     method @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) public void enableVideoCalling(boolean);
     method @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) public String getAidForAppType(int);
     method @Deprecated @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) public java.util.List<android.service.carrier.CarrierIdentifier> getAllowedCarriers(int);
-    method @RequiresPermission(android.Manifest.permission.READ_PHONE_STATE) public int getCardIdForDefaultEuicc();
     method public java.util.List<java.lang.String> getCarrierPackageNamesForIntent(android.content.Intent);
     method public java.util.List<java.lang.String> getCarrierPackageNamesForIntentAndPhone(android.content.Intent, int);
     method @Nullable @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) public android.telephony.CarrierRestrictionRules getCarrierRestrictionRules();
@@ -6297,7 +6308,6 @@
     method @Nullable @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) public String getSimLocale();
     method @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) public int getSupportedRadioAccessFamily();
     method @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) public java.util.List<android.telephony.TelephonyHistogram> getTelephonyHistograms();
-    method @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) public android.telephony.UiccCardInfo[] getUiccCardsInfo();
     method @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) public android.telephony.UiccSlotInfo[] getUiccSlotsInfo();
     method @Nullable public android.os.Bundle getVisualVoicemailSettings();
     method @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) public int getVoiceActivationState();
@@ -6345,7 +6355,6 @@
     field public static final String EXTRA_SIM_STATE = "android.telephony.extra.SIM_STATE";
     field public static final String EXTRA_VISUAL_VOICEMAIL_ENABLED_BY_USER_BOOL = "android.telephony.extra.VISUAL_VOICEMAIL_ENABLED_BY_USER_BOOL";
     field public static final String EXTRA_VOICEMAIL_SCRAMBLED_PIN_STRING = "android.telephony.extra.VOICEMAIL_SCRAMBLED_PIN_STRING";
-    field public static final int INVALID_CARD_ID = -1; // 0xffffffff
     field public static final long MAX_NUMBER_VERIFICATION_TIMEOUT_MILLIS = 60000L; // 0xea60L
     field public static final int NETWORK_MODE_CDMA_EVDO = 4; // 0x4
     field public static final int NETWORK_MODE_CDMA_NO_EVDO = 5; // 0x5
@@ -6420,18 +6429,6 @@
     field public static final android.os.Parcelable.Creator<android.telephony.UiccAccessRule> CREATOR;
   }
 
-  public class UiccCardInfo implements android.os.Parcelable {
-    ctor public UiccCardInfo(boolean, int, String, String, int);
-    method public int describeContents();
-    method public int getCardId();
-    method public String getEid();
-    method public String getIccId();
-    method public int getSlotIndex();
-    method public boolean isEuicc();
-    method public void writeToParcel(android.os.Parcel, int);
-    field public static final android.os.Parcelable.Creator<android.telephony.UiccCardInfo> CREATOR;
-  }
-
   public class UiccSlotInfo implements android.os.Parcelable {
     ctor public UiccSlotInfo(boolean, boolean, String, int, int, boolean);
     method public int describeContents();
diff --git a/api/test-current.txt b/api/test-current.txt
index 8403fa5..af455fb 100644
--- a/api/test-current.txt
+++ b/api/test-current.txt
@@ -613,6 +613,7 @@
 
   public final class IpPrefix implements android.os.Parcelable {
     ctor public IpPrefix(java.net.InetAddress, int);
+    ctor public IpPrefix(String);
   }
 
   public final class IpSecManager {
@@ -621,6 +622,9 @@
 
   public class LinkAddress implements android.os.Parcelable {
     ctor public LinkAddress(java.net.InetAddress, int, int, int);
+    ctor public LinkAddress(java.net.InetAddress, int);
+    ctor public LinkAddress(String);
+    ctor public LinkAddress(String, int, int);
     method public boolean isGlobalPreferred();
     method public boolean isIPv4();
     method public boolean isIPv6();
@@ -630,6 +634,7 @@
   public final class LinkProperties implements android.os.Parcelable {
     ctor public LinkProperties(android.net.LinkProperties);
     method public boolean addDnsServer(java.net.InetAddress);
+    method public boolean addLinkAddress(android.net.LinkAddress);
     method @Nullable public android.net.IpPrefix getNat64Prefix();
     method public java.util.List<java.net.InetAddress> getPcscfServers();
     method public String getTcpBufferSizes();
@@ -642,6 +647,7 @@
     method public boolean isProvisioned();
     method public boolean isReachable(java.net.InetAddress);
     method public boolean removeDnsServer(java.net.InetAddress);
+    method public boolean removeLinkAddress(android.net.LinkAddress);
     method public boolean removeRoute(android.net.RouteInfo);
     method public void setNat64Prefix(android.net.IpPrefix);
     method public void setPcscfServers(java.util.Collection<java.net.InetAddress>);
@@ -652,6 +658,7 @@
   }
 
   public class Network implements android.os.Parcelable {
+    ctor public Network(android.net.Network);
     method public android.net.Network getPrivateDnsBypassingCopy();
   }
 
@@ -703,8 +710,8 @@
 
   public class ApfCapabilities {
     ctor public ApfCapabilities(int, int, int);
-    method public boolean getApfDrop8023Frames(android.content.Context);
-    method public int[] getApfEthTypeBlackList(android.content.Context);
+    method public static boolean getApfDrop8023Frames(android.content.Context);
+    method public static int[] getApfEthTypeBlackList(android.content.Context);
     method public boolean hasDataAccess();
     field public final int apfPacketFormat;
     field public final int apfVersionSupported;
@@ -901,11 +908,19 @@
 package android.net.util {
 
   public class SocketUtils {
+    method public static void addArpEntry(java.net.Inet4Address, android.net.MacAddress, String, java.io.FileDescriptor) throws java.io.IOException;
+    method public static void attachControlPacketFilter(java.io.FileDescriptor, int) throws java.net.SocketException;
+    method public static void attachDhcpFilter(java.io.FileDescriptor) throws java.net.SocketException;
+    method public static void attachRaFilter(java.io.FileDescriptor, int) throws java.net.SocketException;
+    method public static void bindSocket(java.io.FileDescriptor, java.net.SocketAddress) throws android.system.ErrnoException, java.net.SocketException;
     method public static void bindSocketToInterface(java.io.FileDescriptor, String) throws android.system.ErrnoException;
     method public static void closeSocket(java.io.FileDescriptor) throws java.io.IOException;
+    method public static void connectSocket(java.io.FileDescriptor, java.net.SocketAddress) throws android.system.ErrnoException, java.net.SocketException;
     method public static java.net.SocketAddress makeNetlinkSocketAddress(int, int);
     method public static java.net.SocketAddress makePacketSocketAddress(short, int);
     method public static java.net.SocketAddress makePacketSocketAddress(int, byte[]);
+    method public static void sendTo(java.io.FileDescriptor, byte[], int, int, int, java.net.SocketAddress) throws android.system.ErrnoException, java.net.SocketException;
+    method public static void setSocketTimeValueOption(java.io.FileDescriptor, int, int, long) throws android.system.ErrnoException;
   }
 
 }
diff --git a/cmds/statsd/src/atoms.proto b/cmds/statsd/src/atoms.proto
index 3053609..5fd148e 100644
--- a/cmds/statsd/src/atoms.proto
+++ b/cmds/statsd/src/atoms.proto
@@ -23,9 +23,11 @@
 import "frameworks/base/cmds/statsd/src/atom_field_options.proto";
 import "frameworks/base/core/proto/android/app/enums.proto";
 import "frameworks/base/core/proto/android/app/job/enums.proto";
+import "frameworks/base/core/proto/android/bluetooth/a2dp/enums.proto";
 import "frameworks/base/core/proto/android/bluetooth/enums.proto";
 import "frameworks/base/core/proto/android/bluetooth/hci/enums.proto";
 import "frameworks/base/core/proto/android/bluetooth/hfp/enums.proto";
+import "frameworks/base/core/proto/android/bluetooth/smp/enums.proto";
 import "frameworks/base/core/proto/android/net/networkcapabilities.proto";
 import "frameworks/base/core/proto/android/os/enums.proto";
 import "frameworks/base/core/proto/android/server/connectivity/data_stall_event.proto";
@@ -142,6 +144,23 @@
         NfcHceTransactionOccurred nfc_hce_transaction_occurred = 139;
         SeStateChanged se_state_changed = 140;
         SeOmapiReported se_omapi_reported = 141;
+        BluetoothActiveDeviceChanged bluetooth_active_device_changed = 151;
+        BluetoothA2dpPlaybackStateChanged bluetooth_a2dp_playback_state_changed = 152;
+        BluetoothA2dpCodecConfigChanged bluetooth_a2dp_codec_config_changed = 153;
+        BluetoothA2dpCodecCapabilityChanged bluetooth_a2dp_codec_capability_changed = 154;
+        BluetoothA2dpAudioUnderrunReported bluetooth_a2dp_audio_underrun_reported = 155;
+        BluetoothA2dpAudioOverrunReported bluetooth_a2dp_audio_overrun_reported = 156;
+        BluetoothDeviceRssiReported bluetooth_device_rssi_reported = 157;
+        BluetoothDeviceFailedContactCounterReported bluetooth_device_failed_contact_counter_reported = 158;
+        BluetoothDeviceTxPowerLevelReported bluetooth_device_tx_power_level_reported = 159;
+        BluetoothHciTimeoutReported bluetooth_hci_timeout_reported = 160;
+        BluetoothQualityReportReported bluetooth_quality_report_reported = 161;
+        BluetoothManufacturerInfoReported bluetooth_device_info_reported = 162;
+        BluetoothRemoteVersionInfoReported bluetooth_remote_version_info_reported = 163;
+        BluetoothSdpAttributeReported bluetooth_sdp_attribute_reported = 164;
+        BluetoothBondStateChanged bluetooth_bond_state_changed = 165;
+        BluetoothClassicPairingEventReported bluetooth_classic_pairing_event_reported = 166;
+        BluetoothSmpPairingEventReported bluetooth_smp_pairing_event_reported = 167;
     }
 
     // Pulled events will start at field 10000.
@@ -1084,6 +1103,27 @@
     optional android.bluetooth.hfp.ScoCodec codec = 3;
 }
 
+/**
+ * Logged when active device of a profile changes
+ *
+ * Logged from:
+ *     packages/apps/Bluetooth/src/com/android/bluetooth/a2dp/A2dpService.java
+ *     packages/apps/Bluetooth/src/com/android/bluetooth/hfp/HeadsetService.java
+ *     packages/apps/Bluetooth/src/com/android/bluetooth/hearingaid/HearingAidService.java
+ */
+message BluetoothActiveDeviceChanged {
+    // The profile whose active device has changed. Eg. A2DP, HEADSET, HEARING_AID
+    // From android.bluetooth.BluetoothProfile
+    optional int32 bt_profile = 1;
+    // An identifier that can be used to match events for this new active device.
+    // Currently, this is a salted hash of the MAC address of this Bluetooth device.
+    // Salt: Randomly generated 256 bit value
+    // Hash algorithm: HMAC-SHA256
+    // Size: 32 byte
+    // Default: null or empty if there is no active device for this profile
+    optional bytes obfuscated_id = 2 [(android.os.statsd.log_mode) = MODE_BYTES];
+}
+
 // Logs when there is an event affecting Bluetooth device's link layer connection.
 // - This event is triggered when there is a related HCI command or event
 // - Users of this metrics can deduce Bluetooth device's connection state from these events
@@ -1167,6 +1207,516 @@
     optional android.bluetooth.hci.StatusEnum reason_code = 9;
 }
 
+/**
+ * Logs when there is a change in Bluetooth A2DP playback state
+ *
+ * Logged from:
+ *     packages/apps/Bluetooth/src/com/android/bluetooth/a2dp/A2dpService.java
+ */
+message BluetoothA2dpPlaybackStateChanged {
+    // An identifier that can be used to match events for this device.
+    // Currently, this is a salted hash of the MAC address of this Bluetooth device.
+    // Salt: Randomly generated 256 bit value
+    // Hash algorithm: HMAC-SHA256
+    // Size: 32 byte
+    // Default: null or empty if the device identifier is not known
+    optional bytes obfuscated_id = 1 [(android.os.statsd.log_mode) = MODE_BYTES];
+    // Current playback state
+    // Default: PLAYBACK_STATE_UNKNOWN
+    optional android.bluetooth.a2dp.PlaybackStateEnum playback_state = 2;
+    // Current audio coding mode
+    // Default: AUDIO_CODING_MODE_UNKNOWN
+    optional android.bluetooth.a2dp.AudioCodingModeEnum audio_coding_mode = 3;
+}
+
+/**
+ * Logs when there is a change in A2DP codec config for a particular remote device
+ *
+ * Logged from:
+ *     frameworks/base/core/java/android/bluetooth/BluetoothCodecConfig.java
+ *     packages/apps/Bluetooth/src/com/android/bluetooth/a2dp/A2dpService.java
+ */
+message BluetoothA2dpCodecConfigChanged {
+    // An identifier that can be used to match events for this device.
+    // Currently, this is a salted hash of the MAC address of this Bluetooth device.
+    // Salt: Randomly generated 256 bit value
+    // Hash algorithm: HMAC-SHA256
+    // Size: 32 byte
+    // Default: null or empty if the device identifier is not known
+    optional bytes obfuscated_id = 1 [(android.os.statsd.log_mode) = MODE_BYTES];
+    // Type of codec as defined by various SOURCE_CODEC_TYPE_* constants in BluetoothCodecConfig
+    // Default SOURCE_CODEC_TYPE_INVALID
+    optional int32 codec_type = 2;
+    // Codec priroity, the higher the more preferred, -1 for disabled
+    // Default: CODEC_PRIORITY_DEFAULT
+    optional int32 codec_priority = 3;
+    // Sample rate in Hz as defined by various SAMPLE_RATE_* constants in BluetoothCodecConfig
+    // Default: SAMPLE_RATE_NONE
+    optional int32 sample_rate = 4;
+    // Bits per sample as defined by various BITS_PER_SAMPLE_* constants in BluetoothCodecConfig
+    // Default: BITS_PER_SAMPLE_NONE
+    optional int32 bits_per_sample = 5;
+    // Channel mode as defined by various CHANNEL_MODE_* constants in BluetoothCodecConfig
+    // Default: CHANNEL_MODE_NONE
+    optional int32 channel_mode = 6;
+    // Codec specific values
+    // Default 0
+    optional int64 codec_specific_1 = 7;
+    optional int64 codec_specific_2 = 8;
+    optional int64 codec_specific_3 = 9;
+    optional int64 codec_specific_4 = 10;
+}
+
+/**
+ * Logs when there is a change in selectable A2DP codec capability for a paricular remote device
+ * Each codec's capability is logged separately due to statsd restriction
+ *
+ * Logged from:
+ *     frameworks/base/core/java/android/bluetooth/BluetoothCodecConfig.java
+ *     packages/apps/Bluetooth/src/com/android/bluetooth/a2dp/A2dpService.java
+ */
+message BluetoothA2dpCodecCapabilityChanged {
+    // An identifier that can be used to match events for this device.
+    // Currently, this is a salted hash of the MAC address of this Bluetooth device.
+    // Salt: Randomly generated 256 bit value
+    // Hash algorithm: HMAC-SHA256
+    // Size: 32 byte
+    // Default: null or empty if the device identifier is not known
+    optional bytes obfuscated_id = 1 [(android.os.statsd.log_mode) = MODE_BYTES];
+    // Type of codec as defined by various SOURCE_CODEC_TYPE_* constants in BluetoothCodecConfig
+    // Default SOURCE_CODEC_TYPE_INVALID
+    optional int32 codec_type = 2;
+    // Codec priroity, the higher the more preferred, -1 for disabled
+    // Default: CODEC_PRIORITY_DEFAULT
+    optional int32 codec_priority = 3;
+    // A bit field of supported sample rates as defined by various SAMPLE_RATE_* constants
+    // in BluetoothCodecConfig
+    // Default: empty and SAMPLE_RATE_NONE for individual item
+    optional int32 sample_rate = 4;
+    // A bit field of supported bits per sample as defined by various BITS_PER_SAMPLE_* constants
+    // in BluetoothCodecConfig
+    // Default: empty and BITS_PER_SAMPLE_NONE for individual item
+    optional int32 bits_per_sample = 5;
+    // A bit field of supported channel mode as defined by various CHANNEL_MODE_* constants in
+    // BluetoothCodecConfig
+    // Default: empty and CHANNEL_MODE_NONE for individual item
+    optional int32 channel_mode = 6;
+    // Codec specific values
+    // Default 0
+    optional int64 codec_specific_1 = 7;
+    optional int64 codec_specific_2 = 8;
+    optional int64 codec_specific_3 = 9;
+    optional int64 codec_specific_4 = 10;
+}
+
+/**
+ * Logs when A2DP failed to read from PCM source.
+ * This typically happens when audio HAL cannot supply A2DP with data fast enough for encoding.
+ *
+ * Logged from:
+ *     system/bt
+ */
+message BluetoothA2dpAudioUnderrunReported {
+    // An identifier that can be used to match events for this device.
+    // Currently, this is a salted hash of the MAC address of this Bluetooth device.
+    // Salt: Randomly generated 256 bit value
+    // Hash algorithm: HMAC-SHA256
+    // Size: 32 byte
+    // Default: null or empty if the device identifier is not known
+    optional bytes obfuscated_id = 1 [(android.os.statsd.log_mode) = MODE_BYTES];
+    // Encoding interval in nanoseconds
+    // Default: 0
+    optional int64 encoding_interval_nanos = 2;
+    // Number of bytes of PCM data that could not be read from the source
+    // Default: 0
+    optional int32 num_missing_pcm_bytes = 3;
+}
+
+/**
+ * Logs when A2DP failed send encoded data to the remote device fast enough such that the transmit
+ * buffer queue is full and we have to drop data
+ *
+ * Logged from:
+ *     system/bt
+ */
+message BluetoothA2dpAudioOverrunReported {
+    // An identifier that can be used to match events for this device.
+    // Currently, this is a salted hash of the MAC address of this Bluetooth device.
+    // Salt: Randomly generated 256 bit value
+    // Hash algorithm: HMAC-SHA256
+    // Size: 32 byte
+    // Default: null or empty if the device identifier is not known
+    optional bytes obfuscated_id = 1 [(android.os.statsd.log_mode) = MODE_BYTES];
+    // Encoding interval in nanoseconds
+    // Default: 0
+    optional int64 encoding_interval_nanos = 2;
+    // Number of buffers dropped in this event
+    // Each buffer is encoded in one encoding interval and consists of multiple encoded frames
+    // Default: 0
+    optional int32 num_dropped_buffers = 3;
+    // Number of encoded buffers dropped in this event
+    // Default 0
+    optional int32 num_dropped_encoded_frames = 4;
+    // Number of encoded bytes dropped in this event
+    // Default: 0
+    optional int32 num_dropped_encoded_bytes = 5;
+}
+
+/**
+ * Logs when we receive reports regarding a device's RSSI value
+ *
+ * Logged from:
+ *     system/bt
+ */
+message BluetoothDeviceRssiReported {
+    // An identifier that can be used to match events for this device.
+    // Currently, this is a salted hash of the MAC address of this Bluetooth device.
+    // Salt: Randomly generated 256 bit value
+    // Hash algorithm: HMAC-SHA256
+    // Size: 32 byte
+    // Default: null or empty if the device identifier is not known
+    optional bytes obfuscated_id = 1 [(android.os.statsd.log_mode) = MODE_BYTES];
+    // Connection handle of this connection if available
+    // Range: 0x0000 - 0x0EFF (12 bits)
+    // Default: 0xFFFF if the handle is unknown
+    optional int32 connection_handle = 2;
+    // HCI command status code if this is triggerred by hci_cmd
+    // Default: STATUS_UNKNOWN
+    optional android.bluetooth.hci.StatusEnum hci_status = 3;
+    // BR/EDR
+    //   Range: -128 ≤ N ≤ 127 (signed integer)
+    //   Units: dB
+    // LE:
+    //   Range: -127 to 20, 127 (signed integer)
+    //   Units: dBm
+    // Invalid when an out of range value is reported
+    optional int32 rssi = 4;
+}
+
+/**
+ * Logs when we receive reports regarding how many consecutive failed contacts for a connection
+ *
+ * Logged from:
+ *     system/bt
+ */
+message BluetoothDeviceFailedContactCounterReported {
+    // An identifier that can be used to match events for this device.
+    // Currently, this is a salted hash of the MAC address of this Bluetooth device.
+    // Salt: Randomly generated 256 bit value
+    // Hash algorithm: HMAC-SHA256
+    // Size: 32 byte
+    // Default: null or empty if the device identifier is not known
+    optional bytes obfuscated_id = 1 [(android.os.statsd.log_mode) = MODE_BYTES];
+    // Connection handle of this connection if available
+    // Range: 0x0000 - 0x0EFF (12 bits)
+    // Default: 0xFFFF if the handle is unknown
+    optional int32 connection_handle = 2;
+    // HCI command status code if this is triggerred by hci_cmd
+    // Default: STATUS_UNKNOWN
+    optional android.bluetooth.hci.StatusEnum cmd_status = 3;
+    // Number of consecutive failed contacts for a connection corresponding to the Handle
+    // Range: uint16_t, 0-0xFFFF
+    // Default: 0xFFFFF
+    optional int32 failed_contact_counter = 4;
+}
+
+/**
+ * Logs when we receive reports regarding the tranmit power level used for a specific connection
+ *
+ * Logged from:
+ *     system/bt
+ */
+message BluetoothDeviceTxPowerLevelReported {
+    // An identifier that can be used to match events for this device.
+    // Currently, this is a salted hash of the MAC address of this Bluetooth device.
+    // Salt: Randomly generated 256 bit value
+    // Hash algorithm: HMAC-SHA256
+    // Size: 32 byte
+    // Default: null or empty if the device identifier is not known
+    optional bytes obfuscated_id = 1 [(android.os.statsd.log_mode) = MODE_BYTES];
+    // Connection handle of this connection if available
+    // Range: 0x0000 - 0x0EFF (12 bits)
+    // Default: 0xFFFF if the handle is unknown
+    optional int32 connection_handle = 2;
+    // HCI command status code if this is triggered by hci_cmd
+    // Default: STATUS_UNKNOWN
+    optional android.bluetooth.hci.StatusEnum hci_status = 3;
+    // Range: -30 ≤ N ≤ 20
+    // Units: dBm
+    // Invalid when an out of range value is reported
+    optional int32 transmit_power_level = 4;
+}
+
+/**
+ * Logs when Bluetooth controller failed to reply with command status within a timeout period after
+ * receiving an HCI command from the host
+ *
+ * Logged from: system/bt
+ */
+message BluetoothHciTimeoutReported {
+    // HCI command associated with this event
+    // Default: CMD_UNKNOWN
+    optional android.bluetooth.hci.CommandEnum hci_command = 1;
+}
+
+/**
+ * Logs when we receive Bluetooth Link Quality Report event from the controller
+ * See Android Bluetooth HCI specification for more details
+ *
+ * Note: all count and bytes field are counted since last event
+ *
+ * Logged from: system/bt
+ */
+message BluetoothQualityReportReported {
+    // Quality report ID
+    // Original type: uint8_t
+    // Default: BQR_ID_UNKNOWN
+    optional android.bluetooth.hci.BqrIdEnum quality_report_id = 1;
+    // Packet type of the connection
+    // Original type: uint8_t
+    // Default: BQR_PACKET_TYPE_UNKNOWN
+    optional android.bluetooth.hci.BqrPacketTypeEnum packet_types = 2;
+    // Connection handle of the connection
+    // Original type: uint16_t
+    optional int32 connection_handle = 3;
+    // Performing Role for the connection
+    // Original type: uint8_t
+    optional int32 connection_role = 4;
+    // Current Transmit Power Level for the connection. This value is the same as the controller's
+    // response to the HCI_Read_Transmit_Power_Level HCI command
+    // Original type: uint8_t
+    optional int32 tx_power_level = 5;
+    // Received Signal Strength Indication (RSSI) value for the connection. This value is an
+    // absolute receiver signal strength value
+    // Original type: int8_t
+    optional int32 rssi = 6;
+    // Signal-to-Noise Ratio (SNR) value for the connection. It is the average SNR of all the
+    // channels used by the link currently
+    // Original type: uint8_t
+    optional int32 snr = 7;
+    // Indicates the number of unused channels in AFH_channel_map
+    // Original type: uint8_t
+    optional int32 unused_afh_channel_count = 8;
+    // Indicates the number of the channels which are interfered and quality is bad but are still
+    // selected for AFH
+    // Original type: uint8_t
+    optional int32 afh_select_unideal_channel_count = 9;
+    // Current Link Supervision Timeout Setting
+    // Unit: N * 0.3125 ms (1 Bluetooth Clock)
+    // Original type: uint16_t
+    optional int32 lsto = 10;
+    // Piconet Clock for the specified Connection_Handle. This value is the same as the controller's
+    // response to HCI_Read_Clock HCI command with the parameter "Which_Clock" of
+    // 0x01 (Piconet Clock)
+    // Unit: N * 0.3125 ms (1 Bluetooth Clock)
+    // Original type: uint32_t
+    optional int64 connection_piconet_clock = 11;
+    // The count of retransmission
+    // Original type: uint32_t
+    optional int64 retransmission_count = 12;
+    // The count of no RX
+    // Original type: uint32_t
+    optional int64 no_rx_count = 13;
+    // The count of NAK (Negative Acknowledge)
+    // Original type: uint32_t
+    optional int64 nak_count = 14;
+    // Controller timestamp of last TX ACK
+    // Unit: N * 0.3125 ms (1 Bluetooth Clock)
+    // Original type: uint32_t
+    optional int64 last_tx_ack_timestamp = 15;
+    // The count of Flow-off (STOP)
+    // Original type: uint32_t
+    optional int64 flow_off_count = 16;
+    // Controller timestamp of last Flow-on (GO)
+    // Unit: N * 0.3125 ms (1 Bluetooth Clock)
+    // Original type: uint32_t
+    optional int64 last_flow_on_timestamp = 17;
+    // Buffer overflow count (how many bytes of TX data are dropped) since the last event
+    // Original type: uint32_t
+    optional int64 buffer_overflow_bytes = 18;
+    // Buffer underflow count (in byte) since last event
+    // Original type: uint32_t
+    optional int64 buffer_underflow_bytes = 19;
+}
+
+/**
+ * Logs when a Bluetooth device's manufacturer information is learnt by the Bluetooth stack
+ *
+ * Notes:
+ * - Each event can be partially filled as we might learn different pieces of device
+ *   information at different time
+ * - Multiple device info events can be combined to give more complete picture
+ * - When multiple device info events tries to describe the same information, the
+ *   later one wins
+ *
+ * Logged from:
+ *     packages/apps/Bluetooth
+ */
+message BluetoothManufacturerInfoReported {
+    // An identifier that can be used to match events for this device.
+    // Currently, this is a salted hash of the MAC address of this Bluetooth device.
+    // Salt: Randomly generated 256 bit value
+    // Hash algorithm: HMAC-SHA256
+    // Size: 32 byte
+    // Default: null or empty if the device identifier is not known
+    optional bytes obfuscated_id = 1 [(android.os.statsd.log_mode) = MODE_BYTES];
+    // Where is this device info obtained from
+    optional android.bluetooth.DeviceInfoSrcEnum source_type = 2;
+    // Name of the data source
+    // For EXTERNAL: package name of the data source
+    // For INTERNAL: null for general case, component name otherwise
+    optional string source_name = 3;
+    // Name of the manufacturer of this device
+    optional string manufacturer = 4;
+    // Model of this device
+    optional string model = 5;
+    // Hardware version of this device
+    optional string hardware_version = 6;
+    // Software version of this device
+    optional string software_version = 7;
+}
+
+/**
+ * Logs when we receive Bluetooth Read Remote Version Information Complete Event from the remote
+ * device, as documented by the Bluetooth Core HCI specification
+ * Reference: https://www.bluetooth.com/specifications/bluetooth-core-specification
+ * Vol 2, Part E, Page 1118
+ *
+ * Logged from:
+ *     system/bt
+ */
+message BluetoothRemoteVersionInfoReported {
+    // Connection handle of the connection
+    // Original type: uint16_t
+    optional int32 connection_handle = 1;
+    // HCI command status code
+    // Default: STATUS_UNKNOWN
+    optional android.bluetooth.hci.StatusEnum hci_status = 2;
+    // 1 byte Version of current LMP in the remote controller
+    optional int32 lmp_version = 3;
+    // 2 bytes LMP manufacturer code of the remote controller
+    // https://www.bluetooth.com/specifications/assigned-numbers/company-identifiers
+    optional int32 lmp_manufacturer_code = 4;
+    // 4 bytes subversion of the LMP in the remote controller
+    optional int32 lmp_subversion = 5;
+}
+
+/**
+ * Logs when certain Bluetooth SDP attributes are discovered
+ * Constant definitions are from:
+ *     https://www.bluetooth.com/specifications/assigned-numbers/service-discovery
+ *
+ * Current logged attributes:
+ * - BluetoothProfileDescriptorList
+ * - Supported Features Bitmask
+ *
+ * Logged from:
+ *     system/bt
+ */
+message BluetoothSdpAttributeReported {
+    // An identifier that can be used to match events for this device.
+    // Currently, this is a salted hash of the MAC address of this Bluetooth device.
+    // Salt: Randomly generated 256 bit value
+    // Hash algorithm: HMAC-SHA256
+    optional bytes obfuscated_id = 1 [(android.os.statsd.log_mode) = MODE_BYTES];
+    // Short form UUIDs used to identify Bluetooth protocols, profiles, and service classes
+    // Original type: uint16_t
+    optional int32 protocol_uuid = 2;
+    // Short form UUIDs used to identify Bluetooth SDP attribute types
+    // Original type: uint16_t
+    optional int32 attribute_id = 3;
+    // Attribute value for the particular attribute
+    optional bytes attribute_value = 4 [(android.os.statsd.log_mode) = MODE_BYTES];
+}
+
+/**
+ * Logs when bond state of a Bluetooth device changes
+ *
+ * Logged from:
+ *     frameworks/base/core/java/android/bluetooth/BluetoothDevice.java
+ *     packages/apps/Bluetooth/src/com/android/bluetooth/btservice/BondStateMachine.java
+ */
+message BluetoothBondStateChanged {
+    // An identifier that can be used to match events for this device.
+    // Currently, this is a salted hash of the MAC address of this Bluetooth device.
+    // Salt: Randomly generated 256 bit value
+    // Hash algorithm: HMAC-SHA256
+    // Size: 32 byte
+    // Default: null or empty if the device identifier is not known
+    optional bytes obfuscated_id = 1 [(android.os.statsd.log_mode) = MODE_BYTES];
+    // Preferred transport type to remote dual mode device
+    // Default: TRANSPORT_AUTO means no preference
+    optional android.bluetooth.TransportTypeEnum transport = 2;
+    // The type of this Bluetooth device (Classic, LE, or Dual mode)
+    // Default: UNKNOWN
+    optional android.bluetooth.DeviceTypeEnum type = 3;
+    // Current bond state (NONE, BONDING, BONDED)
+    // Default: BOND_STATE_UNKNOWN
+    optional android.bluetooth.BondStateEnum bond_state = 4;
+    // Bonding sub state
+    // Default: BOND_SUB_STATE_UNKNOWN
+    optional android.bluetooth.BondSubStateEnum bonding_sub_state = 5;
+    // Unbond Reason
+    // Default: UNBOND_REASON_UNKNOWN
+    optional android.bluetooth.UnbondReasonEnum unbond_reason = 6;
+}
+
+/**
+ * Logs there is an event related Bluetooth classic pairing
+ *
+ * Logged from:
+ *     system/bt
+ */
+message BluetoothClassicPairingEventReported {
+    // An identifier that can be used to match events for this device.
+    // Currently, this is a salted hash of the MAC address of this Bluetooth device.
+    // Salt: Randomly generated 256 bit value
+    // Hash algorithm: HMAC-SHA256
+    // Size: 32 byte
+    // Default: null or empty if the device identifier is not known
+    optional bytes obfuscated_id = 1 [(android.os.statsd.log_mode) = MODE_BYTES];
+    // Connection handle of this connection if available
+    // Range: 0x0000 - 0x0EFF (12 bits)
+    // Default: 0xFFFF if the handle is unknown
+    optional int32 connection_handle = 2;
+    // HCI command associated with this event
+    // Default: CMD_UNKNOWN
+    optional android.bluetooth.hci.CommandEnum hci_cmd = 3;
+    // HCI event associated with this event
+    // Default: EVT_UNKNOWN
+    optional android.bluetooth.hci.EventEnum hci_event = 4;
+    // HCI command status code if this is triggerred by hci_cmd
+    // Default: STATUS_UNKNOWN
+    optional android.bluetooth.hci.StatusEnum cmd_status = 5;
+    // HCI reason code associated with this event
+    // Default: STATUS_UNKNOWN
+    optional android.bluetooth.hci.StatusEnum reason_code = 6;
+}
+
+/**
+ * Logs when there is an event related to Bluetooth Security Manager Protocol (SMP)
+ *
+ * Logged from:
+ *     system/bt
+ */
+message BluetoothSmpPairingEventReported {
+    // An identifier that can be used to match events for this device.
+    // Currently, this is a salted hash of the MAC address of this Bluetooth device.
+    // Salt: Randomly generated 256 bit value
+    // Hash algorithm: HMAC-SHA256
+    // Size: 32 byte
+    // Default: null or empty if the device identifier is not known
+    optional bytes obfuscated_id = 1 [(android.os.statsd.log_mode) = MODE_BYTES];
+    // SMP command sent or received over L2CAP
+    // Default: CMD_UNKNOWN
+    optional android.bluetooth.smp.CommandEnum smp_command = 2;
+    // Whether this command is sent or received
+    // Default: DIRECTION_UNKNOWN
+    optional android.bluetooth.DirectionEnum direction = 3;
+    // SMP failure reason code
+    // Default: PAIRING_FAIL_REASON_DEFAULT
+    optional android.bluetooth.smp.PairingFailReasonEnum smp_fail_reason = 4;
+}
 
 /**
  * Logs when something is plugged into or removed from the USB-C connector.
diff --git a/core/java/android/net/IpPrefix.java b/core/java/android/net/IpPrefix.java
index b996cda..175263f 100644
--- a/core/java/android/net/IpPrefix.java
+++ b/core/java/android/net/IpPrefix.java
@@ -104,6 +104,8 @@
      *
      * @hide
      */
+    @SystemApi
+    @TestApi
     public IpPrefix(String prefix) {
         // We don't reuse the (InetAddress, int) constructor because "error: call to this must be
         // first statement in constructor". We could factor out setting the member variables to an
diff --git a/core/java/android/net/LinkAddress.java b/core/java/android/net/LinkAddress.java
index fbd602c..8d779aa 100644
--- a/core/java/android/net/LinkAddress.java
+++ b/core/java/android/net/LinkAddress.java
@@ -176,6 +176,7 @@
      * @hide
      */
     @SystemApi
+    @TestApi
     public LinkAddress(InetAddress address, int prefixLength) {
         this(address, prefixLength, 0, 0);
         this.scope = scopeForUnicastAddress(address);
@@ -199,6 +200,7 @@
      * @hide
      */
     @SystemApi
+    @TestApi
     public LinkAddress(String address) {
         this(address, 0, 0);
         this.scope = scopeForUnicastAddress(this.address);
@@ -212,6 +214,8 @@
      * @param scope The address scope.
      * @hide
      */
+    @SystemApi
+    @TestApi
     public LinkAddress(String address, int flags, int scope) {
         // This may throw an IllegalArgumentException; catching it is the caller's responsibility.
         // TODO: consider rejecting mapped IPv4 addresses such as "::ffff:192.0.2.5/24".
diff --git a/core/java/android/net/LinkProperties.java b/core/java/android/net/LinkProperties.java
index 6628701..42db0fd 100644
--- a/core/java/android/net/LinkProperties.java
+++ b/core/java/android/net/LinkProperties.java
@@ -287,7 +287,8 @@
      * @return true if {@code address} was added or updated, false otherwise.
      * @hide
      */
-    @UnsupportedAppUsage
+    @SystemApi
+    @TestApi
     public boolean addLinkAddress(LinkAddress address) {
         if (address == null) {
             return false;
@@ -315,6 +316,8 @@
      * @return true if the address was removed, false if it did not exist.
      * @hide
      */
+    @SystemApi
+    @TestApi
     public boolean removeLinkAddress(LinkAddress toRemove) {
         int i = findLinkAddressIndex(toRemove);
         if (i >= 0) {
diff --git a/core/java/android/net/Network.java b/core/java/android/net/Network.java
index 2c831de..e04b5fc 100644
--- a/core/java/android/net/Network.java
+++ b/core/java/android/net/Network.java
@@ -123,6 +123,8 @@
     /**
      * @hide
      */
+    @SystemApi
+    @TestApi
     public Network(Network that) {
         this(that.netId, that.mPrivateDnsBypass);
     }
diff --git a/core/java/android/net/apf/ApfCapabilities.java b/core/java/android/net/apf/ApfCapabilities.java
index baf5585..e09fa8f 100644
--- a/core/java/android/net/apf/ApfCapabilities.java
+++ b/core/java/android/net/apf/ApfCapabilities.java
@@ -81,14 +81,14 @@
     /**
      * @return Whether the APF Filter in the device should filter out IEEE 802.3 Frames.
      */
-    public boolean getApfDrop8023Frames(Context context) {
+    public static boolean getApfDrop8023Frames(Context context) {
         return context.getResources().getBoolean(R.bool.config_apfDrop802_3Frames);
     }
 
     /**
      * @return An array of blacklisted EtherType, packets with EtherTypes within it will be dropped.
      */
-    public int[] getApfEthTypeBlackList(Context context) {
+    public static int[] getApfEthTypeBlackList(Context context) {
         return context.getResources().getIntArray(R.array.config_apfEthTypeBlackList);
     }
 }
diff --git a/packages/NetworkStack/src/android/net/util/FdEventsReader.java b/core/java/android/net/shared/FdEventsReader.java
similarity index 93%
rename from packages/NetworkStack/src/android/net/util/FdEventsReader.java
rename to core/java/android/net/shared/FdEventsReader.java
index 8bbf449..5ccc560 100644
--- a/packages/NetworkStack/src/android/net/util/FdEventsReader.java
+++ b/core/java/android/net/shared/FdEventsReader.java
@@ -14,10 +14,10 @@
  * limitations under the License.
  */
 
-package android.net.util;
+package android.net.shared;
 
-import static android.os.MessageQueue.OnFileDescriptorEventListener.EVENT_INPUT;
 import static android.os.MessageQueue.OnFileDescriptorEventListener.EVENT_ERROR;
+import static android.os.MessageQueue.OnFileDescriptorEventListener.EVENT_INPUT;
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
@@ -63,6 +63,7 @@
  * All public methods MUST only be called from the same thread with which
  * the Handler constructor argument is associated.
  *
+ * @param <BufferType> the type of the buffer used to read data.
  * @hide
  */
 public abstract class FdEventsReader<BufferType> {
@@ -89,6 +90,7 @@
         mBuffer = buffer;
     }
 
+    /** Start this FdEventsReader. */
     public void start() {
         if (onCorrectThread()) {
             createAndRegisterFd();
@@ -100,6 +102,7 @@
         }
     }
 
+    /** Stop this FdEventsReader and destroy the file descriptor. */
     public void stop() {
         if (onCorrectThread()) {
             unregisterAndDestroyFd();
@@ -112,18 +115,25 @@
     }
 
     @NonNull
-    public Handler getHandler() { return mHandler; }
+    public Handler getHandler() {
+        return mHandler;
+    }
 
     protected abstract int recvBufSize(@NonNull BufferType buffer);
 
-    public int recvBufSize() { return recvBufSize(mBuffer); }
+    /** Returns the size of the receive buffer. */
+    public int recvBufSize() {
+        return recvBufSize(mBuffer);
+    }
 
     /**
      * Get the number of successful calls to {@link #readPacket(FileDescriptor, Object)}.
      *
      * <p>A call was successful if {@link #readPacket(FileDescriptor, Object)} returned a value > 0.
      */
-    public final long numPacketsReceived() { return mPacketsReceived; }
+    public final long numPacketsReceived() {
+        return mPacketsReceived;
+    }
 
     /**
      * Subclasses MUST create the listening socket here, including setting
@@ -199,7 +209,9 @@
         onStart();
     }
 
-    private boolean isRunning() { return (mFd != null) && mFd.valid(); }
+    private boolean isRunning() {
+        return (mFd != null) && mFd.valid();
+    }
 
     // Keep trying to read until we get EAGAIN/EWOULDBLOCK or some fatal error.
     private boolean handleInput() {
diff --git a/core/java/android/net/util/SocketUtils.java b/core/java/android/net/util/SocketUtils.java
index 2df08a1..fbb15ed 100644
--- a/core/java/android/net/util/SocketUtils.java
+++ b/core/java/android/net/util/SocketUtils.java
@@ -21,17 +21,21 @@
 
 import android.annotation.SystemApi;
 import android.annotation.TestApi;
+import android.net.MacAddress;
 import android.net.NetworkUtils;
 import android.system.ErrnoException;
 import android.system.NetlinkSocketAddress;
 import android.system.Os;
 import android.system.PacketSocketAddress;
+import android.system.StructTimeval;
 
 import libcore.io.IoBridge;
 
 import java.io.FileDescriptor;
 import java.io.IOException;
+import java.net.Inet4Address;
 import java.net.SocketAddress;
+import java.net.SocketException;
 
 /**
  * Collection of utilities to interact with raw sockets.
@@ -76,11 +80,80 @@
     }
 
     /**
+     * Set an option on a socket that takes a time value argument.
+     */
+    public static void setSocketTimeValueOption(
+            FileDescriptor fd, int level, int option, long millis) throws ErrnoException {
+        Os.setsockoptTimeval(fd, level, option, StructTimeval.fromMillis(millis));
+    }
+
+    /**
+     * Bind a socket to the specified address.
+     */
+    public static void bindSocket(FileDescriptor fd, SocketAddress addr)
+            throws ErrnoException, SocketException {
+        Os.bind(fd, addr);
+    }
+
+    /**
+     * Connect a socket to the specified address.
+     */
+    public static void connectSocket(FileDescriptor fd, SocketAddress addr)
+            throws ErrnoException, SocketException {
+        Os.connect(fd, addr);
+    }
+
+    /**
+     * Send a message on a socket, using the specified SocketAddress.
+     */
+    public static void sendTo(FileDescriptor fd, byte[] bytes, int byteOffset, int byteCount,
+            int flags, SocketAddress addr) throws ErrnoException, SocketException {
+        Os.sendto(fd, bytes, byteOffset, byteCount, flags, addr);
+    }
+
+    /**
      * @see IoBridge#closeAndSignalBlockedThreads(FileDescriptor)
      */
     public static void closeSocket(FileDescriptor fd) throws IOException {
         IoBridge.closeAndSignalBlockedThreads(fd);
     }
 
+    /**
+     * Attaches a socket filter that accepts DHCP packets to the given socket.
+     */
+    public static void attachDhcpFilter(FileDescriptor fd) throws SocketException {
+        NetworkUtils.attachDhcpFilter(fd);
+    }
+
+    /**
+     * Attaches a socket filter that accepts ICMPv6 router advertisements to the given socket.
+     * @param fd the socket's {@link FileDescriptor}.
+     * @param packetType the hardware address type, one of ARPHRD_*.
+     */
+    public static void attachRaFilter(FileDescriptor fd, int packetType) throws SocketException {
+        NetworkUtils.attachRaFilter(fd, packetType);
+    }
+
+    /**
+     * Attaches a socket filter that accepts L2-L4 signaling traffic required for IP connectivity.
+     *
+     * This includes: all ARP, ICMPv6 RS/RA/NS/NA messages, and DHCPv4 exchanges.
+     *
+     * @param fd the socket's {@link FileDescriptor}.
+     * @param packetType the hardware address type, one of ARPHRD_*.
+     */
+    public static void attachControlPacketFilter(FileDescriptor fd, int packetType)
+            throws SocketException {
+        NetworkUtils.attachControlPacketFilter(fd, packetType);
+    }
+
+    /**
+     * Add an entry into the ARP cache.
+     */
+    public static void addArpEntry(Inet4Address ipv4Addr, MacAddress ethAddr, String ifname,
+            FileDescriptor fd) throws IOException {
+        NetworkUtils.addArpEntry(ipv4Addr, ethAddr, ifname, fd);
+    }
+
     private SocketUtils() {}
 }
diff --git a/core/proto/Android.bp b/core/proto/Android.bp
index 80cc2d4..3b891d6 100644
--- a/core/proto/Android.bp
+++ b/core/proto/Android.bp
@@ -21,7 +21,10 @@
         type: "lite",
     },
     srcs: [
+        "android/bluetooth/a2dp/enums.proto",
         "android/bluetooth/enums.proto",
         "android/bluetooth/hci/enums.proto",
+        "android/bluetooth/hfp/enums.proto",
+        "android/bluetooth/smp/enums.proto",
     ],
 }
diff --git a/core/proto/android/bluetooth/a2dp/enums.proto b/core/proto/android/bluetooth/a2dp/enums.proto
new file mode 100644
index 0000000..5a025bd
--- /dev/null
+++ b/core/proto/android/bluetooth/a2dp/enums.proto
@@ -0,0 +1,35 @@
+/*
+ * 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.
+ */
+
+syntax = "proto2";
+package android.bluetooth.a2dp;
+
+option java_outer_classname = "BluetoothA2dpProtoEnums";
+option java_multiple_files = true;
+
+// A2dp playback state enum, defined from:
+// frameworks/base/core/java/android/bluetooth/BluetoothA2dp.java
+enum PlaybackStateEnum {
+    PLAYBACK_STATE_UNKNOWN = 0;
+    PLAYBACK_STATE_PLAYING = 10;
+    PLAYBACK_STATE_NOT_PLAYING = 11;
+}
+
+enum AudioCodingModeEnum {
+    AUDIO_CODING_MODE_UNKNOWN = 0;
+    AUDIO_CODING_MODE_HARDWARE = 1;
+    AUDIO_CODING_MODE_SOFTWARE = 2;
+}
diff --git a/core/proto/android/bluetooth/enums.proto b/core/proto/android/bluetooth/enums.proto
index 76c240e..a88a06c 100644
--- a/core/proto/android/bluetooth/enums.proto
+++ b/core/proto/android/bluetooth/enums.proto
@@ -56,3 +56,57 @@
     LINK_TYPE_ACL = 0x01;
     LINK_TYPE_ESCO = 0x02;
 }
+
+enum DeviceInfoSrcEnum {
+    DEVICE_INFO_SRC_UNKNOWN = 0;
+    // Within Android Bluetooth stack
+    DEVICE_INFO_INTERNAL = 1;
+    // Outside Android Bluetooth stack
+    DEVICE_INFO_EXTERNAL = 2;
+}
+
+enum DeviceTypeEnum {
+    DEVICE_TYPE_UNKNOWN = 0;
+    DEVICE_TYPE_CLASSIC = 1;
+    DEVICE_TYPE_LE = 2;
+    DEVICE_TYPE_DUAL = 3;
+}
+
+// Defined in frameworks/base/core/java/android/bluetooth/BluetoothDevice.java
+enum TransportTypeEnum {
+    TRANSPORT_TYPE_AUTO = 0;
+    TRANSPORT_TYPE_BREDR = 1;
+    TRANSPORT_TYPE_LE = 2;
+}
+
+// Bond state enum
+// Defined in frameworks/base/core/java/android/bluetooth/BluetoothDevice.java
+enum BondStateEnum {
+    BOND_STATE_UNKNOWN = 0;
+    BOND_STATE_NONE = 10;
+    BOND_STATE_BONDING = 11;
+    BOND_STATE_BONDED = 12;
+}
+
+// Sub states within the bonding general state
+enum BondSubStateEnum {
+    BOND_SUB_STATE_UNKNOWN = 0;
+    BOND_SUB_STATE_LOCAL_OOB_DATA_PROVIDED = 1;
+    BOND_SUB_STATE_LOCAL_PIN_REQUESTED = 2;
+    BOND_SUB_STATE_LOCAL_PIN_REPLIED = 3;
+    BOND_SUB_STATE_LOCAL_SSP_REQUESTED = 4;
+    BOND_SUB_STATE_LOCAL_SSP_REPLIED = 5;
+}
+
+enum UnbondReasonEnum {
+    UNBOND_REASON_UNKNOWN = 0;
+    UNBOND_REASON_AUTH_FAILED = 1;
+    UNBOND_REASON_AUTH_REJECTED = 2;
+    UNBOND_REASON_AUTH_CANCELED = 3;
+    UNBOND_REASON_REMOTE_DEVICE_DOWN = 4;
+    UNBOND_REASON_DISCOVERY_IN_PROGRESS = 5;
+    UNBOND_REASON_AUTH_TIMEOUT = 6;
+    UNBOND_REASON_REPEATED_ATTEMPTS = 7;
+    UNBOND_REASON_REMOTE_AUTH_CANCELED = 8;
+    UNBOND_REASON_REMOVED = 9;
+}
diff --git a/core/proto/android/bluetooth/hci/enums.proto b/core/proto/android/bluetooth/hci/enums.proto
index e1d96bb..ef894e5 100644
--- a/core/proto/android/bluetooth/hci/enums.proto
+++ b/core/proto/android/bluetooth/hci/enums.proto
@@ -351,7 +351,7 @@
     EVT_COMMAND_COMPLETE = 0x0E;
     EVT_COMMAND_STATUS = 0x0F;
     EVT_HARDWARE_ERROR = 0x10;
-    EVT_FLUSH_OCCURED = 0x11;
+    EVT_FLUSH_OCCURRED = 0x11;
     EVT_ROLE_CHANGE = 0x12;
     EVT_NUM_COMPL_DATA_PKTS = 0x13;
     EVT_MODE_CHANGE = 0x14;
@@ -517,3 +517,43 @@
     STATUS_CLB_DATA_TOO_BIG = 0x43;
     STATUS_OPERATION_CANCELED_BY_HOST = 0x44; // Not currently used in system/bt
 }
+
+enum BqrIdEnum {
+    BQR_ID_UNKNOWN = 0x00;
+    BQR_ID_MONITOR_MODE = 0x01;
+    BQR_ID_APPROACH_LSTO = 0x02;
+    BQR_ID_A2DP_AUDIO_CHOPPY = 0x03;
+    BQR_ID_SCO_VOICE_CHOPPY = 0x04;
+}
+
+enum BqrPacketTypeEnum {
+    BQR_PACKET_TYPE_UNKNOWN = 0x00;
+    BQR_PACKET_TYPE_ID = 0x01;
+    BQR_PACKET_TYPE_NULL = 0x02;
+    BQR_PACKET_TYPE_POLL = 0x03;
+    BQR_PACKET_TYPE_FHS = 0x04;
+    BQR_PACKET_TYPE_HV1 = 0x05;
+    BQR_PACKET_TYPE_HV2 = 0x06;
+    BQR_PACKET_TYPE_HV3 = 0x07;
+    BQR_PACKET_TYPE_DV = 0x08;
+    BQR_PACKET_TYPE_EV3 = 0x09;
+    BQR_PACKET_TYPE_EV4 = 0x0A;
+    BQR_PACKET_TYPE_EV5 = 0x0B;
+    BQR_PACKET_TYPE_2EV3 = 0x0C;
+    BQR_PACKET_TYPE_2EV5 = 0x0D;
+    BQR_PACKET_TYPE_3EV3 = 0x0E;
+    BQR_PACKET_TYPE_3EV5 = 0x0F;
+    BQR_PACKET_TYPE_DM1 = 0x10;
+    BQR_PACKET_TYPE_DH1 = 0x11;
+    BQR_PACKET_TYPE_DM3 = 0x12;
+    BQR_PACKET_TYPE_DH3 = 0x13;
+    BQR_PACKET_TYPE_DM5 = 0x14;
+    BQR_PACKET_TYPE_DH5 = 0x15;
+    BQR_PACKET_TYPE_AUX1 = 0x16;
+    BQR_PACKET_TYPE_2DH1 = 0x17;
+    BQR_PACKET_TYPE_2DH3 = 0x18;
+    BQR_PACKET_TYPE_2DH5 = 0x19;
+    BQR_PACKET_TYPE_3DH1 = 0x1A;
+    BQR_PACKET_TYPE_3DH3 = 0x1B;
+    BQR_PACKET_TYPE_3DH5 = 0x1C;
+}
diff --git a/core/proto/android/bluetooth/smp/enums.proto b/core/proto/android/bluetooth/smp/enums.proto
new file mode 100644
index 0000000..c6747b7
--- /dev/null
+++ b/core/proto/android/bluetooth/smp/enums.proto
@@ -0,0 +1,58 @@
+/*
+ * Copyright 2019 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.
+ */
+
+syntax = "proto2";
+package android.bluetooth.smp;
+
+option java_outer_classname = "BluetoothSmpProtoEnums";
+option java_multiple_files = true;
+
+// SMP Pairing command codes
+enum CommandEnum {
+    CMD_UNKNOWN = 0x00;
+    CMD_PAIRING_REQUEST = 0x01;
+    CMD_PAIRING_RESPONSE = 0x02;
+    CMD_PAIRING_CONFIRM = 0x03;
+    CMD_PAIRING_RANDOM = 0x04;
+    CMD_PAIRING_FAILED = 0x05;
+    CMD_ENCRYPTION_INFON = 0x06;
+    CMD_MASTER_IDENTIFICATION = 0x07;
+    CMD_IDENTITY_INFO = 0x08;
+    CMD_IDENTITY_ADDR_INFO = 0x09;
+    CMD_SIGNING_INFO = 0x0A;
+    CMD_SECURITY_REQUEST = 0x0B;
+    CMD_PAIRING_PUBLIC_KEY = 0x0C;
+    CMD_PAIRING_DHKEY_CHECK = 0x0D;
+    CMD_PAIRING_KEYPRESS_INFO = 0x0E;
+}
+
+enum PairingFailReasonEnum {
+    PAIRING_FAIL_REASON_RESERVED = 0x00;
+    PAIRING_FAIL_REASON_PASSKEY_ENTRY = 0x01;
+    PAIRING_FAIL_REASON_OOB = 0x02;
+    PAIRING_FAIL_REASON_AUTH_REQ = 0x03;
+    PAIRING_FAIL_REASON_CONFIRM_VALUE = 0x04;
+    PAIRING_FAIL_REASON_PAIR_NOT_SUPPORT = 0x05;
+    PAIRING_FAIL_REASON_ENC_KEY_SIZE = 0x06;
+    PAIRING_FAIL_REASON_INVALID_CMD = 0x07;
+    PAIRING_FAIL_REASON_UNSPECIFIED = 0x08;
+    PAIRING_FAIL_REASON_REPEATED_ATTEMPTS = 0x09;
+    PAIRING_FAIL_REASON_INVALID_PARAMETERS = 0x0A;
+    PAIRING_FAIL_REASON_DHKEY_CHK = 0x0B;
+    PAIRING_FAIL_REASON_NUMERIC_COMPARISON = 0x0C;
+    PAIRING_FAIL_REASON_CLASSIC_PAIRING_IN_PROGR = 0x0D;
+    PAIRING_FAIL_REASON_XTRANS_DERIVE_NOT_ALLOW = 0x0E;
+}
\ No newline at end of file
diff --git a/packages/NetworkStack/src/android/net/dhcp/DhcpPacketListener.java b/packages/NetworkStack/src/android/net/dhcp/DhcpPacketListener.java
index dce8b61..eac8d2a 100644
--- a/packages/NetworkStack/src/android/net/dhcp/DhcpPacketListener.java
+++ b/packages/NetworkStack/src/android/net/dhcp/DhcpPacketListener.java
@@ -18,7 +18,7 @@
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
-import android.net.util.FdEventsReader;
+import android.net.shared.FdEventsReader;
 import android.os.Handler;
 import android.system.Os;
 
diff --git a/packages/NetworkStack/src/android/net/ip/IpClient.java b/packages/NetworkStack/src/android/net/ip/IpClient.java
index f20e0163..4315d34 100644
--- a/packages/NetworkStack/src/android/net/ip/IpClient.java
+++ b/packages/NetworkStack/src/android/net/ip/IpClient.java
@@ -40,15 +40,12 @@
 import android.net.metrics.IpConnectivityLog;
 import android.net.metrics.IpManagerEvent;
 import android.net.shared.InitialConfiguration;
-import android.net.shared.NetdService;
 import android.net.shared.ProvisioningConfiguration;
 import android.net.util.InterfaceParams;
 import android.net.util.SharedLog;
 import android.os.ConditionVariable;
-import android.os.INetworkManagementService;
 import android.os.Message;
 import android.os.RemoteException;
-import android.os.ServiceManager;
 import android.os.SystemClock;
 import android.text.TextUtils;
 import android.util.LocalLog;
@@ -64,7 +61,7 @@
 import com.android.internal.util.State;
 import com.android.internal.util.StateMachine;
 import com.android.internal.util.WakeupMessage;
-import com.android.server.net.NetlinkTracker;
+import com.android.server.NetworkObserverRegistry;
 
 import java.io.FileDescriptor;
 import java.io.PrintWriter;
@@ -338,8 +335,9 @@
     private final Dependencies mDependencies;
     private final CountDownLatch mShutdownLatch;
     private final ConnectivityManager mCm;
-    private final INetworkManagementService mNwService;
-    private final NetlinkTracker mNetlinkTracker;
+    private final INetd mNetd;
+    private final NetworkObserverRegistry mObserverRegistry;
+    private final IpClientLinkObserver mLinkObserver;
     private final WakeupMessage mProvisioningTimeoutAlarm;
     private final WakeupMessage mDhcpActionTimeoutAlarm;
     private final SharedLog mLog;
@@ -373,15 +371,6 @@
     private final ConditionVariable mApfDataSnapshotComplete = new ConditionVariable();
 
     public static class Dependencies {
-        public INetworkManagementService getNMS() {
-            return INetworkManagementService.Stub.asInterface(
-                    ServiceManager.getService(Context.NETWORKMANAGEMENT_SERVICE));
-        }
-
-        public INetd getNetd() {
-            return NetdService.getInstance();
-        }
-
         /**
          * Get interface parameters for the specified interface.
          */
@@ -390,26 +379,14 @@
         }
     }
 
-    public IpClient(Context context, String ifName, IIpClientCallbacks callback) {
-        this(context, ifName, callback, new Dependencies());
-    }
-
-    /**
-     * An expanded constructor, useful for dependency injection.
-     * TODO: migrate all test users to mock IpClient directly and remove this ctor.
-     */
     public IpClient(Context context, String ifName, IIpClientCallbacks callback,
-            INetworkManagementService nwService) {
-        this(context, ifName, callback, new Dependencies() {
-            @Override
-            public INetworkManagementService getNMS() {
-                return nwService;
-            }
-        });
+            NetworkObserverRegistry observerRegistry) {
+        this(context, ifName, callback, observerRegistry, new Dependencies());
     }
 
     @VisibleForTesting
-    IpClient(Context context, String ifName, IIpClientCallbacks callback, Dependencies deps) {
+    IpClient(Context context, String ifName, IIpClientCallbacks callback,
+            NetworkObserverRegistry observerRegistry, Dependencies deps) {
         super(IpClient.class.getSimpleName() + "." + ifName);
         Preconditions.checkNotNull(ifName);
         Preconditions.checkNotNull(callback);
@@ -422,7 +399,7 @@
         mDependencies = deps;
         mShutdownLatch = new CountDownLatch(1);
         mCm = mContext.getSystemService(ConnectivityManager.class);
-        mNwService = deps.getNMS();
+        mObserverRegistry = observerRegistry;
 
         sSmLogs.putIfAbsent(mInterfaceName, new SharedLog(MAX_LOG_RECORDS, mTag));
         mLog = sSmLogs.get(mInterfaceName);
@@ -433,19 +410,15 @@
 
         // TODO: Consider creating, constructing, and passing in some kind of
         // InterfaceController.Dependencies class.
-        mInterfaceCtrl = new InterfaceController(mInterfaceName, deps.getNetd(), mLog);
+        mNetd = mContext.getSystemService(INetd.class);
+        mInterfaceCtrl = new InterfaceController(mInterfaceName, mNetd, mLog);
 
-        mNetlinkTracker = new NetlinkTracker(
+        mLinkObserver = new IpClientLinkObserver(
                 mInterfaceName,
-                new NetlinkTracker.Callback() {
-                    @Override
-                    public void update() {
-                        sendMessage(EVENT_NETLINK_LINKPROPERTIES_CHANGED);
-                    }
-                }) {
+                () -> sendMessage(EVENT_NETLINK_LINKPROPERTIES_CHANGED)) {
             @Override
-            public void interfaceAdded(String iface) {
-                super.interfaceAdded(iface);
+            public void onInterfaceAdded(String iface) {
+                super.onInterfaceAdded(iface);
                 if (mClatInterfaceName.equals(iface)) {
                     mCallback.setNeighborDiscoveryOffload(false);
                 } else if (!mInterfaceName.equals(iface)) {
@@ -457,8 +430,8 @@
             }
 
             @Override
-            public void interfaceRemoved(String iface) {
-                super.interfaceRemoved(iface);
+            public void onInterfaceRemoved(String iface) {
+                super.onInterfaceRemoved(iface);
                 // TODO: Also observe mInterfaceName going down and take some
                 // kind of appropriate action.
                 if (mClatInterfaceName.equals(iface)) {
@@ -570,19 +543,11 @@
     }
 
     private void startStateMachineUpdaters() {
-        try {
-            mNwService.registerObserver(mNetlinkTracker);
-        } catch (RemoteException e) {
-            logError("Couldn't register NetlinkTracker: %s", e);
-        }
+        mObserverRegistry.registerObserverForNonblockingCallback(mLinkObserver);
     }
 
     private void stopStateMachineUpdaters() {
-        try {
-            mNwService.unregisterObserver(mNetlinkTracker);
-        } catch (RemoteException e) {
-            logError("Couldn't unregister NetlinkTracker: %s", e);
-        }
+        mObserverRegistry.unregisterObserver(mLinkObserver);
     }
 
     @Override
@@ -805,7 +770,7 @@
     // we should only call this if we know for sure that there are no IP addresses
     // assigned to the interface, etc.
     private void resetLinkProperties() {
-        mNetlinkTracker.clearLinkProperties();
+        mLinkObserver.clearLinkProperties();
         mConfiguration = null;
         mDhcpResults = null;
         mTcpBufferSizes = "";
@@ -984,10 +949,10 @@
         //         - IPv6 DNS servers
         //
         // N.B.: this is fundamentally race-prone and should be fixed by
-        // changing NetlinkTracker from a hybrid edge/level model to an
+        // changing IpClientLinkObserver from a hybrid edge/level model to an
         // edge-only model, or by giving IpClient its own netlink socket(s)
         // so as to track all required information directly.
-        LinkProperties netlinkLinkProperties = mNetlinkTracker.getLinkProperties();
+        LinkProperties netlinkLinkProperties = mLinkObserver.getLinkProperties();
         newLp.setLinkAddresses(netlinkLinkProperties.getLinkAddresses());
         for (RouteInfo route : netlinkLinkProperties.getRoutes()) {
             newLp.addRoute(route);
@@ -1166,8 +1131,7 @@
             // necessary or does reading from settings at startup suffice?).
             final int numSolicits = 5;
             final int interSolicitIntervalMs = 750;
-            setNeighborParameters(mDependencies.getNetd(), mInterfaceName,
-                    numSolicits, interSolicitIntervalMs);
+            setNeighborParameters(mNetd, mInterfaceName, numSolicits, interSolicitIntervalMs);
         } catch (Exception e) {
             mLog.e("Failed to adjust neighbor parameters", e);
             // Carry on using the system defaults (currently: 3, 1000);
diff --git a/packages/NetworkStack/src/android/net/ip/IpClientLinkObserver.java b/packages/NetworkStack/src/android/net/ip/IpClientLinkObserver.java
new file mode 100644
index 0000000..8ad99aa0
--- /dev/null
+++ b/packages/NetworkStack/src/android/net/ip/IpClientLinkObserver.java
@@ -0,0 +1,378 @@
+/*
+ * Copyright (C) 2014 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.net.ip;
+
+import android.net.InetAddresses;
+import android.net.LinkAddress;
+import android.net.LinkProperties;
+import android.net.RouteInfo;
+import android.util.Log;
+
+import com.android.server.NetworkObserver;
+
+import java.net.InetAddress;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * Keeps track of link configuration received from Netd.
+ *
+ * An instance of this class is constructed by passing in an interface name and a callback. The
+ * owner is then responsible for registering the tracker with NetworkObserverRegistry. When the
+ * class receives update notifications, it applies the update to its local LinkProperties, and if
+ * something has changed, notifies its owner of the update via the callback.
+ *
+ * The owner can then call {@code getLinkProperties()} in order to find out
+ * what changed. If in the meantime the LinkProperties stored here have changed,
+ * this class will return the current LinkProperties. Because each change
+ * triggers an update callback after the change is made, the owner may get more
+ * callbacks than strictly necessary (some of which may be no-ops), but will not
+ * be out of sync once all callbacks have been processed.
+ *
+ * Threading model:
+ *
+ * - The owner of this class is expected to create it, register it, and call
+ *   getLinkProperties or clearLinkProperties on its thread.
+ * - Most of the methods in the class are implementing NetworkObserver and are called
+ *   on the handler used to register the observer.
+ * - All accesses to mLinkProperties must be synchronized(this). All the other
+ *   member variables are immutable once the object is constructed.
+ *
+ * @hide
+ */
+public class IpClientLinkObserver implements NetworkObserver {
+    private final String mTag;
+
+    /**
+     * Callback used by {@link IpClientLinkObserver} to send update notifications.
+     */
+    public interface Callback {
+        /**
+         * Called when some properties of the link were updated.
+         */
+        void update();
+    }
+
+    private final String mInterfaceName;
+    private final Callback mCallback;
+    private final LinkProperties mLinkProperties;
+    private DnsServerRepository mDnsServerRepository;
+
+    private static final boolean DBG = false;
+
+    public IpClientLinkObserver(String iface, Callback callback) {
+        mTag = "NetlinkTracker/" + iface;
+        mInterfaceName = iface;
+        mCallback = callback;
+        mLinkProperties = new LinkProperties();
+        mLinkProperties.setInterfaceName(mInterfaceName);
+        mDnsServerRepository = new DnsServerRepository();
+    }
+
+    private void maybeLog(String operation, String iface, LinkAddress address) {
+        if (DBG) {
+            Log.d(mTag, operation + ": " + address + " on " + iface
+                    + " flags " + address.getFlags() + " scope " + address.getScope());
+        }
+    }
+
+    private void maybeLog(String operation, Object o) {
+        if (DBG) {
+            Log.d(mTag, operation + ": " + o.toString());
+        }
+    }
+
+    @Override
+    public void onInterfaceRemoved(String iface) {
+        maybeLog("interfaceRemoved", iface);
+        if (mInterfaceName.equals(iface)) {
+            // Our interface was removed. Clear our LinkProperties and tell our owner that they are
+            // now empty. Note that from the moment that the interface is removed, any further
+            // interface-specific messages (e.g., RTM_DELADDR) will not reach us, because the netd
+            // code that parses them will not be able to resolve the ifindex to an interface name.
+            clearLinkProperties();
+            mCallback.update();
+        }
+    }
+
+    @Override
+    public void onInterfaceAddressUpdated(LinkAddress address, String iface) {
+        if (mInterfaceName.equals(iface)) {
+            maybeLog("addressUpdated", iface, address);
+            boolean changed;
+            synchronized (this) {
+                changed = mLinkProperties.addLinkAddress(address);
+            }
+            if (changed) {
+                mCallback.update();
+            }
+        }
+    }
+
+    @Override
+    public void onInterfaceAddressRemoved(LinkAddress address, String iface) {
+        if (mInterfaceName.equals(iface)) {
+            maybeLog("addressRemoved", iface, address);
+            boolean changed;
+            synchronized (this) {
+                changed = mLinkProperties.removeLinkAddress(address);
+            }
+            if (changed) {
+                mCallback.update();
+            }
+        }
+    }
+
+    @Override
+    public void onRouteUpdated(RouteInfo route) {
+        if (mInterfaceName.equals(route.getInterface())) {
+            maybeLog("routeUpdated", route);
+            boolean changed;
+            synchronized (this) {
+                changed = mLinkProperties.addRoute(route);
+            }
+            if (changed) {
+                mCallback.update();
+            }
+        }
+    }
+
+    @Override
+    public void onRouteRemoved(RouteInfo route) {
+        if (mInterfaceName.equals(route.getInterface())) {
+            maybeLog("routeRemoved", route);
+            boolean changed;
+            synchronized (this) {
+                changed = mLinkProperties.removeRoute(route);
+            }
+            if (changed) {
+                mCallback.update();
+            }
+        }
+    }
+
+    @Override
+    public void onInterfaceDnsServerInfo(String iface, long lifetime, String[] addresses) {
+        if (mInterfaceName.equals(iface)) {
+            maybeLog("interfaceDnsServerInfo", Arrays.toString(addresses));
+            boolean changed = mDnsServerRepository.addServers(lifetime, addresses);
+            if (changed) {
+                synchronized (this) {
+                    mDnsServerRepository.setDnsServersOn(mLinkProperties);
+                }
+                mCallback.update();
+            }
+        }
+    }
+
+    /**
+     * Returns a copy of this object's LinkProperties.
+     */
+    public synchronized LinkProperties getLinkProperties() {
+        return new LinkProperties(mLinkProperties);
+    }
+
+    /**
+     * Reset this object's LinkProperties.
+     */
+    public synchronized void clearLinkProperties() {
+        // Clear the repository before clearing mLinkProperties. That way, if a clear() happens
+        // while interfaceDnsServerInfo() is being called, we'll end up with no DNS servers in
+        // mLinkProperties, as desired.
+        mDnsServerRepository = new DnsServerRepository();
+        mLinkProperties.clear();
+        mLinkProperties.setInterfaceName(mInterfaceName);
+    }
+
+    /**
+     * Tracks DNS server updates received from Netlink.
+     *
+     * The network may announce an arbitrary number of DNS servers in Router Advertisements at any
+     * time. Each announcement has a lifetime; when the lifetime expires, the servers should not be
+     * used any more. In this way, the network can gracefully migrate clients from one set of DNS
+     * servers to another. Announcements can both raise and lower the lifetime, and an announcement
+     * can expire servers by announcing them with a lifetime of zero.
+     *
+     * Typically the system will only use a small number (2 or 3; {@code NUM_CURRENT_SERVERS}) of
+     * DNS servers at any given time. These are referred to as the current servers. In case all the
+     * current servers expire, the class also keeps track of a larger (but limited) number of
+     * servers that are promoted to current servers when the current ones expire. In order to
+     * minimize updates to the rest of the system (and potentially expensive cache flushes) this
+     * class attempts to keep the list of current servers constant where possible. More
+     * specifically, the list of current servers is only updated if a new server is learned and
+     * there are not yet {@code NUM_CURRENT_SERVERS} current servers, or if one or more of the
+     * current servers expires or is pushed out of the set. Therefore, the current servers will not
+     * necessarily be the ones with the highest lifetime, but the ones learned first.
+     *
+     * This is by design: if instead the class always preferred the servers with the highest
+     * lifetime, a (misconfigured?) network where two or more routers announce more than
+     * {@code NUM_CURRENT_SERVERS} unique servers would cause persistent oscillations.
+     *
+     * TODO: Currently servers are only expired when a new DNS update is received.
+     * Update them using timers, or possibly on every notification received by NetlinkTracker.
+     *
+     * Threading model: run by NetlinkTracker. Methods are synchronized(this) just in case netlink
+     * notifications are sent by multiple threads. If future threads use alarms to expire, those
+     * alarms must also be synchronized(this).
+     *
+     */
+    private static class DnsServerRepository {
+
+        /** How many DNS servers we will use. 3 is suggested by RFC 6106. */
+        static final int NUM_CURRENT_SERVERS = 3;
+
+        /** How many DNS servers we'll keep track of, in total. */
+        static final int NUM_SERVERS = 12;
+
+        /** Stores up to {@code NUM_CURRENT_SERVERS} DNS servers we're currently using. */
+        private Set<InetAddress> mCurrentServers;
+
+        public static final String TAG = "DnsServerRepository";
+
+        /**
+         * Stores all the DNS servers we know about, for use when the current servers expire.
+         * Always sorted in order of decreasing expiry. The elements in this list are also the
+         * values of mIndex, and may be elements in mCurrentServers.
+         */
+        private ArrayList<DnsServerEntry> mAllServers;
+
+        /**
+         * Indexes the servers so we can update their lifetimes more quickly in the common case
+         * where servers are not being added, but only being refreshed.
+         */
+        private HashMap<InetAddress, DnsServerEntry> mIndex;
+
+        DnsServerRepository() {
+            mCurrentServers = new HashSet<>();
+            mAllServers = new ArrayList<>(NUM_SERVERS);
+            mIndex = new HashMap<>(NUM_SERVERS);
+        }
+
+        /** Sets the DNS servers of the provided LinkProperties object to the current servers. */
+        public synchronized void setDnsServersOn(LinkProperties lp) {
+            lp.setDnsServers(mCurrentServers);
+        }
+
+        /**
+         * Notifies the class of new DNS server information.
+         * @param lifetime the time in seconds that the DNS servers are valid.
+         * @param addresses the string representations of the IP addresses of DNS servers to use.
+         */
+        public synchronized boolean addServers(long lifetime, String[] addresses) {
+            // The lifetime is actually an unsigned 32-bit number, but Java doesn't have unsigned.
+            // Technically 0xffffffff (the maximum) is special and means "forever", but 2^32 seconds
+            // (136 years) is close enough.
+            long now = System.currentTimeMillis();
+            long expiry = now + 1000 * lifetime;
+
+            // Go through the list of servers. For each one, update the entry if one exists, and
+            // create one if it doesn't.
+            for (String addressString : addresses) {
+                InetAddress address;
+                try {
+                    address = InetAddresses.parseNumericAddress(addressString);
+                } catch (IllegalArgumentException ex) {
+                    continue;
+                }
+
+                if (!updateExistingEntry(address, expiry)) {
+                    // There was no entry for this server. Create one, unless it's already expired
+                    // (i.e., if the lifetime is zero; it cannot be < 0 because it's unsigned).
+                    if (expiry > now) {
+                        DnsServerEntry entry = new DnsServerEntry(address, expiry);
+                        mAllServers.add(entry);
+                        mIndex.put(address, entry);
+                    }
+                }
+            }
+
+            // Sort the servers by expiry.
+            Collections.sort(mAllServers);
+
+            // Prune excess entries and update the current server list.
+            return updateCurrentServers();
+        }
+
+        private synchronized boolean updateExistingEntry(InetAddress address, long expiry) {
+            DnsServerEntry existing = mIndex.get(address);
+            if (existing != null) {
+                existing.expiry = expiry;
+                return true;
+            }
+            return false;
+        }
+
+        private synchronized boolean updateCurrentServers() {
+            long now = System.currentTimeMillis();
+            boolean changed = false;
+
+            // Prune excess or expired entries.
+            for (int i = mAllServers.size() - 1; i >= 0; i--) {
+                if (i >= NUM_SERVERS || mAllServers.get(i).expiry < now) {
+                    DnsServerEntry removed = mAllServers.remove(i);
+                    mIndex.remove(removed.address);
+                    changed |= mCurrentServers.remove(removed.address);
+                } else {
+                    break;
+                }
+            }
+
+            // Add servers to the current set, in order of decreasing lifetime, until it has enough.
+            // Prefer existing servers over new servers in order to minimize updates to the rest of
+            // the system and avoid persistent oscillations.
+            for (DnsServerEntry entry : mAllServers) {
+                if (mCurrentServers.size() < NUM_CURRENT_SERVERS) {
+                    changed |= mCurrentServers.add(entry.address);
+                } else {
+                    break;
+                }
+            }
+            return changed;
+        }
+    }
+
+    /**
+     * Represents a DNS server entry with an expiry time.
+     *
+     * Implements Comparable so DNS server entries can be sorted by lifetime, longest-lived first.
+     * The ordering of entries with the same lifetime is unspecified, because given two servers with
+     * identical lifetimes, we don't care which one we use, and only comparing the lifetime is much
+     * faster than comparing the IP address as well.
+     *
+     * Note: this class has a natural ordering that is inconsistent with equals.
+     */
+    private static class DnsServerEntry implements Comparable<DnsServerEntry> {
+        /** The IP address of the DNS server. */
+        public final InetAddress address;
+        /** The time until which the DNS server may be used. A Java millisecond time as might be
+         * returned by currentTimeMillis(). */
+        public long expiry;
+
+        DnsServerEntry(InetAddress address, long expiry) throws IllegalArgumentException {
+            this.address = address;
+            this.expiry = expiry;
+        }
+
+        public int compareTo(DnsServerEntry other) {
+            return Long.compare(other.expiry, this.expiry);
+        }
+    }
+}
diff --git a/packages/NetworkStack/src/android/net/util/PacketReader.java b/packages/NetworkStack/src/android/net/util/PacketReader.java
index 4aec6b6..94b1e9f 100644
--- a/packages/NetworkStack/src/android/net/util/PacketReader.java
+++ b/packages/NetworkStack/src/android/net/util/PacketReader.java
@@ -18,6 +18,7 @@
 
 import static java.lang.Math.max;
 
+import android.net.shared.FdEventsReader;
 import android.os.Handler;
 import android.system.Os;
 
diff --git a/packages/NetworkStack/src/com/android/server/NetworkObserver.java b/packages/NetworkStack/src/com/android/server/NetworkObserver.java
index d3b40a6..cccec0b 100644
--- a/packages/NetworkStack/src/com/android/server/NetworkObserver.java
+++ b/packages/NetworkStack/src/com/android/server/NetworkObserver.java
@@ -17,6 +17,7 @@
 package com.android.server;
 
 import android.net.LinkAddress;
+import android.net.RouteInfo;
 
 /**
  * Observer for network events, to use with {@link NetworkObserverRegistry}.
@@ -77,11 +78,11 @@
      * @see android.net.INetdUnsolicitedEventListener
      *          #onRouteChanged(boolean, String, String, String)
      */
-    default void onRouteUpdated(String route, String gateway, String ifName) {}
+    default void onRouteUpdated(RouteInfo route) {}
 
     /**
      * @see android.net.INetdUnsolicitedEventListener
      *          #onRouteChanged(boolean, String, String, String)
      */
-    default void onRouteRemoved(String route, String gateway, String ifName) {}
+    default void onRouteRemoved(RouteInfo route) {}
 }
diff --git a/packages/NetworkStack/src/com/android/server/NetworkObserverRegistry.java b/packages/NetworkStack/src/com/android/server/NetworkObserverRegistry.java
index 14e6c5f..4f55779 100644
--- a/packages/NetworkStack/src/com/android/server/NetworkObserverRegistry.java
+++ b/packages/NetworkStack/src/com/android/server/NetworkObserverRegistry.java
@@ -18,11 +18,16 @@
 import android.annotation.NonNull;
 import android.net.INetd;
 import android.net.INetdUnsolicitedEventListener;
+import android.net.InetAddresses;
+import android.net.IpPrefix;
 import android.net.LinkAddress;
+import android.net.RouteInfo;
 import android.os.Handler;
 import android.os.RemoteException;
+import android.util.Log;
 
 import java.util.Map;
+import java.util.Optional;
 import java.util.concurrent.ConcurrentHashMap;
 
 /**
@@ -32,6 +37,7 @@
  * all INetworkManagementEventObserver objects that have registered with it.
  */
 public class NetworkObserverRegistry extends INetdUnsolicitedEventListener.Stub {
+    private static final String TAG = NetworkObserverRegistry.class.getSimpleName();
 
     /**
      * Constructs a new NetworkObserverRegistry.
@@ -50,7 +56,7 @@
         netd.registerUnsolicitedEventListener(this);
     }
 
-    private final ConcurrentHashMap<NetworkObserver, Handler> mObservers =
+    private final ConcurrentHashMap<NetworkObserver, Optional<Handler>> mObservers =
             new ConcurrentHashMap<>();
 
     /**
@@ -58,7 +64,20 @@
      * This method may be called on any thread.
      */
     public void registerObserver(@NonNull NetworkObserver observer, @NonNull Handler handler) {
-        mObservers.put(observer, handler);
+        if (handler == null) {
+            throw new IllegalArgumentException("handler must be non-null");
+        }
+        mObservers.put(observer, Optional.of(handler));
+    }
+
+    /**
+     * Registers the specified observer, and start sending callbacks to it.
+     *
+     * <p>This method must only be called with callbacks that are nonblocking, such as callbacks
+     * that only send a message to a StateMachine.
+     */
+    public void registerObserverForNonblockingCallback(@NonNull NetworkObserver observer) {
+        mObservers.put(observer, Optional.empty());
     }
 
     /**
@@ -77,9 +96,19 @@
     private void invokeForAllObservers(@NonNull final NetworkObserverEventCallback callback) {
         // ConcurrentHashMap#entrySet is weakly consistent: observers that were in the map before
         // creation will be processed, those added during traversal may or may not.
-        for (Map.Entry<NetworkObserver, Handler> entry : mObservers.entrySet()) {
+        for (Map.Entry<NetworkObserver, Optional<Handler>> entry : mObservers.entrySet()) {
             final NetworkObserver observer = entry.getKey();
-            entry.getValue().post(() -> callback.sendCallback(observer));
+            final Optional<Handler> handler = entry.getValue();
+            if (handler.isPresent()) {
+                handler.get().post(() -> callback.sendCallback(observer));
+                return;
+            }
+
+            try {
+                callback.sendCallback(observer);
+            } catch (RuntimeException e) {
+                Log.e(TAG, "Error sending callback to observer", e);
+            }
         }
     }
 
@@ -138,10 +167,13 @@
 
     @Override
     public void onRouteChanged(boolean updated, String route, String gateway, String ifName) {
+        final RouteInfo processRoute = new RouteInfo(new IpPrefix(route),
+                ("".equals(gateway)) ? null : InetAddresses.parseNumericAddress(gateway),
+                ifName);
         if (updated) {
-            invokeForAllObservers(o -> o.onRouteUpdated(route, gateway, ifName));
+            invokeForAllObservers(o -> o.onRouteUpdated(processRoute));
         } else {
-            invokeForAllObservers(o -> o.onRouteRemoved(route, gateway, ifName));
+            invokeForAllObservers(o -> o.onRouteRemoved(processRoute));
         }
     }
 
diff --git a/packages/NetworkStack/src/com/android/server/NetworkStackService.java b/packages/NetworkStack/src/com/android/server/NetworkStackService.java
index 631ee45..7405c47 100644
--- a/packages/NetworkStack/src/com/android/server/NetworkStackService.java
+++ b/packages/NetworkStack/src/com/android/server/NetworkStackService.java
@@ -117,7 +117,11 @@
             mObserverRegistry = new NetworkObserverRegistry();
             mCm = context.getSystemService(ConnectivityManager.class);
 
-            // TODO: call mObserverRegistry here after adding sepolicy changes
+            try {
+                mObserverRegistry.register(mNetd);
+            } catch (RemoteException e) {
+                mLog.e("Error registering observer on Netd", e);
+            }
         }
 
         @NonNull
@@ -158,7 +162,7 @@
 
         @Override
         public void makeIpClient(String ifName, IIpClientCallbacks cb) throws RemoteException {
-            final IpClient ipClient = new IpClient(mContext, ifName, cb);
+            final IpClient ipClient = new IpClient(mContext, ifName, cb, mObserverRegistry);
 
             synchronized (mIpClients) {
                 final Iterator<WeakReference<IpClient>> it = mIpClients.iterator();
diff --git a/packages/NetworkStack/tests/src/android/net/ip/IpClientTest.java b/packages/NetworkStack/tests/src/android/net/ip/IpClientTest.java
index 4ae044de..7e57d1e 100644
--- a/packages/NetworkStack/tests/src/android/net/ip/IpClientTest.java
+++ b/packages/NetworkStack/tests/src/android/net/ip/IpClientTest.java
@@ -46,7 +46,6 @@
 import android.net.shared.InitialConfiguration;
 import android.net.shared.ProvisioningConfiguration;
 import android.net.util.InterfaceParams;
-import android.os.INetworkManagementService;
 import android.provider.Settings;
 import android.support.test.filters.SmallTest;
 import android.support.test.runner.AndroidJUnit4;
@@ -54,7 +53,8 @@
 
 import com.android.internal.R;
 import com.android.internal.util.test.FakeSettingsProvider;
-import com.android.server.net.BaseNetworkObserver;
+import com.android.server.NetworkObserver;
+import com.android.server.NetworkObserverRegistry;
 
 import org.junit.Before;
 import org.junit.Test;
@@ -87,15 +87,15 @@
 
     @Mock private Context mContext;
     @Mock private ConnectivityManager mCm;
-    @Mock private INetworkManagementService mNMService;
+    @Mock private NetworkObserverRegistry mObserverRegistry;
     @Mock private INetd mNetd;
     @Mock private Resources mResources;
     @Mock private IIpClientCallbacks mCb;
     @Mock private AlarmManager mAlarm;
-    @Mock private IpClient.Dependencies mDependecies;
+    @Mock private IpClient.Dependencies mDependencies;
     private MockContentResolver mContentResolver;
 
-    private BaseNetworkObserver mObserver;
+    private NetworkObserver mObserver;
     private InterfaceParams mIfParams;
 
     @Before
@@ -104,6 +104,7 @@
 
         when(mContext.getSystemService(eq(Context.ALARM_SERVICE))).thenReturn(mAlarm);
         when(mContext.getSystemService(eq(ConnectivityManager.class))).thenReturn(mCm);
+        when(mContext.getSystemService(INetd.class)).thenReturn(mNetd);
         when(mContext.getResources()).thenReturn(mResources);
         when(mResources.getInteger(R.integer.config_networkAvoidBadWifi))
                 .thenReturn(DEFAULT_AVOIDBADWIFI_CONFIG_VALUE);
@@ -113,28 +114,24 @@
         when(mContext.getContentResolver()).thenReturn(mContentResolver);
 
         mIfParams = null;
-
-        when(mDependecies.getNMS()).thenReturn(mNMService);
-        when(mDependecies.getNetd()).thenReturn(mNetd);
     }
 
     private void setTestInterfaceParams(String ifname) {
         mIfParams = (ifname != null)
                 ? new InterfaceParams(ifname, TEST_IFINDEX, TEST_MAC)
                 : null;
-        when(mDependecies.getInterfaceParams(anyString())).thenReturn(mIfParams);
+        when(mDependencies.getInterfaceParams(anyString())).thenReturn(mIfParams);
     }
 
     private IpClient makeIpClient(String ifname) throws Exception {
         setTestInterfaceParams(ifname);
-        final IpClient ipc = new IpClient(mContext, ifname, mCb, mDependecies);
+        final IpClient ipc = new IpClient(mContext, ifname, mCb, mObserverRegistry, mDependencies);
         verify(mNetd, timeout(TEST_TIMEOUT_MS).times(1)).interfaceSetEnableIPv6(ifname, false);
         verify(mNetd, timeout(TEST_TIMEOUT_MS).times(1)).interfaceClearAddrs(ifname);
-        ArgumentCaptor<BaseNetworkObserver> arg =
-                ArgumentCaptor.forClass(BaseNetworkObserver.class);
-        verify(mNMService, times(1)).registerObserver(arg.capture());
+        ArgumentCaptor<NetworkObserver> arg = ArgumentCaptor.forClass(NetworkObserver.class);
+        verify(mObserverRegistry, times(1)).registerObserverForNonblockingCallback(arg.capture());
         mObserver = arg.getValue();
-        reset(mNMService);
+        reset(mObserverRegistry);
         reset(mNetd);
         // Verify IpClient doesn't call onLinkPropertiesChange() when it starts.
         verify(mCb, never()).onLinkPropertiesChange(any());
@@ -152,7 +149,8 @@
     public void testNullInterfaceNameMostDefinitelyThrows() throws Exception {
         setTestInterfaceParams(null);
         try {
-            final IpClient ipc = new IpClient(mContext, null, mCb, mDependecies);
+            final IpClient ipc = new IpClient(
+                    mContext, null, mCb, mObserverRegistry, mDependencies);
             ipc.shutdown();
             fail();
         } catch (NullPointerException npe) {
@@ -165,7 +163,8 @@
         final String ifname = "lo";
         setTestInterfaceParams(ifname);
         try {
-            final IpClient ipc = new IpClient(mContext, ifname, null, mDependecies);
+            final IpClient ipc = new IpClient(
+                    mContext, ifname, null, mObserverRegistry, mDependencies);
             ipc.shutdown();
             fail();
         } catch (NullPointerException npe) {
@@ -176,14 +175,16 @@
     @Test
     public void testInvalidInterfaceDoesNotThrow() throws Exception {
         setTestInterfaceParams(TEST_IFNAME);
-        final IpClient ipc = new IpClient(mContext, TEST_IFNAME, mCb, mDependecies);
+        final IpClient ipc = new IpClient(
+                mContext, TEST_IFNAME, mCb, mObserverRegistry, mDependencies);
         ipc.shutdown();
     }
 
     @Test
     public void testInterfaceNotFoundFailsImmediately() throws Exception {
         setTestInterfaceParams(null);
-        final IpClient ipc = new IpClient(mContext, TEST_IFNAME, mCb, mDependecies);
+        final IpClient ipc = new IpClient(
+                mContext, TEST_IFNAME, mCb, mObserverRegistry, mDependencies);
         ipc.startProvisioning(new ProvisioningConfiguration());
         verify(mCb, times(1)).onProvisioningFailure(any());
         ipc.shutdown();
@@ -247,13 +248,13 @@
 
         // Add N - 1 addresses
         for (int i = 0; i < lastAddr; i++) {
-            mObserver.addressUpdated(iface, new LinkAddress(addresses[i]));
+            mObserver.onInterfaceAddressUpdated(new LinkAddress(addresses[i]), iface);
             verify(mCb, timeout(TEST_TIMEOUT_MS)).onLinkPropertiesChange(any());
             reset(mCb);
         }
 
         // Add Nth address
-        mObserver.addressUpdated(iface, new LinkAddress(addresses[lastAddr]));
+        mObserver.onInterfaceAddressUpdated(new LinkAddress(addresses[lastAddr]), iface);
         LinkProperties want = linkproperties(links(addresses), routes(prefixes));
         want.setInterfaceName(iface);
         verify(mCb, timeout(TEST_TIMEOUT_MS).times(1)).onProvisioningSuccess(argThat(
diff --git a/services/core/java/com/android/server/NetworkManagementService.java b/services/core/java/com/android/server/NetworkManagementService.java
index 2f66fd7..8592491 100644
--- a/services/core/java/com/android/server/NetworkManagementService.java
+++ b/services/core/java/com/android/server/NetworkManagementService.java
@@ -47,8 +47,10 @@
 import android.content.Context;
 import android.net.ConnectivityManager;
 import android.net.INetd;
+import android.net.INetdUnsolicitedEventListener;
 import android.net.INetworkManagementEventObserver;
 import android.net.ITetheringStatsProvider;
+import android.net.InetAddresses;
 import android.net.InterfaceConfiguration;
 import android.net.InterfaceConfigurationParcel;
 import android.net.IpPrefix;
@@ -61,7 +63,6 @@
 import android.net.TetherStatsParcel;
 import android.net.UidRange;
 import android.net.shared.NetdService;
-import android.net.shared.NetworkObserverRegistry;
 import android.os.BatteryStats;
 import android.os.Binder;
 import android.os.Handler;
@@ -205,13 +206,16 @@
 
     private INetd mNetdService;
 
-    private NMSNetworkObserverRegistry mNetworkObserverRegistry;
+    private final NetdUnsolicitedEventListener mNetdUnsolicitedEventListener;
 
     private IBatteryStats mBatteryStats;
 
     private final Thread mThread;
     private CountDownLatch mConnectedSignal = new CountDownLatch(1);
 
+    private final RemoteCallbackList<INetworkManagementEventObserver> mObservers =
+            new RemoteCallbackList<>();
+
     private final NetworkStatsFactory mStatsFactory = new NetworkStatsFactory();
 
     @GuardedBy("mTetheringStatsProviders")
@@ -321,6 +325,8 @@
 
         mDaemonHandler = new Handler(FgThread.get().getLooper());
 
+        mNetdUnsolicitedEventListener = new NetdUnsolicitedEventListener();
+
         // Add ourself to the Watchdog monitors.
         Watchdog.getInstance().addMonitor(this);
 
@@ -339,7 +345,7 @@
         mFgHandler = null;
         mThread = null;
         mServices = null;
-        mNetworkObserverRegistry = null;
+        mNetdUnsolicitedEventListener = null;
     }
 
     static NetworkManagementService create(Context context, String socket, SystemServices services)
@@ -387,12 +393,14 @@
 
     @Override
     public void registerObserver(INetworkManagementEventObserver observer) {
-        mNetworkObserverRegistry.registerObserver(observer);
+        mContext.enforceCallingOrSelfPermission(CONNECTIVITY_INTERNAL, TAG);
+        mObservers.register(observer);
     }
 
     @Override
     public void unregisterObserver(INetworkManagementEventObserver observer) {
-        mNetworkObserverRegistry.unregisterObserver(observer);
+        mContext.enforceCallingOrSelfPermission(CONNECTIVITY_INTERNAL, TAG);
+        mObservers.unregister(observer);
     }
 
     @FunctionalInterface
@@ -400,97 +408,123 @@
         public void sendCallback(INetworkManagementEventObserver o) throws RemoteException;
     }
 
-    private class NMSNetworkObserverRegistry extends NetworkObserverRegistry {
-        NMSNetworkObserverRegistry(Context context, Handler handler, INetd netd)
-                throws RemoteException {
-            super(context, handler, netd);
+    private void invokeForAllObservers(NetworkManagementEventCallback eventCallback) {
+        final int length = mObservers.beginBroadcast();
+        try {
+            for (int i = 0; i < length; i++) {
+                try {
+                    eventCallback.sendCallback(mObservers.getBroadcastItem(i));
+                } catch (RemoteException | RuntimeException e) {
+                }
+            }
+        } finally {
+            mObservers.finishBroadcast();
         }
+    }
 
-        /**
-         * Notify our observers of a change in the data activity state of the interface
-         */
-        @Override
-        public void notifyInterfaceClassActivity(int type, boolean isActive, long tsNanos,
-                int uid, boolean fromRadio) {
-            final boolean isMobile = ConnectivityManager.isNetworkTypeMobile(type);
-            int powerState = isActive
-                    ? DataConnectionRealTimeInfo.DC_POWER_STATE_HIGH
-                    : DataConnectionRealTimeInfo.DC_POWER_STATE_LOW;
+    /**
+     * Notify our observers of an interface status change
+     */
+    private void notifyInterfaceStatusChanged(String iface, boolean up) {
+        invokeForAllObservers(o -> o.interfaceStatusChanged(iface, up));
+    }
 
-            if (isMobile) {
-                if (!fromRadio) {
-                    if (mMobileActivityFromRadio) {
-                        // If this call is not coming from a report from the radio itself, but we
-                        // have previously received reports from the radio, then we will take the
-                        // power state to just be whatever the radio last reported.
-                        powerState = mLastPowerStateFromRadio;
-                    }
-                } else {
-                    mMobileActivityFromRadio = true;
+    /**
+     * Notify our observers of an interface link state change
+     * (typically, an Ethernet cable has been plugged-in or unplugged).
+     */
+    private void notifyInterfaceLinkStateChanged(String iface, boolean up) {
+        invokeForAllObservers(o -> o.interfaceLinkStateChanged(iface, up));
+    }
+
+    /**
+     * Notify our observers of an interface addition.
+     */
+    private void notifyInterfaceAdded(String iface) {
+        invokeForAllObservers(o -> o.interfaceAdded(iface));
+    }
+
+    /**
+     * Notify our observers of an interface removal.
+     */
+    private void notifyInterfaceRemoved(String iface) {
+        // netd already clears out quota and alerts for removed ifaces; update
+        // our sanity-checking state.
+        mActiveAlerts.remove(iface);
+        mActiveQuotas.remove(iface);
+        invokeForAllObservers(o -> o.interfaceRemoved(iface));
+    }
+
+    /**
+     * Notify our observers of a limit reached.
+     */
+    private void notifyLimitReached(String limitName, String iface) {
+        invokeForAllObservers(o -> o.limitReached(limitName, iface));
+    }
+
+    /**
+     * Notify our observers of a change in the data activity state of the interface
+     */
+    private void notifyInterfaceClassActivity(int type, boolean isActive, long tsNanos,
+            int uid, boolean fromRadio) {
+        final boolean isMobile = ConnectivityManager.isNetworkTypeMobile(type);
+        int powerState = isActive
+                ? DataConnectionRealTimeInfo.DC_POWER_STATE_HIGH
+                : DataConnectionRealTimeInfo.DC_POWER_STATE_LOW;
+        if (isMobile) {
+            if (!fromRadio) {
+                if (mMobileActivityFromRadio) {
+                    // If this call is not coming from a report from the radio itself, but we
+                    // have previously received reports from the radio, then we will take the
+                    // power state to just be whatever the radio last reported.
+                    powerState = mLastPowerStateFromRadio;
                 }
-                if (mLastPowerStateFromRadio != powerState) {
-                    mLastPowerStateFromRadio = powerState;
-                    try {
-                        getBatteryStats().noteMobileRadioPowerState(powerState, tsNanos, uid);
-                    } catch (RemoteException e) {
-                    }
-                }
+            } else {
+                mMobileActivityFromRadio = true;
             }
-
-            if (ConnectivityManager.isNetworkTypeWifi(type)) {
-                if (mLastPowerStateFromWifi != powerState) {
-                    mLastPowerStateFromWifi = powerState;
-                    try {
-                        getBatteryStats().noteWifiRadioPowerState(powerState, tsNanos, uid);
-                    } catch (RemoteException e) {
-                    }
+            if (mLastPowerStateFromRadio != powerState) {
+                mLastPowerStateFromRadio = powerState;
+                try {
+                    getBatteryStats().noteMobileRadioPowerState(powerState, tsNanos, uid);
+                } catch (RemoteException e) {
                 }
             }
-
-            if (!isMobile || fromRadio || !mMobileActivityFromRadio) {
-                // Report the change in data activity.  We don't do this if this is a change
-                // on the mobile network, that is not coming from the radio itself, and we
-                // have previously seen change reports from the radio.  In that case only
-                // the radio is the authority for the current state.
-                final boolean active = isActive;
-                super.notifyInterfaceClassActivity(type, isActive, tsNanos, uid, fromRadio);
-            }
-
-            boolean report = false;
-            synchronized (mIdleTimerLock) {
-                if (mActiveIdleTimers.isEmpty()) {
-                    // If there are no idle timers, we are not monitoring activity, so we
-                    // are always considered active.
-                    isActive = true;
-                }
-                if (mNetworkActive != isActive) {
-                    mNetworkActive = isActive;
-                    report = isActive;
-                }
-            }
-            if (report) {
-                reportNetworkActive();
-            }
         }
 
-        /**
-         * Notify our observers of an interface removal.
-         */
-        @Override
-        public void notifyInterfaceRemoved(String iface) {
-            // netd already clears out quota and alerts for removed ifaces; update
-            // our sanity-checking state.
-            mActiveAlerts.remove(iface);
-            mActiveQuotas.remove(iface);
-            super.notifyInterfaceRemoved(iface);
+        if (ConnectivityManager.isNetworkTypeWifi(type)) {
+            if (mLastPowerStateFromWifi != powerState) {
+                mLastPowerStateFromWifi = powerState;
+                try {
+                    getBatteryStats().noteWifiRadioPowerState(powerState, tsNanos, uid);
+                } catch (RemoteException e) {
+                }
+            }
         }
 
-        @Override
-        public void onStrictCleartextDetected(int uid, String hex) throws RemoteException {
-            // Don't need to post to mDaemonHandler because the only thing
-            // that notifyCleartextNetwork does is post to a handler
-            ActivityManager.getService().notifyCleartextNetwork(uid,
-                    HexDump.hexStringToByteArray(hex));
+        if (!isMobile || fromRadio || !mMobileActivityFromRadio) {
+            // Report the change in data activity.  We don't do this if this is a change
+            // on the mobile network, that is not coming from the radio itself, and we
+            // have previously seen change reports from the radio.  In that case only
+            // the radio is the authority for the current state.
+            final boolean active = isActive;
+            invokeForAllObservers(o -> o.interfaceClassDataActivityChanged(
+                    Integer.toString(type), active, tsNanos));
+        }
+
+        boolean report = false;
+        synchronized (mIdleTimerLock) {
+            if (mActiveIdleTimers.isEmpty()) {
+                // If there are no idle timers, we are not monitoring activity, so we
+                // are always considered active.
+                isActive = true;
+            }
+            if (mNetworkActive != isActive) {
+                mNetworkActive = isActive;
+                report = isActive;
+            }
+        }
+        if (report) {
+            reportNetworkActive();
         }
     }
 
@@ -519,8 +553,7 @@
                 return;
             }
             // No current code examines the interface parameter in a global alert. Just pass null.
-            mDaemonHandler.post(() -> mNetworkObserverRegistry.notifyLimitReached(
-                    LIMIT_GLOBAL_ALERT, null));
+            mDaemonHandler.post(() -> notifyLimitReached(LIMIT_GLOBAL_ALERT, null));
         }
     }
 
@@ -552,11 +585,10 @@
     private void connectNativeNetdService() {
         mNetdService = mServices.getNetd();
         try {
-            mNetworkObserverRegistry = new NMSNetworkObserverRegistry(
-                    mContext, mDaemonHandler, mNetdService);
-            if (DBG) Slog.d(TAG, "Registered NetworkObserverRegistry");
+            mNetdService.registerUnsolicitedEventListener(mNetdUnsolicitedEventListener);
+            if (DBG) Slog.d(TAG, "Register unsolicited event listener");
         } catch (RemoteException | ServiceSpecificException e) {
-            Slog.wtf(TAG, "Failed to register NetworkObserverRegistry: " + e);
+            Slog.e(TAG, "Failed to set Netd unsolicited event listener " + e);
         }
     }
 
@@ -660,6 +692,118 @@
 
     }
 
+    /**
+     * Notify our observers of a new or updated interface address.
+     */
+    private void notifyAddressUpdated(String iface, LinkAddress address) {
+        invokeForAllObservers(o -> o.addressUpdated(iface, address));
+    }
+
+    /**
+     * Notify our observers of a deleted interface address.
+     */
+    private void notifyAddressRemoved(String iface, LinkAddress address) {
+        invokeForAllObservers(o -> o.addressRemoved(iface, address));
+    }
+
+    /**
+     * Notify our observers of DNS server information received.
+     */
+    private void notifyInterfaceDnsServerInfo(String iface, long lifetime, String[] addresses) {
+        invokeForAllObservers(o -> o.interfaceDnsServerInfo(iface, lifetime, addresses));
+    }
+
+    /**
+     * Notify our observers of a route change.
+     */
+    private void notifyRouteChange(boolean updated, RouteInfo route) {
+        if (updated) {
+            invokeForAllObservers(o -> o.routeUpdated(route));
+        } else {
+            invokeForAllObservers(o -> o.routeRemoved(route));
+        }
+    }
+
+    private class NetdUnsolicitedEventListener extends INetdUnsolicitedEventListener.Stub {
+        @Override
+        public void onInterfaceClassActivityChanged(boolean isActive,
+                int label, long timestamp, int uid) throws RemoteException {
+            final long timestampNanos;
+            if (timestamp <= 0) {
+                timestampNanos = SystemClock.elapsedRealtimeNanos();
+            } else {
+                timestampNanos = timestamp;
+            }
+            mDaemonHandler.post(() ->
+                    notifyInterfaceClassActivity(label, isActive, timestampNanos, uid, false));
+        }
+
+        @Override
+        public void onQuotaLimitReached(String alertName, String ifName)
+                throws RemoteException {
+            mDaemonHandler.post(() -> notifyLimitReached(alertName, ifName));
+        }
+
+        @Override
+        public void onInterfaceDnsServerInfo(String ifName,
+                long lifetime, String[] servers) throws RemoteException {
+            mDaemonHandler.post(() -> notifyInterfaceDnsServerInfo(ifName, lifetime, servers));
+        }
+
+        @Override
+        public void onInterfaceAddressUpdated(String addr,
+                String ifName, int flags, int scope) throws RemoteException {
+            final LinkAddress address = new LinkAddress(addr, flags, scope);
+            mDaemonHandler.post(() -> notifyAddressUpdated(ifName, address));
+        }
+
+        @Override
+        public void onInterfaceAddressRemoved(String addr,
+                String ifName, int flags, int scope) throws RemoteException {
+            final LinkAddress address = new LinkAddress(addr, flags, scope);
+            mDaemonHandler.post(() -> notifyAddressRemoved(ifName, address));
+        }
+
+        @Override
+        public void onInterfaceAdded(String ifName) throws RemoteException {
+            mDaemonHandler.post(() -> notifyInterfaceAdded(ifName));
+        }
+
+        @Override
+        public void onInterfaceRemoved(String ifName) throws RemoteException {
+            mDaemonHandler.post(() -> notifyInterfaceRemoved(ifName));
+        }
+
+        @Override
+        public void onInterfaceChanged(String ifName, boolean up)
+                throws RemoteException {
+            mDaemonHandler.post(() -> notifyInterfaceStatusChanged(ifName, up));
+        }
+
+        @Override
+        public void onInterfaceLinkStateChanged(String ifName, boolean up)
+                throws RemoteException {
+            mDaemonHandler.post(() -> notifyInterfaceLinkStateChanged(ifName, up));
+        }
+
+        @Override
+        public void onRouteChanged(boolean updated,
+                String route, String gateway, String ifName) throws RemoteException {
+            final RouteInfo processRoute = new RouteInfo(new IpPrefix(route),
+                    ("".equals(gateway)) ? null : InetAddresses.parseNumericAddress(gateway),
+                    ifName);
+            mDaemonHandler.post(() -> notifyRouteChange(updated, processRoute));
+        }
+
+        @Override
+        public void onStrictCleartextDetected(int uid, String hex) throws RemoteException {
+            // Don't need to post to mDaemonHandler because the only thing
+            // that notifyCleartextNetwork does is post to a handler
+            ActivityManager.getService().notifyCleartextNetwork(uid,
+                    HexDump.hexStringToByteArray(hex));
+        }
+    }
+
     //
     // Netd Callback handling
     //
@@ -708,18 +852,16 @@
                         throw new IllegalStateException(errorMessage);
                     }
                     if (cooked[2].equals("added")) {
-                        mNetworkObserverRegistry.notifyInterfaceAdded(cooked[3]);
+                        notifyInterfaceAdded(cooked[3]);
                         return true;
                     } else if (cooked[2].equals("removed")) {
-                        mNetworkObserverRegistry.notifyInterfaceRemoved(cooked[3]);
+                        notifyInterfaceRemoved(cooked[3]);
                         return true;
                     } else if (cooked[2].equals("changed") && cooked.length == 5) {
-                        mNetworkObserverRegistry.notifyInterfaceStatusChanged(
-                                cooked[3], cooked[4].equals("up"));
+                        notifyInterfaceStatusChanged(cooked[3], cooked[4].equals("up"));
                         return true;
                     } else if (cooked[2].equals("linkstate") && cooked.length == 5) {
-                        mNetworkObserverRegistry.notifyInterfaceLinkStateChanged(
-                                cooked[3], cooked[4].equals("up"));
+                        notifyInterfaceLinkStateChanged(cooked[3], cooked[4].equals("up"));
                         return true;
                     }
                     throw new IllegalStateException(errorMessage);
@@ -733,7 +875,7 @@
                         throw new IllegalStateException(errorMessage);
                     }
                     if (cooked[2].equals("alert")) {
-                        mNetworkObserverRegistry.notifyLimitReached(cooked[3], cooked[4]);
+                        notifyLimitReached(cooked[3], cooked[4]);
                         return true;
                     }
                     throw new IllegalStateException(errorMessage);
@@ -759,9 +901,8 @@
                         timestampNanos = SystemClock.elapsedRealtimeNanos();
                     }
                     boolean isActive = cooked[2].equals("active");
-                    mNetworkObserverRegistry.notifyInterfaceClassActivity(
-                            Integer.parseInt(cooked[3]), isActive,
-                            timestampNanos, processUid, false);
+                    notifyInterfaceClassActivity(Integer.parseInt(cooked[3]),
+                            isActive, timestampNanos, processUid, false);
                     return true;
                     // break;
             case NetdResponseCode.InterfaceAddressChange:
@@ -787,9 +928,9 @@
                     }
 
                     if (cooked[2].equals("updated")) {
-                        mNetworkObserverRegistry.notifyAddressUpdated(iface, address);
+                        notifyAddressUpdated(iface, address);
                     } else {
-                        mNetworkObserverRegistry.notifyAddressRemoved(iface, address);
+                        notifyAddressRemoved(iface, address);
                     }
                     return true;
                     // break;
@@ -809,8 +950,7 @@
                             throw new IllegalStateException(errorMessage);
                         }
                         String[] servers = cooked[5].split(",");
-                        mNetworkObserverRegistry.notifyInterfaceDnsServerInfo(
-                                cooked[3], lifetime, servers);
+                        notifyInterfaceDnsServerInfo(cooked[3], lifetime, servers);
                     }
                     return true;
                     // break;
@@ -849,8 +989,7 @@
                             InetAddress gateway = null;
                             if (via != null) gateway = InetAddress.parseNumericAddress(via);
                             RouteInfo route = new RouteInfo(new IpPrefix(cooked[3]), gateway, dev);
-                            mNetworkObserverRegistry.notifyRouteChange(
-                                    cooked[2].equals("updated"), route);
+                            notifyRouteChange(cooked[2].equals("updated"), route);
                             return true;
                         } catch (IllegalArgumentException e) {}
                     }
@@ -1313,8 +1452,8 @@
             if (ConnectivityManager.isNetworkTypeMobile(type)) {
                 mNetworkActive = false;
             }
-            mDaemonHandler.post(() -> mNetworkObserverRegistry.notifyInterfaceClassActivity(
-                    type, true /* isActive */, SystemClock.elapsedRealtimeNanos(), -1, false));
+            mDaemonHandler.post(() -> notifyInterfaceClassActivity(type, true,
+                    SystemClock.elapsedRealtimeNanos(), -1, false));
         }
     }
 
@@ -1337,9 +1476,8 @@
                 throw new IllegalStateException(e);
             }
             mActiveIdleTimers.remove(iface);
-            mDaemonHandler.post(() -> mNetworkObserverRegistry.notifyInterfaceClassActivity(
-                    params.type, false /* isActive */, SystemClock.elapsedRealtimeNanos(), -1,
-                    false));
+            mDaemonHandler.post(() -> notifyInterfaceClassActivity(params.type, false,
+                    SystemClock.elapsedRealtimeNanos(), -1, false));
         }
     }
 
diff --git a/services/net/java/android/net/shared/NetworkObserverRegistry.java b/services/net/java/android/net/shared/NetworkObserverRegistry.java
deleted file mode 100644
index 36945f5..0000000
--- a/services/net/java/android/net/shared/NetworkObserverRegistry.java
+++ /dev/null
@@ -1,255 +0,0 @@
-/*
- * Copyright (C) 2019 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.net.shared;
-
-import static android.Manifest.permission.NETWORK_STACK;
-
-import android.content.Context;
-import android.net.INetd;
-import android.net.INetdUnsolicitedEventListener;
-import android.net.INetworkManagementEventObserver;
-import android.net.InetAddresses;
-import android.net.IpPrefix;
-import android.net.LinkAddress;
-import android.net.RouteInfo;
-import android.os.Handler;
-import android.os.RemoteCallbackList;
-import android.os.RemoteException;
-import android.os.SystemClock;
-
-/**
- * A class for reporting network events to clients.
- *
- * Implements INetdUnsolicitedEventListener and registers with netd, and relays those events to
- * all INetworkManagementEventObserver objects that have registered with it.
- *
- * TODO: Make the notifyXyz methods protected once subclasses (e.g., the NetworkManagementService
- * subclass) no longer call them directly.
- *
- * TODO: change from RemoteCallbackList to direct in-process callbacks.
- */
-public class NetworkObserverRegistry extends INetdUnsolicitedEventListener.Stub {
-
-    private final Context mContext;
-    private final Handler mDaemonHandler;
-    private static final String TAG = "NetworkObserverRegistry";
-
-    /**
-     * Constructs a new instance and registers it with netd.
-     * This method should only be called once since netd will reject multiple registrations from
-     * the same process.
-     */
-    public NetworkObserverRegistry(Context context, Handler handler, INetd netd)
-            throws RemoteException {
-        mContext = context;
-        mDaemonHandler = handler;
-        netd.registerUnsolicitedEventListener(this);
-    }
-
-    private final RemoteCallbackList<INetworkManagementEventObserver> mObservers =
-            new RemoteCallbackList<>();
-
-    /**
-     * Registers the specified observer and start sending callbacks to it.
-     * This method may be called on any thread.
-     */
-    public void registerObserver(INetworkManagementEventObserver observer) {
-        mContext.enforceCallingOrSelfPermission(NETWORK_STACK, TAG);
-        mObservers.register(observer);
-    }
-
-    /**
-     * Unregisters the specified observer and stop sending callbacks to it.
-     * This method may be called on any thread.
-     */
-    public void unregisterObserver(INetworkManagementEventObserver observer) {
-        mContext.enforceCallingOrSelfPermission(NETWORK_STACK, TAG);
-        mObservers.unregister(observer);
-    }
-
-    @FunctionalInterface
-    private interface NetworkManagementEventCallback {
-        void sendCallback(INetworkManagementEventObserver o) throws RemoteException;
-    }
-
-    private void invokeForAllObservers(NetworkManagementEventCallback eventCallback) {
-        final int length = mObservers.beginBroadcast();
-        try {
-            for (int i = 0; i < length; i++) {
-                try {
-                    eventCallback.sendCallback(mObservers.getBroadcastItem(i));
-                } catch (RemoteException | RuntimeException e) {
-                }
-            }
-        } finally {
-            mObservers.finishBroadcast();
-        }
-    }
-
-    /**
-     * Notify our observers of a change in the data activity state of the interface
-     */
-    public void notifyInterfaceClassActivity(int type, boolean isActive, long tsNanos,
-            int uid, boolean fromRadio) {
-        invokeForAllObservers(o -> o.interfaceClassDataActivityChanged(
-                Integer.toString(type), isActive, tsNanos));
-    }
-
-    @Override
-    public void onInterfaceClassActivityChanged(boolean isActive,
-            int label, long timestamp, int uid) throws RemoteException {
-        final long timestampNanos;
-        if (timestamp <= 0) {
-            timestampNanos = SystemClock.elapsedRealtimeNanos();
-        } else {
-            timestampNanos = timestamp;
-        }
-        mDaemonHandler.post(() -> notifyInterfaceClassActivity(label, isActive,
-                timestampNanos, uid, false));
-    }
-
-    /**
-     * Notify our observers of a limit reached.
-     */
-    @Override
-    public void onQuotaLimitReached(String alertName, String ifName) throws RemoteException {
-        mDaemonHandler.post(() -> notifyLimitReached(alertName, ifName));
-    }
-
-    /**
-     * Notify our observers of a limit reached.
-     */
-    public void notifyLimitReached(String limitName, String iface) {
-        invokeForAllObservers(o -> o.limitReached(limitName, iface));
-    }
-
-    @Override
-    public void onInterfaceDnsServerInfo(String ifName,
-            long lifetime, String[] servers) throws RemoteException {
-        mDaemonHandler.post(() -> notifyInterfaceDnsServerInfo(ifName, lifetime, servers));
-    }
-
-    /**
-     * Notify our observers of DNS server information received.
-     */
-    public void notifyInterfaceDnsServerInfo(String iface, long lifetime, String[] addresses) {
-        invokeForAllObservers(o -> o.interfaceDnsServerInfo(iface, lifetime, addresses));
-    }
-
-    @Override
-    public void onInterfaceAddressUpdated(String addr,
-            String ifName, int flags, int scope) throws RemoteException {
-        final LinkAddress address = new LinkAddress(addr, flags, scope);
-        mDaemonHandler.post(() -> notifyAddressUpdated(ifName, address));
-    }
-
-    /**
-     * Notify our observers of a new or updated interface address.
-     */
-    public void notifyAddressUpdated(String iface, LinkAddress address) {
-        invokeForAllObservers(o -> o.addressUpdated(iface, address));
-    }
-
-    @Override
-    public void onInterfaceAddressRemoved(String addr,
-            String ifName, int flags, int scope) throws RemoteException {
-        final LinkAddress address = new LinkAddress(addr, flags, scope);
-        mDaemonHandler.post(() -> notifyAddressRemoved(ifName, address));
-    }
-
-    /**
-     * Notify our observers of a deleted interface address.
-     */
-    public void notifyAddressRemoved(String iface, LinkAddress address) {
-        invokeForAllObservers(o -> o.addressRemoved(iface, address));
-    }
-
-
-    @Override
-    public void onInterfaceAdded(String ifName) throws RemoteException {
-        mDaemonHandler.post(() -> notifyInterfaceAdded(ifName));
-    }
-
-    /**
-     * Notify our observers of an interface addition.
-     */
-    public void notifyInterfaceAdded(String iface) {
-        invokeForAllObservers(o -> o.interfaceAdded(iface));
-    }
-
-    @Override
-    public void onInterfaceRemoved(String ifName) throws RemoteException {
-        mDaemonHandler.post(() -> notifyInterfaceRemoved(ifName));
-    }
-
-    /**
-     * Notify our observers of an interface removal.
-     */
-    public void notifyInterfaceRemoved(String iface) {
-        invokeForAllObservers(o -> o.interfaceRemoved(iface));
-    }
-
-    @Override
-    public void onInterfaceChanged(String ifName, boolean up) throws RemoteException {
-        mDaemonHandler.post(() -> notifyInterfaceStatusChanged(ifName, up));
-    }
-
-    /**
-     * Notify our observers of an interface status change
-     */
-    public void notifyInterfaceStatusChanged(String iface, boolean up) {
-        invokeForAllObservers(o -> o.interfaceStatusChanged(iface, up));
-    }
-
-    @Override
-    public void onInterfaceLinkStateChanged(String ifName, boolean up) throws RemoteException {
-        mDaemonHandler.post(() -> notifyInterfaceLinkStateChanged(ifName, up));
-    }
-
-    /**
-     * Notify our observers of an interface link state change
-     * (typically, an Ethernet cable has been plugged-in or unplugged).
-     */
-    public void notifyInterfaceLinkStateChanged(String iface, boolean up) {
-        invokeForAllObservers(o -> o.interfaceLinkStateChanged(iface, up));
-    }
-
-    @Override
-    public void onRouteChanged(boolean updated,
-            String route, String gateway, String ifName) throws RemoteException {
-        final RouteInfo processRoute = new RouteInfo(new IpPrefix(route),
-                ("".equals(gateway)) ? null : InetAddresses.parseNumericAddress(gateway),
-                ifName);
-        mDaemonHandler.post(() -> notifyRouteChange(updated, processRoute));
-    }
-
-    /**
-     * Notify our observers of a route change.
-     */
-    public void notifyRouteChange(boolean updated, RouteInfo route) {
-        if (updated) {
-            invokeForAllObservers(o -> o.routeUpdated(route));
-        } else {
-            invokeForAllObservers(o -> o.routeRemoved(route));
-        }
-    }
-
-    @Override
-    public void onStrictCleartextDetected(int uid, String hex) throws RemoteException {
-        // Don't do anything here because this is not a method of INetworkManagementEventObserver.
-        // Only the NMS subclass will implement this.
-    }
-}
diff --git a/telephony/java/android/telephony/CarrierConfigManager.java b/telephony/java/android/telephony/CarrierConfigManager.java
index 26cba77..190e82b 100644
--- a/telephony/java/android/telephony/CarrierConfigManager.java
+++ b/telephony/java/android/telephony/CarrierConfigManager.java
@@ -2371,33 +2371,54 @@
             "support_emergency_dialer_shortcut_bool";
 
     /**
-     * Controls RSRP threshold at which AlternativeNetworkService will decide whether
+     * Controls RSRP threshold at which OpportunisticNetworkService will decide whether
      * the opportunistic network is good enough for internet data.
      */
     public static final String KEY_OPPORTUNISTIC_NETWORK_ENTRY_THRESHOLD_RSRP_INT =
             "opportunistic_network_entry_threshold_rsrp_int";
 
     /**
-     * Controls RSSNR threshold at which AlternativeNetworkService will decide whether
+     * Controls RSSNR threshold at which OpportunisticNetworkService will decide whether
      * the opportunistic network is good enough for internet data.
      */
     public static final String KEY_OPPORTUNISTIC_NETWORK_ENTRY_THRESHOLD_RSSNR_INT =
             "opportunistic_network_entry_threshold_rssnr_int";
 
     /**
-     * Controls RSRP threshold below which AlternativeNetworkService will decide whether
+     * Controls RSRP threshold below which OpportunisticNetworkService will decide whether
      * the opportunistic network available is not good enough for internet data.
      */
     public static final String KEY_OPPORTUNISTIC_NETWORK_EXIT_THRESHOLD_RSRP_INT =
             "opportunistic_network_exit_threshold_rsrp_int";
 
     /**
-     * Controls RSSNR threshold below which AlternativeNetworkService will decide whether
+     * Controls RSSNR threshold below which OpportunisticNetworkService will decide whether
      * the opportunistic network available is not good enough for internet data.
      */
     public static final String KEY_OPPORTUNISTIC_NETWORK_EXIT_THRESHOLD_RSSNR_INT =
             "opportunistic_network_exit_threshold_rssnr_int";
 
+    /**
+     * Controls bandwidth threshold in Kbps at which OpportunisticNetworkService will decide whether
+     * the opportunistic network is good enough for internet data.
+     */
+    public static final String KEY_OPPORTUNISTIC_NETWORK_ENTRY_THRESHOLD_BANDWIDTH_INT =
+            "opportunistic_network_entry_threshold_bandwidth_int";
+
+    /**
+     * Controls hysteresis time in milli seconds for which OpportunisticNetworkService
+     * will wait before attaching to a network.
+     */
+    public static final String KEY_OPPORTUNISTIC_NETWORK_ENTRY_OR_EXIT_HYSTERESIS_TIME_LONG =
+            "opportunistic_network_entry_or_exit_hysteresis_time_long";
+
+    /**
+     * Controls hysteresis time in milli seconds for which OpportunisticNetworkService
+     * will wait before switching data to a network.
+     */
+    public static final String KEY_OPPORTUNISTIC_NETWORK_DATA_SWITCH_HYSTERESIS_TIME_LONG =
+            "opportunistic_network_data_switch_hysteresis_time_long";
+
     /** The default value for every variable. */
     private final static PersistableBundle sDefaults;
 
@@ -2767,6 +2788,12 @@
         sDefaults.putInt(KEY_OPPORTUNISTIC_NETWORK_ENTRY_THRESHOLD_RSSNR_INT, 45);
         /* Default value is minimum RSSNR level needed for SIGNAL_STRENGTH_MODERATE */
         sDefaults.putInt(KEY_OPPORTUNISTIC_NETWORK_EXIT_THRESHOLD_RSSNR_INT, 10);
+        /* Default value is 1024 kbps */
+        sDefaults.putInt(KEY_OPPORTUNISTIC_NETWORK_ENTRY_THRESHOLD_BANDWIDTH_INT, 1024);
+        /* Default value is 10 seconds */
+        sDefaults.putInt(KEY_OPPORTUNISTIC_NETWORK_ENTRY_OR_EXIT_HYSTERESIS_TIME_LONG, 10000);
+        /* Default value is 10 seconds. */
+        sDefaults.putInt(KEY_OPPORTUNISTIC_NETWORK_DATA_SWITCH_HYSTERESIS_TIME_LONG, 10000);
     }
 
     /**
diff --git a/telephony/java/android/telephony/SubscriptionInfo.java b/telephony/java/android/telephony/SubscriptionInfo.java
index 4e4ef4d..443f908 100644
--- a/telephony/java/android/telephony/SubscriptionInfo.java
+++ b/telephony/java/android/telephony/SubscriptionInfo.java
@@ -577,10 +577,10 @@
     }
 
     /**
-     * @return the cardId of the SIM card which contains the subscription.
-     * @hide
+     * Returns the card ID of the SIM card which contains the subscription (see
+     * {@link UiccCardInfo#getCardId()}.
+     * @return the cardId
      */
-    @SystemApi
     public int getCardId() {
         return this.mCardId;
     }
diff --git a/telephony/java/android/telephony/TelephonyManager.java b/telephony/java/android/telephony/TelephonyManager.java
index 2461aad..9a279d3 100644
--- a/telephony/java/android/telephony/TelephonyManager.java
+++ b/telephony/java/android/telephony/TelephonyManager.java
@@ -227,10 +227,9 @@
     public static final int SRVCC_STATE_HANDOVER_CANCELED  = 3;
 
     /**
-     * An invalid card identifier.
-     * @hide
+     * An invalid UICC card identifier. See {@link #getCardIdForDefaultEuicc()} and
+     * {@link UiccCardInfo#getCardId()}.
      */
-    @SystemApi
     public static final int INVALID_CARD_ID = -1;
 
     /** @hide */
@@ -3118,14 +3117,8 @@
      * unique to a device, and always refer to the same UICC or eUICC card unless the device goes
      * through a factory reset.
      *
-     * <p>Requires Permission: {@link android.Manifest.permission#READ_PHONE_STATE READ_PHONE_STATE}
-     *
      * @return card ID of the default eUICC card.
-     * @hide
      */
-    @SystemApi
-    @SuppressAutoDoc // Blocked by b/72967236 - no support for carrier privileges
-    @RequiresPermission(android.Manifest.permission.READ_PHONE_STATE)
     public int getCardIdForDefaultEuicc() {
         try {
             ITelephony telephony = getITelephony();
@@ -3139,25 +3132,37 @@
     }
 
     /**
-     * Gets information about currently inserted UICCs and eUICCs. See {@link UiccCardInfo} for more
-     * details on the kind of information available.
+     * Gets information about currently inserted UICCs and enabled eUICCs.
+     * <p>
+     * Requires that the calling app has carrier privileges (see {@link #hasCarrierPrivileges}).
+     * <p>
+     * If the caller has carrier priviliges on any active subscription, then they have permission to
+     * get simple information like the card ID ({@link UiccCardInfo#getCardId()}), whether the card
+     * is an eUICC ({@link UiccCardInfo#isEuicc()}), and the slot index where the card is inserted
+     * ({@link UiccCardInfo#getSlotIndex()}).
+     * <p>
+     * To get private information such as the EID ({@link UiccCardInfo#getEid()}) or ICCID
+     * ({@link UiccCardInfo#getIccId()}), the caller must have carrier priviliges on that specific
+     * UICC or eUICC card.
+     * <p>
+     * See {@link UiccCardInfo} for more details on the kind of information available.
      *
-     * @return UiccCardInfo an array of UiccCardInfo objects, representing information on the
-     * currently inserted UICCs and eUICCs.
-     *
-     * @hide
+     * @return a list of UiccCardInfo objects, representing information on the currently inserted
+     * UICCs and eUICCs. Each UiccCardInfo in the list will have private information filtered out if
+     * the caller does not have adequate permissions for that card.
      */
-    @SystemApi
     @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE)
-    public UiccCardInfo[] getUiccCardsInfo() {
+    public List<UiccCardInfo> getUiccCardsInfo() {
         try {
             ITelephony telephony = getITelephony();
             if (telephony == null) {
-                return null;
+                Log.e(TAG, "Error in getUiccCardsInfo: unable to connect to Telephony service.");
+                return new ArrayList<UiccCardInfo>();
             }
-            return telephony.getUiccCardsInfo();
+            return telephony.getUiccCardsInfo(mContext.getOpPackageName());
         } catch (RemoteException e) {
-            return null;
+            Log.e(TAG, "Error in getUiccCardsInfo: " + e);
+            return new ArrayList<UiccCardInfo>();
         }
     }
 
@@ -8949,6 +8954,9 @@
     @SystemApi
     @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE)
     public int setAllowedCarriers(int slotIndex, List<CarrierIdentifier> carriers) {
+        if (carriers == null || !SubscriptionManager.isValidPhoneId(slotIndex)) {
+            return -1;
+        }
         // Execute the method setCarrierRestrictionRules with an empty excluded list and
         // indicating priority for the allowed list.
         CarrierRestrictionRules carrierRestrictionRules = CarrierRestrictionRules.newBuilder()
@@ -8959,7 +8967,7 @@
 
         int result = setCarrierRestrictionRules(carrierRestrictionRules);
 
-        // Convert boolean result into int, as required by this method.
+        // Convert result into int, as required by this method.
         if (result == SET_CARRIER_RESTRICTION_SUCCESS) {
             return carriers.size();
         } else {
@@ -9052,9 +9060,11 @@
     @SystemApi
     @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE)
     public List<CarrierIdentifier> getAllowedCarriers(int slotIndex) {
-        CarrierRestrictionRules carrierRestrictionRule = getCarrierRestrictionRules();
-        if (carrierRestrictionRule != null) {
-            return carrierRestrictionRule.getAllowedCarriers();
+        if (SubscriptionManager.isValidPhoneId(slotIndex)) {
+            CarrierRestrictionRules carrierRestrictionRule = getCarrierRestrictionRules();
+            if (carrierRestrictionRule != null) {
+                return carrierRestrictionRule.getAllowedCarriers();
+            }
         }
         return new ArrayList<CarrierIdentifier>(0);
     }
diff --git a/telephony/java/android/telephony/UiccCardInfo.java b/telephony/java/android/telephony/UiccCardInfo.java
index 45e4704..19f357a 100644
--- a/telephony/java/android/telephony/UiccCardInfo.java
+++ b/telephony/java/android/telephony/UiccCardInfo.java
@@ -15,7 +15,6 @@
  */
 package android.telephony;
 
-import android.annotation.SystemApi;
 import android.os.Parcel;
 import android.os.Parcelable;
 
@@ -23,10 +22,8 @@
 
 /**
  * The UiccCardInfo represents information about a currently inserted UICC or embedded eUICC.
- * @hide
  */
-@SystemApi
-public class UiccCardInfo implements Parcelable {
+public final class UiccCardInfo implements Parcelable {
 
     private final boolean mIsEuicc;
     private final int mCardId;
@@ -95,6 +92,9 @@
     /**
      * Get the embedded ID (EID) of the eUICC. If the UiccCardInfo is not an eUICC
      * (see {@link #isEuicc()}), returns null.
+     * <p>
+     * Note that this field may be omitted if the caller does not have the correct permissions
+     * (see {@link TelephonyManager#getUiccCardsInfo()}).
      */
     public String getEid() {
         if (!mIsEuicc) {
@@ -105,6 +105,9 @@
 
     /**
      * Get the ICCID of the UICC.
+     * <p>
+     * Note that this field may be omitted if the caller does not have the correct permissions
+     * (see {@link TelephonyManager#getUiccCardsInfo()}).
      */
     public String getIccId() {
         return mIccId;
@@ -117,6 +120,16 @@
         return mSlotIndex;
     }
 
+    /**
+     * Returns a copy of the UiccCardinfo with the clears the EID and ICCID set to null. These
+     * values are generally private and require carrier privileges to view.
+     *
+     * @hide
+     */
+    public UiccCardInfo getUnprivileged() {
+        return new UiccCardInfo(mIsEuicc, mCardId, null, null, mSlotIndex);
+    }
+
     @Override
     public boolean equals(Object obj) {
         if (this == obj) {
diff --git a/telephony/java/com/android/internal/telephony/ITelephony.aidl b/telephony/java/com/android/internal/telephony/ITelephony.aidl
index d381514..77a63bd 100644
--- a/telephony/java/com/android/internal/telephony/ITelephony.aidl
+++ b/telephony/java/com/android/internal/telephony/ITelephony.aidl
@@ -1484,25 +1484,34 @@
      * Get the card ID of the default eUICC card. If there is no eUICC, returns
      * {@link #INVALID_CARD_ID}.
      *
-     * <p>Requires Permission: {@link android.Manifest.permission#READ_PHONE_STATE READ_PHONE_STATE}
-     *
      * @param subId subscription ID used for authentication
      * @param callingPackage package making the call
      * @return card ID of the default eUICC card.
-     * @hide
      */
-    int getCardIdForDefaultEuicc(int subId, String callingPackage); 
+    int getCardIdForDefaultEuicc(int subId, String callingPackage);
 
     /**
-     * Gets information about currently inserted UICCs and eUICCs. See {@link UiccCardInfo} for more
-     * details on the kind of information available.
+     * Gets information about currently inserted UICCs and enabled eUICCs.
+     * <p>
+     * Requires that the calling app has carrier privileges (see {@link #hasCarrierPrivileges}).
+     * <p>
+     * If the caller has carrier priviliges on any active subscription, then they have permission to
+     * get simple information like the card ID ({@link UiccCardInfo#getCardId()}), whether the card
+     * is an eUICC ({@link UiccCardInfo#isEuicc()}), and the slot index where the card is inserted
+     * ({@link UiccCardInfo#getSlotIndex()}).
+     * <p>
+     * To get private information such as the EID ({@link UiccCardInfo#getEid()}) or ICCID
+     * ({@link UiccCardInfo#getIccId()}), the caller must have carrier priviliges on that specific
+     * UICC or eUICC card.
+     * <p>
+     * See {@link UiccCardInfo} for more details on the kind of information available.
      *
-     * @return UiccCardInfo an array of UiccCardInfo objects, representing information on the
-     * currently inserted UICCs and eUICCs.
-     *
-     * @hide
+     * @param callingPackage package making the call, used to evaluate carrier privileges 
+     * @return a list of UiccCardInfo objects, representing information on the currently inserted
+     * UICCs and eUICCs. Each UiccCardInfo in the list will have private information filtered out if
+     * the caller does not have adequate permissions for that card.
      */
-    UiccCardInfo[] getUiccCardsInfo();
+    List<UiccCardInfo> getUiccCardsInfo(String callingPackage);
 
     /**
      * Get slot info for all the UICC slots.
diff --git a/tools/apilint/apilint.py b/tools/apilint/apilint.py
index 75c3eba..59e89f5 100644
--- a/tools/apilint/apilint.py
+++ b/tools/apilint/apilint.py
@@ -223,6 +223,7 @@
 class V2Tokenizer(object):
     __slots__ = ["raw"]
 
+    SIGNATURE_PREFIX = "// Signature format: "
     DELIMITER = re.compile(r'\s+|[()@<>;,={}/"!?]|\[\]|\.\.\.')
     STRING_SPECIAL = re.compile(r'["\\]')
 
@@ -610,8 +611,12 @@
         else:
             blame = None
 
-        if line == 1 and raw == "// Signature format: 2.0":
-            sig_format = 2
+        if line == 1 and raw.startswith("// Signature format: "):
+            sig_format_string = raw[len(V2Tokenizer.SIGNATURE_PREFIX):]
+            if sig_format_string in ["2.0", "3.0"]:
+                sig_format = 2
+            else:
+                raise ValueError("Unknown format: %s" % (sig_format_string,))
         elif raw.startswith("package"):
             pkg = Package(line, raw, blame)
         elif raw.startswith("  ") and raw.endswith("{"):
diff --git a/tools/apilint/apilint_test.py b/tools/apilint/apilint_test.py
index 9c261d5..3716bf9 100644
--- a/tools/apilint/apilint_test.py
+++ b/tools/apilint/apilint_test.py
@@ -164,6 +164,23 @@
         self.assertEquals(api['android.SomeEnum'].ctors[0].split[0], 'ctor')
         self.assertEquals(api['android.SomeEnum'].methods[0].split[0], 'method')
 
+class ParseV3Stream(unittest.TestCase):
+    def test_field_kinds(self):
+        api = apilint._parse_stream("""
+// Signature format: 3.0
+package a {
+
+  public final class ContextKt {
+    method public static inline <reified T> T! getSystemService(android.content.Context);
+    method public static inline void withStyledAttributes(android.content.Context, android.util.AttributeSet? set = null, int[] attrs, @AttrRes int defStyleAttr = 0, @StyleRes int defStyleRes = 0, kotlin.jvm.functions.Function1<? super android.content.res.TypedArray,kotlin.Unit> block);
+  }
+}
+        """.strip().split('\n'))
+        self.assertEquals(api['a.ContextKt'].methods[0].name, 'getSystemService')
+        self.assertEquals(api['a.ContextKt'].methods[0].split[:4], ['method', 'public', 'static', 'inline'])
+        self.assertEquals(api['a.ContextKt'].methods[1].name, 'withStyledAttributes')
+        self.assertEquals(api['a.ContextKt'].methods[1].split[:4], ['method', 'public', 'static', 'inline'])
+
 class V2TokenizerTests(unittest.TestCase):
     def _test(self, raw, expected):
         self.assertEquals(apilint.V2Tokenizer(raw).tokenize(), expected)