Merge "Use Ble scan for Fast Pair" into tm-dev
diff --git a/framework-t/api/system-current.txt b/framework-t/api/system-current.txt
index c59fb68..755bf1c 100644
--- a/framework-t/api/system-current.txt
+++ b/framework-t/api/system-current.txt
@@ -51,208 +51,6 @@
     field @NonNull public static final android.os.Parcelable.Creator<android.nearby.DataElement> CREATOR;
   }
 
-  public class FastPairAccountKeyDeviceMetadata {
-    method @Nullable public byte[] getDeviceAccountKey();
-    method @Nullable public android.nearby.FastPairDeviceMetadata getFastPairDeviceMetadata();
-    method @Nullable public android.nearby.FastPairDiscoveryItem getFastPairDiscoveryItem();
-    method @Nullable public byte[] getSha256DeviceAccountKeyPublicAddress();
-  }
-
-  public static final class FastPairAccountKeyDeviceMetadata.Builder {
-    ctor public FastPairAccountKeyDeviceMetadata.Builder();
-    method @NonNull public android.nearby.FastPairAccountKeyDeviceMetadata build();
-    method @NonNull public android.nearby.FastPairAccountKeyDeviceMetadata.Builder setDeviceAccountKey(@Nullable byte[]);
-    method @NonNull public android.nearby.FastPairAccountKeyDeviceMetadata.Builder setFastPairDeviceMetadata(@Nullable android.nearby.FastPairDeviceMetadata);
-    method @NonNull public android.nearby.FastPairAccountKeyDeviceMetadata.Builder setFastPairDiscoveryItem(@Nullable android.nearby.FastPairDiscoveryItem);
-    method @NonNull public android.nearby.FastPairAccountKeyDeviceMetadata.Builder setSha256DeviceAccountKeyPublicAddress(@Nullable byte[]);
-  }
-
-  public class FastPairAntispoofKeyDeviceMetadata {
-    method @Nullable public byte[] getAntispoofPublicKey();
-    method @Nullable public android.nearby.FastPairDeviceMetadata getFastPairDeviceMetadata();
-  }
-
-  public static final class FastPairAntispoofKeyDeviceMetadata.Builder {
-    ctor public FastPairAntispoofKeyDeviceMetadata.Builder();
-    method @NonNull public android.nearby.FastPairAntispoofKeyDeviceMetadata build();
-    method @NonNull public android.nearby.FastPairAntispoofKeyDeviceMetadata.Builder setAntispoofPublicKey(@Nullable byte[]);
-    method @NonNull public android.nearby.FastPairAntispoofKeyDeviceMetadata.Builder setFastPairDeviceMetadata(@Nullable android.nearby.FastPairDeviceMetadata);
-  }
-
-  public abstract class FastPairDataProviderService extends android.app.Service {
-    ctor public FastPairDataProviderService(@NonNull String);
-    method @NonNull public final android.os.IBinder onBind(@NonNull android.content.Intent);
-    method public abstract void onLoadFastPairAccountDevicesMetadata(@NonNull android.nearby.FastPairDataProviderService.FastPairAccountDevicesMetadataRequest, @NonNull android.nearby.FastPairDataProviderService.FastPairAccountDevicesMetadataCallback);
-    method public abstract void onLoadFastPairAntispoofKeyDeviceMetadata(@NonNull android.nearby.FastPairDataProviderService.FastPairAntispoofKeyDeviceMetadataRequest, @NonNull android.nearby.FastPairDataProviderService.FastPairAntispoofKeyDeviceMetadataCallback);
-    method public abstract void onLoadFastPairEligibleAccounts(@NonNull android.nearby.FastPairDataProviderService.FastPairEligibleAccountsRequest, @NonNull android.nearby.FastPairDataProviderService.FastPairEligibleAccountsCallback);
-    method public abstract void onManageFastPairAccount(@NonNull android.nearby.FastPairDataProviderService.FastPairManageAccountRequest, @NonNull android.nearby.FastPairDataProviderService.FastPairManageActionCallback);
-    method public abstract void onManageFastPairAccountDevice(@NonNull android.nearby.FastPairDataProviderService.FastPairManageAccountDeviceRequest, @NonNull android.nearby.FastPairDataProviderService.FastPairManageActionCallback);
-    field public static final String ACTION_FAST_PAIR_DATA_PROVIDER = "android.nearby.action.FAST_PAIR_DATA_PROVIDER";
-    field public static final int ERROR_CODE_BAD_REQUEST = 0; // 0x0
-    field public static final int ERROR_CODE_INTERNAL_ERROR = 1; // 0x1
-    field public static final int MANAGE_REQUEST_ADD = 0; // 0x0
-    field public static final int MANAGE_REQUEST_REMOVE = 1; // 0x1
-  }
-
-  public static interface FastPairDataProviderService.FastPairAccountDevicesMetadataCallback {
-    method public void onError(int, @Nullable String);
-    method public void onFastPairAccountDevicesMetadataReceived(@NonNull java.util.Collection<android.nearby.FastPairAccountKeyDeviceMetadata>);
-  }
-
-  public static class FastPairDataProviderService.FastPairAccountDevicesMetadataRequest {
-    method @NonNull public android.accounts.Account getAccount();
-    method @NonNull public java.util.Collection<byte[]> getDeviceAccountKeys();
-  }
-
-  public static interface FastPairDataProviderService.FastPairAntispoofKeyDeviceMetadataCallback {
-    method public void onError(int, @Nullable String);
-    method public void onFastPairAntispoofKeyDeviceMetadataReceived(@NonNull android.nearby.FastPairAntispoofKeyDeviceMetadata);
-  }
-
-  public static class FastPairDataProviderService.FastPairAntispoofKeyDeviceMetadataRequest {
-    method @NonNull public byte[] getModelId();
-  }
-
-  public static interface FastPairDataProviderService.FastPairEligibleAccountsCallback {
-    method public void onError(int, @Nullable String);
-    method public void onFastPairEligibleAccountsReceived(@NonNull java.util.Collection<android.nearby.FastPairEligibleAccount>);
-  }
-
-  public static class FastPairDataProviderService.FastPairEligibleAccountsRequest {
-  }
-
-  public static class FastPairDataProviderService.FastPairManageAccountDeviceRequest {
-    method @NonNull public android.accounts.Account getAccount();
-    method @NonNull public android.nearby.FastPairAccountKeyDeviceMetadata getAccountKeyDeviceMetadata();
-    method public int getRequestType();
-  }
-
-  public static class FastPairDataProviderService.FastPairManageAccountRequest {
-    method @NonNull public android.accounts.Account getAccount();
-    method public int getRequestType();
-  }
-
-  public static interface FastPairDataProviderService.FastPairManageActionCallback {
-    method public void onError(int, @Nullable String);
-    method public void onSuccess();
-  }
-
-  public class FastPairDeviceMetadata {
-    method public int getBleTxPower();
-    method @Nullable public String getConnectSuccessCompanionAppInstalled();
-    method @Nullable public String getConnectSuccessCompanionAppNotInstalled();
-    method public int getDeviceType();
-    method @Nullable public String getDownloadCompanionAppDescription();
-    method @Nullable public String getFailConnectGoToSettingsDescription();
-    method @Nullable public byte[] getImage();
-    method @Nullable public String getImageUrl();
-    method @Nullable public String getInitialNotificationDescription();
-    method @Nullable public String getInitialNotificationDescriptionNoAccount();
-    method @Nullable public String getInitialPairingDescription();
-    method @Nullable public String getIntentUri();
-    method @Nullable public String getName();
-    method @Nullable public String getOpenCompanionAppDescription();
-    method @Nullable public String getRetroactivePairingDescription();
-    method @Nullable public String getSubsequentPairingDescription();
-    method public float getTriggerDistance();
-    method @Nullable public String getTrueWirelessImageUrlCase();
-    method @Nullable public String getTrueWirelessImageUrlLeftBud();
-    method @Nullable public String getTrueWirelessImageUrlRightBud();
-    method @Nullable public String getUnableToConnectDescription();
-    method @Nullable public String getUnableToConnectTitle();
-    method @Nullable public String getUpdateCompanionAppDescription();
-    method @Nullable public String getWaitLaunchCompanionAppDescription();
-  }
-
-  public static final class FastPairDeviceMetadata.Builder {
-    ctor public FastPairDeviceMetadata.Builder();
-    method @NonNull public android.nearby.FastPairDeviceMetadata build();
-    method @NonNull public android.nearby.FastPairDeviceMetadata.Builder setBleTxPower(int);
-    method @NonNull public android.nearby.FastPairDeviceMetadata.Builder setConnectSuccessCompanionAppInstalled(@Nullable String);
-    method @NonNull public android.nearby.FastPairDeviceMetadata.Builder setConnectSuccessCompanionAppNotInstalled(@Nullable String);
-    method @NonNull public android.nearby.FastPairDeviceMetadata.Builder setDeviceType(int);
-    method @NonNull public android.nearby.FastPairDeviceMetadata.Builder setDownloadCompanionAppDescription(@Nullable String);
-    method @NonNull public android.nearby.FastPairDeviceMetadata.Builder setFailConnectGoToSettingsDescription(@Nullable String);
-    method @NonNull public android.nearby.FastPairDeviceMetadata.Builder setImage(@Nullable byte[]);
-    method @NonNull public android.nearby.FastPairDeviceMetadata.Builder setImageUrl(@Nullable String);
-    method @NonNull public android.nearby.FastPairDeviceMetadata.Builder setInitialNotificationDescription(@Nullable String);
-    method @NonNull public android.nearby.FastPairDeviceMetadata.Builder setInitialNotificationDescriptionNoAccount(@Nullable String);
-    method @NonNull public android.nearby.FastPairDeviceMetadata.Builder setInitialPairingDescription(@Nullable String);
-    method @NonNull public android.nearby.FastPairDeviceMetadata.Builder setIntentUri(@Nullable String);
-    method @NonNull public android.nearby.FastPairDeviceMetadata.Builder setName(@Nullable String);
-    method @NonNull public android.nearby.FastPairDeviceMetadata.Builder setOpenCompanionAppDescription(@Nullable String);
-    method @NonNull public android.nearby.FastPairDeviceMetadata.Builder setRetroactivePairingDescription(@Nullable String);
-    method @NonNull public android.nearby.FastPairDeviceMetadata.Builder setSubsequentPairingDescription(@Nullable String);
-    method @NonNull public android.nearby.FastPairDeviceMetadata.Builder setTriggerDistance(float);
-    method @NonNull public android.nearby.FastPairDeviceMetadata.Builder setTrueWirelessImageUrlCase(@Nullable String);
-    method @NonNull public android.nearby.FastPairDeviceMetadata.Builder setTrueWirelessImageUrlLeftBud(@Nullable String);
-    method @NonNull public android.nearby.FastPairDeviceMetadata.Builder setTrueWirelessImageUrlRightBud(@Nullable String);
-    method @NonNull public android.nearby.FastPairDeviceMetadata.Builder setUnableToConnectDescription(@Nullable String);
-    method @NonNull public android.nearby.FastPairDeviceMetadata.Builder setUnableToConnectTitle(@Nullable String);
-    method @NonNull public android.nearby.FastPairDeviceMetadata.Builder setUpdateCompanionAppDescription(@Nullable String);
-    method @NonNull public android.nearby.FastPairDeviceMetadata.Builder setWaitLaunchCompanionAppDescription(@Nullable String);
-  }
-
-  public class FastPairDiscoveryItem {
-    method @Nullable public String getActionUrl();
-    method public int getActionUrlType();
-    method @Nullable public String getAppName();
-    method @Nullable public byte[] getAuthenticationPublicKeySecp256r1();
-    method @Nullable public String getDescription();
-    method @Nullable public String getDeviceName();
-    method @Nullable public String getDisplayUrl();
-    method public long getFirstObservationTimestampMillis();
-    method @Nullable public String getIconFfeUrl();
-    method @Nullable public byte[] getIconPng();
-    method @Nullable public String getId();
-    method public long getLastObservationTimestampMillis();
-    method @Nullable public String getMacAddress();
-    method @Nullable public String getPackageName();
-    method public long getPendingAppInstallTimestampMillis();
-    method public int getRssi();
-    method public int getState();
-    method @Nullable public String getTitle();
-    method @Nullable public String getTriggerId();
-    method public int getTxPower();
-  }
-
-  public static final class FastPairDiscoveryItem.Builder {
-    ctor public FastPairDiscoveryItem.Builder();
-    method @NonNull public android.nearby.FastPairDiscoveryItem build();
-    method @NonNull public android.nearby.FastPairDiscoveryItem.Builder setActionUrl(@Nullable String);
-    method @NonNull public android.nearby.FastPairDiscoveryItem.Builder setActionUrlType(int);
-    method @NonNull public android.nearby.FastPairDiscoveryItem.Builder setAppName(@Nullable String);
-    method @NonNull public android.nearby.FastPairDiscoveryItem.Builder setAuthenticationPublicKeySecp256r1(@Nullable byte[]);
-    method @NonNull public android.nearby.FastPairDiscoveryItem.Builder setDescription(@Nullable String);
-    method @NonNull public android.nearby.FastPairDiscoveryItem.Builder setDeviceName(@Nullable String);
-    method @NonNull public android.nearby.FastPairDiscoveryItem.Builder setDisplayUrl(@Nullable String);
-    method @NonNull public android.nearby.FastPairDiscoveryItem.Builder setFirstObservationTimestampMillis(long);
-    method @NonNull public android.nearby.FastPairDiscoveryItem.Builder setIconFfeUrl(@Nullable String);
-    method @NonNull public android.nearby.FastPairDiscoveryItem.Builder setIconPng(@Nullable byte[]);
-    method @NonNull public android.nearby.FastPairDiscoveryItem.Builder setId(@Nullable String);
-    method @NonNull public android.nearby.FastPairDiscoveryItem.Builder setLastObservationTimestampMillis(long);
-    method @NonNull public android.nearby.FastPairDiscoveryItem.Builder setMacAddress(@Nullable String);
-    method @NonNull public android.nearby.FastPairDiscoveryItem.Builder setPackageName(@Nullable String);
-    method @NonNull public android.nearby.FastPairDiscoveryItem.Builder setPendingAppInstallTimestampMillis(long);
-    method @NonNull public android.nearby.FastPairDiscoveryItem.Builder setRssi(int);
-    method @NonNull public android.nearby.FastPairDiscoveryItem.Builder setState(int);
-    method @NonNull public android.nearby.FastPairDiscoveryItem.Builder setTitle(@Nullable String);
-    method @NonNull public android.nearby.FastPairDiscoveryItem.Builder setTriggerId(@Nullable String);
-    method @NonNull public android.nearby.FastPairDiscoveryItem.Builder setTxPower(int);
-  }
-
-  public class FastPairEligibleAccount {
-    method @Nullable public android.accounts.Account getAccount();
-    method public boolean isOptIn();
-  }
-
-  public static final class FastPairEligibleAccount.Builder {
-    ctor public FastPairEligibleAccount.Builder();
-    method @NonNull public android.nearby.FastPairEligibleAccount build();
-    method @NonNull public android.nearby.FastPairEligibleAccount.Builder setAccount(@Nullable android.accounts.Account);
-    method @NonNull public android.nearby.FastPairEligibleAccount.Builder setOptIn(boolean);
-  }
-
   public abstract class NearbyDevice {
     method @NonNull public java.util.List<java.lang.Integer> getMediums();
     method @Nullable public String getName();
diff --git a/nearby/framework/java/android/nearby/FastPairAccountKeyDeviceMetadata.java b/nearby/framework/java/android/nearby/FastPairAccountKeyDeviceMetadata.java
index 160ad75..d42fbf4 100644
--- a/nearby/framework/java/android/nearby/FastPairAccountKeyDeviceMetadata.java
+++ b/nearby/framework/java/android/nearby/FastPairAccountKeyDeviceMetadata.java
@@ -18,7 +18,6 @@
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
-import android.annotation.SystemApi;
 import android.nearby.aidl.FastPairAccountKeyDeviceMetadataParcel;
 
 /**
@@ -26,7 +25,6 @@
  *
  * @hide
  */
-@SystemApi
 public class FastPairAccountKeyDeviceMetadata {
 
     FastPairAccountKeyDeviceMetadataParcel mMetadataParcel;
@@ -42,7 +40,6 @@
      * @return 16-byte Account Key.
      * @hide
      */
-    @SystemApi
     @Nullable
     public byte[] getDeviceAccountKey() {
         return mMetadataParcel.deviceAccountKey;
@@ -55,7 +52,6 @@
      * @return 32-byte Sha256 hash value.
      * @hide
      */
-    @SystemApi
     @Nullable
     public byte[] getSha256DeviceAccountKeyPublicAddress() {
         return mMetadataParcel.sha256DeviceAccountKeyPublicAddress;
@@ -66,7 +62,6 @@
      *
      * @hide
      */
-    @SystemApi
     @Nullable
     public FastPairDeviceMetadata getFastPairDeviceMetadata() {
         if (mMetadataParcel.metadata == null) {
@@ -80,7 +75,6 @@
      *
      * @hide
      */
-    @SystemApi
     @Nullable
     public FastPairDiscoveryItem getFastPairDiscoveryItem() {
         if (mMetadataParcel.discoveryItem == null) {
@@ -94,7 +88,6 @@
      *
      * @hide
      */
-    @SystemApi
     public static final class Builder {
 
         private final FastPairAccountKeyDeviceMetadataParcel mBuilderParcel;
@@ -104,7 +97,6 @@
          *
          * @hide
          */
-        @SystemApi
         public Builder() {
             mBuilderParcel = new FastPairAccountKeyDeviceMetadataParcel();
             mBuilderParcel.deviceAccountKey = null;
@@ -121,7 +113,6 @@
          * @return The builder, to facilitate chaining {@code builder.setXXX(..).setXXX(..)}.
          * @hide
          */
-        @SystemApi
         @NonNull
         public Builder setDeviceAccountKey(@Nullable byte[] deviceAccountKey) {
             mBuilderParcel.deviceAccountKey = deviceAccountKey;
@@ -136,7 +127,6 @@
          * @return The builder, to facilitate chaining {@code builder.setXXX(..).setXXX(..)}.
          * @hide
          */
-        @SystemApi
         @NonNull
         public Builder setSha256DeviceAccountKeyPublicAddress(
                 @Nullable byte[] sha256DeviceAccountKeyPublicAddress) {
@@ -153,7 +143,6 @@
          * @return The builder, to facilitate chaining {@code builder.setXXX(..).setXXX(..)}.
          * @hide
          */
-        @SystemApi
         @NonNull
         public Builder setFastPairDeviceMetadata(@Nullable FastPairDeviceMetadata metadata) {
             if (metadata == null) {
@@ -171,7 +160,6 @@
          * @return The builder, to facilitate chaining {@code builder.setXXX(..).setXXX(..)}.
          * @hide
          */
-        @SystemApi
         @NonNull
         public Builder setFastPairDiscoveryItem(@Nullable FastPairDiscoveryItem discoveryItem) {
             if (discoveryItem == null) {
@@ -187,7 +175,6 @@
          *
          * @hide
          */
-        @SystemApi
         @NonNull
         public FastPairAccountKeyDeviceMetadata build() {
             return new FastPairAccountKeyDeviceMetadata(mBuilderParcel);
diff --git a/nearby/framework/java/android/nearby/FastPairAntispoofKeyDeviceMetadata.java b/nearby/framework/java/android/nearby/FastPairAntispoofKeyDeviceMetadata.java
index 1837671..74831d5 100644
--- a/nearby/framework/java/android/nearby/FastPairAntispoofKeyDeviceMetadata.java
+++ b/nearby/framework/java/android/nearby/FastPairAntispoofKeyDeviceMetadata.java
@@ -17,7 +17,6 @@
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
-import android.annotation.SystemApi;
 import android.nearby.aidl.FastPairAntispoofKeyDeviceMetadataParcel;
 
 /**
@@ -25,7 +24,6 @@
  *
  * @hide
  */
-@SystemApi
 public class FastPairAntispoofKeyDeviceMetadata {
 
     FastPairAntispoofKeyDeviceMetadataParcel mMetadataParcel;
@@ -39,7 +37,6 @@
      *
      * @hide
      */
-    @SystemApi
     @Nullable
     public byte[] getAntispoofPublicKey() {
         return this.mMetadataParcel.antispoofPublicKey;
@@ -50,7 +47,6 @@
      *
      * @hide
      */
-    @SystemApi
     @Nullable
     public FastPairDeviceMetadata getFastPairDeviceMetadata() {
         if (this.mMetadataParcel.deviceMetadata == null) {
@@ -64,7 +60,6 @@
      *
      * @hide
      */
-    @SystemApi
     public static final class Builder {
 
         private final FastPairAntispoofKeyDeviceMetadataParcel mBuilderParcel;
@@ -74,7 +69,6 @@
          *
          * @hide
          */
-        @SystemApi
         public Builder() {
             mBuilderParcel = new FastPairAntispoofKeyDeviceMetadataParcel();
             mBuilderParcel.antispoofPublicKey = null;
@@ -88,7 +82,6 @@
          * @return The builder, to facilitate chaining {@code builder.setXXX(..).setXXX(..)}.
          * @hide
          */
-        @SystemApi
         @NonNull
         public Builder setAntispoofPublicKey(@Nullable byte[] antispoofPublicKey) {
             mBuilderParcel.antispoofPublicKey = antispoofPublicKey;
@@ -103,7 +96,6 @@
          * @return The builder, to facilitate chaining {@code builder.setXXX(..).setXXX(..)}.
          * @hide
          */
-        @SystemApi
         @NonNull
         public Builder setFastPairDeviceMetadata(@Nullable FastPairDeviceMetadata metadata) {
             if (metadata != null) {
@@ -119,7 +111,6 @@
          *
          * @hide
          */
-        @SystemApi
         @NonNull
         public FastPairAntispoofKeyDeviceMetadata build() {
             return new FastPairAntispoofKeyDeviceMetadata(mBuilderParcel);
diff --git a/nearby/framework/java/android/nearby/FastPairDataProviderService.java b/nearby/framework/java/android/nearby/FastPairDataProviderService.java
index b6c840d..f1d5074 100644
--- a/nearby/framework/java/android/nearby/FastPairDataProviderService.java
+++ b/nearby/framework/java/android/nearby/FastPairDataProviderService.java
@@ -20,7 +20,6 @@
 import android.annotation.IntDef;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
-import android.annotation.SystemApi;
 import android.app.Service;
 import android.content.Intent;
 import android.nearby.aidl.ByteArrayParcel;
@@ -59,7 +58,6 @@
  *
  * @hide
  */
-@SystemApi
 public abstract class FastPairDataProviderService extends Service {
     /**
      * The action the wrapping service should have in its intent filter to implement the
@@ -67,7 +65,6 @@
      *
      * @hide
      */
-    @SystemApi
     public static final String ACTION_FAST_PAIR_DATA_PROVIDER =
             "android.nearby.action.FAST_PAIR_DATA_PROVIDER";
 
@@ -76,7 +73,6 @@
      *
      * @hide
      */
-    @SystemApi
     public static final int MANAGE_REQUEST_ADD = 0;
 
     /**
@@ -84,7 +80,6 @@
      *
      * @hide
      */
-    @SystemApi
     public static final int MANAGE_REQUEST_REMOVE = 1;
 
     /**
@@ -101,7 +96,6 @@
      *
      * @hide
      */
-    @SystemApi
     public static final int ERROR_CODE_BAD_REQUEST = 0;
 
     /**
@@ -109,7 +103,6 @@
      *
      * @hide
      */
-    @SystemApi
     public static final int ERROR_CODE_INTERNAL_ERROR = 1;
 
     /**
@@ -130,7 +123,6 @@
      * @param tag TAG for on device logging.
      * @hide
      */
-    @SystemApi
     public FastPairDataProviderService(@NonNull String tag) {
         mBinder = new Service();
         mTag = tag;
@@ -147,7 +139,6 @@
      *
      * @hide
      */
-    @SystemApi
     public interface FastPairAntispoofKeyDeviceMetadataCallback {
 
         /**
@@ -155,7 +146,6 @@
          *
          * @hide
          */
-        @SystemApi
         void onFastPairAntispoofKeyDeviceMetadataReceived(
                 @NonNull FastPairAntispoofKeyDeviceMetadata metadata);
 
@@ -163,7 +153,6 @@
          *
          * @hide
          */
-        @SystemApi
         void onError(@ErrorCode int code, @Nullable String message);
     }
 
@@ -172,7 +161,6 @@
      *
      * @hide
      */
-    @SystemApi
     public interface FastPairAccountDevicesMetadataCallback {
 
         /**
@@ -180,7 +168,6 @@
          *
          * @hide
          */
-        @SystemApi
         void onFastPairAccountDevicesMetadataReceived(
                 @NonNull Collection<FastPairAccountKeyDeviceMetadata> metadatas);
         /**
@@ -188,7 +175,6 @@
          *
          * @hide
          */
-        @SystemApi
         void onError(@ErrorCode int code, @Nullable String message);
     }
 
@@ -197,7 +183,6 @@
      *
      * @hide
      */
-    @SystemApi
     public interface FastPairEligibleAccountsCallback {
 
         /**
@@ -205,7 +190,6 @@
          *
          * @hide
          */
-        @SystemApi
         void onFastPairEligibleAccountsReceived(
                 @NonNull Collection<FastPairEligibleAccount> accounts);
         /**
@@ -213,7 +197,6 @@
          *
          * @hide
          */
-        @SystemApi
         void onError(@ErrorCode int code, @Nullable String message);
     }
 
@@ -222,7 +205,6 @@
      *
      * @hide
      */
-    @SystemApi
     public interface FastPairManageActionCallback {
 
         /**
@@ -230,14 +212,12 @@
          *
          * @hide
          */
-        @SystemApi
         void onSuccess();
         /**
          * Invoked in case of error.
          *
          * @hide
          */
-        @SystemApi
         void onError(@ErrorCode int code, @Nullable String message);
     }
 
@@ -247,7 +227,6 @@
      *
      * @hide
      */
-    @SystemApi
     public abstract void onLoadFastPairAntispoofKeyDeviceMetadata(
             @NonNull FastPairAntispoofKeyDeviceMetadataRequest request,
             @NonNull FastPairAntispoofKeyDeviceMetadataCallback callback);
@@ -258,7 +237,6 @@
      *
      * @hide
      */
-    @SystemApi
     public abstract void onLoadFastPairAccountDevicesMetadata(
             @NonNull FastPairAccountDevicesMetadataRequest request,
             @NonNull FastPairAccountDevicesMetadataCallback callback);
@@ -269,7 +247,6 @@
      *
      * @hide
      */
-    @SystemApi
     public abstract void onLoadFastPairEligibleAccounts(
             @NonNull FastPairEligibleAccountsRequest request,
             @NonNull FastPairEligibleAccountsCallback callback);
@@ -279,7 +256,6 @@
      *
      * @hide
      */
-    @SystemApi
     public abstract void onManageFastPairAccount(
             @NonNull FastPairManageAccountRequest request,
             @NonNull FastPairManageActionCallback callback);
@@ -289,7 +265,6 @@
      *
      * @hide
      */
-    @SystemApi
     public abstract void onManageFastPairAccountDevice(
             @NonNull FastPairManageAccountDeviceRequest request,
             @NonNull FastPairManageActionCallback callback);
@@ -301,7 +276,6 @@
      *
      * @hide
      */
-    @SystemApi
     public static class FastPairAntispoofKeyDeviceMetadataRequest {
 
         private final FastPairAntispoofKeyDeviceMetadataRequestParcel mMetadataRequestParcel;
@@ -321,7 +295,6 @@
          *         time.
          * @hide
          */
-        @SystemApi
         public @NonNull byte[] getModelId() {
             return this.mMetadataRequestParcel.modelId;
         }
@@ -342,7 +315,6 @@
      * needs to set account with a non-empty allow list.
      * @hide
      */
-    @SystemApi
     public static class FastPairAccountDevicesMetadataRequest {
 
         private final FastPairAccountDevicesMetadataRequestParcel mMetadataRequestParcel;
@@ -358,7 +330,6 @@
          * @return a FastPair account.
          * @hide
          */
-        @SystemApi
         public @NonNull Account getAccount() {
             return this.mMetadataRequestParcel.account;
         }
@@ -373,7 +344,6 @@
          * @return allowlist of Fast Pair devices using a collection of deviceAccountKeys.
          * @hide
          */
-        @SystemApi
         public @NonNull Collection<byte[]> getDeviceAccountKeys()  {
             if (this.mMetadataRequestParcel.deviceAccountKeys == null) {
                 return new ArrayList<byte[]>(0);
@@ -393,7 +363,6 @@
      *
      * @hide
      */
-    @SystemApi
     public static class FastPairEligibleAccountsRequest {
         @SuppressWarnings("UnusedVariable")
         private final FastPairEligibleAccountsRequestParcel mAccountsRequestParcel;
@@ -412,7 +381,6 @@
      *
      * @hide
      */
-    @SystemApi
     public static class FastPairManageAccountRequest {
 
         private final FastPairManageAccountRequestParcel mAccountRequestParcel;
@@ -427,7 +395,6 @@
          *
          * @hide
          */
-        @SystemApi
         public @ManageRequestType int getRequestType() {
             return this.mAccountRequestParcel.requestType;
         }
@@ -436,7 +403,6 @@
          *
          * @hide
          */
-        @SystemApi
         public @NonNull Account getAccount() {
             return this.mAccountRequestParcel.account;
         }
@@ -450,7 +416,6 @@
      *
      * @hide
      */
-    @SystemApi
     public static class FastPairManageAccountDeviceRequest {
 
         private final FastPairManageAccountDeviceRequestParcel mRequestParcel;
@@ -465,7 +430,6 @@
          *
          * @hide
          */
-        @SystemApi
         public @ManageRequestType int getRequestType() {
             return this.mRequestParcel.requestType;
         }
@@ -474,7 +438,6 @@
          *
          * @hide
          */
-        @SystemApi
         public @NonNull Account getAccount() {
             return this.mRequestParcel.account;
         }
@@ -483,7 +446,6 @@
          *
          * @hide
          */
-        @SystemApi
         public @NonNull FastPairAccountKeyDeviceMetadata getAccountKeyDeviceMetadata() {
             return new FastPairAccountKeyDeviceMetadata(
                     this.mRequestParcel.accountKeyDeviceMetadata);
diff --git a/nearby/framework/java/android/nearby/FastPairDeviceMetadata.java b/nearby/framework/java/android/nearby/FastPairDeviceMetadata.java
index 04845c0..0e2e79d 100644
--- a/nearby/framework/java/android/nearby/FastPairDeviceMetadata.java
+++ b/nearby/framework/java/android/nearby/FastPairDeviceMetadata.java
@@ -18,7 +18,6 @@
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
-import android.annotation.SystemApi;
 import android.nearby.aidl.FastPairDeviceMetadataParcel;
 
 /**
@@ -26,7 +25,6 @@
  *
  * @hide
  */
-@SystemApi
 public class FastPairDeviceMetadata {
 
     FastPairDeviceMetadataParcel mMetadataParcel;
@@ -41,7 +39,6 @@
      *
      * @hide
      */
-    @SystemApi
     @Nullable
     public String getImageUrl() {
         return mMetadataParcel.imageUrl;
@@ -52,7 +49,6 @@
      *
      * @hide
      */
-    @SystemApi
     @Nullable
     public String getIntentUri() {
         return mMetadataParcel.intentUri;
@@ -64,7 +60,6 @@
      *
      * @hide
      */
-    @SystemApi
     public int getBleTxPower() {
         return mMetadataParcel.bleTxPower;
     }
@@ -74,7 +69,6 @@
      *
      * @hide
      */
-    @SystemApi
     public float getTriggerDistance() {
         return mMetadataParcel.triggerDistance;
     }
@@ -86,7 +80,6 @@
      * @return Fast Pair device image in 32-bit PNG with dimensions of 512px by 512px.
      * @hide
      */
-    @SystemApi
     @Nullable
     public byte[] getImage() {
         return mMetadataParcel.image;
@@ -99,7 +92,6 @@
      * TRUE_WIRELESS_HEADPHONES = 7;
      * @hide
      */
-    @SystemApi
     public int getDeviceType() {
         return mMetadataParcel.deviceType;
     }
@@ -109,7 +101,6 @@
      *
      * @hide
      */
-    @SystemApi
     @Nullable
     public String getName() {
         return mMetadataParcel.name;
@@ -120,7 +111,6 @@
      *
      * @hide
      */
-    @SystemApi
     @Nullable
     public String getTrueWirelessImageUrlLeftBud() {
         return mMetadataParcel.trueWirelessImageUrlLeftBud;
@@ -131,7 +121,6 @@
      *
      * @hide
      */
-    @SystemApi
     @Nullable
     public String getTrueWirelessImageUrlRightBud() {
         return mMetadataParcel.trueWirelessImageUrlRightBud;
@@ -142,7 +131,6 @@
      *
      * @hide
      */
-    @SystemApi
     @Nullable
     public String getTrueWirelessImageUrlCase() {
         return mMetadataParcel.trueWirelessImageUrlCase;
@@ -154,7 +142,6 @@
      *
      * @hide
      */
-    @SystemApi
     @Nullable
     public String getInitialNotificationDescription() {
         return mMetadataParcel.initialNotificationDescription;
@@ -166,7 +153,6 @@
      *
      * @hide
      */
-    @SystemApi
     @Nullable
     public String getInitialNotificationDescriptionNoAccount() {
         return mMetadataParcel.initialNotificationDescriptionNoAccount;
@@ -178,7 +164,6 @@
      *
      * @hide
      */
-    @SystemApi
     @Nullable
     public String getOpenCompanionAppDescription() {
         return mMetadataParcel.openCompanionAppDescription;
@@ -190,7 +175,6 @@
      *
      * @hide
      */
-    @SystemApi
     @Nullable
     public String getUpdateCompanionAppDescription() {
         return mMetadataParcel.updateCompanionAppDescription;
@@ -202,7 +186,6 @@
      *
      * @hide
      */
-    @SystemApi
     @Nullable
     public String getDownloadCompanionAppDescription() {
         return mMetadataParcel.downloadCompanionAppDescription;
@@ -223,7 +206,6 @@
      *
      * @hide
      */
-    @SystemApi
     @Nullable
     public String getUnableToConnectDescription() {
         return mMetadataParcel.unableToConnectDescription;
@@ -235,7 +217,6 @@
      *
      * @hide
      */
-    @SystemApi
     @Nullable
     public String getInitialPairingDescription() {
         return mMetadataParcel.initialPairingDescription;
@@ -247,7 +228,6 @@
      *
      * @hide
      */
-    @SystemApi
     @Nullable
     public String getConnectSuccessCompanionAppInstalled() {
         return mMetadataParcel.connectSuccessCompanionAppInstalled;
@@ -259,7 +239,6 @@
      *
      * @hide
      */
-    @SystemApi
     @Nullable
     public String getConnectSuccessCompanionAppNotInstalled() {
         return mMetadataParcel.connectSuccessCompanionAppNotInstalled;
@@ -271,7 +250,6 @@
      *
      * @hide
      */
-    @SystemApi
     @Nullable
     public String getSubsequentPairingDescription() {
         return mMetadataParcel.subsequentPairingDescription;
@@ -283,7 +261,6 @@
      *
      * @hide
      */
-    @SystemApi
     @Nullable
     public String getRetroactivePairingDescription() {
         return mMetadataParcel.retroactivePairingDescription;
@@ -295,7 +272,6 @@
      *
      * @hide
      */
-    @SystemApi
     @Nullable
     public String getWaitLaunchCompanionAppDescription() {
         return mMetadataParcel.waitLaunchCompanionAppDescription;
@@ -307,7 +283,6 @@
      *
      * @hide
      */
-    @SystemApi
     @Nullable
     public String getFailConnectGoToSettingsDescription() {
         return mMetadataParcel.failConnectGoToSettingsDescription;
@@ -318,7 +293,6 @@
      *
      * @hide
      */
-    @SystemApi
     public static final class Builder {
 
         private final FastPairDeviceMetadataParcel mBuilderParcel;
@@ -328,7 +302,6 @@
          *
          * @hide
          */
-        @SystemApi
         public Builder() {
             mBuilderParcel = new FastPairDeviceMetadataParcel();
             mBuilderParcel.imageUrl = null;
@@ -364,7 +337,6 @@
          * @return The builder, to facilitate chaining {@code builder.setXXX(..).setXXX(..)}.
          * @hide
          */
-        @SystemApi
         @NonNull
         public Builder setImageUrl(@Nullable String imageUrl) {
             mBuilderParcel.imageUrl = imageUrl;
@@ -378,7 +350,6 @@
          * @return The builder, to facilitate chaining {@code builder.setXXX(..).setXXX(..)}.
          * @hide
          */
-        @SystemApi
         @NonNull
         public Builder setIntentUri(@Nullable String intentUri) {
             mBuilderParcel.intentUri = intentUri;
@@ -392,7 +363,6 @@
          * @return The builder, to facilitate chaining {@code builder.setXXX(..).setXXX(..)}.
          * @hide
          */
-        @SystemApi
         @NonNull
         public Builder setName(@Nullable String name) {
             mBuilderParcel.name = name;
@@ -406,7 +376,6 @@
          * @return The builder, to facilitate chaining {@code builder.setXXX(..).setXXX(..)}.
          * @hide
          */
-        @SystemApi
         @NonNull
         public Builder setBleTxPower(int bleTxPower) {
             mBuilderParcel.bleTxPower = bleTxPower;
@@ -420,7 +389,6 @@
          * @return The builder, to facilitate chaining {@code builder.setXXX(..).setXXX(..)}.
          * @hide
          */
-        @SystemApi
         @NonNull
         public Builder setTriggerDistance(float triggerDistance) {
             mBuilderParcel.triggerDistance = triggerDistance;
@@ -436,7 +404,6 @@
          * @return The builder, to facilitate chaining {@code builder.setXXX(..).setXXX(..)}.
          * @hide
          */
-        @SystemApi
         @NonNull
         public Builder setImage(@Nullable byte[] image) {
             mBuilderParcel.image = image;
@@ -450,7 +417,6 @@
          * @return The builder, to facilitate chaining {@code builder.setXXX(..).setXXX(..)}.
          * @hide
          */
-        @SystemApi
         @NonNull
         public Builder setDeviceType(int deviceType) {
             mBuilderParcel.deviceType = deviceType;
@@ -464,7 +430,6 @@
          * @return The builder, to facilitate chaining {@code builder.setXXX(..).setXXX(..)}.
          * @hide
          */
-        @SystemApi
         @NonNull
         public Builder setTrueWirelessImageUrlLeftBud(
                 @Nullable String trueWirelessImageUrlLeftBud) {
@@ -479,7 +444,6 @@
          * @return The builder, to facilitate chaining {@code builder.setXXX(..).setXXX(..)}.
          * @hide
          */
-        @SystemApi
         @NonNull
         public Builder setTrueWirelessImageUrlRightBud(
                 @Nullable String trueWirelessImageUrlRightBud) {
@@ -494,7 +458,6 @@
          * @return The builder, to facilitate chaining {@code builder.setXXX(..).setXXX(..)}.
          * @hide
          */
-        @SystemApi
         @NonNull
         public Builder setTrueWirelessImageUrlCase(@Nullable String trueWirelessImageUrlCase) {
             mBuilderParcel.trueWirelessImageUrlCase = trueWirelessImageUrlCase;
@@ -508,7 +471,6 @@
          * @return The builder, to facilitate chaining {@code builder.setXXX(..).setXXX(..)}.
          * @hide
          */
-        @SystemApi
         @NonNull
         public Builder setInitialNotificationDescription(
                 @Nullable String initialNotificationDescription) {
@@ -524,7 +486,6 @@
          * @return The builder, to facilitate chaining {@code builder.setXXX(..).setXXX(..)}.
          * @hide
          */
-        @SystemApi
         @NonNull
         public Builder setInitialNotificationDescriptionNoAccount(
                 @Nullable String initialNotificationDescriptionNoAccount) {
@@ -540,7 +501,6 @@
          * @return The builder, to facilitate chaining {@code builder.setXXX(..).setXXX(..)}.
          * @hide
          */
-        @SystemApi
         @NonNull
         public Builder setOpenCompanionAppDescription(
                 @Nullable String openCompanionAppDescription) {
@@ -555,7 +515,6 @@
          * @return The builder, to facilitate chaining {@code builder.setXXX(..).setXXX(..)}.
          * @hide
          */
-        @SystemApi
         @NonNull
         public Builder setUpdateCompanionAppDescription(
                 @Nullable String updateCompanionAppDescription) {
@@ -570,7 +529,6 @@
          * @return The builder, to facilitate chaining {@code builder.setXXX(..).setXXX(..)}.
          * @hide
          */
-        @SystemApi
         @NonNull
         public Builder setDownloadCompanionAppDescription(
                 @Nullable String downloadCompanionAppDescription) {
@@ -585,7 +543,6 @@
          * @return The builder, to facilitate chaining {@code builder.setXXX(..).setXXX(..)}.
          * @hide
          */
-        @SystemApi
         @NonNull
         public Builder setUnableToConnectTitle(@Nullable String unableToConnectTitle) {
             mBuilderParcel.unableToConnectTitle = unableToConnectTitle;
@@ -600,7 +557,6 @@
          * @return The builder, to facilitate chaining {@code builder.setXXX(..).setXXX(..)}.
          * @hide
          */
-        @SystemApi
         @NonNull
         public Builder setUnableToConnectDescription(
                 @Nullable String unableToConnectDescription) {
@@ -615,7 +571,6 @@
          * @return The builder, to facilitate chaining {@code builder.setXXX(..).setXXX(..)}.
          * @hide
          */
-        @SystemApi
         @NonNull
         public Builder setInitialPairingDescription(@Nullable String initialPairingDescription) {
             mBuilderParcel.initialPairingDescription = initialPairingDescription;
@@ -630,7 +585,6 @@
          * @return The builder, to facilitate chaining {@code builder.setXXX(..).setXXX(..)}.
          * @hide
          */
-        @SystemApi
         @NonNull
         public Builder setConnectSuccessCompanionAppInstalled(
                 @Nullable String connectSuccessCompanionAppInstalled) {
@@ -647,7 +601,6 @@
          * @return The builder, to facilitate chaining {@code builder.setXXX(..).setXXX(..)}.
          * @hide
          */
-        @SystemApi
         @NonNull
         public Builder setConnectSuccessCompanionAppNotInstalled(
                 @Nullable String connectSuccessCompanionAppNotInstalled) {
@@ -664,7 +617,6 @@
          * @return The builder, to facilitate chaining {@code builder.setXXX(..).setXXX(..)}.
          * @hide
          */
-        @SystemApi
         @NonNull
         public Builder setSubsequentPairingDescription(
                 @Nullable String subsequentPairingDescription) {
@@ -679,7 +631,6 @@
          * @return The builder, to facilitate chaining {@code builder.setXXX(..).setXXX(..)}.
          * @hide
          */
-        @SystemApi
         @NonNull
         public Builder setRetroactivePairingDescription(
                 @Nullable String retroactivePairingDescription) {
@@ -695,7 +646,6 @@
          * @return The builder, to facilitate chaining {@code builder.setXXX(..).setXXX(..)}.
          * @hide
          */
-        @SystemApi
         @NonNull
         public Builder setWaitLaunchCompanionAppDescription(
                 @Nullable String waitLaunchCompanionAppDescription) {
@@ -712,7 +662,6 @@
          * @return The builder, to facilitate chaining {@code builder.setXXX(..).setXXX(..)}.
          * @hide
          */
-        @SystemApi
         @NonNull
         public Builder setFailConnectGoToSettingsDescription(
                 @Nullable String failConnectGoToSettingsDescription) {
@@ -726,7 +675,6 @@
          *
          * @hide
          */
-        @SystemApi
         @NonNull
         public FastPairDeviceMetadata build() {
             return new FastPairDeviceMetadata(mBuilderParcel);
diff --git a/nearby/framework/java/android/nearby/FastPairDiscoveryItem.java b/nearby/framework/java/android/nearby/FastPairDiscoveryItem.java
index ce7413a..d8dfe29 100644
--- a/nearby/framework/java/android/nearby/FastPairDiscoveryItem.java
+++ b/nearby/framework/java/android/nearby/FastPairDiscoveryItem.java
@@ -18,7 +18,6 @@
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
-import android.annotation.SystemApi;
 import android.nearby.aidl.FastPairDiscoveryItemParcel;
 
 /**
@@ -26,7 +25,6 @@
  *
  * @hide
  */
-@SystemApi
 public class FastPairDiscoveryItem {
 
     FastPairDiscoveryItemParcel mMetadataParcel;
@@ -41,7 +39,6 @@
      *
      * @hide
      */
-    @SystemApi
     @Nullable
     public String getId() {
         return mMetadataParcel.id;
@@ -52,7 +49,6 @@
      *
      * @hide
      */
-    @SystemApi
     @Nullable
     public String getMacAddress() {
         return mMetadataParcel.macAddress;
@@ -63,7 +59,6 @@
      *
      * @hide
      */
-    @SystemApi
     @Nullable
     public String getActionUrl() {
         return mMetadataParcel.actionUrl;
@@ -74,7 +69,6 @@
      *
      * @hide
      */
-    @SystemApi
     @Nullable
     public String getDeviceName() {
         return mMetadataParcel.deviceName;
@@ -85,7 +79,6 @@
      *
      * @hide
      */
-    @SystemApi
     @Nullable
     public String getTitle() {
         return mMetadataParcel.title;
@@ -96,7 +89,6 @@
      *
      * @hide
      */
-    @SystemApi
     @Nullable
     public String getDescription() {
         return mMetadataParcel.description;
@@ -107,7 +99,6 @@
      *
      * @hide
      */
-    @SystemApi
     @Nullable
     public String getDisplayUrl() {
         return mMetadataParcel.displayUrl;
@@ -118,7 +109,6 @@
      *
      * @hide
      */
-    @SystemApi
     public long getLastObservationTimestampMillis() {
         return mMetadataParcel.lastObservationTimestampMillis;
     }
@@ -128,7 +118,6 @@
      *
      * @hide
      */
-    @SystemApi
     public long getFirstObservationTimestampMillis() {
         return mMetadataParcel.firstObservationTimestampMillis;
     }
@@ -138,7 +127,6 @@
      *
      * @hide
      */
-    @SystemApi
     public int getState() {
         return mMetadataParcel.state;
     }
@@ -148,7 +136,6 @@
      *
      * @hide
      */
-    @SystemApi
     public int getActionUrlType() {
         return mMetadataParcel.actionUrlType;
     }
@@ -158,7 +145,6 @@
      *
      * @hide
      */
-    @SystemApi
     public int getRssi() {
         return mMetadataParcel.rssi;
     }
@@ -168,7 +154,6 @@
      *
      * @hide
      */
-    @SystemApi
     public long getPendingAppInstallTimestampMillis() {
         return mMetadataParcel.pendingAppInstallTimestampMillis;
     }
@@ -178,7 +163,6 @@
      *
      * @hide
      */
-    @SystemApi
     public int getTxPower() {
         return mMetadataParcel.txPower;
     }
@@ -188,7 +172,6 @@
      *
      * @hide
      */
-    @SystemApi
     @Nullable
     public String getAppName() {
         return mMetadataParcel.appName;
@@ -199,7 +182,6 @@
      *
      * @hide
      */
-    @SystemApi
     @Nullable
     public String getPackageName() {
         return mMetadataParcel.packageName;
@@ -210,7 +192,6 @@
      *
      * @hide
      */
-    @SystemApi
     @Nullable
     public String getTriggerId() {
         return mMetadataParcel.triggerId;
@@ -223,7 +204,6 @@
      * @return IconPng in 32-bit PNG with dimensions of 512px by 512px.
      * @hide
      */
-    @SystemApi
     @Nullable
     public byte[] getIconPng() {
         return mMetadataParcel.iconPng;
@@ -234,7 +214,6 @@
      *
      * @hide
      */
-    @SystemApi
     @Nullable
     public String getIconFfeUrl() {
         return mMetadataParcel.iconFifeUrl;
@@ -247,7 +226,6 @@
      * @return 64-byte authenticationPublicKeySecp256r1.
      * @hide
      */
-    @SystemApi
     @Nullable
     public byte[] getAuthenticationPublicKeySecp256r1() {
         return mMetadataParcel.authenticationPublicKeySecp256r1;
@@ -258,7 +236,6 @@
      *
      * @hide
      */
-    @SystemApi
     public static final class Builder {
 
         private final FastPairDiscoveryItemParcel mBuilderParcel;
@@ -268,7 +245,6 @@
          *
          * @hide
          */
-        @SystemApi
         public Builder() {
             mBuilderParcel = new FastPairDiscoveryItemParcel();
         }
@@ -281,7 +257,6 @@
          *
          * @hide
          */
-        @SystemApi
         @NonNull
         public Builder setId(@Nullable String id) {
             mBuilderParcel.id = id;
@@ -295,7 +270,6 @@
          * @return The builder, to facilitate chaining {@code builder.setXXX(..).setXXX(..)}.
          * @hide
          */
-        @SystemApi
         @NonNull
         public Builder setMacAddress(@Nullable String macAddress) {
             mBuilderParcel.macAddress = macAddress;
@@ -309,7 +283,6 @@
          * @return The builder, to facilitate chaining {@code builder.setXXX(..).setXXX(..)}.
          * @hide
          */
-        @SystemApi
         @NonNull
         public Builder setActionUrl(@Nullable String actionUrl) {
             mBuilderParcel.actionUrl = actionUrl;
@@ -322,7 +295,6 @@
          * @return The builder, to facilitate chaining {@code builder.setXXX(..).setXXX(..)}.
          * @hide
          */
-        @SystemApi
         @NonNull
         public Builder setDeviceName(@Nullable String deviceName) {
             mBuilderParcel.deviceName = deviceName;
@@ -336,7 +308,6 @@
          * @return The builder, to facilitate chaining {@code builder.setXXX(..).setXXX(..)}.
          * @hide
          */
-        @SystemApi
         @NonNull
         public Builder setTitle(@Nullable String title) {
             mBuilderParcel.title = title;
@@ -350,7 +321,6 @@
          * @return The builder, to facilitate chaining {@code builder.setXXX(..).setXXX(..)}.
          * @hide
          */
-        @SystemApi
         @NonNull
         public Builder setDescription(@Nullable String description) {
             mBuilderParcel.description = description;
@@ -364,7 +334,6 @@
          * @return The builder, to facilitate chaining {@code builder.setXXX(..).setXXX(..)}.
          * @hide
          */
-        @SystemApi
         @NonNull
         public Builder setDisplayUrl(@Nullable String displayUrl) {
             mBuilderParcel.displayUrl = displayUrl;
@@ -379,7 +348,6 @@
          * @return The builder, to facilitate chaining {@code builder.setXXX(..).setXXX(..)}.
          * @hide
          */
-        @SystemApi
         @NonNull
         public Builder setLastObservationTimestampMillis(
                 long lastObservationTimestampMillis) {
@@ -395,7 +363,6 @@
          * @return The builder, to facilitate chaining {@code builder.setXXX(..).setXXX(..)}.
          * @hide
          */
-        @SystemApi
         @NonNull
         public Builder setFirstObservationTimestampMillis(
                 long firstObservationTimestampMillis) {
@@ -410,7 +377,6 @@
          * @return The builder, to facilitate chaining {@code builder.setXXX(..).setXXX(..)}.
          * @hide
          */
-        @SystemApi
         @NonNull
         public Builder setState(int state) {
             mBuilderParcel.state = state;
@@ -424,7 +390,6 @@
          * @return The builder, to facilitate chaining {@code builder.setXXX(..).setXXX(..)}.
          * @hide
          */
-        @SystemApi
         @NonNull
         public Builder setActionUrlType(int actionUrlType) {
             mBuilderParcel.actionUrlType = actionUrlType;
@@ -438,7 +403,6 @@
          * @return The builder, to facilitate chaining {@code builder.setXXX(..).setXXX(..)}.
          * @hide
          */
-        @SystemApi
         @NonNull
         public Builder setRssi(int rssi) {
             mBuilderParcel.rssi = rssi;
@@ -453,7 +417,6 @@
          * @return The builder, to facilitate chaining {@code builder.setXXX(..).setXXX(..)}.
          * @hide
          */
-        @SystemApi
         @NonNull
         public Builder setPendingAppInstallTimestampMillis(long pendingAppInstallTimestampMillis) {
             mBuilderParcel.pendingAppInstallTimestampMillis = pendingAppInstallTimestampMillis;
@@ -467,7 +430,6 @@
          * @return The builder, to facilitate chaining {@code builder.setXXX(..).setXXX(..)}.
          * @hide
          */
-        @SystemApi
         @NonNull
         public Builder setTxPower(int txPower) {
             mBuilderParcel.txPower = txPower;
@@ -481,7 +443,6 @@
          * @return The builder, to facilitate chaining {@code builder.setXXX(..).setXXX(..)}.
          * @hide
          */
-        @SystemApi
         @NonNull
         public Builder setAppName(@Nullable String appName) {
             mBuilderParcel.appName = appName;
@@ -495,7 +456,6 @@
          * @return The builder, to facilitate chaining {@code builder.setXXX(..).setXXX(..)}.
          * @hide
          */
-        @SystemApi
         @NonNull
         public Builder setPackageName(@Nullable String packageName) {
             mBuilderParcel.packageName = packageName;
@@ -509,7 +469,6 @@
          * @return The builder, to facilitate chaining {@code builder.setXXX(..).setXXX(..)}.
          * @hide
          */
-        @SystemApi
         @NonNull
         public Builder setTriggerId(@Nullable String triggerId) {
             mBuilderParcel.triggerId = triggerId;
@@ -523,7 +482,6 @@
          * @return The builder, to facilitate chaining {@code builder.setXXX(..).setXXX(..)}.
          * @hide
          */
-        @SystemApi
         @NonNull
         public Builder setIconPng(@Nullable byte[] iconPng) {
             mBuilderParcel.iconPng = iconPng;
@@ -537,7 +495,6 @@
          * @return The builder, to facilitate chaining {@code builder.setXXX(..).setXXX(..)}.
          * @hide
          */
-        @SystemApi
         @NonNull
         public Builder setIconFfeUrl(@Nullable String iconFifeUrl) {
             mBuilderParcel.iconFifeUrl = iconFifeUrl;
@@ -552,7 +509,6 @@
          * @return The builder, to facilitate chaining {@code builder.setXXX(..).setXXX(..)}.
          * @hide
          */
-        @SystemApi
         @NonNull
         public Builder setAuthenticationPublicKeySecp256r1(
                 @Nullable byte[] authenticationPublicKeySecp256r1) {
@@ -565,7 +521,6 @@
          *
          * @hide
          */
-        @SystemApi
         @NonNull
         public FastPairDiscoveryItem build() {
             return new FastPairDiscoveryItem(mBuilderParcel);
diff --git a/nearby/framework/java/android/nearby/FastPairEligibleAccount.java b/nearby/framework/java/android/nearby/FastPairEligibleAccount.java
index e6c3047..8be4cca 100644
--- a/nearby/framework/java/android/nearby/FastPairEligibleAccount.java
+++ b/nearby/framework/java/android/nearby/FastPairEligibleAccount.java
@@ -19,7 +19,6 @@
 import android.accounts.Account;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
-import android.annotation.SystemApi;
 import android.nearby.aidl.FastPairEligibleAccountParcel;
 
 /**
@@ -27,7 +26,6 @@
  *
  * @hide
  */
-@SystemApi
 public class FastPairEligibleAccount {
 
     FastPairEligibleAccountParcel mAccountParcel;
@@ -41,7 +39,6 @@
      *
      * @hide
      */
-    @SystemApi
     @Nullable
     public Account getAccount() {
         return this.mAccountParcel.account;
@@ -52,7 +49,6 @@
      *
      * @hide
      */
-    @SystemApi
     public boolean isOptIn() {
         return this.mAccountParcel.optIn;
     }
@@ -62,7 +58,6 @@
      *
      * @hide
      */
-    @SystemApi
     public static final class Builder {
 
         private final FastPairEligibleAccountParcel mBuilderParcel;
@@ -72,7 +67,6 @@
          *
          * @hide
          */
-        @SystemApi
         public Builder() {
             mBuilderParcel = new FastPairEligibleAccountParcel();
             mBuilderParcel.account = null;
@@ -86,7 +80,6 @@
          * @return The builder, to facilitate chaining {@code builder.setXXX(..).setXXX(..)}.
          * @hide
          */
-        @SystemApi
         @NonNull
         public Builder setAccount(@Nullable Account account) {
             mBuilderParcel.account = account;
@@ -100,7 +93,6 @@
          * @return The builder, to facilitate chaining {@code builder.setXXX(..).setXXX(..)}.
          * @hide
          */
-        @SystemApi
         @NonNull
         public Builder setOptIn(boolean optIn) {
             mBuilderParcel.optIn = optIn;
@@ -112,7 +104,6 @@
          *
          * @hide
          */
-        @SystemApi
         @NonNull
         public FastPairEligibleAccount build() {
             return new FastPairEligibleAccount(mBuilderParcel);
diff --git a/nearby/service/Android.bp b/nearby/service/Android.bp
index 802e2c8..7112bb1 100644
--- a/nearby/service/Android.bp
+++ b/nearby/service/Android.bp
@@ -41,6 +41,39 @@
     sdk_version: "module_current",
 }
 
+// Common lib for nearby end-to-end testing.
+java_library {
+    name: "nearby-common-lib",
+    srcs: [
+        "java/com/android/server/nearby/common/bloomfilter/*.java",
+        "java/com/android/server/nearby/common/bluetooth/*.java",
+        "java/com/android/server/nearby/common/bluetooth/fastpair/AesCtrMultipleBlockEncryption.java",
+        "java/com/android/server/nearby/common/bluetooth/fastpair/AesEcbSingleBlockEncryption.java",
+        "java/com/android/server/nearby/common/bluetooth/fastpair/BluetoothAddress.java",
+        "java/com/android/server/nearby/common/bluetooth/fastpair/BluetoothUuids.java",
+        "java/com/android/server/nearby/common/bluetooth/fastpair/Bytes.java",
+        "java/com/android/server/nearby/common/bluetooth/fastpair/Constants.java",
+        "java/com/android/server/nearby/common/bluetooth/fastpair/EllipticCurveDiffieHellmanExchange.java",
+        "java/com/android/server/nearby/common/bluetooth/fastpair/HmacSha256.java",
+        "java/com/android/server/nearby/common/bluetooth/fastpair/Ltv.java",
+        "java/com/android/server/nearby/common/bluetooth/fastpair/MessageStreamHmacEncoder.java",
+        "java/com/android/server/nearby/common/bluetooth/fastpair/NamingEncoder.java",
+        "java/com/android/server/nearby/common/bluetooth/testability/**/*.java",
+        "java/com/android/server/nearby/common/bluetooth/gatt/*.java",
+        "java/com/android/server/nearby/common/bluetooth/util/*.java",
+    ],
+    libs: [
+        "androidx.annotation_annotation",
+        "androidx.core_core",
+        "error_prone_annotations",
+        "framework-bluetooth",
+        "guava",
+    ],
+    sdk_version: "module_current",
+    visibility: [
+        "//packages/modules/Connectivity/nearby/tests/multidevices/clients/test_support/fastpair_provider",
+    ],
+}
 
 // Main lib for nearby services.
 java_library {
diff --git a/nearby/tests/cts/fastpair/src/android/nearby/cts/FastPairAntispoofKeyDeviceMetadataTest.java b/nearby/tests/cts/fastpair/src/android/nearby/cts/FastPairAntispoofKeyDeviceMetadataTest.java
deleted file mode 100644
index 65c061b..0000000
--- a/nearby/tests/cts/fastpair/src/android/nearby/cts/FastPairAntispoofKeyDeviceMetadataTest.java
+++ /dev/null
@@ -1,191 +0,0 @@
-/*
- * Copyright (C) 2021 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.nearby.cts;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import android.nearby.FastPairAntispoofKeyDeviceMetadata;
-import android.nearby.FastPairDeviceMetadata;
-
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.SdkSuppress;
-
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-@RunWith(AndroidJUnit4.class)
-public class FastPairAntispoofKeyDeviceMetadataTest {
-
-    private static final String ASSISTANT_SETUP_HALFSHEET = "ASSISTANT_SETUP_HALFSHEET";
-    private static final String ASSISTANT_SETUP_NOTIFICATION = "ASSISTANT_SETUP_NOTIFICATION";
-    private static final int BLE_TX_POWER  = 5;
-    private static final String CONFIRM_PIN_DESCRIPTION = "CONFIRM_PIN_DESCRIPTION";
-    private static final String CONFIRM_PIN_TITLE = "CONFIRM_PIN_TITLE";
-    private static final String CONNECT_SUCCESS_COMPANION_APP_INSTALLED =
-            "CONNECT_SUCCESS_COMPANION_APP_INSTALLED";
-    private static final String CONNECT_SUCCESS_COMPANION_APP_NOT_INSTALLED =
-            "CONNECT_SUCCESS_COMPANION_APP_NOT_INSTALLED";
-    private static final float DELTA = 0.001f;
-    private static final int DEVICE_TYPE = 7;
-    private static final String DOWNLOAD_COMPANION_APP_DESCRIPTION =
-            "DOWNLOAD_COMPANION_APP_DESCRIPTION";
-    private static final String FAIL_CONNECT_GOTO_SETTINGS_DESCRIPTION =
-            "FAIL_CONNECT_GOTO_SETTINGS_DESCRIPTION";
-    private static final String FAST_PAIR_TV_CONNECT_DEVICE_NO_ACCOUNT_DESCRIPTION =
-            "FAST_PAIR_TV_CONNECT_DEVICE_NO_ACCOUNT_DESCRIPTION";
-    private static final byte[] IMAGE = new byte[] {7, 9};
-    private static final String IMAGE_URL = "IMAGE_URL";
-    private static final String INITIAL_NOTIFICATION_DESCRIPTION =
-            "INITIAL_NOTIFICATION_DESCRIPTION";
-    private static final String INITIAL_NOTIFICATION_DESCRIPTION_NO_ACCOUNT =
-            "INITIAL_NOTIFICATION_DESCRIPTION_NO_ACCOUNT";
-    private static final String INITIAL_PAIRING_DESCRIPTION = "INITIAL_PAIRING_DESCRIPTION";
-    private static final String INTENT_URI = "INTENT_URI";
-    private static final String LOCALE = "LOCALE";
-    private static final String OPEN_COMPANION_APP_DESCRIPTION = "OPEN_COMPANION_APP_DESCRIPTION";
-    private static final String RETRO_ACTIVE_PAIRING_DESCRIPTION =
-            "RETRO_ACTIVE_PAIRING_DESCRIPTION";
-    private static final String SUBSEQUENT_PAIRING_DESCRIPTION = "SUBSEQUENT_PAIRING_DESCRIPTION";
-    private static final String SYNC_CONTACT_DESCRPTION = "SYNC_CONTACT_DESCRPTION";
-    private static final String SYNC_CONTACTS_TITLE = "SYNC_CONTACTS_TITLE";
-    private static final String SYNC_SMS_DESCRIPTION = "SYNC_SMS_DESCRIPTION";
-    private static final String SYNC_SMS_TITLE = "SYNC_SMS_TITLE";
-    private static final float TRIGGER_DISTANCE = 111;
-    private static final String TRUE_WIRELESS_IMAGE_URL_CASE = "TRUE_WIRELESS_IMAGE_URL_CASE";
-    private static final String TRUE_WIRELESS_IMAGE_URL_LEFT_BUD =
-            "TRUE_WIRELESS_IMAGE_URL_LEFT_BUD";
-    private static final String TRUE_WIRELESS_IMAGE_URL_RIGHT_BUD =
-            "TRUE_WIRELESS_IMAGE_URL_RIGHT_BUD";
-    private static final String UNABLE_TO_CONNECT_DESCRIPTION = "UNABLE_TO_CONNECT_DESCRIPTION";
-    private static final String UNABLE_TO_CONNECT_TITLE = "UNABLE_TO_CONNECT_TITLE";
-    private static final String UPDATE_COMPANION_APP_DESCRIPTION =
-            "UPDATE_COMPANION_APP_DESCRIPTION";
-    private static final String WAIT_LAUNCH_COMPANION_APP_DESCRIPTION =
-            "WAIT_LAUNCH_COMPANION_APP_DESCRIPTION";
-    private static final byte[] ANTI_SPOOFING_KEY = new byte[] {4, 5, 6};
-    private static final String NAME = "NAME";
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testSetGetFastPairAntispoofKeyDeviceMetadataNotNull() {
-        FastPairDeviceMetadata fastPairDeviceMetadata = genFastPairDeviceMetadata();
-        FastPairAntispoofKeyDeviceMetadata fastPairAntispoofKeyDeviceMetadata =
-                genFastPairAntispoofKeyDeviceMetadata(ANTI_SPOOFING_KEY, fastPairDeviceMetadata);
-
-        assertThat(fastPairAntispoofKeyDeviceMetadata.getAntispoofPublicKey()).isEqualTo(
-                ANTI_SPOOFING_KEY);
-        ensureFastPairDeviceMetadataAsExpected(
-                fastPairAntispoofKeyDeviceMetadata.getFastPairDeviceMetadata());
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testSetGetFastPairAntispoofKeyDeviceMetadataNull() {
-        FastPairAntispoofKeyDeviceMetadata fastPairAntispoofKeyDeviceMetadata =
-                genFastPairAntispoofKeyDeviceMetadata(null, null);
-        assertThat(fastPairAntispoofKeyDeviceMetadata.getAntispoofPublicKey()).isEqualTo(
-                null);
-        assertThat(fastPairAntispoofKeyDeviceMetadata.getFastPairDeviceMetadata()).isEqualTo(
-                null);
-    }
-
-    /* Verifies DeviceMetadata. */
-    private static void ensureFastPairDeviceMetadataAsExpected(FastPairDeviceMetadata metadata) {
-        assertThat(metadata.getBleTxPower()).isEqualTo(BLE_TX_POWER);
-        assertThat(metadata.getConnectSuccessCompanionAppInstalled())
-                .isEqualTo(CONNECT_SUCCESS_COMPANION_APP_INSTALLED);
-        assertThat(metadata.getConnectSuccessCompanionAppNotInstalled())
-                .isEqualTo(CONNECT_SUCCESS_COMPANION_APP_NOT_INSTALLED);
-        assertThat(metadata.getDeviceType()).isEqualTo(DEVICE_TYPE);
-        assertThat(metadata.getDownloadCompanionAppDescription())
-                .isEqualTo(DOWNLOAD_COMPANION_APP_DESCRIPTION);
-        assertThat(metadata.getFailConnectGoToSettingsDescription())
-                .isEqualTo(FAIL_CONNECT_GOTO_SETTINGS_DESCRIPTION);
-        assertThat(metadata.getImage()).isEqualTo(IMAGE);
-        assertThat(metadata.getImageUrl()).isEqualTo(IMAGE_URL);
-        assertThat(metadata.getInitialNotificationDescription())
-                .isEqualTo(INITIAL_NOTIFICATION_DESCRIPTION);
-        assertThat(metadata.getInitialNotificationDescriptionNoAccount())
-                .isEqualTo(INITIAL_NOTIFICATION_DESCRIPTION_NO_ACCOUNT);
-        assertThat(metadata.getInitialPairingDescription()).isEqualTo(INITIAL_PAIRING_DESCRIPTION);
-        assertThat(metadata.getIntentUri()).isEqualTo(INTENT_URI);
-        assertThat(metadata.getName()).isEqualTo(NAME);
-        assertThat(metadata.getOpenCompanionAppDescription())
-                .isEqualTo(OPEN_COMPANION_APP_DESCRIPTION);
-        assertThat(metadata.getRetroactivePairingDescription())
-                .isEqualTo(RETRO_ACTIVE_PAIRING_DESCRIPTION);
-        assertThat(metadata.getSubsequentPairingDescription())
-                .isEqualTo(SUBSEQUENT_PAIRING_DESCRIPTION);
-        assertThat(metadata.getTriggerDistance()).isWithin(DELTA).of(TRIGGER_DISTANCE);
-        assertThat(metadata.getTrueWirelessImageUrlCase()).isEqualTo(TRUE_WIRELESS_IMAGE_URL_CASE);
-        assertThat(metadata.getTrueWirelessImageUrlLeftBud())
-                .isEqualTo(TRUE_WIRELESS_IMAGE_URL_LEFT_BUD);
-        assertThat(metadata.getTrueWirelessImageUrlRightBud())
-                .isEqualTo(TRUE_WIRELESS_IMAGE_URL_RIGHT_BUD);
-        assertThat(metadata.getUnableToConnectDescription())
-                .isEqualTo(UNABLE_TO_CONNECT_DESCRIPTION);
-        assertThat(metadata.getUnableToConnectTitle()).isEqualTo(UNABLE_TO_CONNECT_TITLE);
-        assertThat(metadata.getUpdateCompanionAppDescription())
-                .isEqualTo(UPDATE_COMPANION_APP_DESCRIPTION);
-        assertThat(metadata.getWaitLaunchCompanionAppDescription())
-                .isEqualTo(WAIT_LAUNCH_COMPANION_APP_DESCRIPTION);
-    }
-
-    /* Generates FastPairAntispoofKeyDeviceMetadata. */
-    private static FastPairAntispoofKeyDeviceMetadata genFastPairAntispoofKeyDeviceMetadata(
-            byte[] antispoofPublicKey, FastPairDeviceMetadata deviceMetadata) {
-        FastPairAntispoofKeyDeviceMetadata.Builder builder =
-                new FastPairAntispoofKeyDeviceMetadata.Builder();
-        builder.setAntispoofPublicKey(antispoofPublicKey);
-        builder.setFastPairDeviceMetadata(deviceMetadata);
-
-        return builder.build();
-    }
-
-    /* Generates FastPairDeviceMetadata. */
-    private static FastPairDeviceMetadata genFastPairDeviceMetadata() {
-        FastPairDeviceMetadata.Builder builder = new FastPairDeviceMetadata.Builder();
-        builder.setBleTxPower(BLE_TX_POWER);
-        builder.setConnectSuccessCompanionAppInstalled(CONNECT_SUCCESS_COMPANION_APP_INSTALLED);
-        builder.setConnectSuccessCompanionAppNotInstalled(
-                CONNECT_SUCCESS_COMPANION_APP_NOT_INSTALLED);
-        builder.setDeviceType(DEVICE_TYPE);
-        builder.setDownloadCompanionAppDescription(DOWNLOAD_COMPANION_APP_DESCRIPTION);
-        builder.setFailConnectGoToSettingsDescription(FAIL_CONNECT_GOTO_SETTINGS_DESCRIPTION);
-        builder.setImage(IMAGE);
-        builder.setImageUrl(IMAGE_URL);
-        builder.setInitialNotificationDescription(INITIAL_NOTIFICATION_DESCRIPTION);
-        builder.setInitialNotificationDescriptionNoAccount(
-                INITIAL_NOTIFICATION_DESCRIPTION_NO_ACCOUNT);
-        builder.setInitialPairingDescription(INITIAL_PAIRING_DESCRIPTION);
-        builder.setIntentUri(INTENT_URI);
-        builder.setName(NAME);
-        builder.setOpenCompanionAppDescription(OPEN_COMPANION_APP_DESCRIPTION);
-        builder.setRetroactivePairingDescription(RETRO_ACTIVE_PAIRING_DESCRIPTION);
-        builder.setSubsequentPairingDescription(SUBSEQUENT_PAIRING_DESCRIPTION);
-        builder.setTriggerDistance(TRIGGER_DISTANCE);
-        builder.setTrueWirelessImageUrlCase(TRUE_WIRELESS_IMAGE_URL_CASE);
-        builder.setTrueWirelessImageUrlLeftBud(TRUE_WIRELESS_IMAGE_URL_LEFT_BUD);
-        builder.setTrueWirelessImageUrlRightBud(TRUE_WIRELESS_IMAGE_URL_RIGHT_BUD);
-        builder.setUnableToConnectDescription(UNABLE_TO_CONNECT_DESCRIPTION);
-        builder.setUnableToConnectTitle(UNABLE_TO_CONNECT_TITLE);
-        builder.setUpdateCompanionAppDescription(UPDATE_COMPANION_APP_DESCRIPTION);
-        builder.setWaitLaunchCompanionAppDescription(WAIT_LAUNCH_COMPANION_APP_DESCRIPTION);
-
-        return builder.build();
-    }
-}
diff --git a/nearby/tests/cts/fastpair/src/android/nearby/cts/FastPairDataProviderServiceTest.java b/nearby/tests/cts/fastpair/src/android/nearby/cts/FastPairDataProviderServiceTest.java
deleted file mode 100644
index 160da56..0000000
--- a/nearby/tests/cts/fastpair/src/android/nearby/cts/FastPairDataProviderServiceTest.java
+++ /dev/null
@@ -1,994 +0,0 @@
-/*
- * Copyright (C) 2021 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.nearby.cts;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.Mockito.verify;
-import static org.mockito.MockitoAnnotations.initMocks;
-
-import android.accounts.Account;
-import android.content.Intent;
-import android.nearby.FastPairAccountKeyDeviceMetadata;
-import android.nearby.FastPairAntispoofKeyDeviceMetadata;
-import android.nearby.FastPairDataProviderService;
-import android.nearby.FastPairDeviceMetadata;
-import android.nearby.FastPairDiscoveryItem;
-import android.nearby.FastPairEligibleAccount;
-import android.nearby.aidl.ByteArrayParcel;
-import android.nearby.aidl.FastPairAccountDevicesMetadataRequestParcel;
-import android.nearby.aidl.FastPairAccountKeyDeviceMetadataParcel;
-import android.nearby.aidl.FastPairAntispoofKeyDeviceMetadataParcel;
-import android.nearby.aidl.FastPairAntispoofKeyDeviceMetadataRequestParcel;
-import android.nearby.aidl.FastPairDeviceMetadataParcel;
-import android.nearby.aidl.FastPairDiscoveryItemParcel;
-import android.nearby.aidl.FastPairEligibleAccountParcel;
-import android.nearby.aidl.FastPairEligibleAccountsRequestParcel;
-import android.nearby.aidl.FastPairManageAccountDeviceRequestParcel;
-import android.nearby.aidl.FastPairManageAccountRequestParcel;
-import android.nearby.aidl.IFastPairAccountDevicesMetadataCallback;
-import android.nearby.aidl.IFastPairAntispoofKeyDeviceMetadataCallback;
-import android.nearby.aidl.IFastPairDataProvider;
-import android.nearby.aidl.IFastPairEligibleAccountsCallback;
-import android.nearby.aidl.IFastPairManageAccountCallback;
-import android.nearby.aidl.IFastPairManageAccountDeviceCallback;
-
-import androidx.annotation.NonNull;
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.SdkSuppress;
-
-import com.google.common.collect.ImmutableList;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.ArgumentCaptor;
-import org.mockito.Captor;
-import org.mockito.Mock;
-
-@RunWith(AndroidJUnit4.class)
-public class FastPairDataProviderServiceTest {
-
-    private static final String TAG = "FastPairDataProviderServiceTest";
-
-    private static final String ASSISTANT_SETUP_HALFSHEET = "ASSISTANT_SETUP_HALFSHEET";
-    private static final String ASSISTANT_SETUP_NOTIFICATION = "ASSISTANT_SETUP_NOTIFICATION";
-    private static final int BLE_TX_POWER  = 5;
-    private static final String CONFIRM_PIN_DESCRIPTION = "CONFIRM_PIN_DESCRIPTION";
-    private static final String CONFIRM_PIN_TITLE = "CONFIRM_PIN_TITLE";
-    private static final String CONNECT_SUCCESS_COMPANION_APP_INSTALLED =
-            "CONNECT_SUCCESS_COMPANION_APP_INSTALLED";
-    private static final String CONNECT_SUCCESS_COMPANION_APP_NOT_INSTALLED =
-            "CONNECT_SUCCESS_COMPANION_APP_NOT_INSTALLED";
-    private static final float DELTA = 0.001f;
-    private static final int DEVICE_TYPE = 7;
-    private static final String DOWNLOAD_COMPANION_APP_DESCRIPTION =
-            "DOWNLOAD_COMPANION_APP_DESCRIPTION";
-    private static final Account ELIGIBLE_ACCOUNT_1 = new Account("abc@google.com", "type1");
-    private static final boolean ELIGIBLE_ACCOUNT_1_OPT_IN = true;
-    private static final Account ELIGIBLE_ACCOUNT_2 = new Account("def@gmail.com", "type2");
-    private static final boolean ELIGIBLE_ACCOUNT_2_OPT_IN = false;
-    private static final Account MANAGE_ACCOUNT = new Account("ghi@gmail.com", "type3");
-    private static final Account ACCOUNTDEVICES_METADATA_ACCOUNT =
-            new Account("jk@gmail.com", "type4");
-    private static final int NUM_ACCOUNT_DEVICES = 2;
-
-    private static final int ERROR_CODE_BAD_REQUEST =
-            FastPairDataProviderService.ERROR_CODE_BAD_REQUEST;
-    private static final int MANAGE_ACCOUNT_REQUEST_TYPE =
-            FastPairDataProviderService.MANAGE_REQUEST_ADD;
-    private static final String ERROR_STRING = "ERROR_STRING";
-    private static final String FAIL_CONNECT_GOTO_SETTINGS_DESCRIPTION =
-            "FAIL_CONNECT_GOTO_SETTINGS_DESCRIPTION";
-    private static final String FAST_PAIR_TV_CONNECT_DEVICE_NO_ACCOUNT_DESCRIPTION =
-            "FAST_PAIR_TV_CONNECT_DEVICE_NO_ACCOUNT_DESCRIPTION";
-    private static final byte[] IMAGE = new byte[] {7, 9};
-    private static final String IMAGE_URL = "IMAGE_URL";
-    private static final String INITIAL_NOTIFICATION_DESCRIPTION =
-            "INITIAL_NOTIFICATION_DESCRIPTION";
-    private static final String INITIAL_NOTIFICATION_DESCRIPTION_NO_ACCOUNT =
-            "INITIAL_NOTIFICATION_DESCRIPTION_NO_ACCOUNT";
-    private static final String INITIAL_PAIRING_DESCRIPTION = "INITIAL_PAIRING_DESCRIPTION";
-    private static final String INTENT_URI = "INTENT_URI";
-    private static final String LOCALE = "LOCALE";
-    private static final String OPEN_COMPANION_APP_DESCRIPTION = "OPEN_COMPANION_APP_DESCRIPTION";
-    private static final String RETRO_ACTIVE_PAIRING_DESCRIPTION =
-            "RETRO_ACTIVE_PAIRING_DESCRIPTION";
-    private static final String SUBSEQUENT_PAIRING_DESCRIPTION = "SUBSEQUENT_PAIRING_DESCRIPTION";
-    private static final String SYNC_CONTACT_DESCRPTION = "SYNC_CONTACT_DESCRPTION";
-    private static final String SYNC_CONTACTS_TITLE = "SYNC_CONTACTS_TITLE";
-    private static final String SYNC_SMS_DESCRIPTION = "SYNC_SMS_DESCRIPTION";
-    private static final String SYNC_SMS_TITLE = "SYNC_SMS_TITLE";
-    private static final float TRIGGER_DISTANCE = 111;
-    private static final String TRUE_WIRELESS_IMAGE_URL_CASE = "TRUE_WIRELESS_IMAGE_URL_CASE";
-    private static final String TRUE_WIRELESS_IMAGE_URL_LEFT_BUD =
-            "TRUE_WIRELESS_IMAGE_URL_LEFT_BUD";
-    private static final String TRUE_WIRELESS_IMAGE_URL_RIGHT_BUD =
-            "TRUE_WIRELESS_IMAGE_URL_RIGHT_BUD";
-    private static final String UNABLE_TO_CONNECT_DESCRIPTION = "UNABLE_TO_CONNECT_DESCRIPTION";
-    private static final String UNABLE_TO_CONNECT_TITLE = "UNABLE_TO_CONNECT_TITLE";
-    private static final String UPDATE_COMPANION_APP_DESCRIPTION =
-            "UPDATE_COMPANION_APP_DESCRIPTION";
-    private static final String WAIT_LAUNCH_COMPANION_APP_DESCRIPTION =
-            "WAIT_LAUNCH_COMPANION_APP_DESCRIPTION";
-    private static final byte[] ACCOUNT_KEY = new byte[] {3};
-    private static final byte[] ACCOUNT_KEY_2 = new byte[] {9, 3};
-    private static final byte[] SHA256_ACCOUNT_KEY_PUBLIC_ADDRESS = new byte[] {2, 8};
-    private static final byte[] REQUEST_MODEL_ID = new byte[] {1, 2, 3};
-    private static final byte[] ANTI_SPOOFING_KEY = new byte[] {4, 5, 6};
-    private static final String ACTION_URL = "ACTION_URL";
-    private static final int ACTION_URL_TYPE = 5;
-    private static final String APP_NAME = "APP_NAME";
-    private static final int ATTACHMENT_TYPE = 8;
-    private static final byte[] AUTHENTICATION_PUBLIC_KEY_SEC_P256R1 = new byte[] {5, 7};
-    private static final byte[] BLE_RECORD_BYTES = new byte[]{2, 4};
-    private static final int DEBUG_CATEGORY = 9;
-    private static final String DEBUG_MESSAGE = "DEBUG_MESSAGE";
-    private static final String DESCRIPTION = "DESCRIPTION";
-    private static final String DEVICE_NAME = "DEVICE_NAME";
-    private static final String DISPLAY_URL = "DISPLAY_URL";
-    private static final String ENTITY_ID = "ENTITY_ID";
-    private static final String FEATURE_GRAPHIC_URL = "FEATURE_GRAPHIC_URL";
-    private static final long FIRST_OBSERVATION_TIMESTAMP_MILLIS = 8393L;
-    private static final String GROUP_ID = "GROUP_ID";
-    private static final String  ICON_FIFE_URL = "ICON_FIFE_URL";
-    private static final byte[]  ICON_PNG = new byte[]{2, 5};
-    private static final String ID = "ID";
-    private static final long LAST_OBSERVATION_TIMESTAMP_MILLIS = 934234L;
-    private static final int LAST_USER_EXPERIENCE = 93;
-    private static final long LOST_MILLIS = 393284L;
-    private static final String MAC_ADDRESS = "MAC_ADDRESS";
-    private static final String NAME = "NAME";
-    private static final String PACKAGE_NAME = "PACKAGE_NAME";
-    private static final long PENDING_APP_INSTALL_TIMESTAMP_MILLIS = 832393L;
-    private static final int RSSI = 9;
-    private static final int STATE = 63;
-    private static final String TITLE = "TITLE";
-    private static final String TRIGGER_ID = "TRIGGER_ID";
-    private static final int TX_POWER = 62;
-    private static final int TYPE = 73;
-    private static final String BLE_ADDRESS = "BLE_ADDRESS";
-
-    private static final int ELIGIBLE_ACCOUNTS_NUM = 2;
-    private static final ImmutableList<FastPairEligibleAccount> ELIGIBLE_ACCOUNTS =
-            ImmutableList.of(
-                    genHappyPathFastPairEligibleAccount(ELIGIBLE_ACCOUNT_1,
-                            ELIGIBLE_ACCOUNT_1_OPT_IN),
-                    genHappyPathFastPairEligibleAccount(ELIGIBLE_ACCOUNT_2,
-                            ELIGIBLE_ACCOUNT_2_OPT_IN));
-    private static final int ACCOUNTKEY_DEVICE_NUM = 2;
-    private static final ImmutableList<FastPairAccountKeyDeviceMetadata>
-            FAST_PAIR_ACCOUNT_DEVICES_METADATA =
-            ImmutableList.of(
-                    genHappyPathFastPairAccountkeyDeviceMetadata(),
-                    genHappyPathFastPairAccountkeyDeviceMetadata());
-
-    private static final FastPairAntispoofKeyDeviceMetadataRequestParcel
-            FAST_PAIR_ANTI_SPOOF_KEY_DEVICE_METADATA_REQUEST_PARCEL =
-            genFastPairAntispoofKeyDeviceMetadataRequestParcel();
-    private static final FastPairAccountDevicesMetadataRequestParcel
-            FAST_PAIR_ACCOUNT_DEVICES_METADATA_REQUEST_PARCEL =
-            genFastPairAccountDevicesMetadataRequestParcel();
-    private static final FastPairEligibleAccountsRequestParcel
-            FAST_PAIR_ELIGIBLE_ACCOUNTS_REQUEST_PARCEL =
-            genFastPairEligibleAccountsRequestParcel();
-    private static final FastPairManageAccountRequestParcel
-            FAST_PAIR_MANAGE_ACCOUNT_REQUEST_PARCEL =
-            genFastPairManageAccountRequestParcel();
-    private static final FastPairManageAccountDeviceRequestParcel
-            FAST_PAIR_MANAGE_ACCOUNT_DEVICE_REQUEST_PARCEL =
-            genFastPairManageAccountDeviceRequestParcel();
-    private static final FastPairAntispoofKeyDeviceMetadata
-            HAPPY_PATH_FAST_PAIR_ANTI_SPOOF_KEY_DEVICE_METADATA =
-            genHappyPathFastPairAntispoofKeyDeviceMetadata();
-
-    @Captor private ArgumentCaptor<FastPairEligibleAccountParcel[]>
-            mFastPairEligibleAccountParcelsArgumentCaptor;
-    @Captor private ArgumentCaptor<FastPairAccountKeyDeviceMetadataParcel[]>
-            mFastPairAccountKeyDeviceMetadataParcelsArgumentCaptor;
-
-    @Mock private FastPairDataProviderService mMockFastPairDataProviderService;
-    @Mock private IFastPairAntispoofKeyDeviceMetadataCallback.Stub
-            mAntispoofKeyDeviceMetadataCallback;
-    @Mock private IFastPairAccountDevicesMetadataCallback.Stub mAccountDevicesMetadataCallback;
-    @Mock private IFastPairEligibleAccountsCallback.Stub mEligibleAccountsCallback;
-    @Mock private IFastPairManageAccountCallback.Stub mManageAccountCallback;
-    @Mock private IFastPairManageAccountDeviceCallback.Stub mManageAccountDeviceCallback;
-
-    private MyHappyPathProvider mHappyPathFastPairDataProvider;
-    private MyErrorPathProvider mErrorPathFastPairDataProvider;
-
-    @Before
-    public void setUp() throws Exception {
-        initMocks(this);
-
-        mHappyPathFastPairDataProvider =
-                new MyHappyPathProvider(TAG, mMockFastPairDataProviderService);
-        mErrorPathFastPairDataProvider =
-                new MyErrorPathProvider(TAG, mMockFastPairDataProviderService);
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testHappyPathLoadFastPairAntispoofKeyDeviceMetadata() throws Exception {
-        // AOSP sends calls to OEM via Parcelable.
-        mHappyPathFastPairDataProvider.asProvider().loadFastPairAntispoofKeyDeviceMetadata(
-                FAST_PAIR_ANTI_SPOOF_KEY_DEVICE_METADATA_REQUEST_PARCEL,
-                mAntispoofKeyDeviceMetadataCallback);
-
-        // OEM receives request and verifies that it is as expected.
-        final ArgumentCaptor<FastPairDataProviderService.FastPairAntispoofKeyDeviceMetadataRequest>
-                fastPairAntispoofKeyDeviceMetadataRequestCaptor =
-                ArgumentCaptor.forClass(
-                        FastPairDataProviderService.FastPairAntispoofKeyDeviceMetadataRequest.class
-                );
-        verify(mMockFastPairDataProviderService).onLoadFastPairAntispoofKeyDeviceMetadata(
-                fastPairAntispoofKeyDeviceMetadataRequestCaptor.capture(),
-                any(FastPairDataProviderService.FastPairAntispoofKeyDeviceMetadataCallback.class));
-        ensureHappyPathAsExpected(fastPairAntispoofKeyDeviceMetadataRequestCaptor.getValue());
-
-        // AOSP receives responses and verifies that it is as expected.
-        final ArgumentCaptor<FastPairAntispoofKeyDeviceMetadataParcel>
-                fastPairAntispoofKeyDeviceMetadataParcelCaptor =
-                ArgumentCaptor.forClass(FastPairAntispoofKeyDeviceMetadataParcel.class);
-        verify(mAntispoofKeyDeviceMetadataCallback).onFastPairAntispoofKeyDeviceMetadataReceived(
-                fastPairAntispoofKeyDeviceMetadataParcelCaptor.capture());
-        ensureHappyPathAsExpected(fastPairAntispoofKeyDeviceMetadataParcelCaptor.getValue());
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testHappyPathLoadFastPairAccountDevicesMetadata() throws Exception {
-        // AOSP sends calls to OEM via Parcelable.
-        mHappyPathFastPairDataProvider.asProvider().loadFastPairAccountDevicesMetadata(
-                FAST_PAIR_ACCOUNT_DEVICES_METADATA_REQUEST_PARCEL,
-                mAccountDevicesMetadataCallback);
-
-        // OEM receives request and verifies that it is as expected.
-        final ArgumentCaptor<FastPairDataProviderService.FastPairAccountDevicesMetadataRequest>
-                fastPairAccountDevicesMetadataRequestCaptor =
-                ArgumentCaptor.forClass(
-                        FastPairDataProviderService.FastPairAccountDevicesMetadataRequest.class);
-        verify(mMockFastPairDataProviderService).onLoadFastPairAccountDevicesMetadata(
-                fastPairAccountDevicesMetadataRequestCaptor.capture(),
-                any(FastPairDataProviderService.FastPairAccountDevicesMetadataCallback.class));
-        ensureHappyPathAsExpected(fastPairAccountDevicesMetadataRequestCaptor.getValue());
-
-        // AOSP receives responses and verifies that it is as expected.
-        verify(mAccountDevicesMetadataCallback).onFastPairAccountDevicesMetadataReceived(
-                mFastPairAccountKeyDeviceMetadataParcelsArgumentCaptor.capture());
-        ensureHappyPathAsExpected(
-                mFastPairAccountKeyDeviceMetadataParcelsArgumentCaptor.getValue());
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testHappyPathLoadFastPairEligibleAccounts() throws Exception {
-        // AOSP sends calls to OEM via Parcelable.
-        mHappyPathFastPairDataProvider.asProvider().loadFastPairEligibleAccounts(
-                FAST_PAIR_ELIGIBLE_ACCOUNTS_REQUEST_PARCEL,
-                mEligibleAccountsCallback);
-
-        // OEM receives request and verifies that it is as expected.
-        final ArgumentCaptor<FastPairDataProviderService.FastPairEligibleAccountsRequest>
-                fastPairEligibleAccountsRequestCaptor =
-                ArgumentCaptor.forClass(
-                        FastPairDataProviderService.FastPairEligibleAccountsRequest.class);
-        verify(mMockFastPairDataProviderService).onLoadFastPairEligibleAccounts(
-                fastPairEligibleAccountsRequestCaptor.capture(),
-                any(FastPairDataProviderService.FastPairEligibleAccountsCallback.class));
-        ensureHappyPathAsExpected(fastPairEligibleAccountsRequestCaptor.getValue());
-
-        // AOSP receives responses and verifies that it is as expected.
-        verify(mEligibleAccountsCallback).onFastPairEligibleAccountsReceived(
-                mFastPairEligibleAccountParcelsArgumentCaptor.capture());
-        ensureHappyPathAsExpected(mFastPairEligibleAccountParcelsArgumentCaptor.getValue());
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testHappyPathManageFastPairAccount() throws Exception {
-        // AOSP sends calls to OEM via Parcelable.
-        mHappyPathFastPairDataProvider.asProvider().manageFastPairAccount(
-                FAST_PAIR_MANAGE_ACCOUNT_REQUEST_PARCEL,
-                mManageAccountCallback);
-
-        // OEM receives request and verifies that it is as expected.
-        final ArgumentCaptor<FastPairDataProviderService.FastPairManageAccountRequest>
-                fastPairManageAccountRequestCaptor =
-                ArgumentCaptor.forClass(
-                        FastPairDataProviderService.FastPairManageAccountRequest.class);
-        verify(mMockFastPairDataProviderService).onManageFastPairAccount(
-                fastPairManageAccountRequestCaptor.capture(),
-                any(FastPairDataProviderService.FastPairManageActionCallback.class));
-        ensureHappyPathAsExpected(fastPairManageAccountRequestCaptor.getValue());
-
-        // AOSP receives SUCCESS response.
-        verify(mManageAccountCallback).onSuccess();
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testHappyPathManageFastPairAccountDevice() throws Exception {
-        // AOSP sends calls to OEM via Parcelable.
-        mHappyPathFastPairDataProvider.asProvider().manageFastPairAccountDevice(
-                FAST_PAIR_MANAGE_ACCOUNT_DEVICE_REQUEST_PARCEL,
-                mManageAccountDeviceCallback);
-
-        // OEM receives request and verifies that it is as expected.
-        final ArgumentCaptor<FastPairDataProviderService.FastPairManageAccountDeviceRequest>
-                fastPairManageAccountDeviceRequestCaptor =
-                ArgumentCaptor.forClass(
-                        FastPairDataProviderService.FastPairManageAccountDeviceRequest.class);
-        verify(mMockFastPairDataProviderService).onManageFastPairAccountDevice(
-                fastPairManageAccountDeviceRequestCaptor.capture(),
-                any(FastPairDataProviderService.FastPairManageActionCallback.class));
-        ensureHappyPathAsExpected(fastPairManageAccountDeviceRequestCaptor.getValue());
-
-        // AOSP receives SUCCESS response.
-        verify(mManageAccountDeviceCallback).onSuccess();
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testErrorPathLoadFastPairAntispoofKeyDeviceMetadata() throws Exception {
-        mErrorPathFastPairDataProvider.asProvider().loadFastPairAntispoofKeyDeviceMetadata(
-                FAST_PAIR_ANTI_SPOOF_KEY_DEVICE_METADATA_REQUEST_PARCEL,
-                mAntispoofKeyDeviceMetadataCallback);
-        verify(mMockFastPairDataProviderService).onLoadFastPairAntispoofKeyDeviceMetadata(
-                any(FastPairDataProviderService.FastPairAntispoofKeyDeviceMetadataRequest.class),
-                any(FastPairDataProviderService.FastPairAntispoofKeyDeviceMetadataCallback.class));
-        verify(mAntispoofKeyDeviceMetadataCallback).onError(
-                eq(ERROR_CODE_BAD_REQUEST), eq(ERROR_STRING));
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testErrorPathLoadFastPairAccountDevicesMetadata() throws Exception {
-        mErrorPathFastPairDataProvider.asProvider().loadFastPairAccountDevicesMetadata(
-                FAST_PAIR_ACCOUNT_DEVICES_METADATA_REQUEST_PARCEL,
-                mAccountDevicesMetadataCallback);
-        verify(mMockFastPairDataProviderService).onLoadFastPairAccountDevicesMetadata(
-                any(FastPairDataProviderService.FastPairAccountDevicesMetadataRequest.class),
-                any(FastPairDataProviderService.FastPairAccountDevicesMetadataCallback.class));
-        verify(mAccountDevicesMetadataCallback).onError(
-                eq(ERROR_CODE_BAD_REQUEST), eq(ERROR_STRING));
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testErrorPathLoadFastPairEligibleAccounts() throws Exception {
-        mErrorPathFastPairDataProvider.asProvider().loadFastPairEligibleAccounts(
-                FAST_PAIR_ELIGIBLE_ACCOUNTS_REQUEST_PARCEL,
-                mEligibleAccountsCallback);
-        verify(mMockFastPairDataProviderService).onLoadFastPairEligibleAccounts(
-                any(FastPairDataProviderService.FastPairEligibleAccountsRequest.class),
-                any(FastPairDataProviderService.FastPairEligibleAccountsCallback.class));
-        verify(mEligibleAccountsCallback).onError(
-                eq(ERROR_CODE_BAD_REQUEST), eq(ERROR_STRING));
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testErrorPathManageFastPairAccount() throws Exception {
-        mErrorPathFastPairDataProvider.asProvider().manageFastPairAccount(
-                FAST_PAIR_MANAGE_ACCOUNT_REQUEST_PARCEL,
-                mManageAccountCallback);
-        verify(mMockFastPairDataProviderService).onManageFastPairAccount(
-                any(FastPairDataProviderService.FastPairManageAccountRequest.class),
-                any(FastPairDataProviderService.FastPairManageActionCallback.class));
-        verify(mManageAccountCallback).onError(eq(ERROR_CODE_BAD_REQUEST), eq(ERROR_STRING));
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testErrorPathManageFastPairAccountDevice() throws Exception {
-        mErrorPathFastPairDataProvider.asProvider().manageFastPairAccountDevice(
-                FAST_PAIR_MANAGE_ACCOUNT_DEVICE_REQUEST_PARCEL,
-                mManageAccountDeviceCallback);
-        verify(mMockFastPairDataProviderService).onManageFastPairAccountDevice(
-                any(FastPairDataProviderService.FastPairManageAccountDeviceRequest.class),
-                any(FastPairDataProviderService.FastPairManageActionCallback.class));
-        verify(mManageAccountDeviceCallback).onError(eq(ERROR_CODE_BAD_REQUEST), eq(ERROR_STRING));
-    }
-
-    public static class MyHappyPathProvider extends FastPairDataProviderService {
-
-        private final FastPairDataProviderService mMockFastPairDataProviderService;
-
-        public MyHappyPathProvider(@NonNull String tag, FastPairDataProviderService mock) {
-            super(tag);
-            mMockFastPairDataProviderService = mock;
-        }
-
-        public IFastPairDataProvider asProvider() {
-            Intent intent = new Intent();
-            return IFastPairDataProvider.Stub.asInterface(onBind(intent));
-        }
-
-        @Override
-        public void onLoadFastPairAntispoofKeyDeviceMetadata(
-                @NonNull FastPairAntispoofKeyDeviceMetadataRequest request,
-                @NonNull FastPairAntispoofKeyDeviceMetadataCallback callback) {
-            mMockFastPairDataProviderService.onLoadFastPairAntispoofKeyDeviceMetadata(
-                    request, callback);
-            callback.onFastPairAntispoofKeyDeviceMetadataReceived(
-                    HAPPY_PATH_FAST_PAIR_ANTI_SPOOF_KEY_DEVICE_METADATA);
-        }
-
-        @Override
-        public void onLoadFastPairAccountDevicesMetadata(
-                @NonNull FastPairAccountDevicesMetadataRequest request,
-                @NonNull FastPairAccountDevicesMetadataCallback callback) {
-            mMockFastPairDataProviderService.onLoadFastPairAccountDevicesMetadata(
-                    request, callback);
-            callback.onFastPairAccountDevicesMetadataReceived(FAST_PAIR_ACCOUNT_DEVICES_METADATA);
-        }
-
-        @Override
-        public void onLoadFastPairEligibleAccounts(
-                @NonNull FastPairEligibleAccountsRequest request,
-                @NonNull FastPairEligibleAccountsCallback callback) {
-            mMockFastPairDataProviderService.onLoadFastPairEligibleAccounts(
-                    request, callback);
-            callback.onFastPairEligibleAccountsReceived(ELIGIBLE_ACCOUNTS);
-        }
-
-        @Override
-        public void onManageFastPairAccount(
-                @NonNull FastPairManageAccountRequest request,
-                @NonNull FastPairManageActionCallback callback) {
-            mMockFastPairDataProviderService.onManageFastPairAccount(request, callback);
-            callback.onSuccess();
-        }
-
-        @Override
-        public void onManageFastPairAccountDevice(
-                @NonNull FastPairManageAccountDeviceRequest request,
-                @NonNull FastPairManageActionCallback callback) {
-            mMockFastPairDataProviderService.onManageFastPairAccountDevice(request, callback);
-            callback.onSuccess();
-        }
-    }
-
-    public static class MyErrorPathProvider extends FastPairDataProviderService {
-
-        private final FastPairDataProviderService mMockFastPairDataProviderService;
-
-        public MyErrorPathProvider(@NonNull String tag, FastPairDataProviderService mock) {
-            super(tag);
-            mMockFastPairDataProviderService = mock;
-        }
-
-        public IFastPairDataProvider asProvider() {
-            Intent intent = new Intent();
-            return IFastPairDataProvider.Stub.asInterface(onBind(intent));
-        }
-
-        @Override
-        public void onLoadFastPairAntispoofKeyDeviceMetadata(
-                @NonNull FastPairAntispoofKeyDeviceMetadataRequest request,
-                @NonNull FastPairAntispoofKeyDeviceMetadataCallback callback) {
-            mMockFastPairDataProviderService.onLoadFastPairAntispoofKeyDeviceMetadata(
-                    request, callback);
-            callback.onError(ERROR_CODE_BAD_REQUEST, ERROR_STRING);
-        }
-
-        @Override
-        public void onLoadFastPairAccountDevicesMetadata(
-                @NonNull FastPairAccountDevicesMetadataRequest request,
-                @NonNull FastPairAccountDevicesMetadataCallback callback) {
-            mMockFastPairDataProviderService.onLoadFastPairAccountDevicesMetadata(
-                    request, callback);
-            callback.onError(ERROR_CODE_BAD_REQUEST, ERROR_STRING);
-        }
-
-        @Override
-        public void onLoadFastPairEligibleAccounts(
-                @NonNull FastPairEligibleAccountsRequest request,
-                @NonNull FastPairEligibleAccountsCallback callback) {
-            mMockFastPairDataProviderService.onLoadFastPairEligibleAccounts(request, callback);
-            callback.onError(ERROR_CODE_BAD_REQUEST, ERROR_STRING);
-        }
-
-        @Override
-        public void onManageFastPairAccount(
-                @NonNull FastPairManageAccountRequest request,
-                @NonNull FastPairManageActionCallback callback) {
-            mMockFastPairDataProviderService.onManageFastPairAccount(request, callback);
-            callback.onError(ERROR_CODE_BAD_REQUEST, ERROR_STRING);
-        }
-
-        @Override
-        public void onManageFastPairAccountDevice(
-                @NonNull FastPairManageAccountDeviceRequest request,
-                @NonNull FastPairManageActionCallback callback) {
-            mMockFastPairDataProviderService.onManageFastPairAccountDevice(request, callback);
-            callback.onError(ERROR_CODE_BAD_REQUEST, ERROR_STRING);
-        }
-    }
-
-    /* Generates AntispoofKeyDeviceMetadataRequestParcel. */
-    private static FastPairAntispoofKeyDeviceMetadataRequestParcel
-            genFastPairAntispoofKeyDeviceMetadataRequestParcel() {
-        FastPairAntispoofKeyDeviceMetadataRequestParcel requestParcel =
-                new FastPairAntispoofKeyDeviceMetadataRequestParcel();
-        requestParcel.modelId = REQUEST_MODEL_ID;
-
-        return requestParcel;
-    }
-
-    /* Generates AccountDevicesMetadataRequestParcel. */
-    private static FastPairAccountDevicesMetadataRequestParcel
-            genFastPairAccountDevicesMetadataRequestParcel() {
-        FastPairAccountDevicesMetadataRequestParcel requestParcel =
-                new FastPairAccountDevicesMetadataRequestParcel();
-
-        requestParcel.account = ACCOUNTDEVICES_METADATA_ACCOUNT;
-        requestParcel.deviceAccountKeys = new ByteArrayParcel[NUM_ACCOUNT_DEVICES];
-        requestParcel.deviceAccountKeys[0] = new ByteArrayParcel();
-        requestParcel.deviceAccountKeys[1] = new ByteArrayParcel();
-        requestParcel.deviceAccountKeys[0].byteArray = ACCOUNT_KEY;
-        requestParcel.deviceAccountKeys[1].byteArray = ACCOUNT_KEY_2;
-
-        return requestParcel;
-    }
-
-    /* Generates FastPairEligibleAccountsRequestParcel. */
-    private static FastPairEligibleAccountsRequestParcel
-            genFastPairEligibleAccountsRequestParcel() {
-        FastPairEligibleAccountsRequestParcel requestParcel =
-                new FastPairEligibleAccountsRequestParcel();
-        // No fields since FastPairEligibleAccountsRequestParcel is just a place holder now.
-        return requestParcel;
-    }
-
-    /* Generates FastPairManageAccountRequestParcel. */
-    private static FastPairManageAccountRequestParcel
-            genFastPairManageAccountRequestParcel() {
-        FastPairManageAccountRequestParcel requestParcel =
-                new FastPairManageAccountRequestParcel();
-        requestParcel.account = MANAGE_ACCOUNT;
-        requestParcel.requestType = MANAGE_ACCOUNT_REQUEST_TYPE;
-
-        return requestParcel;
-    }
-
-    /* Generates FastPairManageAccountDeviceRequestParcel. */
-    private static FastPairManageAccountDeviceRequestParcel
-            genFastPairManageAccountDeviceRequestParcel() {
-        FastPairManageAccountDeviceRequestParcel requestParcel =
-                new FastPairManageAccountDeviceRequestParcel();
-        requestParcel.account = MANAGE_ACCOUNT;
-        requestParcel.requestType = MANAGE_ACCOUNT_REQUEST_TYPE;
-        requestParcel.accountKeyDeviceMetadata =
-                genHappyPathFastPairAccountkeyDeviceMetadataParcel();
-
-        return requestParcel;
-    }
-
-    /* Generates Happy Path AntispoofKeyDeviceMetadata. */
-    private static FastPairAntispoofKeyDeviceMetadata
-            genHappyPathFastPairAntispoofKeyDeviceMetadata() {
-        FastPairAntispoofKeyDeviceMetadata.Builder builder =
-                new FastPairAntispoofKeyDeviceMetadata.Builder();
-        builder.setAntispoofPublicKey(ANTI_SPOOFING_KEY);
-        builder.setFastPairDeviceMetadata(genHappyPathFastPairDeviceMetadata());
-
-        return builder.build();
-    }
-
-    /* Generates Happy Path FastPairAccountKeyDeviceMetadata. */
-    private static FastPairAccountKeyDeviceMetadata
-            genHappyPathFastPairAccountkeyDeviceMetadata() {
-        FastPairAccountKeyDeviceMetadata.Builder builder =
-                new FastPairAccountKeyDeviceMetadata.Builder();
-        builder.setDeviceAccountKey(ACCOUNT_KEY);
-        builder.setFastPairDeviceMetadata(genHappyPathFastPairDeviceMetadata());
-        builder.setSha256DeviceAccountKeyPublicAddress(SHA256_ACCOUNT_KEY_PUBLIC_ADDRESS);
-        builder.setFastPairDiscoveryItem(genHappyPathFastPairDiscoveryItem());
-
-        return builder.build();
-    }
-
-    /* Generates Happy Path FastPairAccountKeyDeviceMetadataParcel. */
-    private static FastPairAccountKeyDeviceMetadataParcel
-            genHappyPathFastPairAccountkeyDeviceMetadataParcel() {
-        FastPairAccountKeyDeviceMetadataParcel parcel =
-                new FastPairAccountKeyDeviceMetadataParcel();
-        parcel.deviceAccountKey = ACCOUNT_KEY;
-        parcel.metadata = genHappyPathFastPairDeviceMetadataParcel();
-        parcel.sha256DeviceAccountKeyPublicAddress = SHA256_ACCOUNT_KEY_PUBLIC_ADDRESS;
-        parcel.discoveryItem = genHappyPathFastPairDiscoveryItemParcel();
-
-        return parcel;
-    }
-
-    /* Generates Happy Path DiscoveryItem. */
-    private static FastPairDiscoveryItem genHappyPathFastPairDiscoveryItem() {
-        FastPairDiscoveryItem.Builder builder = new FastPairDiscoveryItem.Builder();
-
-        builder.setActionUrl(ACTION_URL);
-        builder.setActionUrlType(ACTION_URL_TYPE);
-        builder.setAppName(APP_NAME);
-        builder.setAuthenticationPublicKeySecp256r1(AUTHENTICATION_PUBLIC_KEY_SEC_P256R1);
-        builder.setDescription(DESCRIPTION);
-        builder.setDeviceName(DEVICE_NAME);
-        builder.setDisplayUrl(DISPLAY_URL);
-        builder.setFirstObservationTimestampMillis(FIRST_OBSERVATION_TIMESTAMP_MILLIS);
-        builder.setIconFfeUrl(ICON_FIFE_URL);
-        builder.setIconPng(ICON_PNG);
-        builder.setId(ID);
-        builder.setLastObservationTimestampMillis(LAST_OBSERVATION_TIMESTAMP_MILLIS);
-        builder.setMacAddress(MAC_ADDRESS);
-        builder.setPackageName(PACKAGE_NAME);
-        builder.setPendingAppInstallTimestampMillis(PENDING_APP_INSTALL_TIMESTAMP_MILLIS);
-        builder.setRssi(RSSI);
-        builder.setState(STATE);
-        builder.setTitle(TITLE);
-        builder.setTriggerId(TRIGGER_ID);
-        builder.setTxPower(TX_POWER);
-
-        return builder.build();
-    }
-
-    /* Generates Happy Path DiscoveryItemParcel. */
-    private static FastPairDiscoveryItemParcel genHappyPathFastPairDiscoveryItemParcel() {
-        FastPairDiscoveryItemParcel parcel = new FastPairDiscoveryItemParcel();
-
-        parcel.actionUrl = ACTION_URL;
-        parcel.actionUrlType = ACTION_URL_TYPE;
-        parcel.appName = APP_NAME;
-        parcel.authenticationPublicKeySecp256r1 = AUTHENTICATION_PUBLIC_KEY_SEC_P256R1;
-        parcel.description = DESCRIPTION;
-        parcel.deviceName = DEVICE_NAME;
-        parcel.displayUrl = DISPLAY_URL;
-        parcel.firstObservationTimestampMillis = FIRST_OBSERVATION_TIMESTAMP_MILLIS;
-        parcel.iconFifeUrl = ICON_FIFE_URL;
-        parcel.iconPng = ICON_PNG;
-        parcel.id = ID;
-        parcel.lastObservationTimestampMillis = LAST_OBSERVATION_TIMESTAMP_MILLIS;
-        parcel.macAddress = MAC_ADDRESS;
-        parcel.packageName = PACKAGE_NAME;
-        parcel.pendingAppInstallTimestampMillis = PENDING_APP_INSTALL_TIMESTAMP_MILLIS;
-        parcel.rssi = RSSI;
-        parcel.state = STATE;
-        parcel.title = TITLE;
-        parcel.triggerId = TRIGGER_ID;
-        parcel.txPower = TX_POWER;
-
-        return parcel;
-    }
-
-    /* Generates Happy Path DeviceMetadata. */
-    private static FastPairDeviceMetadata genHappyPathFastPairDeviceMetadata() {
-        FastPairDeviceMetadata.Builder builder = new FastPairDeviceMetadata.Builder();
-        builder.setBleTxPower(BLE_TX_POWER);
-        builder.setConnectSuccessCompanionAppInstalled(CONNECT_SUCCESS_COMPANION_APP_INSTALLED);
-        builder.setConnectSuccessCompanionAppNotInstalled(
-                CONNECT_SUCCESS_COMPANION_APP_NOT_INSTALLED);
-        builder.setDeviceType(DEVICE_TYPE);
-        builder.setDownloadCompanionAppDescription(DOWNLOAD_COMPANION_APP_DESCRIPTION);
-        builder.setFailConnectGoToSettingsDescription(FAIL_CONNECT_GOTO_SETTINGS_DESCRIPTION);
-        builder.setImage(IMAGE);
-        builder.setImageUrl(IMAGE_URL);
-        builder.setInitialNotificationDescription(INITIAL_NOTIFICATION_DESCRIPTION);
-        builder.setInitialNotificationDescriptionNoAccount(
-                INITIAL_NOTIFICATION_DESCRIPTION_NO_ACCOUNT);
-        builder.setInitialPairingDescription(INITIAL_PAIRING_DESCRIPTION);
-        builder.setIntentUri(INTENT_URI);
-        builder.setName(NAME);
-        builder.setOpenCompanionAppDescription(OPEN_COMPANION_APP_DESCRIPTION);
-        builder.setRetroactivePairingDescription(RETRO_ACTIVE_PAIRING_DESCRIPTION);
-        builder.setSubsequentPairingDescription(SUBSEQUENT_PAIRING_DESCRIPTION);
-        builder.setTriggerDistance(TRIGGER_DISTANCE);
-        builder.setTrueWirelessImageUrlCase(TRUE_WIRELESS_IMAGE_URL_CASE);
-        builder.setTrueWirelessImageUrlLeftBud(TRUE_WIRELESS_IMAGE_URL_LEFT_BUD);
-        builder.setTrueWirelessImageUrlRightBud(TRUE_WIRELESS_IMAGE_URL_RIGHT_BUD);
-        builder.setUnableToConnectDescription(UNABLE_TO_CONNECT_DESCRIPTION);
-        builder.setUnableToConnectTitle(UNABLE_TO_CONNECT_TITLE);
-        builder.setUpdateCompanionAppDescription(UPDATE_COMPANION_APP_DESCRIPTION);
-        builder.setWaitLaunchCompanionAppDescription(WAIT_LAUNCH_COMPANION_APP_DESCRIPTION);
-
-        return builder.build();
-    }
-
-    /* Generates Happy Path DeviceMetadataParcel. */
-    private static FastPairDeviceMetadataParcel genHappyPathFastPairDeviceMetadataParcel() {
-        FastPairDeviceMetadataParcel parcel = new FastPairDeviceMetadataParcel();
-
-        parcel.bleTxPower = BLE_TX_POWER;
-        parcel.connectSuccessCompanionAppInstalled = CONNECT_SUCCESS_COMPANION_APP_INSTALLED;
-        parcel.connectSuccessCompanionAppNotInstalled =
-                CONNECT_SUCCESS_COMPANION_APP_NOT_INSTALLED;
-        parcel.deviceType = DEVICE_TYPE;
-        parcel.downloadCompanionAppDescription = DOWNLOAD_COMPANION_APP_DESCRIPTION;
-        parcel.failConnectGoToSettingsDescription = FAIL_CONNECT_GOTO_SETTINGS_DESCRIPTION;
-        parcel.image = IMAGE;
-        parcel.imageUrl = IMAGE_URL;
-        parcel.initialNotificationDescription = INITIAL_NOTIFICATION_DESCRIPTION;
-        parcel.initialNotificationDescriptionNoAccount =
-                INITIAL_NOTIFICATION_DESCRIPTION_NO_ACCOUNT;
-        parcel.initialPairingDescription = INITIAL_PAIRING_DESCRIPTION;
-        parcel.intentUri = INTENT_URI;
-        parcel.name = NAME;
-        parcel.openCompanionAppDescription = OPEN_COMPANION_APP_DESCRIPTION;
-        parcel.retroactivePairingDescription = RETRO_ACTIVE_PAIRING_DESCRIPTION;
-        parcel.subsequentPairingDescription = SUBSEQUENT_PAIRING_DESCRIPTION;
-        parcel.triggerDistance = TRIGGER_DISTANCE;
-        parcel.trueWirelessImageUrlCase = TRUE_WIRELESS_IMAGE_URL_CASE;
-        parcel.trueWirelessImageUrlLeftBud = TRUE_WIRELESS_IMAGE_URL_LEFT_BUD;
-        parcel.trueWirelessImageUrlRightBud = TRUE_WIRELESS_IMAGE_URL_RIGHT_BUD;
-        parcel.unableToConnectDescription = UNABLE_TO_CONNECT_DESCRIPTION;
-        parcel.unableToConnectTitle = UNABLE_TO_CONNECT_TITLE;
-        parcel.updateCompanionAppDescription = UPDATE_COMPANION_APP_DESCRIPTION;
-        parcel.waitLaunchCompanionAppDescription = WAIT_LAUNCH_COMPANION_APP_DESCRIPTION;
-
-        return parcel;
-    }
-
-    /* Generates Happy Path FastPairEligibleAccount. */
-    private static FastPairEligibleAccount genHappyPathFastPairEligibleAccount(
-            Account account, boolean optIn) {
-        FastPairEligibleAccount.Builder builder = new FastPairEligibleAccount.Builder();
-        builder.setAccount(account);
-        builder.setOptIn(optIn);
-
-        return builder.build();
-    }
-
-    /* Verifies Happy Path AntispoofKeyDeviceMetadataRequest. */
-    private static void ensureHappyPathAsExpected(
-            FastPairDataProviderService.FastPairAntispoofKeyDeviceMetadataRequest request) {
-        assertThat(request.getModelId()).isEqualTo(REQUEST_MODEL_ID);
-    }
-
-    /* Verifies Happy Path AccountDevicesMetadataRequest. */
-    private static void ensureHappyPathAsExpected(
-            FastPairDataProviderService.FastPairAccountDevicesMetadataRequest request) {
-        assertThat(request.getAccount()).isEqualTo(ACCOUNTDEVICES_METADATA_ACCOUNT);
-        assertThat(request.getDeviceAccountKeys().size()).isEqualTo(ACCOUNTKEY_DEVICE_NUM);
-        assertThat(request.getDeviceAccountKeys()).contains(ACCOUNT_KEY);
-        assertThat(request.getDeviceAccountKeys()).contains(ACCOUNT_KEY_2);
-    }
-
-    /* Verifies Happy Path FastPairEligibleAccountsRequest. */
-    @SuppressWarnings("UnusedVariable")
-    private static void ensureHappyPathAsExpected(
-            FastPairDataProviderService.FastPairEligibleAccountsRequest request) {
-        // No fields since FastPairEligibleAccountsRequest is just a place holder now.
-    }
-
-    /* Verifies Happy Path FastPairManageAccountRequest. */
-    private static void ensureHappyPathAsExpected(
-            FastPairDataProviderService.FastPairManageAccountRequest request) {
-        assertThat(request.getAccount()).isEqualTo(MANAGE_ACCOUNT);
-        assertThat(request.getRequestType()).isEqualTo(MANAGE_ACCOUNT_REQUEST_TYPE);
-    }
-
-    /* Verifies Happy Path FastPairManageAccountDeviceRequest. */
-    private static void ensureHappyPathAsExpected(
-            FastPairDataProviderService.FastPairManageAccountDeviceRequest request) {
-        assertThat(request.getAccount()).isEqualTo(MANAGE_ACCOUNT);
-        assertThat(request.getRequestType()).isEqualTo(MANAGE_ACCOUNT_REQUEST_TYPE);
-        ensureHappyPathAsExpected(request.getAccountKeyDeviceMetadata());
-    }
-
-    /* Verifies Happy Path AntispoofKeyDeviceMetadataParcel. */
-    private static void ensureHappyPathAsExpected(
-            FastPairAntispoofKeyDeviceMetadataParcel metadataParcel) {
-        assertThat(metadataParcel).isNotNull();
-        assertThat(metadataParcel.antispoofPublicKey).isEqualTo(ANTI_SPOOFING_KEY);
-        ensureHappyPathAsExpected(metadataParcel.deviceMetadata);
-    }
-
-    /* Verifies Happy Path FastPairAccountKeyDeviceMetadataParcel[]. */
-    private static void ensureHappyPathAsExpected(
-            FastPairAccountKeyDeviceMetadataParcel[] metadataParcels) {
-        assertThat(metadataParcels).isNotNull();
-        assertThat(metadataParcels).hasLength(ACCOUNTKEY_DEVICE_NUM);
-        for (FastPairAccountKeyDeviceMetadataParcel parcel: metadataParcels) {
-            ensureHappyPathAsExpected(parcel);
-        }
-    }
-
-    /* Verifies Happy Path FastPairAccountKeyDeviceMetadataParcel. */
-    private static void ensureHappyPathAsExpected(
-            FastPairAccountKeyDeviceMetadataParcel metadataParcel) {
-        assertThat(metadataParcel).isNotNull();
-        assertThat(metadataParcel.deviceAccountKey).isEqualTo(ACCOUNT_KEY);
-        assertThat(metadataParcel.sha256DeviceAccountKeyPublicAddress)
-                .isEqualTo(SHA256_ACCOUNT_KEY_PUBLIC_ADDRESS);
-        ensureHappyPathAsExpected(metadataParcel.metadata);
-        ensureHappyPathAsExpected(metadataParcel.discoveryItem);
-    }
-
-    /* Verifies Happy Path FastPairAccountKeyDeviceMetadata. */
-    private static void ensureHappyPathAsExpected(
-            FastPairAccountKeyDeviceMetadata metadata) {
-        assertThat(metadata.getDeviceAccountKey()).isEqualTo(ACCOUNT_KEY);
-        assertThat(metadata.getSha256DeviceAccountKeyPublicAddress())
-                .isEqualTo(SHA256_ACCOUNT_KEY_PUBLIC_ADDRESS);
-        ensureHappyPathAsExpected(metadata.getFastPairDeviceMetadata());
-        ensureHappyPathAsExpected(metadata.getFastPairDiscoveryItem());
-    }
-
-    /* Verifies Happy Path DeviceMetadataParcel. */
-    private static void ensureHappyPathAsExpected(FastPairDeviceMetadataParcel metadataParcel) {
-        assertThat(metadataParcel).isNotNull();
-        assertThat(metadataParcel.bleTxPower).isEqualTo(BLE_TX_POWER);
-
-        assertThat(metadataParcel.connectSuccessCompanionAppInstalled).isEqualTo(
-                CONNECT_SUCCESS_COMPANION_APP_INSTALLED);
-        assertThat(metadataParcel.connectSuccessCompanionAppNotInstalled).isEqualTo(
-                CONNECT_SUCCESS_COMPANION_APP_NOT_INSTALLED);
-
-        assertThat(metadataParcel.deviceType).isEqualTo(DEVICE_TYPE);
-        assertThat(metadataParcel.downloadCompanionAppDescription).isEqualTo(
-                DOWNLOAD_COMPANION_APP_DESCRIPTION);
-
-        assertThat(metadataParcel.failConnectGoToSettingsDescription).isEqualTo(
-                FAIL_CONNECT_GOTO_SETTINGS_DESCRIPTION);
-
-        assertThat(metadataParcel.image).isEqualTo(IMAGE);
-        assertThat(metadataParcel.imageUrl).isEqualTo(IMAGE_URL);
-        assertThat(metadataParcel.initialNotificationDescription).isEqualTo(
-                INITIAL_NOTIFICATION_DESCRIPTION);
-        assertThat(metadataParcel.initialNotificationDescriptionNoAccount).isEqualTo(
-                INITIAL_NOTIFICATION_DESCRIPTION_NO_ACCOUNT);
-        assertThat(metadataParcel.initialPairingDescription).isEqualTo(INITIAL_PAIRING_DESCRIPTION);
-        assertThat(metadataParcel.intentUri).isEqualTo(INTENT_URI);
-
-        assertThat(metadataParcel.name).isEqualTo(NAME);
-
-        assertThat(metadataParcel.openCompanionAppDescription).isEqualTo(
-                OPEN_COMPANION_APP_DESCRIPTION);
-
-        assertThat(metadataParcel.retroactivePairingDescription).isEqualTo(
-                RETRO_ACTIVE_PAIRING_DESCRIPTION);
-
-        assertThat(metadataParcel.subsequentPairingDescription).isEqualTo(
-                SUBSEQUENT_PAIRING_DESCRIPTION);
-
-        assertThat(metadataParcel.triggerDistance).isWithin(DELTA).of(TRIGGER_DISTANCE);
-        assertThat(metadataParcel.trueWirelessImageUrlCase).isEqualTo(TRUE_WIRELESS_IMAGE_URL_CASE);
-        assertThat(metadataParcel.trueWirelessImageUrlLeftBud).isEqualTo(
-                TRUE_WIRELESS_IMAGE_URL_LEFT_BUD);
-        assertThat(metadataParcel.trueWirelessImageUrlRightBud).isEqualTo(
-                TRUE_WIRELESS_IMAGE_URL_RIGHT_BUD);
-
-        assertThat(metadataParcel.unableToConnectDescription).isEqualTo(
-                UNABLE_TO_CONNECT_DESCRIPTION);
-        assertThat(metadataParcel.unableToConnectTitle).isEqualTo(UNABLE_TO_CONNECT_TITLE);
-        assertThat(metadataParcel.updateCompanionAppDescription).isEqualTo(
-                UPDATE_COMPANION_APP_DESCRIPTION);
-
-        assertThat(metadataParcel.waitLaunchCompanionAppDescription).isEqualTo(
-                WAIT_LAUNCH_COMPANION_APP_DESCRIPTION);
-    }
-
-    /* Verifies Happy Path DeviceMetadata. */
-    private static void ensureHappyPathAsExpected(FastPairDeviceMetadata metadata) {
-        assertThat(metadata.getBleTxPower()).isEqualTo(BLE_TX_POWER);
-        assertThat(metadata.getConnectSuccessCompanionAppInstalled())
-                .isEqualTo(CONNECT_SUCCESS_COMPANION_APP_INSTALLED);
-        assertThat(metadata.getConnectSuccessCompanionAppNotInstalled())
-                .isEqualTo(CONNECT_SUCCESS_COMPANION_APP_NOT_INSTALLED);
-        assertThat(metadata.getDeviceType()).isEqualTo(DEVICE_TYPE);
-        assertThat(metadata.getDownloadCompanionAppDescription())
-                .isEqualTo(DOWNLOAD_COMPANION_APP_DESCRIPTION);
-        assertThat(metadata.getFailConnectGoToSettingsDescription())
-                .isEqualTo(FAIL_CONNECT_GOTO_SETTINGS_DESCRIPTION);
-        assertThat(metadata.getImage()).isEqualTo(IMAGE);
-        assertThat(metadata.getImageUrl()).isEqualTo(IMAGE_URL);
-        assertThat(metadata.getInitialNotificationDescription())
-                .isEqualTo(INITIAL_NOTIFICATION_DESCRIPTION);
-        assertThat(metadata.getInitialNotificationDescriptionNoAccount())
-                .isEqualTo(INITIAL_NOTIFICATION_DESCRIPTION_NO_ACCOUNT);
-        assertThat(metadata.getInitialPairingDescription()).isEqualTo(INITIAL_PAIRING_DESCRIPTION);
-        assertThat(metadata.getIntentUri()).isEqualTo(INTENT_URI);
-        assertThat(metadata.getName()).isEqualTo(NAME);
-        assertThat(metadata.getOpenCompanionAppDescription())
-                .isEqualTo(OPEN_COMPANION_APP_DESCRIPTION);
-        assertThat(metadata.getRetroactivePairingDescription())
-                .isEqualTo(RETRO_ACTIVE_PAIRING_DESCRIPTION);
-        assertThat(metadata.getSubsequentPairingDescription())
-                .isEqualTo(SUBSEQUENT_PAIRING_DESCRIPTION);
-        assertThat(metadata.getTriggerDistance()).isWithin(DELTA).of(TRIGGER_DISTANCE);
-        assertThat(metadata.getTrueWirelessImageUrlCase()).isEqualTo(TRUE_WIRELESS_IMAGE_URL_CASE);
-        assertThat(metadata.getTrueWirelessImageUrlLeftBud())
-                .isEqualTo(TRUE_WIRELESS_IMAGE_URL_LEFT_BUD);
-        assertThat(metadata.getTrueWirelessImageUrlRightBud())
-                .isEqualTo(TRUE_WIRELESS_IMAGE_URL_RIGHT_BUD);
-        assertThat(metadata.getUnableToConnectDescription())
-                .isEqualTo(UNABLE_TO_CONNECT_DESCRIPTION);
-        assertThat(metadata.getUnableToConnectTitle()).isEqualTo(UNABLE_TO_CONNECT_TITLE);
-        assertThat(metadata.getUpdateCompanionAppDescription())
-                .isEqualTo(UPDATE_COMPANION_APP_DESCRIPTION);
-        assertThat(metadata.getWaitLaunchCompanionAppDescription())
-                .isEqualTo(WAIT_LAUNCH_COMPANION_APP_DESCRIPTION);
-    }
-
-    /* Verifies Happy Path FastPairDiscoveryItemParcel. */
-    private static void ensureHappyPathAsExpected(FastPairDiscoveryItemParcel itemParcel) {
-        assertThat(itemParcel.actionUrl).isEqualTo(ACTION_URL);
-        assertThat(itemParcel.actionUrlType).isEqualTo(ACTION_URL_TYPE);
-        assertThat(itemParcel.appName).isEqualTo(APP_NAME);
-        assertThat(itemParcel.authenticationPublicKeySecp256r1)
-                .isEqualTo(AUTHENTICATION_PUBLIC_KEY_SEC_P256R1);
-        assertThat(itemParcel.description).isEqualTo(DESCRIPTION);
-        assertThat(itemParcel.deviceName).isEqualTo(DEVICE_NAME);
-        assertThat(itemParcel.displayUrl).isEqualTo(DISPLAY_URL);
-        assertThat(itemParcel.firstObservationTimestampMillis)
-                .isEqualTo(FIRST_OBSERVATION_TIMESTAMP_MILLIS);
-        assertThat(itemParcel.iconFifeUrl).isEqualTo(ICON_FIFE_URL);
-        assertThat(itemParcel.iconPng).isEqualTo(ICON_PNG);
-        assertThat(itemParcel.id).isEqualTo(ID);
-        assertThat(itemParcel.lastObservationTimestampMillis)
-                .isEqualTo(LAST_OBSERVATION_TIMESTAMP_MILLIS);
-        assertThat(itemParcel.macAddress).isEqualTo(MAC_ADDRESS);
-        assertThat(itemParcel.packageName).isEqualTo(PACKAGE_NAME);
-        assertThat(itemParcel.pendingAppInstallTimestampMillis)
-                .isEqualTo(PENDING_APP_INSTALL_TIMESTAMP_MILLIS);
-        assertThat(itemParcel.rssi).isEqualTo(RSSI);
-        assertThat(itemParcel.state).isEqualTo(STATE);
-        assertThat(itemParcel.title).isEqualTo(TITLE);
-        assertThat(itemParcel.triggerId).isEqualTo(TRIGGER_ID);
-        assertThat(itemParcel.txPower).isEqualTo(TX_POWER);
-    }
-
-    /* Verifies Happy Path FastPairDiscoveryItem. */
-    private static void ensureHappyPathAsExpected(FastPairDiscoveryItem item) {
-        assertThat(item.getActionUrl()).isEqualTo(ACTION_URL);
-        assertThat(item.getActionUrlType()).isEqualTo(ACTION_URL_TYPE);
-        assertThat(item.getAppName()).isEqualTo(APP_NAME);
-        assertThat(item.getAuthenticationPublicKeySecp256r1())
-                .isEqualTo(AUTHENTICATION_PUBLIC_KEY_SEC_P256R1);
-        assertThat(item.getDescription()).isEqualTo(DESCRIPTION);
-        assertThat(item.getDeviceName()).isEqualTo(DEVICE_NAME);
-        assertThat(item.getDisplayUrl()).isEqualTo(DISPLAY_URL);
-        assertThat(item.getFirstObservationTimestampMillis())
-                .isEqualTo(FIRST_OBSERVATION_TIMESTAMP_MILLIS);
-        assertThat(item.getIconFfeUrl()).isEqualTo(ICON_FIFE_URL);
-        assertThat(item.getIconPng()).isEqualTo(ICON_PNG);
-        assertThat(item.getId()).isEqualTo(ID);
-        assertThat(item.getLastObservationTimestampMillis())
-                .isEqualTo(LAST_OBSERVATION_TIMESTAMP_MILLIS);
-        assertThat(item.getMacAddress()).isEqualTo(MAC_ADDRESS);
-        assertThat(item.getPackageName()).isEqualTo(PACKAGE_NAME);
-        assertThat(item.getPendingAppInstallTimestampMillis())
-                .isEqualTo(PENDING_APP_INSTALL_TIMESTAMP_MILLIS);
-        assertThat(item.getRssi()).isEqualTo(RSSI);
-        assertThat(item.getState()).isEqualTo(STATE);
-        assertThat(item.getTitle()).isEqualTo(TITLE);
-        assertThat(item.getTriggerId()).isEqualTo(TRIGGER_ID);
-        assertThat(item.getTxPower()).isEqualTo(TX_POWER);
-    }
-
-    /* Verifies Happy Path EligibleAccountParcel[]. */
-    private static void ensureHappyPathAsExpected(FastPairEligibleAccountParcel[] accountsParcel) {
-        assertThat(accountsParcel).hasLength(ELIGIBLE_ACCOUNTS_NUM);
-
-        assertThat(accountsParcel[0].account).isEqualTo(ELIGIBLE_ACCOUNT_1);
-        assertThat(accountsParcel[0].optIn).isEqualTo(ELIGIBLE_ACCOUNT_1_OPT_IN);
-
-        assertThat(accountsParcel[1].account).isEqualTo(ELIGIBLE_ACCOUNT_2);
-        assertThat(accountsParcel[1].optIn).isEqualTo(ELIGIBLE_ACCOUNT_2_OPT_IN);
-    }
-}
diff --git a/nearby/tests/cts/fastpair/src/android/nearby/cts/FastPairEligibleAccountTest.java b/nearby/tests/cts/fastpair/src/android/nearby/cts/FastPairEligibleAccountTest.java
deleted file mode 100644
index 0d91d4e..0000000
--- a/nearby/tests/cts/fastpair/src/android/nearby/cts/FastPairEligibleAccountTest.java
+++ /dev/null
@@ -1,67 +0,0 @@
-/*
- * Copyright (C) 2021 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.nearby.cts;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import android.accounts.Account;
-import android.nearby.FastPairEligibleAccount;
-
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.SdkSuppress;
-
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-@RunWith(AndroidJUnit4.class)
-public class FastPairEligibleAccountTest {
-
-    private static final Account ACCOUNT = new Account("abc@google.com", "type1");
-    private static final Account ACCOUNT_NULL = null;
-
-    private static final boolean OPT_IN_TRUE = true;
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testSetGetFastPairEligibleAccountNotNull() {
-        FastPairEligibleAccount eligibleAccount =
-                genFastPairEligibleAccount(ACCOUNT, OPT_IN_TRUE);
-
-        assertThat(eligibleAccount.getAccount()).isEqualTo(ACCOUNT);
-        assertThat(eligibleAccount.isOptIn()).isEqualTo(OPT_IN_TRUE);
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testSetGetFastPairEligibleAccountNull() {
-        FastPairEligibleAccount eligibleAccount =
-                genFastPairEligibleAccount(ACCOUNT_NULL, OPT_IN_TRUE);
-
-        assertThat(eligibleAccount.getAccount()).isEqualTo(ACCOUNT_NULL);
-        assertThat(eligibleAccount.isOptIn()).isEqualTo(OPT_IN_TRUE);
-    }
-
-    /* Generates FastPairEligibleAccount. */
-    private static FastPairEligibleAccount genFastPairEligibleAccount(
-            Account account, boolean optIn) {
-        FastPairEligibleAccount.Builder builder = new FastPairEligibleAccount.Builder();
-        builder.setAccount(account);
-        builder.setOptIn(optIn);
-
-        return builder.build();
-    }
-}
diff --git a/nearby/tests/multidevices/clients/test_service/fastpair_seeker_data_provider/Android.bp b/nearby/tests/multidevices/clients/test_service/fastpair_seeker_data_provider/Android.bp
index a6d51ca..1fcee37 100644
--- a/nearby/tests/multidevices/clients/test_service/fastpair_seeker_data_provider/Android.bp
+++ b/nearby/tests/multidevices/clients/test_service/fastpair_seeker_data_provider/Android.bp
@@ -21,6 +21,8 @@
     srcs: ["shared/**/*.kt"],
     sdk_version: "test_current",
     static_libs: [
+        // TODO(b/227513829): Remove "framework-nearby-static" once Fast Pair system APIs add back.
+        "framework-nearby-static",
         "guava",
         "gson-prebuilt-jar",
     ],
diff --git a/nearby/tests/multidevices/clients/test_support/fastpair_provider/Android.bp b/nearby/tests/multidevices/clients/test_support/fastpair_provider/Android.bp
index e01c436..298c9dc 100644
--- a/nearby/tests/multidevices/clients/test_support/fastpair_provider/Android.bp
+++ b/nearby/tests/multidevices/clients/test_support/fastpair_provider/Android.bp
@@ -34,11 +34,13 @@
     ],
     static_libs: [
         "NearbyFastPairProviderLiteProtos",
+        "androidx.core_core",
         "androidx.test.core",
         "error_prone_annotations",
         "fast-pair-lite-protos",
         "framework-annotations-lib",
+        "guava",
         "kotlin-stdlib",
-        "service-nearby-pre-jarjar",
+        "nearby-common-lib",
     ],
 }
diff --git a/nearby/tests/multidevices/clients/test_support/fastpair_provider/simulator_app/Android.bp b/nearby/tests/multidevices/clients/test_support/fastpair_provider/simulator_app/Android.bp
index 87d352f..125c34e 100644
--- a/nearby/tests/multidevices/clients/test_support/fastpair_provider/simulator_app/Android.bp
+++ b/nearby/tests/multidevices/clients/test_support/fastpair_provider/simulator_app/Android.bp
@@ -25,6 +25,7 @@
 // adb remount
 // adb push ${ANDROID_PRODUCT_OUT}/system/app/NearbyFastPairProviderSimulatorApp /system/app/
 // adb reboot
+// Grant all permissions requested to NearbyFastPairProviderSimulatorApp before launching it.
 android_app {
     name: "NearbyFastPairProviderSimulatorApp",
     sdk_version: "test_current",
diff --git a/nearby/tests/multidevices/host/AndroidTest.xml b/nearby/tests/multidevices/host/AndroidTest.xml
index a5f6839..5926cc1 100644
--- a/nearby/tests/multidevices/host/AndroidTest.xml
+++ b/nearby/tests/multidevices/host/AndroidTest.xml
@@ -21,11 +21,13 @@
         <!-- For coverage to work, the APK should not be uninstalled until after coverage is pulled.
              So it's a lot easier to install APKs outside the python code.
         -->
-        <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
-            <option name="test-file-name" value="NearbyMultiDevicesClientsSnippets.apk" />
-            <option name="test-file-name" value="NearbyFastPairSeekerDataProvider.apk" />
-            <option name="check-min-sdk" value="true" />
+        <target_preparer class="com.android.tradefed.targetprep.RootTargetPreparer" />
+        <target_preparer class="com.android.compatibility.common.tradefed.targetprep.FilePusher">
+            <option name="remount-system" value="true" />
+            <option name="push" value="NearbyMultiDevicesClientsSnippets.apk->/system/app/NearbyMultiDevicesClientsSnippets/NearbyMultiDevicesClientsSnippets.apk" />
+            <option name="push" value="NearbyFastPairSeekerDataProvider.apk->/system/app/NearbyFastPairSeekerDataProvider/NearbyFastPairSeekerDataProvider.apk" />
         </target_preparer>
+        <target_preparer class="com.android.tradefed.targetprep.RebootTargetPreparer" />
         <target_preparer class="com.android.tradefed.targetprep.RunCommandTargetPreparer">
             <option name="run-command" value="input keyevent KEYCODE_WAKEUP" />
             <option name="run-command" value="wm dismiss-keyguard" />
@@ -39,14 +41,47 @@
         <target_preparer class="com.android.tradefed.targetprep.DeviceSetup">
             <option name="force-skip-system-props" value="true" /> <!-- avoid restarting device -->
             <option name="screen-always-on" value="on" />
+            <!-- List permissions requested by the APK: aapt d permissions <PATH_TO_YOUR_APK> -->
+            <option
+                name="run-command"
+                value="pm grant android.nearby.multidevices android.permission.BLUETOOTH" />
+            <option
+                name="run-command"
+                value="pm grant android.nearby.multidevices android.permission.BLUETOOTH_ADMIN" />
+            <option
+                name="run-command"
+                value="pm grant android.nearby.multidevices android.permission.BLUETOOTH_ADVERTISE" />
+            <option
+                name="run-command"
+                value="pm grant android.nearby.multidevices android.permission.BLUETOOTH_CONNECT" />
+            <option
+                name="run-command"
+                value="pm grant android.nearby.multidevices android.permission.BLUETOOTH_PRIVILEGED" />
+            <option
+                name="run-command"
+                value="pm grant android.nearby.multidevices android.permission.BLUETOOTH_SCAN" />
+            <option
+                name="run-command"
+                value="pm grant android.nearby.multidevices android.permission.INTERNET" />
+            <option
+                name="run-command"
+                value="pm grant android.nearby.multidevices android.permission.GET_ACCOUNTS" />
+            <option
+                name="run-command"
+                value="pm grant android.nearby.multidevices android.permission.WRITE_SECURE_SETTINGS" />
+            <option
+                name="run-command"
+                value="pm grant android.nearby.multidevices android.permission.REORDER_TASKS" />
         </target_preparer>
     </device>
     <device name="device2">
-        <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
-            <option name="test-file-name" value="NearbyMultiDevicesClientsSnippets.apk" />
-            <option name="test-file-name" value="NearbyFastPairSeekerDataProvider.apk" />
-            <option name="check-min-sdk" value="true" />
+        <target_preparer class="com.android.tradefed.targetprep.RootTargetPreparer" />
+        <target_preparer class="com.android.compatibility.common.tradefed.targetprep.FilePusher">
+            <option name="remount-system" value="true" />
+            <option name="push" value="NearbyMultiDevicesClientsSnippets.apk->/system/app/NearbyMultiDevicesClientsSnippets/NearbyMultiDevicesClientsSnippets.apk" />
+            <option name="push" value="NearbyFastPairSeekerDataProvider.apk->/system/app/NearbyFastPairSeekerDataProvider/NearbyFastPairSeekerDataProvider.apk" />
         </target_preparer>
+        <target_preparer class="com.android.tradefed.targetprep.RebootTargetPreparer" />
         <target_preparer class="com.android.tradefed.targetprep.RunCommandTargetPreparer">
             <option name="run-command" value="input keyevent KEYCODE_WAKEUP" />
             <option name="run-command" value="wm dismiss-keyguard" />
@@ -54,6 +89,37 @@
         <target_preparer class="com.android.tradefed.targetprep.DeviceSetup">
             <option name="force-skip-system-props" value="true" /> <!-- avoid restarting device -->
             <option name="screen-always-on" value="on" />
+            <!-- List permissions requested by the APK: aapt d permissions <PATH_TO_YOUR_APK> -->
+            <option
+                name="run-command"
+                value="pm grant android.nearby.multidevices android.permission.BLUETOOTH" />
+            <option
+                name="run-command"
+                value="pm grant android.nearby.multidevices android.permission.BLUETOOTH_ADMIN" />
+            <option
+                name="run-command"
+                value="pm grant android.nearby.multidevices android.permission.BLUETOOTH_ADVERTISE" />
+            <option
+                name="run-command"
+                value="pm grant android.nearby.multidevices android.permission.BLUETOOTH_CONNECT" />
+            <option
+                name="run-command"
+                value="pm grant android.nearby.multidevices android.permission.BLUETOOTH_PRIVILEGED" />
+            <option
+                name="run-command"
+                value="pm grant android.nearby.multidevices android.permission.BLUETOOTH_SCAN" />
+            <option
+                name="run-command"
+                value="pm grant android.nearby.multidevices android.permission.INTERNET" />
+            <option
+                name="run-command"
+                value="pm grant android.nearby.multidevices android.permission.GET_ACCOUNTS" />
+            <option
+                name="run-command"
+                value="pm grant android.nearby.multidevices android.permission.WRITE_SECURE_SETTINGS" />
+            <option
+                name="run-command"
+                value="pm grant android.nearby.multidevices android.permission.REORDER_TASKS" />
         </target_preparer>
     </device>
 
diff --git a/service-t/Android.bp b/service-t/Android.bp
index 50dd995..0beb4c8 100644
--- a/service-t/Android.bp
+++ b/service-t/Android.bp
@@ -22,6 +22,17 @@
 // Include build rules from Sources.bp
 build = ["Sources.bp"]
 
+filegroup {
+    name: "service-connectivity-tiramisu-sources",
+    srcs: [
+        "src/**/*.java",
+    ],
+    visibility: ["//visibility:private"],
+}
+// The above filegroup can be used to specify different sources depending
+// on the branch, while minimizing merge conflicts in the rest of the
+// build rules.
+
 // This builds T+ services depending on framework-connectivity-t
 // hidden symbols separately from the S+ services, to ensure that S+
 // services cannot accidentally depend on T+ hidden symbols from
@@ -32,9 +43,7 @@
     // TODO(b/210962470): Bump this to at least S, and then T.
     min_sdk_version: "30",
     srcs: [
-        "src/**/*.java",
-        ":ethernet-service-updatable-sources",
-        ":services.connectivity-tiramisu-updatable-sources",
+        ":service-connectivity-tiramisu-sources",
     ],
     libs: [
         "framework-annotations-lib",
@@ -63,3 +72,18 @@
         "//packages/modules/IPsec/tests/iketests",
     ],
 }
+
+// Ethernet related libraries.
+// TODO: remove when ethernet tests are merged into connectivity tests
+filegroup {
+    name: "ethernet-service-test-sources",
+    srcs: [
+        "src/com/android/server/ethernet/**/*.java",
+        "src/com/android/server/net/DelayedDiskWrite.java",
+        "src/com/android/server/net/IpConfigStore.java",
+    ],
+    path: "src",
+    visibility: [
+        "//packages/modules/Connectivity/tests:__subpackages__",
+    ],
+}
diff --git a/service-t/Sources.bp b/service-t/Sources.bp
index 04866fb..4e669b6 100644
--- a/service-t/Sources.bp
+++ b/service-t/Sources.bp
@@ -59,48 +59,6 @@
     ],
 }
 
-// Nsd related libraries.
-
-filegroup {
-    name: "services.connectivity-nsd-sources",
-    srcs: [
-        "src/com/android/server/INativeDaemon*.java",
-        "src/com/android/server/NativeDaemon*.java",
-        "src/com/android/server/Nsd*.java",
-    ],
-    path: "src",
-    visibility: [
-        "//visibility:private",
-    ],
-}
-
-// IpSec related libraries.
-
-filegroup {
-    name: "services.connectivity-ipsec-sources",
-    srcs: [
-        "src/com/android/server/IpSecService.java",
-    ],
-    path: "src",
-    visibility: [
-        "//visibility:private",
-    ],
-}
-
-// Ethernet related libraries.
-
-filegroup {
-    name: "services.connectivity-ethernet-sources",
-    srcs: [
-        "src/com/android/server/net/DelayedDiskWrite.java",
-        "src/com/android/server/net/IpConfigStore.java",
-    ],
-    path: "src",
-    visibility: [
-        "//frameworks/opt/net/ethernet/tests",
-    ],
-}
-
 // Connectivity-T common libraries.
 
 // TODO: remove this empty filegroup.
@@ -111,20 +69,6 @@
     visibility: ["//frameworks/base/services/core"],
 }
 
-filegroup {
-    name: "services.connectivity-tiramisu-updatable-sources",
-    srcs: [
-        ":services.connectivity-ethernet-sources",
-        ":services.connectivity-ipsec-sources",
-        ":services.connectivity-netstats-sources",
-        ":services.connectivity-nsd-sources",
-    ],
-    path: "src",
-    visibility: [
-        "//packages/modules/Connectivity:__subpackages__",
-    ],
-}
-
 cc_library_shared {
     name: "libcom_android_net_module_util_jni",
     min_sdk_version: "30",
diff --git a/service-t/src/com/android/server/ethernet/EthernetConfigStore.java b/service-t/src/com/android/server/ethernet/EthernetConfigStore.java
new file mode 100644
index 0000000..6b623f4
--- /dev/null
+++ b/service-t/src/com/android/server/ethernet/EthernetConfigStore.java
@@ -0,0 +1,88 @@
+/*
+ * 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 com.android.server.ethernet;
+
+import android.annotation.Nullable;
+import android.net.IpConfiguration;
+import android.os.Environment;
+import android.util.ArrayMap;
+
+import com.android.server.net.IpConfigStore;
+
+
+/**
+ * This class provides an API to store and manage Ethernet network configuration.
+ */
+public class EthernetConfigStore {
+    private static final String ipConfigFile = Environment.getDataDirectory() +
+            "/misc/ethernet/ipconfig.txt";
+
+    private IpConfigStore mStore = new IpConfigStore();
+    private ArrayMap<String, IpConfiguration> mIpConfigurations;
+    private IpConfiguration mIpConfigurationForDefaultInterface;
+    private final Object mSync = new Object();
+
+    public EthernetConfigStore() {
+        mIpConfigurations = new ArrayMap<>(0);
+    }
+
+    public void read() {
+        synchronized (mSync) {
+            ArrayMap<String, IpConfiguration> configs =
+                    IpConfigStore.readIpConfigurations(ipConfigFile);
+
+            // This configuration may exist in old file versions when there was only a single active
+            // Ethernet interface.
+            if (configs.containsKey("0")) {
+                mIpConfigurationForDefaultInterface = configs.remove("0");
+            }
+
+            mIpConfigurations = configs;
+        }
+    }
+
+    public void write(String iface, IpConfiguration config) {
+        boolean modified;
+
+        synchronized (mSync) {
+            if (config == null) {
+                modified = mIpConfigurations.remove(iface) != null;
+            } else {
+                IpConfiguration oldConfig = mIpConfigurations.put(iface, config);
+                modified = !config.equals(oldConfig);
+            }
+
+            if (modified) {
+                mStore.writeIpConfigurations(ipConfigFile, mIpConfigurations);
+            }
+        }
+    }
+
+    public ArrayMap<String, IpConfiguration> getIpConfigurations() {
+        synchronized (mSync) {
+            return new ArrayMap<>(mIpConfigurations);
+        }
+    }
+
+    @Nullable
+    public IpConfiguration getIpConfigurationForDefaultInterface() {
+        synchronized (mSync) {
+            return mIpConfigurationForDefaultInterface == null
+                    ? null : new IpConfiguration(mIpConfigurationForDefaultInterface);
+        }
+    }
+}
diff --git a/service-t/src/com/android/server/ethernet/EthernetNetworkAgent.java b/service-t/src/com/android/server/ethernet/EthernetNetworkAgent.java
new file mode 100644
index 0000000..57fbce7
--- /dev/null
+++ b/service-t/src/com/android/server/ethernet/EthernetNetworkAgent.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.ethernet;
+
+import android.content.Context;
+import android.net.LinkProperties;
+import android.net.NetworkAgent;
+import android.net.NetworkAgentConfig;
+import android.net.NetworkCapabilities;
+import android.net.NetworkProvider;
+import android.net.NetworkScore;
+import android.os.Looper;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+
+public class EthernetNetworkAgent extends NetworkAgent {
+
+    private static final String TAG = "EthernetNetworkAgent";
+
+    public interface Callbacks {
+        void onNetworkUnwanted();
+    }
+
+    private final Callbacks mCallbacks;
+
+    EthernetNetworkAgent(
+            @NonNull Context context,
+            @NonNull Looper looper,
+            @NonNull NetworkCapabilities nc,
+            @NonNull LinkProperties lp,
+            @NonNull NetworkAgentConfig config,
+            @Nullable NetworkProvider provider,
+            @NonNull Callbacks cb) {
+        super(context, looper, TAG, nc, lp, new NetworkScore.Builder().build(), config, provider);
+        mCallbacks = cb;
+    }
+
+    @Override
+    public void onNetworkUnwanted() {
+        mCallbacks.onNetworkUnwanted();
+    }
+
+    // sendLinkProperties is final in NetworkAgent, so it cannot be mocked.
+    public void sendLinkPropertiesImpl(LinkProperties lp) {
+        sendLinkProperties(lp);
+    }
+
+    public Callbacks getCallbacks() {
+        return mCallbacks;
+    }
+}
diff --git a/service-t/src/com/android/server/ethernet/EthernetNetworkFactory.java b/service-t/src/com/android/server/ethernet/EthernetNetworkFactory.java
new file mode 100644
index 0000000..d910629
--- /dev/null
+++ b/service-t/src/com/android/server/ethernet/EthernetNetworkFactory.java
@@ -0,0 +1,785 @@
+/*
+ * 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 com.android.server.ethernet;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.Context;
+import android.content.res.Resources;
+import android.net.ConnectivityManager;
+import android.net.ConnectivityResources;
+import android.net.EthernetManager;
+import android.net.EthernetNetworkSpecifier;
+import android.net.EthernetNetworkManagementException;
+import android.net.INetworkInterfaceOutcomeReceiver;
+import android.net.IpConfiguration;
+import android.net.IpConfiguration.IpAssignment;
+import android.net.IpConfiguration.ProxySettings;
+import android.net.LinkProperties;
+import android.net.Network;
+import android.net.NetworkAgentConfig;
+import android.net.NetworkCapabilities;
+import android.net.NetworkFactory;
+import android.net.NetworkProvider;
+import android.net.NetworkRequest;
+import android.net.NetworkSpecifier;
+import android.net.ip.IIpClient;
+import android.net.ip.IpClientCallbacks;
+import android.net.ip.IpClientManager;
+import android.net.ip.IpClientUtil;
+import android.net.shared.ProvisioningConfiguration;
+import android.os.ConditionVariable;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.RemoteException;
+import android.text.TextUtils;
+import android.util.AndroidRuntimeException;
+import android.util.Log;
+import android.util.SparseArray;
+
+import com.android.connectivity.resources.R;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.util.IndentingPrintWriter;
+import com.android.net.module.util.InterfaceParams;
+
+import java.io.FileDescriptor;
+import java.util.Objects;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * {@link NetworkFactory} that represents Ethernet networks.
+ *
+ * This class reports a static network score of 70 when it is tracking an interface and that
+ * interface's link is up, and a score of 0 otherwise.
+ */
+public class EthernetNetworkFactory extends NetworkFactory {
+    private final static String TAG = EthernetNetworkFactory.class.getSimpleName();
+    final static boolean DBG = true;
+
+    private final static int NETWORK_SCORE = 70;
+    private static final String NETWORK_TYPE = "Ethernet";
+    private static final String LEGACY_TCP_BUFFER_SIZES =
+            "524288,1048576,3145728,524288,1048576,2097152";
+
+    private final ConcurrentHashMap<String, NetworkInterfaceState> mTrackingInterfaces =
+            new ConcurrentHashMap<>();
+    private final Handler mHandler;
+    private final Context mContext;
+    final Dependencies mDeps;
+
+    public static class Dependencies {
+        public void makeIpClient(Context context, String iface, IpClientCallbacks callbacks) {
+            IpClientUtil.makeIpClient(context, iface, callbacks);
+        }
+
+        public IpClientManager makeIpClientManager(@NonNull final IIpClient ipClient) {
+            return new IpClientManager(ipClient, TAG);
+        }
+
+        public EthernetNetworkAgent makeEthernetNetworkAgent(Context context, Looper looper,
+                NetworkCapabilities nc, LinkProperties lp, NetworkAgentConfig config,
+                NetworkProvider provider, EthernetNetworkAgent.Callbacks cb) {
+            return new EthernetNetworkAgent(context, looper, nc, lp, config, provider, cb);
+        }
+
+        public InterfaceParams getNetworkInterfaceByName(String name) {
+            return InterfaceParams.getByName(name);
+        }
+
+        // TODO: remove legacy resource fallback after migrating its overlays.
+        private String getPlatformTcpBufferSizes(Context context) {
+            final Resources r = context.getResources();
+            final int resId = r.getIdentifier("config_ethernet_tcp_buffers", "string",
+                    context.getPackageName());
+            return r.getString(resId);
+        }
+
+        public String getTcpBufferSizesFromResource(Context context) {
+            final String tcpBufferSizes;
+            final String platformTcpBufferSizes = getPlatformTcpBufferSizes(context);
+            if (!LEGACY_TCP_BUFFER_SIZES.equals(platformTcpBufferSizes)) {
+                // Platform resource is not the historical default: use the overlay.
+                tcpBufferSizes = platformTcpBufferSizes;
+            } else {
+                final ConnectivityResources resources = new ConnectivityResources(context);
+                tcpBufferSizes = resources.get().getString(R.string.config_ethernet_tcp_buffers);
+            }
+            return tcpBufferSizes;
+        }
+    }
+
+    public static class ConfigurationException extends AndroidRuntimeException {
+        public ConfigurationException(String msg) {
+            super(msg);
+        }
+    }
+
+    public EthernetNetworkFactory(Handler handler, Context context) {
+        this(handler, context, new Dependencies());
+    }
+
+    @VisibleForTesting
+    EthernetNetworkFactory(Handler handler, Context context, Dependencies deps) {
+        super(handler.getLooper(), context, NETWORK_TYPE, createDefaultNetworkCapabilities());
+
+        mHandler = handler;
+        mContext = context;
+        mDeps = deps;
+
+        setScoreFilter(NETWORK_SCORE);
+    }
+
+    @Override
+    public boolean acceptRequest(NetworkRequest request) {
+        if (DBG) {
+            Log.d(TAG, "acceptRequest, request: " + request);
+        }
+
+        return networkForRequest(request) != null;
+    }
+
+    @Override
+    protected void needNetworkFor(NetworkRequest networkRequest) {
+        NetworkInterfaceState network = networkForRequest(networkRequest);
+
+        if (network == null) {
+            Log.e(TAG, "needNetworkFor, failed to get a network for " + networkRequest);
+            return;
+        }
+
+        if (++network.refCount == 1) {
+            network.start();
+        }
+    }
+
+    @Override
+    protected void releaseNetworkFor(NetworkRequest networkRequest) {
+        NetworkInterfaceState network = networkForRequest(networkRequest);
+        if (network == null) {
+            Log.e(TAG, "releaseNetworkFor, failed to get a network for " + networkRequest);
+            return;
+        }
+
+        if (--network.refCount == 0) {
+            network.stop();
+        }
+    }
+
+    /**
+     * Returns an array of available interface names. The array is sorted: unrestricted interfaces
+     * goes first, then sorted by name.
+     */
+    @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
+    protected String[] getAvailableInterfaces(boolean includeRestricted) {
+        return mTrackingInterfaces.values()
+                .stream()
+                .filter(iface -> !iface.isRestricted() || includeRestricted)
+                .sorted((iface1, iface2) -> {
+                    int r = Boolean.compare(iface1.isRestricted(), iface2.isRestricted());
+                    return r == 0 ? iface1.name.compareTo(iface2.name) : r;
+                })
+                .map(iface -> iface.name)
+                .toArray(String[]::new);
+    }
+
+    @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
+    protected void addInterface(@NonNull final String ifaceName, @NonNull final String hwAddress,
+            @NonNull final IpConfiguration ipConfig,
+            @NonNull final NetworkCapabilities capabilities) {
+        if (mTrackingInterfaces.containsKey(ifaceName)) {
+            Log.e(TAG, "Interface with name " + ifaceName + " already exists.");
+            return;
+        }
+
+        final NetworkCapabilities nc = new NetworkCapabilities.Builder(capabilities)
+                .setNetworkSpecifier(new EthernetNetworkSpecifier(ifaceName))
+                .build();
+
+        if (DBG) {
+            Log.d(TAG, "addInterface, iface: " + ifaceName + ", capabilities: " + nc);
+        }
+
+        final NetworkInterfaceState iface = new NetworkInterfaceState(
+                ifaceName, hwAddress, mHandler, mContext, ipConfig, nc, this, mDeps);
+        mTrackingInterfaces.put(ifaceName, iface);
+        updateCapabilityFilter();
+    }
+
+    @VisibleForTesting
+    protected int getInterfaceState(@NonNull String iface) {
+        final NetworkInterfaceState interfaceState = mTrackingInterfaces.get(iface);
+        if (interfaceState == null) {
+            return EthernetManager.STATE_ABSENT;
+        } else if (!interfaceState.mLinkUp) {
+            return EthernetManager.STATE_LINK_DOWN;
+        } else {
+            return EthernetManager.STATE_LINK_UP;
+        }
+    }
+
+    /**
+     * Update a network's configuration and restart it if necessary.
+     *
+     * @param ifaceName the interface name of the network to be updated.
+     * @param ipConfig the desired {@link IpConfiguration} for the given network or null. If
+     *                 {@code null} is passed, the existing IpConfiguration is not updated.
+     * @param capabilities the desired {@link NetworkCapabilities} for the given network. If
+     *                     {@code null} is passed, then the network's current
+     *                     {@link NetworkCapabilities} will be used in support of existing APIs as
+     *                     the public API does not allow this.
+     * @param listener an optional {@link INetworkInterfaceOutcomeReceiver} to notify callers of
+     *                 completion.
+     */
+    @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
+    protected void updateInterface(@NonNull final String ifaceName,
+            @Nullable final IpConfiguration ipConfig,
+            @Nullable final NetworkCapabilities capabilities,
+            @Nullable final INetworkInterfaceOutcomeReceiver listener) {
+        if (!hasInterface(ifaceName)) {
+            maybeSendNetworkManagementCallbackForUntracked(ifaceName, listener);
+            return;
+        }
+
+        final NetworkInterfaceState iface = mTrackingInterfaces.get(ifaceName);
+        iface.updateInterface(ipConfig, capabilities, listener);
+        mTrackingInterfaces.put(ifaceName, iface);
+        updateCapabilityFilter();
+    }
+
+    private static NetworkCapabilities mixInCapabilities(NetworkCapabilities nc,
+            NetworkCapabilities addedNc) {
+       final NetworkCapabilities.Builder builder = new NetworkCapabilities.Builder(nc);
+       for (int transport : addedNc.getTransportTypes()) builder.addTransportType(transport);
+       for (int capability : addedNc.getCapabilities()) builder.addCapability(capability);
+       return builder.build();
+    }
+
+    private void updateCapabilityFilter() {
+        NetworkCapabilities capabilitiesFilter = createDefaultNetworkCapabilities();
+        for (NetworkInterfaceState iface:  mTrackingInterfaces.values()) {
+            capabilitiesFilter = mixInCapabilities(capabilitiesFilter, iface.mCapabilities);
+        }
+
+        if (DBG) Log.d(TAG, "updateCapabilityFilter: " + capabilitiesFilter);
+        setCapabilityFilter(capabilitiesFilter);
+    }
+
+    private static NetworkCapabilities createDefaultNetworkCapabilities() {
+        return NetworkCapabilities.Builder
+                .withoutDefaultCapabilities()
+                .addTransportType(NetworkCapabilities.TRANSPORT_ETHERNET).build();
+    }
+
+    @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
+    protected void removeInterface(String interfaceName) {
+        NetworkInterfaceState iface = mTrackingInterfaces.remove(interfaceName);
+        if (iface != null) {
+            iface.maybeSendNetworkManagementCallbackForAbort();
+            iface.stop();
+        }
+
+        updateCapabilityFilter();
+    }
+
+    /** Returns true if state has been modified */
+    @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
+    protected boolean updateInterfaceLinkState(@NonNull final String ifaceName, final boolean up,
+            @Nullable final INetworkInterfaceOutcomeReceiver listener) {
+        if (!hasInterface(ifaceName)) {
+            maybeSendNetworkManagementCallbackForUntracked(ifaceName, listener);
+            return false;
+        }
+
+        if (DBG) {
+            Log.d(TAG, "updateInterfaceLinkState, iface: " + ifaceName + ", up: " + up);
+        }
+
+        NetworkInterfaceState iface = mTrackingInterfaces.get(ifaceName);
+        return iface.updateLinkState(up, listener);
+    }
+
+    private void maybeSendNetworkManagementCallbackForUntracked(
+            String ifaceName, INetworkInterfaceOutcomeReceiver listener) {
+        maybeSendNetworkManagementCallback(listener, null,
+                new EthernetNetworkManagementException(
+                        ifaceName + " can't be updated as it is not available."));
+    }
+
+    @VisibleForTesting
+    protected boolean hasInterface(String ifaceName) {
+        return mTrackingInterfaces.containsKey(ifaceName);
+    }
+
+    private NetworkInterfaceState networkForRequest(NetworkRequest request) {
+        String requestedIface = null;
+
+        NetworkSpecifier specifier = request.getNetworkSpecifier();
+        if (specifier instanceof EthernetNetworkSpecifier) {
+            requestedIface = ((EthernetNetworkSpecifier) specifier)
+                .getInterfaceName();
+        }
+
+        NetworkInterfaceState network = null;
+        if (!TextUtils.isEmpty(requestedIface)) {
+            NetworkInterfaceState n = mTrackingInterfaces.get(requestedIface);
+            if (n != null && request.canBeSatisfiedBy(n.mCapabilities)) {
+                network = n;
+            }
+        } else {
+            for (NetworkInterfaceState n : mTrackingInterfaces.values()) {
+                if (request.canBeSatisfiedBy(n.mCapabilities) && n.mLinkUp) {
+                    network = n;
+                    break;
+                }
+            }
+        }
+
+        if (DBG) {
+            Log.i(TAG, "networkForRequest, request: " + request + ", network: " + network);
+        }
+
+        return network;
+    }
+
+    private static void maybeSendNetworkManagementCallback(
+            @Nullable final INetworkInterfaceOutcomeReceiver listener,
+            @Nullable final String iface,
+            @Nullable final EthernetNetworkManagementException e) {
+        if (null == listener) {
+            return;
+        }
+
+        try {
+            if (iface != null) {
+                listener.onResult(iface);
+            } else {
+                listener.onError(e);
+            }
+        } catch (RemoteException re) {
+            Log.e(TAG, "Can't send onComplete for network management callback", re);
+        }
+    }
+
+    @VisibleForTesting
+    static class NetworkInterfaceState {
+        final String name;
+
+        private final String mHwAddress;
+        private final Handler mHandler;
+        private final Context mContext;
+        private final NetworkFactory mNetworkFactory;
+        private final Dependencies mDeps;
+
+        private static String sTcpBufferSizes = null;  // Lazy initialized.
+
+        private boolean mLinkUp;
+        private int mLegacyType;
+        private LinkProperties mLinkProperties = new LinkProperties();
+
+        private volatile @Nullable IpClientManager mIpClient;
+        private @NonNull NetworkCapabilities mCapabilities;
+        private @Nullable EthernetIpClientCallback mIpClientCallback;
+        private @Nullable EthernetNetworkAgent mNetworkAgent;
+        private @Nullable IpConfiguration mIpConfig;
+
+        /**
+         * A map of TRANSPORT_* types to legacy transport types available for each type an ethernet
+         * interface could propagate.
+         *
+         * There are no legacy type equivalents to LOWPAN or WIFI_AWARE. These types are set to
+         * TYPE_NONE to match the behavior of their own network factories.
+         */
+        private static final SparseArray<Integer> sTransports = new SparseArray();
+        static {
+            sTransports.put(NetworkCapabilities.TRANSPORT_ETHERNET,
+                    ConnectivityManager.TYPE_ETHERNET);
+            sTransports.put(NetworkCapabilities.TRANSPORT_BLUETOOTH,
+                    ConnectivityManager.TYPE_BLUETOOTH);
+            sTransports.put(NetworkCapabilities.TRANSPORT_WIFI, ConnectivityManager.TYPE_WIFI);
+            sTransports.put(NetworkCapabilities.TRANSPORT_CELLULAR,
+                    ConnectivityManager.TYPE_MOBILE);
+            sTransports.put(NetworkCapabilities.TRANSPORT_LOWPAN, ConnectivityManager.TYPE_NONE);
+            sTransports.put(NetworkCapabilities.TRANSPORT_WIFI_AWARE,
+                    ConnectivityManager.TYPE_NONE);
+        }
+
+        long refCount = 0;
+
+        private class EthernetIpClientCallback extends IpClientCallbacks {
+            private final ConditionVariable mIpClientStartCv = new ConditionVariable(false);
+            private final ConditionVariable mIpClientShutdownCv = new ConditionVariable(false);
+            @Nullable INetworkInterfaceOutcomeReceiver mNetworkManagementListener;
+
+            EthernetIpClientCallback(@Nullable final INetworkInterfaceOutcomeReceiver listener) {
+                mNetworkManagementListener = listener;
+            }
+
+            @Override
+            public void onIpClientCreated(IIpClient ipClient) {
+                mIpClient = mDeps.makeIpClientManager(ipClient);
+                mIpClientStartCv.open();
+            }
+
+            private void awaitIpClientStart() {
+                mIpClientStartCv.block();
+            }
+
+            private void awaitIpClientShutdown() {
+                mIpClientShutdownCv.block();
+            }
+
+            // At the time IpClient is stopped, an IpClient event may have already been posted on
+            // the back of the handler and is awaiting execution. Once that event is executed, the
+            // associated callback object may not be valid anymore
+            // (NetworkInterfaceState#mIpClientCallback points to a different object / null).
+            private boolean isCurrentCallback() {
+                return this == mIpClientCallback;
+            }
+
+            private void handleIpEvent(final @NonNull Runnable r) {
+                mHandler.post(() -> {
+                    if (!isCurrentCallback()) {
+                        Log.i(TAG, "Ignoring stale IpClientCallbacks " + this);
+                        return;
+                    }
+                    r.run();
+                });
+            }
+
+            @Override
+            public void onProvisioningSuccess(LinkProperties newLp) {
+                handleIpEvent(() -> onIpLayerStarted(newLp, mNetworkManagementListener));
+            }
+
+            @Override
+            public void onProvisioningFailure(LinkProperties newLp) {
+                // This cannot happen due to provisioning timeout, because our timeout is 0. It can
+                // happen due to errors while provisioning or on provisioning loss.
+                handleIpEvent(() -> onIpLayerStopped(mNetworkManagementListener));
+            }
+
+            @Override
+            public void onLinkPropertiesChange(LinkProperties newLp) {
+                handleIpEvent(() -> updateLinkProperties(newLp));
+            }
+
+            @Override
+            public void onReachabilityLost(String logMsg) {
+                handleIpEvent(() -> updateNeighborLostEvent(logMsg));
+            }
+
+            @Override
+            public void onQuit() {
+                mIpClient = null;
+                mIpClientShutdownCv.open();
+            }
+        }
+
+        NetworkInterfaceState(String ifaceName, String hwAddress, Handler handler, Context context,
+                @NonNull IpConfiguration ipConfig, @NonNull NetworkCapabilities capabilities,
+                NetworkFactory networkFactory, Dependencies deps) {
+            name = ifaceName;
+            mIpConfig = Objects.requireNonNull(ipConfig);
+            mCapabilities = Objects.requireNonNull(capabilities);
+            mLegacyType = getLegacyType(mCapabilities);
+            mHandler = handler;
+            mContext = context;
+            mNetworkFactory = networkFactory;
+            mDeps = deps;
+            mHwAddress = hwAddress;
+        }
+
+        /**
+         * Determines the legacy transport type from a NetworkCapabilities transport type. Defaults
+         * to legacy TYPE_NONE if there is no known conversion
+         */
+        private static int getLegacyType(int transport) {
+            return sTransports.get(transport, ConnectivityManager.TYPE_NONE);
+        }
+
+        private static int getLegacyType(@NonNull final NetworkCapabilities capabilities) {
+            final int[] transportTypes = capabilities.getTransportTypes();
+            if (transportTypes.length > 0) {
+                return getLegacyType(transportTypes[0]);
+            }
+
+            // Should never happen as transport is always one of ETHERNET or a valid override
+            throw new ConfigurationException("Network Capabilities do not have an associated "
+                    + "transport type.");
+        }
+
+        private void setCapabilities(@NonNull final NetworkCapabilities capabilities) {
+            mCapabilities = new NetworkCapabilities(capabilities);
+            mLegacyType = getLegacyType(mCapabilities);
+        }
+
+        void updateInterface(@Nullable final IpConfiguration ipConfig,
+                @Nullable final NetworkCapabilities capabilities,
+                @Nullable final INetworkInterfaceOutcomeReceiver listener) {
+            if (DBG) {
+                Log.d(TAG, "updateInterface, iface: " + name
+                        + ", ipConfig: " + ipConfig + ", old ipConfig: " + mIpConfig
+                        + ", capabilities: " + capabilities + ", old capabilities: " + mCapabilities
+                        + ", listener: " + listener
+                );
+            }
+
+            if (null != ipConfig){
+                mIpConfig = ipConfig;
+            }
+            if (null != capabilities) {
+                setCapabilities(capabilities);
+            }
+            // Send an abort callback if a request is filed before the previous one has completed.
+            maybeSendNetworkManagementCallbackForAbort();
+            // TODO: Update this logic to only do a restart if required. Although a restart may
+            //  be required due to the capabilities or ipConfiguration values, not all
+            //  capabilities changes require a restart.
+            restart(listener);
+        }
+
+        boolean isRestricted() {
+            return !mCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED);
+        }
+
+        private void start() {
+            start(null);
+        }
+
+        private void start(@Nullable final INetworkInterfaceOutcomeReceiver listener) {
+            if (mIpClient != null) {
+                if (DBG) Log.d(TAG, "IpClient already started");
+                return;
+            }
+            if (DBG) {
+                Log.d(TAG, String.format("Starting Ethernet IpClient(%s)", name));
+            }
+
+            mIpClientCallback = new EthernetIpClientCallback(listener);
+            mDeps.makeIpClient(mContext, name, mIpClientCallback);
+            mIpClientCallback.awaitIpClientStart();
+
+            if (sTcpBufferSizes == null) {
+                sTcpBufferSizes = mDeps.getTcpBufferSizesFromResource(mContext);
+            }
+            provisionIpClient(mIpClient, mIpConfig, sTcpBufferSizes);
+        }
+
+        void onIpLayerStarted(@NonNull final LinkProperties linkProperties,
+                @Nullable final INetworkInterfaceOutcomeReceiver listener) {
+            if (mNetworkAgent != null) {
+                Log.e(TAG, "Already have a NetworkAgent - aborting new request");
+                stop();
+                return;
+            }
+            mLinkProperties = linkProperties;
+
+            // Create our NetworkAgent.
+            final NetworkAgentConfig config = new NetworkAgentConfig.Builder()
+                    .setLegacyType(mLegacyType)
+                    .setLegacyTypeName(NETWORK_TYPE)
+                    .setLegacyExtraInfo(mHwAddress)
+                    .build();
+            mNetworkAgent = mDeps.makeEthernetNetworkAgent(mContext, mHandler.getLooper(),
+                    mCapabilities, mLinkProperties, config, mNetworkFactory.getProvider(),
+                    new EthernetNetworkAgent.Callbacks() {
+                        @Override
+                        public void onNetworkUnwanted() {
+                            // if mNetworkAgent is null, we have already called stop.
+                            if (mNetworkAgent == null) return;
+
+                            if (this == mNetworkAgent.getCallbacks()) {
+                                stop();
+                            } else {
+                                Log.d(TAG, "Ignoring unwanted as we have a more modern " +
+                                        "instance");
+                            }
+                        }
+                    });
+            mNetworkAgent.register();
+            mNetworkAgent.markConnected();
+            realizeNetworkManagementCallback(name, null);
+        }
+
+        void onIpLayerStopped(@Nullable final INetworkInterfaceOutcomeReceiver listener) {
+            // There is no point in continuing if the interface is gone as stop() will be triggered
+            // by removeInterface() when processed on the handler thread and start() won't
+            // work for a non-existent interface.
+            if (null == mDeps.getNetworkInterfaceByName(name)) {
+                if (DBG) Log.d(TAG, name + " is no longer available.");
+                // Send a callback in case a provisioning request was in progress.
+                maybeSendNetworkManagementCallbackForAbort();
+                return;
+            }
+            restart(listener);
+        }
+
+        private void maybeSendNetworkManagementCallbackForAbort() {
+            realizeNetworkManagementCallback(null,
+                    new EthernetNetworkManagementException(
+                            "The IP provisioning request has been aborted."));
+        }
+
+        // Must be called on the handler thread
+        private void realizeNetworkManagementCallback(@Nullable final String iface,
+                @Nullable final EthernetNetworkManagementException e) {
+            ensureRunningOnEthernetHandlerThread();
+            if (null == mIpClientCallback) {
+                return;
+            }
+
+            EthernetNetworkFactory.maybeSendNetworkManagementCallback(
+                    mIpClientCallback.mNetworkManagementListener, iface, e);
+            // Only send a single callback per listener.
+            mIpClientCallback.mNetworkManagementListener = null;
+        }
+
+        private void ensureRunningOnEthernetHandlerThread() {
+            if (mHandler.getLooper().getThread() != Thread.currentThread()) {
+                throw new IllegalStateException(
+                        "Not running on the Ethernet thread: "
+                                + Thread.currentThread().getName());
+            }
+        }
+
+        void updateLinkProperties(LinkProperties linkProperties) {
+            mLinkProperties = linkProperties;
+            if (mNetworkAgent != null) {
+                mNetworkAgent.sendLinkPropertiesImpl(linkProperties);
+            }
+        }
+
+        void updateNeighborLostEvent(String logMsg) {
+            Log.i(TAG, "updateNeighborLostEvent " + logMsg);
+            // Reachability lost will be seen only if the gateway is not reachable.
+            // Since ethernet FW doesn't have the mechanism to scan for new networks
+            // like WiFi, simply restart.
+            // If there is a better network, that will become default and apps
+            // will be able to use internet. If ethernet gets connected again,
+            // and has backhaul connectivity, it will become default.
+            restart();
+        }
+
+        /** Returns true if state has been modified */
+        boolean updateLinkState(final boolean up,
+                @Nullable final INetworkInterfaceOutcomeReceiver listener) {
+            if (mLinkUp == up)  {
+                EthernetNetworkFactory.maybeSendNetworkManagementCallback(listener, null,
+                        new EthernetNetworkManagementException(
+                                "No changes with requested link state " + up + " for " + name));
+                return false;
+            }
+            mLinkUp = up;
+
+            if (!up) { // was up, goes down
+                // Send an abort on a provisioning request callback if necessary before stopping.
+                maybeSendNetworkManagementCallbackForAbort();
+                stop();
+                // If only setting the interface down, send a callback to signal completion.
+                EthernetNetworkFactory.maybeSendNetworkManagementCallback(listener, name, null);
+            } else { // was down, goes up
+                stop();
+                start(listener);
+            }
+
+            return true;
+        }
+
+        void stop() {
+            // Invalidate all previous start requests
+            if (mIpClient != null) {
+                mIpClient.shutdown();
+                mIpClientCallback.awaitIpClientShutdown();
+                mIpClient = null;
+            }
+            mIpClientCallback = null;
+
+            if (mNetworkAgent != null) {
+                mNetworkAgent.unregister();
+                mNetworkAgent = null;
+            }
+            mLinkProperties.clear();
+        }
+
+        private static void provisionIpClient(@NonNull final IpClientManager ipClient,
+                @NonNull final IpConfiguration config, @NonNull final String tcpBufferSizes) {
+            if (config.getProxySettings() == ProxySettings.STATIC ||
+                    config.getProxySettings() == ProxySettings.PAC) {
+                ipClient.setHttpProxy(config.getHttpProxy());
+            }
+
+            if (!TextUtils.isEmpty(tcpBufferSizes)) {
+                ipClient.setTcpBufferSizes(tcpBufferSizes);
+            }
+
+            ipClient.startProvisioning(createProvisioningConfiguration(config));
+        }
+
+        private static ProvisioningConfiguration createProvisioningConfiguration(
+                @NonNull final IpConfiguration config) {
+            if (config.getIpAssignment() == IpAssignment.STATIC) {
+                return new ProvisioningConfiguration.Builder()
+                        .withStaticConfiguration(config.getStaticIpConfiguration())
+                        .build();
+            }
+            return new ProvisioningConfiguration.Builder()
+                        .withProvisioningTimeoutMs(0)
+                        .build();
+        }
+
+        void restart() {
+            restart(null);
+        }
+
+        void restart(@Nullable final INetworkInterfaceOutcomeReceiver listener) {
+            if (DBG) Log.d(TAG, "reconnecting Ethernet");
+            stop();
+            start(listener);
+        }
+
+        @Override
+        public String toString() {
+            return getClass().getSimpleName() + "{ "
+                    + "refCount: " + refCount + ", "
+                    + "iface: " + name + ", "
+                    + "up: " + mLinkUp + ", "
+                    + "hwAddress: " + mHwAddress + ", "
+                    + "networkCapabilities: " + mCapabilities + ", "
+                    + "networkAgent: " + mNetworkAgent + ", "
+                    + "ipClient: " + mIpClient + ","
+                    + "linkProperties: " + mLinkProperties
+                    + "}";
+        }
+    }
+
+    void dump(FileDescriptor fd, IndentingPrintWriter pw, String[] args) {
+        super.dump(fd, pw, args);
+        pw.println(getClass().getSimpleName());
+        pw.println("Tracking interfaces:");
+        pw.increaseIndent();
+        for (String iface: mTrackingInterfaces.keySet()) {
+            NetworkInterfaceState ifaceState = mTrackingInterfaces.get(iface);
+            pw.println(iface + ":" + ifaceState);
+            pw.increaseIndent();
+            if (null == ifaceState.mIpClient) {
+                pw.println("IpClient is null");
+            }
+            pw.decreaseIndent();
+        }
+        pw.decreaseIndent();
+    }
+}
diff --git a/service-t/src/com/android/server/ethernet/EthernetService.java b/service-t/src/com/android/server/ethernet/EthernetService.java
new file mode 100644
index 0000000..d405fd5
--- /dev/null
+++ b/service-t/src/com/android/server/ethernet/EthernetService.java
@@ -0,0 +1,47 @@
+/*
+ * 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 com.android.server.ethernet;
+
+import android.content.Context;
+import android.net.INetd;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.IBinder;
+
+import java.util.Objects;
+
+// TODO: consider renaming EthernetServiceImpl to EthernetService and deleting this file.
+public final class EthernetService {
+    private static final String TAG = "EthernetService";
+    private static final String THREAD_NAME = "EthernetServiceThread";
+
+    private static INetd getNetd(Context context) {
+        final INetd netd =
+                INetd.Stub.asInterface((IBinder) context.getSystemService(Context.NETD_SERVICE));
+        Objects.requireNonNull(netd, "could not get netd instance");
+        return netd;
+    }
+
+    public static EthernetServiceImpl create(Context context) {
+        final HandlerThread handlerThread = new HandlerThread(THREAD_NAME);
+        handlerThread.start();
+        final Handler handler = new Handler(handlerThread.getLooper());
+        final EthernetNetworkFactory factory = new EthernetNetworkFactory(handler, context);
+        return new EthernetServiceImpl(context, handler,
+                new EthernetTracker(context, handler, factory, getNetd(context)));
+    }
+}
diff --git a/service-t/src/com/android/server/ethernet/EthernetServiceImpl.java b/service-t/src/com/android/server/ethernet/EthernetServiceImpl.java
new file mode 100644
index 0000000..5e830ad
--- /dev/null
+++ b/service-t/src/com/android/server/ethernet/EthernetServiceImpl.java
@@ -0,0 +1,299 @@
+/*
+ * 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 com.android.server.ethernet;
+
+import static android.net.NetworkCapabilities.TRANSPORT_TEST;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.net.IEthernetManager;
+import android.net.IEthernetServiceListener;
+import android.net.INetworkInterfaceOutcomeReceiver;
+import android.net.ITetheredInterfaceCallback;
+import android.net.EthernetNetworkUpdateRequest;
+import android.net.IpConfiguration;
+import android.net.NetworkCapabilities;
+import android.os.Binder;
+import android.os.Handler;
+import android.os.RemoteException;
+import android.util.Log;
+import android.util.PrintWriterPrinter;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.util.IndentingPrintWriter;
+import com.android.net.module.util.PermissionUtils;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+import java.util.List;
+import java.util.Objects;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+/**
+ * EthernetServiceImpl handles remote Ethernet operation requests by implementing
+ * the IEthernetManager interface.
+ */
+public class EthernetServiceImpl extends IEthernetManager.Stub {
+    private static final String TAG = "EthernetServiceImpl";
+
+    @VisibleForTesting
+    final AtomicBoolean mStarted = new AtomicBoolean(false);
+    private final Context mContext;
+    private final Handler mHandler;
+    private final EthernetTracker mTracker;
+
+    EthernetServiceImpl(@NonNull final Context context, @NonNull final Handler handler,
+            @NonNull final EthernetTracker tracker) {
+        mContext = context;
+        mHandler = handler;
+        mTracker = tracker;
+    }
+
+    private void enforceAutomotiveDevice(final @NonNull String methodName) {
+        PermissionUtils.enforceSystemFeature(mContext, PackageManager.FEATURE_AUTOMOTIVE,
+                methodName + " is only available on automotive devices.");
+    }
+
+    private boolean checkUseRestrictedNetworksPermission() {
+        return PermissionUtils.checkAnyPermissionOf(mContext,
+                android.Manifest.permission.CONNECTIVITY_USE_RESTRICTED_NETWORKS);
+    }
+
+    public void start() {
+        Log.i(TAG, "Starting Ethernet service");
+        mTracker.start();
+        mStarted.set(true);
+    }
+
+    private void throwIfEthernetNotStarted() {
+        if (!mStarted.get()) {
+            throw new IllegalStateException("System isn't ready to change ethernet configurations");
+        }
+    }
+
+    @Override
+    public String[] getAvailableInterfaces() throws RemoteException {
+        PermissionUtils.enforceAccessNetworkStatePermission(mContext, TAG);
+        return mTracker.getInterfaces(checkUseRestrictedNetworksPermission());
+    }
+
+    /**
+     * Get Ethernet configuration
+     * @return the Ethernet Configuration, contained in {@link IpConfiguration}.
+     */
+    @Override
+    public IpConfiguration getConfiguration(String iface) {
+        PermissionUtils.enforceAccessNetworkStatePermission(mContext, TAG);
+        if (mTracker.isRestrictedInterface(iface)) {
+            PermissionUtils.enforceRestrictedNetworkPermission(mContext, TAG);
+        }
+
+        return new IpConfiguration(mTracker.getIpConfiguration(iface));
+    }
+
+    /**
+     * Set Ethernet configuration
+     */
+    @Override
+    public void setConfiguration(String iface, IpConfiguration config) {
+        throwIfEthernetNotStarted();
+
+        PermissionUtils.enforceNetworkStackPermission(mContext);
+        if (mTracker.isRestrictedInterface(iface)) {
+            PermissionUtils.enforceRestrictedNetworkPermission(mContext, TAG);
+        }
+
+        // TODO: this does not check proxy settings, gateways, etc.
+        // Fix this by making IpConfiguration a complete representation of static configuration.
+        mTracker.updateIpConfiguration(iface, new IpConfiguration(config));
+    }
+
+    /**
+     * Indicates whether given interface is available.
+     */
+    @Override
+    public boolean isAvailable(String iface) {
+        PermissionUtils.enforceAccessNetworkStatePermission(mContext, TAG);
+        if (mTracker.isRestrictedInterface(iface)) {
+            PermissionUtils.enforceRestrictedNetworkPermission(mContext, TAG);
+        }
+
+        return mTracker.isTrackingInterface(iface);
+    }
+
+    /**
+     * Adds a listener.
+     * @param listener A {@link IEthernetServiceListener} to add.
+     */
+    public void addListener(IEthernetServiceListener listener) throws RemoteException {
+        Objects.requireNonNull(listener, "listener must not be null");
+        PermissionUtils.enforceAccessNetworkStatePermission(mContext, TAG);
+        mTracker.addListener(listener, checkUseRestrictedNetworksPermission());
+    }
+
+    /**
+     * Removes a listener.
+     * @param listener A {@link IEthernetServiceListener} to remove.
+     */
+    public void removeListener(IEthernetServiceListener listener) {
+        if (listener == null) {
+            throw new IllegalArgumentException("listener must not be null");
+        }
+        PermissionUtils.enforceAccessNetworkStatePermission(mContext, TAG);
+        mTracker.removeListener(listener);
+    }
+
+    @Override
+    public void setIncludeTestInterfaces(boolean include) {
+        PermissionUtils.enforceNetworkStackPermissionOr(mContext,
+                android.Manifest.permission.NETWORK_SETTINGS);
+        mTracker.setIncludeTestInterfaces(include);
+    }
+
+    @Override
+    public void requestTetheredInterface(ITetheredInterfaceCallback callback) {
+        Objects.requireNonNull(callback, "callback must not be null");
+        PermissionUtils.enforceNetworkStackPermissionOr(mContext,
+                android.Manifest.permission.NETWORK_SETTINGS);
+        mTracker.requestTetheredInterface(callback);
+    }
+
+    @Override
+    public void releaseTetheredInterface(ITetheredInterfaceCallback callback) {
+        Objects.requireNonNull(callback, "callback must not be null");
+        PermissionUtils.enforceNetworkStackPermissionOr(mContext,
+                android.Manifest.permission.NETWORK_SETTINGS);
+        mTracker.releaseTetheredInterface(callback);
+    }
+
+    @Override
+    protected void dump(FileDescriptor fd, PrintWriter writer, String[] args) {
+        final IndentingPrintWriter pw = new IndentingPrintWriter(writer, "  ");
+        if (mContext.checkCallingOrSelfPermission(android.Manifest.permission.DUMP)
+                != PackageManager.PERMISSION_GRANTED) {
+            pw.println("Permission Denial: can't dump EthernetService from pid="
+                    + Binder.getCallingPid()
+                    + ", uid=" + Binder.getCallingUid());
+            return;
+        }
+
+        pw.println("Current Ethernet state: ");
+        pw.increaseIndent();
+        mTracker.dump(fd, pw, args);
+        pw.decreaseIndent();
+
+        pw.println("Handler:");
+        pw.increaseIndent();
+        mHandler.dump(new PrintWriterPrinter(pw), "EthernetServiceImpl");
+        pw.decreaseIndent();
+    }
+
+    private void enforceNetworkManagementPermission() {
+        mContext.enforceCallingOrSelfPermission(
+                android.Manifest.permission.MANAGE_ETHERNET_NETWORKS,
+                "EthernetServiceImpl");
+    }
+
+    private void enforceManageTestNetworksPermission() {
+        mContext.enforceCallingOrSelfPermission(
+                android.Manifest.permission.MANAGE_TEST_NETWORKS,
+                "EthernetServiceImpl");
+    }
+
+    private void maybeValidateTestCapabilities(final String iface,
+            @Nullable final NetworkCapabilities nc) {
+        if (!mTracker.isValidTestInterface(iface)) {
+            return;
+        }
+        // For test interfaces, only null or capabilities that include TRANSPORT_TEST are
+        // allowed.
+        if (nc != null && !nc.hasTransport(TRANSPORT_TEST)) {
+            throw new IllegalArgumentException(
+                    "Updates to test interfaces must have NetworkCapabilities.TRANSPORT_TEST.");
+        }
+    }
+
+    private void enforceAdminPermission(final String iface, boolean enforceAutomotive,
+            final String logMessage) {
+        if (mTracker.isValidTestInterface(iface)) {
+            enforceManageTestNetworksPermission();
+        } else {
+            enforceNetworkManagementPermission();
+            if (enforceAutomotive) {
+                enforceAutomotiveDevice(logMessage);
+            }
+        }
+    }
+
+    @Override
+    public void updateConfiguration(@NonNull final String iface,
+            @NonNull final EthernetNetworkUpdateRequest request,
+            @Nullable final INetworkInterfaceOutcomeReceiver listener) {
+        Objects.requireNonNull(iface);
+        Objects.requireNonNull(request);
+        throwIfEthernetNotStarted();
+
+        // TODO: validate that iface is listed in overlay config_ethernet_interfaces
+        // only automotive devices are allowed to set the NetworkCapabilities using this API
+        enforceAdminPermission(iface, request.getNetworkCapabilities() != null,
+                "updateConfiguration() with non-null capabilities");
+        maybeValidateTestCapabilities(iface, request.getNetworkCapabilities());
+
+        mTracker.updateConfiguration(
+                iface, request.getIpConfiguration(), request.getNetworkCapabilities(), listener);
+    }
+
+    @Override
+    public void connectNetwork(@NonNull final String iface,
+            @Nullable final INetworkInterfaceOutcomeReceiver listener) {
+        Log.i(TAG, "connectNetwork called with: iface=" + iface + ", listener=" + listener);
+        Objects.requireNonNull(iface);
+        throwIfEthernetNotStarted();
+
+        enforceAdminPermission(iface, true, "connectNetwork()");
+
+        mTracker.connectNetwork(iface, listener);
+    }
+
+    @Override
+    public void disconnectNetwork(@NonNull final String iface,
+            @Nullable final INetworkInterfaceOutcomeReceiver listener) {
+        Log.i(TAG, "disconnectNetwork called with: iface=" + iface + ", listener=" + listener);
+        Objects.requireNonNull(iface);
+        throwIfEthernetNotStarted();
+
+        enforceAdminPermission(iface, true, "connectNetwork()");
+
+        mTracker.disconnectNetwork(iface, listener);
+    }
+
+    @Override
+    public void setEthernetEnabled(boolean enabled) {
+        PermissionUtils.enforceNetworkStackPermissionOr(mContext,
+                android.Manifest.permission.NETWORK_SETTINGS);
+
+        mTracker.setEthernetEnabled(enabled);
+    }
+
+    @Override
+    public List<String> getInterfaceList() {
+        PermissionUtils.enforceAccessNetworkStatePermission(mContext, TAG);
+        return mTracker.getInterfaceList();
+    }
+}
diff --git a/service-t/src/com/android/server/ethernet/EthernetTracker.java b/service-t/src/com/android/server/ethernet/EthernetTracker.java
new file mode 100644
index 0000000..abb1635
--- /dev/null
+++ b/service-t/src/com/android/server/ethernet/EthernetTracker.java
@@ -0,0 +1,943 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.ethernet;
+
+import static android.net.EthernetManager.ETHERNET_STATE_DISABLED;
+import static android.net.EthernetManager.ETHERNET_STATE_ENABLED;
+import static android.net.TestNetworkManager.TEST_TAP_PREFIX;
+
+import static com.android.internal.annotations.VisibleForTesting.Visibility.PACKAGE;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.Context;
+import android.content.res.Resources;
+import android.net.ConnectivityResources;
+import android.net.EthernetManager;
+import android.net.IEthernetServiceListener;
+import android.net.INetworkInterfaceOutcomeReceiver;
+import android.net.INetd;
+import android.net.ITetheredInterfaceCallback;
+import android.net.InterfaceConfigurationParcel;
+import android.net.IpConfiguration;
+import android.net.IpConfiguration.IpAssignment;
+import android.net.IpConfiguration.ProxySettings;
+import android.net.LinkAddress;
+import android.net.NetworkCapabilities;
+import android.net.StaticIpConfiguration;
+import android.os.ConditionVariable;
+import android.os.Handler;
+import android.os.RemoteCallbackList;
+import android.os.RemoteException;
+import android.os.ServiceSpecificException;
+import android.text.TextUtils;
+import android.util.ArrayMap;
+import android.util.Log;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.util.IndentingPrintWriter;
+import com.android.net.module.util.BaseNetdUnsolicitedEventListener;
+import com.android.net.module.util.NetdUtils;
+import com.android.net.module.util.PermissionUtils;
+
+import java.io.FileDescriptor;
+import java.net.InetAddress;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * Tracks Ethernet interfaces and manages interface configurations.
+ *
+ * <p>Interfaces may have different {@link android.net.NetworkCapabilities}. This mapping is defined
+ * in {@code config_ethernet_interfaces}. Notably, some interfaces could be marked as restricted by
+ * not specifying {@link android.net.NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED} flag.
+ * Interfaces could have associated {@link android.net.IpConfiguration}.
+ * Ethernet Interfaces may be present at boot time or appear after boot (e.g., for Ethernet adapters
+ * connected over USB). This class supports multiple interfaces. When an interface appears on the
+ * system (or is present at boot time) this class will start tracking it and bring it up. Only
+ * interfaces whose names match the {@code config_ethernet_iface_regex} regular expression are
+ * tracked.
+ *
+ * <p>All public or package private methods must be thread-safe unless stated otherwise.
+ */
+@VisibleForTesting(visibility = PACKAGE)
+public class EthernetTracker {
+    private static final int INTERFACE_MODE_CLIENT = 1;
+    private static final int INTERFACE_MODE_SERVER = 2;
+
+    private static final String TAG = EthernetTracker.class.getSimpleName();
+    private static final boolean DBG = EthernetNetworkFactory.DBG;
+
+    private static final String TEST_IFACE_REGEXP = TEST_TAP_PREFIX + "\\d+";
+    private static final String LEGACY_IFACE_REGEXP = "eth\\d";
+
+    /**
+     * Interface names we track. This is a product-dependent regular expression, plus,
+     * if setIncludeTestInterfaces is true, any test interfaces.
+     */
+    private volatile String mIfaceMatch;
+
+    /**
+     * Track test interfaces if true, don't track otherwise.
+     */
+    private boolean mIncludeTestInterfaces = false;
+
+    /** Mapping between {iface name | mac address} -> {NetworkCapabilities} */
+    private final ConcurrentHashMap<String, NetworkCapabilities> mNetworkCapabilities =
+            new ConcurrentHashMap<>();
+    private final ConcurrentHashMap<String, IpConfiguration> mIpConfigurations =
+            new ConcurrentHashMap<>();
+
+    private final Context mContext;
+    private final INetd mNetd;
+    private final Handler mHandler;
+    private final EthernetNetworkFactory mFactory;
+    private final EthernetConfigStore mConfigStore;
+    private final Dependencies mDeps;
+
+    private final RemoteCallbackList<IEthernetServiceListener> mListeners =
+            new RemoteCallbackList<>();
+    private final TetheredInterfaceRequestList mTetheredInterfaceRequests =
+            new TetheredInterfaceRequestList();
+
+    // Used only on the handler thread
+    private String mDefaultInterface;
+    private int mDefaultInterfaceMode = INTERFACE_MODE_CLIENT;
+    // Tracks whether clients were notified that the tethered interface is available
+    private boolean mTetheredInterfaceWasAvailable = false;
+    private volatile IpConfiguration mIpConfigForDefaultInterface;
+
+    private int mEthernetState = ETHERNET_STATE_ENABLED;
+
+    private class TetheredInterfaceRequestList extends
+            RemoteCallbackList<ITetheredInterfaceCallback> {
+        @Override
+        public void onCallbackDied(ITetheredInterfaceCallback cb, Object cookie) {
+            mHandler.post(EthernetTracker.this::maybeUntetherDefaultInterface);
+        }
+    }
+
+    public static class Dependencies {
+        // TODO: remove legacy resource fallback after migrating its overlays.
+        private String getPlatformRegexResource(Context context) {
+            final Resources r = context.getResources();
+            final int resId =
+                r.getIdentifier("config_ethernet_iface_regex", "string", context.getPackageName());
+            return r.getString(resId);
+        }
+
+        // TODO: remove legacy resource fallback after migrating its overlays.
+        private String[] getPlatformInterfaceConfigs(Context context) {
+            final Resources r = context.getResources();
+            final int resId = r.getIdentifier("config_ethernet_interfaces", "array",
+                    context.getPackageName());
+            return r.getStringArray(resId);
+        }
+
+        public String getInterfaceRegexFromResource(Context context) {
+            final String platformRegex = getPlatformRegexResource(context);
+            final String match;
+            if (!LEGACY_IFACE_REGEXP.equals(platformRegex)) {
+                // Platform resource is not the historical default: use the overlay
+                match = platformRegex;
+            } else {
+                final ConnectivityResources resources = new ConnectivityResources(context);
+                match = resources.get().getString(
+                        com.android.connectivity.resources.R.string.config_ethernet_iface_regex);
+            }
+            return match;
+        }
+
+        public String[] getInterfaceConfigFromResource(Context context) {
+            final String[] platformInterfaceConfigs = getPlatformInterfaceConfigs(context);
+            final String[] interfaceConfigs;
+            if (platformInterfaceConfigs.length != 0) {
+                // Platform resource is not the historical default: use the overlay
+                interfaceConfigs = platformInterfaceConfigs;
+            } else {
+                final ConnectivityResources resources = new ConnectivityResources(context);
+                interfaceConfigs = resources.get().getStringArray(
+                        com.android.connectivity.resources.R.array.config_ethernet_interfaces);
+            }
+            return interfaceConfigs;
+        }
+    }
+
+    EthernetTracker(@NonNull final Context context, @NonNull final Handler handler,
+            @NonNull final EthernetNetworkFactory factory, @NonNull final INetd netd) {
+        this(context, handler, factory, netd, new Dependencies());
+    }
+
+    @VisibleForTesting
+    EthernetTracker(@NonNull final Context context, @NonNull final Handler handler,
+            @NonNull final EthernetNetworkFactory factory, @NonNull final INetd netd,
+            @NonNull final Dependencies deps) {
+        mContext = context;
+        mHandler = handler;
+        mFactory = factory;
+        mNetd = netd;
+        mDeps = deps;
+
+        // Interface match regex.
+        updateIfaceMatchRegexp();
+
+        // Read default Ethernet interface configuration from resources
+        final String[] interfaceConfigs = mDeps.getInterfaceConfigFromResource(context);
+        for (String strConfig : interfaceConfigs) {
+            parseEthernetConfig(strConfig);
+        }
+
+        mConfigStore = new EthernetConfigStore();
+    }
+
+    void start() {
+        mFactory.register();
+        mConfigStore.read();
+
+        // Default interface is just the first one we want to track.
+        mIpConfigForDefaultInterface = mConfigStore.getIpConfigurationForDefaultInterface();
+        final ArrayMap<String, IpConfiguration> configs = mConfigStore.getIpConfigurations();
+        for (int i = 0; i < configs.size(); i++) {
+            mIpConfigurations.put(configs.keyAt(i), configs.valueAt(i));
+        }
+
+        try {
+            PermissionUtils.enforceNetworkStackPermission(mContext);
+            mNetd.registerUnsolicitedEventListener(new InterfaceObserver());
+        } catch (RemoteException | ServiceSpecificException e) {
+            Log.e(TAG, "Could not register InterfaceObserver " + e);
+        }
+
+        mHandler.post(this::trackAvailableInterfaces);
+    }
+
+    void updateIpConfiguration(String iface, IpConfiguration ipConfiguration) {
+        if (DBG) {
+            Log.i(TAG, "updateIpConfiguration, iface: " + iface + ", cfg: " + ipConfiguration);
+        }
+        writeIpConfiguration(iface, ipConfiguration);
+        mHandler.post(() -> {
+            mFactory.updateInterface(iface, ipConfiguration, null, null);
+            broadcastInterfaceStateChange(iface);
+        });
+    }
+
+    private void writeIpConfiguration(@NonNull final String iface,
+            @NonNull final IpConfiguration ipConfig) {
+        mConfigStore.write(iface, ipConfig);
+        mIpConfigurations.put(iface, ipConfig);
+    }
+
+    private IpConfiguration getIpConfigurationForCallback(String iface, int state) {
+        return (state == EthernetManager.STATE_ABSENT) ? null : getOrCreateIpConfiguration(iface);
+    }
+
+    private void ensureRunningOnEthernetServiceThread() {
+        if (mHandler.getLooper().getThread() != Thread.currentThread()) {
+            throw new IllegalStateException(
+                    "Not running on EthernetService thread: "
+                            + Thread.currentThread().getName());
+        }
+    }
+
+    /**
+     * Broadcast the link state or IpConfiguration change of existing Ethernet interfaces to all
+     * listeners.
+     */
+    protected void broadcastInterfaceStateChange(@NonNull String iface) {
+        ensureRunningOnEthernetServiceThread();
+        final int state = mFactory.getInterfaceState(iface);
+        final int role = getInterfaceRole(iface);
+        final IpConfiguration config = getIpConfigurationForCallback(iface, state);
+        final int n = mListeners.beginBroadcast();
+        for (int i = 0; i < n; i++) {
+            try {
+                mListeners.getBroadcastItem(i).onInterfaceStateChanged(iface, state, role, config);
+            } catch (RemoteException e) {
+                // Do nothing here.
+            }
+        }
+        mListeners.finishBroadcast();
+    }
+
+    /**
+     * Unicast the interface state or IpConfiguration change of existing Ethernet interfaces to a
+     * specific listener.
+     */
+    protected void unicastInterfaceStateChange(@NonNull IEthernetServiceListener listener,
+            @NonNull String iface) {
+        ensureRunningOnEthernetServiceThread();
+        final int state = mFactory.getInterfaceState(iface);
+        final int role = getInterfaceRole(iface);
+        final IpConfiguration config = getIpConfigurationForCallback(iface, state);
+        try {
+            listener.onInterfaceStateChanged(iface, state, role, config);
+        } catch (RemoteException e) {
+            // Do nothing here.
+        }
+    }
+
+    @VisibleForTesting(visibility = PACKAGE)
+    protected void updateConfiguration(@NonNull final String iface,
+            @Nullable final IpConfiguration ipConfig,
+            @Nullable final NetworkCapabilities capabilities,
+            @Nullable final INetworkInterfaceOutcomeReceiver listener) {
+        if (DBG) {
+            Log.i(TAG, "updateConfiguration, iface: " + iface + ", capabilities: " + capabilities
+                    + ", ipConfig: " + ipConfig);
+        }
+
+        final IpConfiguration localIpConfig = ipConfig == null
+                ? null : new IpConfiguration(ipConfig);
+        if (ipConfig != null) {
+            writeIpConfiguration(iface, localIpConfig);
+        }
+
+        if (null != capabilities) {
+            mNetworkCapabilities.put(iface, capabilities);
+        }
+        mHandler.post(() -> {
+            mFactory.updateInterface(iface, localIpConfig, capabilities, listener);
+            broadcastInterfaceStateChange(iface);
+        });
+    }
+
+    @VisibleForTesting(visibility = PACKAGE)
+    protected void connectNetwork(@NonNull final String iface,
+            @Nullable final INetworkInterfaceOutcomeReceiver listener) {
+        mHandler.post(() -> updateInterfaceState(iface, true, listener));
+    }
+
+    @VisibleForTesting(visibility = PACKAGE)
+    protected void disconnectNetwork(@NonNull final String iface,
+            @Nullable final INetworkInterfaceOutcomeReceiver listener) {
+        mHandler.post(() -> updateInterfaceState(iface, false, listener));
+    }
+
+    IpConfiguration getIpConfiguration(String iface) {
+        return mIpConfigurations.get(iface);
+    }
+
+    @VisibleForTesting(visibility = PACKAGE)
+    protected boolean isTrackingInterface(String iface) {
+        return mFactory.hasInterface(iface);
+    }
+
+    String[] getInterfaces(boolean includeRestricted) {
+        return mFactory.getAvailableInterfaces(includeRestricted);
+    }
+
+    List<String> getInterfaceList() {
+        final List<String> interfaceList = new ArrayList<String>();
+        final String[] ifaces;
+        try {
+            ifaces = mNetd.interfaceGetList();
+        } catch (RemoteException e) {
+            Log.e(TAG, "Could not get list of interfaces " + e);
+            return interfaceList;
+        }
+        final String ifaceMatch = mIfaceMatch;
+        for (String iface : ifaces) {
+            if (iface.matches(ifaceMatch)) interfaceList.add(iface);
+        }
+        return interfaceList;
+    }
+
+    /**
+     * Returns true if given interface was configured as restricted (doesn't have
+     * NET_CAPABILITY_NOT_RESTRICTED) capability. Otherwise, returns false.
+     */
+    boolean isRestrictedInterface(String iface) {
+        final NetworkCapabilities nc = mNetworkCapabilities.get(iface);
+        return nc != null && !nc.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED);
+    }
+
+    void addListener(IEthernetServiceListener listener, boolean canUseRestrictedNetworks) {
+        mHandler.post(() -> {
+            if (!mListeners.register(listener, new ListenerInfo(canUseRestrictedNetworks))) {
+                // Remote process has already died
+                return;
+            }
+            for (String iface : getInterfaces(canUseRestrictedNetworks)) {
+                unicastInterfaceStateChange(listener, iface);
+            }
+
+            unicastEthernetStateChange(listener, mEthernetState);
+        });
+    }
+
+    void removeListener(IEthernetServiceListener listener) {
+        mHandler.post(() -> mListeners.unregister(listener));
+    }
+
+    public void setIncludeTestInterfaces(boolean include) {
+        mHandler.post(() -> {
+            mIncludeTestInterfaces = include;
+            updateIfaceMatchRegexp();
+            mHandler.post(() -> trackAvailableInterfaces());
+        });
+    }
+
+    public void requestTetheredInterface(ITetheredInterfaceCallback callback) {
+        mHandler.post(() -> {
+            if (!mTetheredInterfaceRequests.register(callback)) {
+                // Remote process has already died
+                return;
+            }
+            if (mDefaultInterfaceMode == INTERFACE_MODE_SERVER) {
+                if (mTetheredInterfaceWasAvailable) {
+                    notifyTetheredInterfaceAvailable(callback, mDefaultInterface);
+                }
+                return;
+            }
+
+            setDefaultInterfaceMode(INTERFACE_MODE_SERVER);
+        });
+    }
+
+    public void releaseTetheredInterface(ITetheredInterfaceCallback callback) {
+        mHandler.post(() -> {
+            mTetheredInterfaceRequests.unregister(callback);
+            maybeUntetherDefaultInterface();
+        });
+    }
+
+    private void notifyTetheredInterfaceAvailable(ITetheredInterfaceCallback cb, String iface) {
+        try {
+            cb.onAvailable(iface);
+        } catch (RemoteException e) {
+            Log.e(TAG, "Error sending tethered interface available callback", e);
+        }
+    }
+
+    private void notifyTetheredInterfaceUnavailable(ITetheredInterfaceCallback cb) {
+        try {
+            cb.onUnavailable();
+        } catch (RemoteException e) {
+            Log.e(TAG, "Error sending tethered interface available callback", e);
+        }
+    }
+
+    private void maybeUntetherDefaultInterface() {
+        if (mTetheredInterfaceRequests.getRegisteredCallbackCount() > 0) return;
+        if (mDefaultInterfaceMode == INTERFACE_MODE_CLIENT) return;
+        setDefaultInterfaceMode(INTERFACE_MODE_CLIENT);
+    }
+
+    private void setDefaultInterfaceMode(int mode) {
+        Log.d(TAG, "Setting default interface mode to " + mode);
+        mDefaultInterfaceMode = mode;
+        if (mDefaultInterface != null) {
+            removeInterface(mDefaultInterface);
+            addInterface(mDefaultInterface);
+        }
+    }
+
+    private int getInterfaceRole(final String iface) {
+        if (!mFactory.hasInterface(iface)) return EthernetManager.ROLE_NONE;
+        final int mode = getInterfaceMode(iface);
+        return (mode == INTERFACE_MODE_CLIENT)
+                ? EthernetManager.ROLE_CLIENT
+                : EthernetManager.ROLE_SERVER;
+    }
+
+    private int getInterfaceMode(final String iface) {
+        if (iface.equals(mDefaultInterface)) {
+            return mDefaultInterfaceMode;
+        }
+        return INTERFACE_MODE_CLIENT;
+    }
+
+    private void removeInterface(String iface) {
+        mFactory.removeInterface(iface);
+        maybeUpdateServerModeInterfaceState(iface, false);
+    }
+
+    private void stopTrackingInterface(String iface) {
+        removeInterface(iface);
+        if (iface.equals(mDefaultInterface)) {
+            mDefaultInterface = null;
+        }
+        broadcastInterfaceStateChange(iface);
+    }
+
+    private void addInterface(String iface) {
+        InterfaceConfigurationParcel config = null;
+        // Bring up the interface so we get link status indications.
+        try {
+            PermissionUtils.enforceNetworkStackPermission(mContext);
+            NetdUtils.setInterfaceUp(mNetd, iface);
+            config = NetdUtils.getInterfaceConfigParcel(mNetd, iface);
+        } catch (IllegalStateException e) {
+            // Either the system is crashing or the interface has disappeared. Just ignore the
+            // error; we haven't modified any state because we only do that if our calls succeed.
+            Log.e(TAG, "Error upping interface " + iface, e);
+        }
+
+        if (config == null) {
+            Log.e(TAG, "Null interface config parcelable for " + iface + ". Bailing out.");
+            return;
+        }
+
+        final String hwAddress = config.hwAddr;
+
+        NetworkCapabilities nc = mNetworkCapabilities.get(iface);
+        if (nc == null) {
+            // Try to resolve using mac address
+            nc = mNetworkCapabilities.get(hwAddress);
+            if (nc == null) {
+                final boolean isTestIface = iface.matches(TEST_IFACE_REGEXP);
+                nc = createDefaultNetworkCapabilities(isTestIface);
+            }
+        }
+
+        final int mode = getInterfaceMode(iface);
+        if (mode == INTERFACE_MODE_CLIENT) {
+            IpConfiguration ipConfiguration = getOrCreateIpConfiguration(iface);
+            Log.d(TAG, "Tracking interface in client mode: " + iface);
+            mFactory.addInterface(iface, hwAddress, ipConfiguration, nc);
+        } else {
+            maybeUpdateServerModeInterfaceState(iface, true);
+        }
+
+        // Note: if the interface already has link (e.g., if we crashed and got
+        // restarted while it was running), we need to fake a link up notification so we
+        // start configuring it.
+        if (NetdUtils.hasFlag(config, "running")) {
+            updateInterfaceState(iface, true);
+        }
+    }
+
+    private void updateInterfaceState(String iface, boolean up) {
+        updateInterfaceState(iface, up, null /* listener */);
+    }
+
+    private void updateInterfaceState(@NonNull final String iface, final boolean up,
+            @Nullable final INetworkInterfaceOutcomeReceiver listener) {
+        final int mode = getInterfaceMode(iface);
+        final boolean factoryLinkStateUpdated = (mode == INTERFACE_MODE_CLIENT)
+                && mFactory.updateInterfaceLinkState(iface, up, listener);
+
+        if (factoryLinkStateUpdated) {
+            broadcastInterfaceStateChange(iface);
+        }
+    }
+
+    private void maybeUpdateServerModeInterfaceState(String iface, boolean available) {
+        if (available == mTetheredInterfaceWasAvailable || !iface.equals(mDefaultInterface)) return;
+
+        Log.d(TAG, (available ? "Tracking" : "No longer tracking")
+                + " interface in server mode: " + iface);
+
+        final int pendingCbs = mTetheredInterfaceRequests.beginBroadcast();
+        for (int i = 0; i < pendingCbs; i++) {
+            ITetheredInterfaceCallback item = mTetheredInterfaceRequests.getBroadcastItem(i);
+            if (available) {
+                notifyTetheredInterfaceAvailable(item, iface);
+            } else {
+                notifyTetheredInterfaceUnavailable(item);
+            }
+        }
+        mTetheredInterfaceRequests.finishBroadcast();
+        mTetheredInterfaceWasAvailable = available;
+    }
+
+    private void maybeTrackInterface(String iface) {
+        if (!iface.matches(mIfaceMatch)) {
+            return;
+        }
+
+        // If we don't already track this interface, and if this interface matches
+        // our regex, start tracking it.
+        if (mFactory.hasInterface(iface) || iface.equals(mDefaultInterface)) {
+            if (DBG) Log.w(TAG, "Ignoring already-tracked interface " + iface);
+            return;
+        }
+        if (DBG) Log.i(TAG, "maybeTrackInterface: " + iface);
+
+        // TODO: avoid making an interface default if it has configured NetworkCapabilities.
+        if (mDefaultInterface == null) {
+            mDefaultInterface = iface;
+        }
+
+        if (mIpConfigForDefaultInterface != null) {
+            updateIpConfiguration(iface, mIpConfigForDefaultInterface);
+            mIpConfigForDefaultInterface = null;
+        }
+
+        addInterface(iface);
+
+        broadcastInterfaceStateChange(iface);
+    }
+
+    private void trackAvailableInterfaces() {
+        try {
+            final String[] ifaces = mNetd.interfaceGetList();
+            for (String iface : ifaces) {
+                maybeTrackInterface(iface);
+            }
+        } catch (RemoteException | ServiceSpecificException e) {
+            Log.e(TAG, "Could not get list of interfaces " + e);
+        }
+    }
+
+    private class InterfaceObserver extends BaseNetdUnsolicitedEventListener {
+
+        @Override
+        public void onInterfaceLinkStateChanged(String iface, boolean up) {
+            if (DBG) {
+                Log.i(TAG, "interfaceLinkStateChanged, iface: " + iface + ", up: " + up);
+            }
+            mHandler.post(() -> updateInterfaceState(iface, up));
+        }
+
+        @Override
+        public void onInterfaceAdded(String iface) {
+            if (DBG) {
+                Log.i(TAG, "onInterfaceAdded, iface: " + iface);
+            }
+            mHandler.post(() -> maybeTrackInterface(iface));
+        }
+
+        @Override
+        public void onInterfaceRemoved(String iface) {
+            if (DBG) {
+                Log.i(TAG, "onInterfaceRemoved, iface: " + iface);
+            }
+            mHandler.post(() -> stopTrackingInterface(iface));
+        }
+    }
+
+    private static class ListenerInfo {
+
+        boolean canUseRestrictedNetworks = false;
+
+        ListenerInfo(boolean canUseRestrictedNetworks) {
+            this.canUseRestrictedNetworks = canUseRestrictedNetworks;
+        }
+    }
+
+    /**
+     * Parses an Ethernet interface configuration
+     *
+     * @param configString represents an Ethernet configuration in the following format: {@code
+     * <interface name|mac address>;[Network Capabilities];[IP config];[Override Transport]}
+     */
+    private void parseEthernetConfig(String configString) {
+        final EthernetTrackerConfig config = createEthernetTrackerConfig(configString);
+        NetworkCapabilities nc = createNetworkCapabilities(
+                !TextUtils.isEmpty(config.mCapabilities)  /* clear default capabilities */,
+                config.mCapabilities, config.mTransport).build();
+        mNetworkCapabilities.put(config.mIface, nc);
+
+        if (null != config.mIpConfig) {
+            IpConfiguration ipConfig = parseStaticIpConfiguration(config.mIpConfig);
+            mIpConfigurations.put(config.mIface, ipConfig);
+        }
+    }
+
+    @VisibleForTesting
+    static EthernetTrackerConfig createEthernetTrackerConfig(@NonNull final String configString) {
+        Objects.requireNonNull(configString, "EthernetTrackerConfig requires non-null config");
+        return new EthernetTrackerConfig(configString.split(";", /* limit of tokens */ 4));
+    }
+
+    private static NetworkCapabilities createDefaultNetworkCapabilities(boolean isTestIface) {
+        NetworkCapabilities.Builder builder = createNetworkCapabilities(
+                false /* clear default capabilities */, null, null)
+                .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED)
+                .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED)
+                .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_ROAMING)
+                .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_CONGESTED)
+                .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_SUSPENDED)
+                .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VCN_MANAGED);
+
+        if (isTestIface) {
+            builder.addTransportType(NetworkCapabilities.TRANSPORT_TEST);
+        } else {
+            builder.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET);
+        }
+
+        return builder.build();
+    }
+
+    /**
+     * Parses a static list of network capabilities
+     *
+     * @param clearDefaultCapabilities Indicates whether or not to clear any default capabilities
+     * @param commaSeparatedCapabilities A comma separated string list of integer encoded
+     *                                   NetworkCapability.NET_CAPABILITY_* values
+     * @param overrideTransport A string representing a single integer encoded override transport
+     *                          type. Must be one of the NetworkCapability.TRANSPORT_*
+     *                          values. TRANSPORT_VPN is not supported. Errors with input
+     *                          will cause the override to be ignored.
+     */
+    @VisibleForTesting
+    static NetworkCapabilities.Builder createNetworkCapabilities(
+            boolean clearDefaultCapabilities, @Nullable String commaSeparatedCapabilities,
+            @Nullable String overrideTransport) {
+
+        final NetworkCapabilities.Builder builder = clearDefaultCapabilities
+                ? NetworkCapabilities.Builder.withoutDefaultCapabilities()
+                : new NetworkCapabilities.Builder();
+
+        // Determine the transport type. If someone has tried to define an override transport then
+        // attempt to add it. Since we can only have one override, all errors with it will
+        // gracefully default back to TRANSPORT_ETHERNET and warn the user. VPN is not allowed as an
+        // override type. Wifi Aware and LoWPAN are currently unsupported as well.
+        int transport = NetworkCapabilities.TRANSPORT_ETHERNET;
+        if (!TextUtils.isEmpty(overrideTransport)) {
+            try {
+                int parsedTransport = Integer.valueOf(overrideTransport);
+                if (parsedTransport == NetworkCapabilities.TRANSPORT_VPN
+                        || parsedTransport == NetworkCapabilities.TRANSPORT_WIFI_AWARE
+                        || parsedTransport == NetworkCapabilities.TRANSPORT_LOWPAN) {
+                    Log.e(TAG, "Override transport '" + parsedTransport + "' is not supported. "
+                            + "Defaulting to TRANSPORT_ETHERNET");
+                } else {
+                    transport = parsedTransport;
+                }
+            } catch (NumberFormatException nfe) {
+                Log.e(TAG, "Override transport type '" + overrideTransport + "' "
+                        + "could not be parsed. Defaulting to TRANSPORT_ETHERNET");
+            }
+        }
+
+        // Apply the transport. If the user supplied a valid number that is not a valid transport
+        // then adding will throw an exception. Default back to TRANSPORT_ETHERNET if that happens
+        try {
+            builder.addTransportType(transport);
+        } catch (IllegalArgumentException iae) {
+            Log.e(TAG, transport + " is not a valid NetworkCapability.TRANSPORT_* value. "
+                    + "Defaulting to TRANSPORT_ETHERNET");
+            builder.addTransportType(NetworkCapabilities.TRANSPORT_ETHERNET);
+        }
+
+        builder.setLinkUpstreamBandwidthKbps(100 * 1000);
+        builder.setLinkDownstreamBandwidthKbps(100 * 1000);
+
+        if (!TextUtils.isEmpty(commaSeparatedCapabilities)) {
+            for (String strNetworkCapability : commaSeparatedCapabilities.split(",")) {
+                if (!TextUtils.isEmpty(strNetworkCapability)) {
+                    try {
+                        builder.addCapability(Integer.valueOf(strNetworkCapability));
+                    } catch (NumberFormatException nfe) {
+                        Log.e(TAG, "Capability '" + strNetworkCapability + "' could not be parsed");
+                    } catch (IllegalArgumentException iae) {
+                        Log.e(TAG, strNetworkCapability + " is not a valid "
+                                + "NetworkCapability.NET_CAPABILITY_* value");
+                    }
+                }
+            }
+        }
+        // Ethernet networks have no way to update the following capabilities, so they always
+        // have them.
+        builder.addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_ROAMING);
+        builder.addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_CONGESTED);
+        builder.addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_SUSPENDED);
+
+        return builder;
+    }
+
+    /**
+     * Parses static IP configuration.
+     *
+     * @param staticIpConfig represents static IP configuration in the following format: {@code
+     * ip=<ip-address/mask> gateway=<ip-address> dns=<comma-sep-ip-addresses>
+     *     domains=<comma-sep-domains>}
+     */
+    @VisibleForTesting
+    static IpConfiguration parseStaticIpConfiguration(String staticIpConfig) {
+        final StaticIpConfiguration.Builder staticIpConfigBuilder =
+                new StaticIpConfiguration.Builder();
+
+        for (String keyValueAsString : staticIpConfig.trim().split(" ")) {
+            if (TextUtils.isEmpty(keyValueAsString)) continue;
+
+            String[] pair = keyValueAsString.split("=");
+            if (pair.length != 2) {
+                throw new IllegalArgumentException("Unexpected token: " + keyValueAsString
+                        + " in " + staticIpConfig);
+            }
+
+            String key = pair[0];
+            String value = pair[1];
+
+            switch (key) {
+                case "ip":
+                    staticIpConfigBuilder.setIpAddress(new LinkAddress(value));
+                    break;
+                case "domains":
+                    staticIpConfigBuilder.setDomains(value);
+                    break;
+                case "gateway":
+                    staticIpConfigBuilder.setGateway(InetAddress.parseNumericAddress(value));
+                    break;
+                case "dns": {
+                    ArrayList<InetAddress> dnsAddresses = new ArrayList<>();
+                    for (String address: value.split(",")) {
+                        dnsAddresses.add(InetAddress.parseNumericAddress(address));
+                    }
+                    staticIpConfigBuilder.setDnsServers(dnsAddresses);
+                    break;
+                }
+                default : {
+                    throw new IllegalArgumentException("Unexpected key: " + key
+                            + " in " + staticIpConfig);
+                }
+            }
+        }
+        return createIpConfiguration(staticIpConfigBuilder.build());
+    }
+
+    private static IpConfiguration createIpConfiguration(
+            @NonNull final StaticIpConfiguration staticIpConfig) {
+        return new IpConfiguration.Builder().setStaticIpConfiguration(staticIpConfig).build();
+    }
+
+    private IpConfiguration getOrCreateIpConfiguration(String iface) {
+        IpConfiguration ret = mIpConfigurations.get(iface);
+        if (ret != null) return ret;
+        ret = new IpConfiguration();
+        ret.setIpAssignment(IpAssignment.DHCP);
+        ret.setProxySettings(ProxySettings.NONE);
+        return ret;
+    }
+
+    private void updateIfaceMatchRegexp() {
+        final String match = mDeps.getInterfaceRegexFromResource(mContext);
+        mIfaceMatch = mIncludeTestInterfaces
+                ? "(" + match + "|" + TEST_IFACE_REGEXP + ")"
+                : match;
+        Log.d(TAG, "Interface match regexp set to '" + mIfaceMatch + "'");
+    }
+
+    /**
+     * Validate if a given interface is valid for testing.
+     *
+     * @param iface the name of the interface to validate.
+     * @return {@code true} if test interfaces are enabled and the given {@code iface} has a test
+     * interface prefix, {@code false} otherwise.
+     */
+    public boolean isValidTestInterface(@NonNull final String iface) {
+        return mIncludeTestInterfaces && iface.matches(TEST_IFACE_REGEXP);
+    }
+
+    private void postAndWaitForRunnable(Runnable r) {
+        final ConditionVariable cv = new ConditionVariable();
+        if (mHandler.post(() -> {
+            r.run();
+            cv.open();
+        })) {
+            cv.block(2000L);
+        }
+    }
+
+    @VisibleForTesting(visibility = PACKAGE)
+    protected void setEthernetEnabled(boolean enabled) {
+        mHandler.post(() -> {
+            int newState = enabled ? ETHERNET_STATE_ENABLED : ETHERNET_STATE_DISABLED;
+            if (mEthernetState == newState) return;
+
+            mEthernetState = newState;
+
+            if (enabled) {
+                trackAvailableInterfaces();
+            } else {
+                // TODO: maybe also disable server mode interface as well.
+                untrackFactoryInterfaces();
+            }
+            broadcastEthernetStateChange(mEthernetState);
+        });
+    }
+
+    private void untrackFactoryInterfaces() {
+        for (String iface : mFactory.getAvailableInterfaces(true /* includeRestricted */)) {
+            stopTrackingInterface(iface);
+        }
+    }
+
+    private void unicastEthernetStateChange(@NonNull IEthernetServiceListener listener,
+            int state) {
+        ensureRunningOnEthernetServiceThread();
+        try {
+            listener.onEthernetStateChanged(state);
+        } catch (RemoteException e) {
+            // Do nothing here.
+        }
+    }
+
+    private void broadcastEthernetStateChange(int state) {
+        ensureRunningOnEthernetServiceThread();
+        final int n = mListeners.beginBroadcast();
+        for (int i = 0; i < n; i++) {
+            try {
+                mListeners.getBroadcastItem(i).onEthernetStateChanged(state);
+            } catch (RemoteException e) {
+                // Do nothing here.
+            }
+        }
+        mListeners.finishBroadcast();
+    }
+
+    void dump(FileDescriptor fd, IndentingPrintWriter pw, String[] args) {
+        postAndWaitForRunnable(() -> {
+            pw.println(getClass().getSimpleName());
+            pw.println("Ethernet interface name filter: " + mIfaceMatch);
+            pw.println("Default interface: " + mDefaultInterface);
+            pw.println("Default interface mode: " + mDefaultInterfaceMode);
+            pw.println("Tethered interface requests: "
+                    + mTetheredInterfaceRequests.getRegisteredCallbackCount());
+            pw.println("Listeners: " + mListeners.getRegisteredCallbackCount());
+            pw.println("IP Configurations:");
+            pw.increaseIndent();
+            for (String iface : mIpConfigurations.keySet()) {
+                pw.println(iface + ": " + mIpConfigurations.get(iface));
+            }
+            pw.decreaseIndent();
+            pw.println();
+
+            pw.println("Network Capabilities:");
+            pw.increaseIndent();
+            for (String iface : mNetworkCapabilities.keySet()) {
+                pw.println(iface + ": " + mNetworkCapabilities.get(iface));
+            }
+            pw.decreaseIndent();
+            pw.println();
+
+            mFactory.dump(fd, pw, args);
+        });
+    }
+
+    @VisibleForTesting
+    static class EthernetTrackerConfig {
+        final String mIface;
+        final String mCapabilities;
+        final String mIpConfig;
+        final String mTransport;
+
+        EthernetTrackerConfig(@NonNull final String[] tokens) {
+            Objects.requireNonNull(tokens, "EthernetTrackerConfig requires non-null tokens");
+            mIface = tokens[0];
+            mCapabilities = tokens.length > 1 ? tokens[1] : null;
+            mIpConfig = tokens.length > 2 && !TextUtils.isEmpty(tokens[2]) ? tokens[2] : null;
+            mTransport = tokens.length > 3 ? tokens[3] : null;
+        }
+    }
+}
diff --git a/service/src/com/android/server/connectivity/CarrierPrivilegeAuthenticator.java b/service/src/com/android/server/connectivity/CarrierPrivilegeAuthenticator.java
index b761762..ce955fd 100644
--- a/service/src/com/android/server/connectivity/CarrierPrivilegeAuthenticator.java
+++ b/service/src/com/android/server/connectivity/CarrierPrivilegeAuthenticator.java
@@ -19,8 +19,6 @@
 import static android.net.NetworkCapabilities.NET_CAPABILITY_CBS;
 import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR;
 
-import static com.android.networkstack.apishim.ConstantsShim.RECEIVER_NOT_EXPORTED;
-
 import android.annotation.NonNull;
 import android.content.BroadcastReceiver;
 import android.content.Context;
@@ -160,7 +158,7 @@
     private void registerForCarrierChanges() {
         final IntentFilter filter = new IntentFilter();
         filter.addAction(TelephonyManager.ACTION_MULTI_SIM_CONFIG_CHANGED);
-        mContext.registerReceiver(this, filter, null, mHandler, RECEIVER_NOT_EXPORTED /* flags */);
+        mContext.registerReceiver(this, filter, null, mHandler);
         registerCarrierPrivilegesListeners();
     }
 
diff --git a/tests/ethernet/Android.bp b/tests/ethernet/Android.bp
new file mode 100644
index 0000000..8342490
--- /dev/null
+++ b/tests/ethernet/Android.bp
@@ -0,0 +1,52 @@
+// Copyright (C) 2018 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+// TODO: merge the tests into service-connectivity tests after
+// ethernet service migration completes. So far just import the
+// ethernet service source to fix the dependencies.
+android_test {
+    name: "EthernetServiceTests",
+
+    srcs: [
+        ":ethernet-service-test-sources",
+        "java/**/*.java",
+    ],
+
+    certificate: "platform",
+    platform_apis: true,
+
+    libs: [
+        "android.test.runner",
+        "android.test.base",
+        "android.test.mock",
+        "framework-connectivity.impl",
+        "framework-connectivity-t.impl",
+        "ServiceConnectivityResources",
+    ],
+
+    static_libs: [
+        "androidx.test.rules",
+        "frameworks-base-testutils",
+        "mockito-target-minus-junit4",
+        "net-tests-utils",
+        "services.core",
+        "services.net",
+    ],
+    test_suites: ["general-tests"],
+}
diff --git a/tests/ethernet/AndroidManifest.xml b/tests/ethernet/AndroidManifest.xml
new file mode 100644
index 0000000..cd875b0
--- /dev/null
+++ b/tests/ethernet/AndroidManifest.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2018 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License
+  -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.android.server.ethernet.tests">
+
+    <application>
+        <uses-library android:name="android.test.runner" />
+    </application>
+
+    <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+        android:targetPackage="com.android.server.ethernet.tests"
+        android:label="Ethernet Service Tests" />
+</manifest>
diff --git a/tests/ethernet/java/com/android/server/ethernet/EthernetNetworkFactoryTest.java b/tests/ethernet/java/com/android/server/ethernet/EthernetNetworkFactoryTest.java
new file mode 100644
index 0000000..4d3e4d3
--- /dev/null
+++ b/tests/ethernet/java/com/android/server/ethernet/EthernetNetworkFactoryTest.java
@@ -0,0 +1,783 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.ethernet;
+
+import static com.android.testutils.DevSdkIgnoreRuleKt.SC_V2;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNotSame;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.argThat;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.ArgumentMatchers.same;
+import static org.mockito.Mockito.clearInvocations;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.annotation.NonNull;
+import android.app.test.MockAnswerUtil.AnswerWithArguments;
+import android.content.Context;
+import android.content.res.Resources;
+import android.net.ConnectivityManager;
+import android.net.EthernetNetworkSpecifier;
+import android.net.EthernetNetworkManagementException;
+import android.net.INetworkInterfaceOutcomeReceiver;
+import android.net.IpConfiguration;
+import android.net.LinkAddress;
+import android.net.LinkProperties;
+import android.net.Network;
+import android.net.NetworkAgentConfig;
+import android.net.NetworkCapabilities;
+import android.net.NetworkProvider;
+import android.net.NetworkRequest;
+import android.net.StaticIpConfiguration;
+import android.net.ip.IpClientCallbacks;
+import android.net.ip.IpClientManager;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Looper;
+import android.os.test.TestLooper;
+import android.util.Pair;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.connectivity.resources.R;
+import com.android.net.module.util.InterfaceParams;
+
+import com.android.testutils.DevSdkIgnoreRule;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.util.Objects;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.TimeUnit;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class EthernetNetworkFactoryTest {
+    private static final int TIMEOUT_MS = 2_000;
+    private static final String TEST_IFACE = "test123";
+    private static final INetworkInterfaceOutcomeReceiver NULL_LISTENER = null;
+    private static final String IP_ADDR = "192.0.2.2/25";
+    private static final LinkAddress LINK_ADDR = new LinkAddress(IP_ADDR);
+    private static final String HW_ADDR = "01:02:03:04:05:06";
+    private TestLooper mLooper;
+    private Handler mHandler;
+    private EthernetNetworkFactory mNetFactory = null;
+    private IpClientCallbacks mIpClientCallbacks;
+    @Mock private Context mContext;
+    @Mock private Resources mResources;
+    @Mock private EthernetNetworkFactory.Dependencies mDeps;
+    @Mock private IpClientManager mIpClient;
+    @Mock private EthernetNetworkAgent mNetworkAgent;
+    @Mock private InterfaceParams mInterfaceParams;
+    @Mock private Network mMockNetwork;
+
+    @Before
+    public void setUp() throws Exception {
+        MockitoAnnotations.initMocks(this);
+        setupNetworkAgentMock();
+        setupIpClientMock();
+        setupContext();
+    }
+
+    //TODO: Move away from usage of TestLooper in order to move this logic back into @Before.
+    private void initEthernetNetworkFactory() {
+        mLooper = new TestLooper();
+        mHandler = new Handler(mLooper.getLooper());
+        mNetFactory = new EthernetNetworkFactory(mHandler, mContext, mDeps);
+    }
+
+    private void setupNetworkAgentMock() {
+        when(mDeps.makeEthernetNetworkAgent(any(), any(), any(), any(), any(), any(), any()))
+                .thenAnswer(new AnswerWithArguments() {
+                                       public EthernetNetworkAgent answer(
+                                               Context context,
+                                               Looper looper,
+                                               NetworkCapabilities nc,
+                                               LinkProperties lp,
+                                               NetworkAgentConfig config,
+                                               NetworkProvider provider,
+                                               EthernetNetworkAgent.Callbacks cb) {
+                                           when(mNetworkAgent.getCallbacks()).thenReturn(cb);
+                                           when(mNetworkAgent.getNetwork())
+                                                   .thenReturn(mMockNetwork);
+                                           return mNetworkAgent;
+                                       }
+                                   }
+        );
+    }
+
+    private void setupIpClientMock() throws Exception {
+        doAnswer(inv -> {
+            // these tests only support one concurrent IpClient, so make sure we do not accidentally
+            // create a mess.
+            assertNull("An IpClient has already been created.", mIpClientCallbacks);
+
+            mIpClientCallbacks = inv.getArgument(2);
+            mIpClientCallbacks.onIpClientCreated(null);
+            mLooper.dispatchAll();
+            return null;
+        }).when(mDeps).makeIpClient(any(Context.class), anyString(), any());
+
+        doAnswer(inv -> {
+            mIpClientCallbacks.onQuit();
+            mLooper.dispatchAll();
+            mIpClientCallbacks = null;
+            return null;
+        }).when(mIpClient).shutdown();
+
+        when(mDeps.makeIpClientManager(any())).thenReturn(mIpClient);
+    }
+
+    private void triggerOnProvisioningSuccess() {
+        mIpClientCallbacks.onProvisioningSuccess(new LinkProperties());
+        mLooper.dispatchAll();
+    }
+
+    private void triggerOnProvisioningFailure() {
+        mIpClientCallbacks.onProvisioningFailure(new LinkProperties());
+        mLooper.dispatchAll();
+    }
+
+    private void triggerOnReachabilityLost() {
+        mIpClientCallbacks.onReachabilityLost("ReachabilityLost");
+        mLooper.dispatchAll();
+    }
+
+    private void setupContext() {
+        when(mDeps.getTcpBufferSizesFromResource(eq(mContext))).thenReturn("");
+    }
+
+    @After
+    public void tearDown() {
+        // looper is shared with the network agents, so there may still be messages to dispatch on
+        // tear down.
+        mLooper.dispatchAll();
+    }
+
+    private NetworkCapabilities createDefaultFilterCaps() {
+        return NetworkCapabilities.Builder.withoutDefaultCapabilities()
+                .addTransportType(NetworkCapabilities.TRANSPORT_ETHERNET)
+                .build();
+    }
+
+    private NetworkCapabilities.Builder createInterfaceCapsBuilder(final int transportType) {
+        return new NetworkCapabilities.Builder()
+                .addTransportType(transportType)
+                .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
+                .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VCN_MANAGED);
+    }
+
+    private NetworkRequest.Builder createDefaultRequestBuilder() {
+        return new NetworkRequest.Builder()
+                .addTransportType(NetworkCapabilities.TRANSPORT_ETHERNET)
+                .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET);
+    }
+
+    private NetworkRequest createDefaultRequest() {
+        return createDefaultRequestBuilder().build();
+    }
+
+    private IpConfiguration createDefaultIpConfig() {
+        IpConfiguration ipConfig = new IpConfiguration();
+        ipConfig.setIpAssignment(IpConfiguration.IpAssignment.DHCP);
+        ipConfig.setProxySettings(IpConfiguration.ProxySettings.NONE);
+        return ipConfig;
+    }
+
+    /**
+     * Create an {@link IpConfiguration} with an associated {@link StaticIpConfiguration}.
+     *
+     * @return {@link IpConfiguration} with its {@link StaticIpConfiguration} set.
+     */
+    private IpConfiguration createStaticIpConfig() {
+        final IpConfiguration ipConfig = new IpConfiguration();
+        ipConfig.setIpAssignment(IpConfiguration.IpAssignment.STATIC);
+        ipConfig.setStaticIpConfiguration(
+                new StaticIpConfiguration.Builder().setIpAddress(LINK_ADDR).build());
+        return ipConfig;
+    }
+
+    // creates an interface with provisioning in progress (since updating the interface link state
+    // automatically starts the provisioning process)
+    private void createInterfaceUndergoingProvisioning(String iface) {
+        // Default to the ethernet transport type.
+        createInterfaceUndergoingProvisioning(iface, NetworkCapabilities.TRANSPORT_ETHERNET);
+    }
+
+    private void createInterfaceUndergoingProvisioning(
+            @NonNull final String iface, final int transportType) {
+        final IpConfiguration ipConfig = createDefaultIpConfig();
+        mNetFactory.addInterface(iface, HW_ADDR, ipConfig,
+                createInterfaceCapsBuilder(transportType).build());
+        assertTrue(mNetFactory.updateInterfaceLinkState(iface, true, NULL_LISTENER));
+        verifyStart(ipConfig);
+        clearInvocations(mDeps);
+        clearInvocations(mIpClient);
+    }
+
+    // creates a provisioned interface
+    private void createAndVerifyProvisionedInterface(String iface) throws Exception {
+        // Default to the ethernet transport type.
+        createAndVerifyProvisionedInterface(iface, NetworkCapabilities.TRANSPORT_ETHERNET,
+                ConnectivityManager.TYPE_ETHERNET);
+    }
+
+    private void createVerifyAndRemoveProvisionedInterface(final int transportType,
+            final int expectedLegacyType) throws Exception {
+        createAndVerifyProvisionedInterface(TEST_IFACE, transportType,
+                expectedLegacyType);
+        mNetFactory.removeInterface(TEST_IFACE);
+    }
+
+    private void createAndVerifyProvisionedInterface(
+            @NonNull final String iface, final int transportType, final int expectedLegacyType)
+            throws Exception {
+        createInterfaceUndergoingProvisioning(iface, transportType);
+        triggerOnProvisioningSuccess();
+        // provisioning succeeded, verify that the network agent is created, registered, marked
+        // as connected and legacy type are correctly set.
+        final ArgumentCaptor<NetworkCapabilities> ncCaptor = ArgumentCaptor.forClass(
+                NetworkCapabilities.class);
+        verify(mDeps).makeEthernetNetworkAgent(any(), any(), ncCaptor.capture(), any(),
+                argThat(x -> x.getLegacyType() == expectedLegacyType), any(), any());
+        assertEquals(
+                new EthernetNetworkSpecifier(iface), ncCaptor.getValue().getNetworkSpecifier());
+        verifyNetworkAgentRegistersAndConnects();
+        clearInvocations(mDeps);
+        clearInvocations(mNetworkAgent);
+    }
+
+    // creates an unprovisioned interface
+    private void createUnprovisionedInterface(String iface) throws Exception {
+        // To create an unprovisioned interface, provision and then "stop" it, i.e. stop its
+        // NetworkAgent and IpClient. One way this can be done is by provisioning an interface and
+        // then calling onNetworkUnwanted.
+        createAndVerifyProvisionedInterface(iface);
+
+        mNetworkAgent.getCallbacks().onNetworkUnwanted();
+        mLooper.dispatchAll();
+        verifyStop();
+
+        clearInvocations(mIpClient);
+        clearInvocations(mNetworkAgent);
+    }
+
+    @Test
+    public void testAcceptRequest() throws Exception {
+        initEthernetNetworkFactory();
+        createInterfaceUndergoingProvisioning(TEST_IFACE);
+        assertTrue(mNetFactory.acceptRequest(createDefaultRequest()));
+
+        NetworkRequest wifiRequest = createDefaultRequestBuilder()
+                .removeTransportType(NetworkCapabilities.TRANSPORT_ETHERNET)
+                .addTransportType(NetworkCapabilities.TRANSPORT_WIFI).build();
+        assertFalse(mNetFactory.acceptRequest(wifiRequest));
+    }
+
+    @Test
+    public void testUpdateInterfaceLinkStateForActiveProvisioningInterface() throws Exception {
+        initEthernetNetworkFactory();
+        createInterfaceUndergoingProvisioning(TEST_IFACE);
+        final TestNetworkManagementListener listener = new TestNetworkManagementListener();
+
+        // verify that the IpClient gets shut down when interface state changes to down.
+        final boolean ret =
+                mNetFactory.updateInterfaceLinkState(TEST_IFACE, false /* up */, listener);
+
+        assertTrue(ret);
+        verify(mIpClient).shutdown();
+        assertEquals(listener.expectOnResult(), TEST_IFACE);
+    }
+
+    @Test
+    public void testUpdateInterfaceLinkStateForProvisionedInterface() throws Exception {
+        initEthernetNetworkFactory();
+        createAndVerifyProvisionedInterface(TEST_IFACE);
+        final TestNetworkManagementListener listener = new TestNetworkManagementListener();
+
+        final boolean ret =
+                mNetFactory.updateInterfaceLinkState(TEST_IFACE, false /* up */, listener);
+
+        assertTrue(ret);
+        verifyStop();
+        assertEquals(listener.expectOnResult(), TEST_IFACE);
+    }
+
+    @Test
+    public void testUpdateInterfaceLinkStateForUnprovisionedInterface() throws Exception {
+        initEthernetNetworkFactory();
+        createUnprovisionedInterface(TEST_IFACE);
+        final TestNetworkManagementListener listener = new TestNetworkManagementListener();
+
+        final boolean ret =
+                mNetFactory.updateInterfaceLinkState(TEST_IFACE, false /* up */, listener);
+
+        assertTrue(ret);
+        // There should not be an active IPClient or NetworkAgent.
+        verify(mDeps, never()).makeIpClient(any(), any(), any());
+        verify(mDeps, never())
+                .makeEthernetNetworkAgent(any(), any(), any(), any(), any(), any(), any());
+        assertEquals(listener.expectOnResult(), TEST_IFACE);
+    }
+
+    @Test
+    public void testUpdateInterfaceLinkStateForNonExistingInterface() throws Exception {
+        initEthernetNetworkFactory();
+        final TestNetworkManagementListener listener = new TestNetworkManagementListener();
+
+        // if interface was never added, link state cannot be updated.
+        final boolean ret =
+                mNetFactory.updateInterfaceLinkState(TEST_IFACE, true /* up */, listener);
+
+        assertFalse(ret);
+        verifyNoStopOrStart();
+        listener.expectOnErrorWithMessage("can't be updated as it is not available");
+    }
+
+    @Test
+    public void testUpdateInterfaceLinkStateWithNoChanges() throws Exception {
+        initEthernetNetworkFactory();
+        createAndVerifyProvisionedInterface(TEST_IFACE);
+        final TestNetworkManagementListener listener = new TestNetworkManagementListener();
+
+        final boolean ret =
+                mNetFactory.updateInterfaceLinkState(TEST_IFACE, true /* up */, listener);
+
+        assertFalse(ret);
+        verifyNoStopOrStart();
+        listener.expectOnErrorWithMessage("No changes");
+    }
+
+    @Test
+    public void testNeedNetworkForOnProvisionedInterface() throws Exception {
+        initEthernetNetworkFactory();
+        createAndVerifyProvisionedInterface(TEST_IFACE);
+        mNetFactory.needNetworkFor(createDefaultRequest());
+        verify(mIpClient, never()).startProvisioning(any());
+    }
+
+    @Test
+    public void testNeedNetworkForOnUnprovisionedInterface() throws Exception {
+        initEthernetNetworkFactory();
+        createUnprovisionedInterface(TEST_IFACE);
+        mNetFactory.needNetworkFor(createDefaultRequest());
+        verify(mIpClient).startProvisioning(any());
+
+        triggerOnProvisioningSuccess();
+        verifyNetworkAgentRegistersAndConnects();
+    }
+
+    @Test
+    public void testNeedNetworkForOnInterfaceUndergoingProvisioning() throws Exception {
+        initEthernetNetworkFactory();
+        createInterfaceUndergoingProvisioning(TEST_IFACE);
+        mNetFactory.needNetworkFor(createDefaultRequest());
+        verify(mIpClient, never()).startProvisioning(any());
+
+        triggerOnProvisioningSuccess();
+        verifyNetworkAgentRegistersAndConnects();
+    }
+
+    @Test
+    public void testProvisioningLoss() throws Exception {
+        initEthernetNetworkFactory();
+        when(mDeps.getNetworkInterfaceByName(TEST_IFACE)).thenReturn(mInterfaceParams);
+        createAndVerifyProvisionedInterface(TEST_IFACE);
+
+        triggerOnProvisioningFailure();
+        verifyStop();
+        // provisioning loss should trigger a retry, since the interface is still there
+        verify(mIpClient).startProvisioning(any());
+    }
+
+    @Test
+    public void testProvisioningLossForDisappearedInterface() throws Exception {
+        initEthernetNetworkFactory();
+        // mocked method returns null by default, but just to be explicit in the test:
+        when(mDeps.getNetworkInterfaceByName(eq(TEST_IFACE))).thenReturn(null);
+
+        createAndVerifyProvisionedInterface(TEST_IFACE);
+        triggerOnProvisioningFailure();
+
+        // the interface disappeared and getNetworkInterfaceByName returns null, we should not retry
+        verify(mIpClient, never()).startProvisioning(any());
+        verifyNoStopOrStart();
+    }
+
+    private void verifyNoStopOrStart() {
+        verify(mNetworkAgent, never()).register();
+        verify(mIpClient, never()).shutdown();
+        verify(mNetworkAgent, never()).unregister();
+        verify(mIpClient, never()).startProvisioning(any());
+    }
+
+    @Test
+    public void testIpClientIsNotStartedWhenLinkIsDown() throws Exception {
+        initEthernetNetworkFactory();
+        createUnprovisionedInterface(TEST_IFACE);
+        mNetFactory.updateInterfaceLinkState(TEST_IFACE, false, NULL_LISTENER);
+
+        mNetFactory.needNetworkFor(createDefaultRequest());
+
+        verify(mDeps, never()).makeIpClient(any(), any(), any());
+
+        // BUG(b/191854824): requesting a network with a specifier (Android Auto use case) should
+        // not start an IpClient when the link is down, but fixing this may make matters worse by
+        // tiggering b/197548738.
+        NetworkRequest specificNetRequest = new NetworkRequest.Builder()
+                .addTransportType(NetworkCapabilities.TRANSPORT_ETHERNET)
+                .setNetworkSpecifier(new EthernetNetworkSpecifier(TEST_IFACE))
+                .build();
+        mNetFactory.needNetworkFor(specificNetRequest);
+        mNetFactory.releaseNetworkFor(specificNetRequest);
+
+        mNetFactory.updateInterfaceLinkState(TEST_IFACE, true, NULL_LISTENER);
+        // TODO: change to once when b/191854824 is fixed.
+        verify(mDeps, times(2)).makeIpClient(any(), eq(TEST_IFACE), any());
+    }
+
+    @Test
+    public void testLinkPropertiesChanged() throws Exception {
+        initEthernetNetworkFactory();
+        createAndVerifyProvisionedInterface(TEST_IFACE);
+
+        LinkProperties lp = new LinkProperties();
+        mIpClientCallbacks.onLinkPropertiesChange(lp);
+        mLooper.dispatchAll();
+        verify(mNetworkAgent).sendLinkPropertiesImpl(same(lp));
+    }
+
+    @Test
+    public void testNetworkUnwanted() throws Exception {
+        initEthernetNetworkFactory();
+        createAndVerifyProvisionedInterface(TEST_IFACE);
+
+        mNetworkAgent.getCallbacks().onNetworkUnwanted();
+        mLooper.dispatchAll();
+        verifyStop();
+    }
+
+    @Test
+    public void testNetworkUnwantedWithStaleNetworkAgent() throws Exception {
+        initEthernetNetworkFactory();
+        // ensures provisioning is restarted after provisioning loss
+        when(mDeps.getNetworkInterfaceByName(TEST_IFACE)).thenReturn(mInterfaceParams);
+        createAndVerifyProvisionedInterface(TEST_IFACE);
+
+        EthernetNetworkAgent.Callbacks oldCbs = mNetworkAgent.getCallbacks();
+        // replace network agent in EthernetNetworkFactory
+        // Loss of provisioning will restart the ip client and network agent.
+        triggerOnProvisioningFailure();
+        verify(mDeps).makeIpClient(any(), any(), any());
+
+        triggerOnProvisioningSuccess();
+        verify(mDeps).makeEthernetNetworkAgent(any(), any(), any(), any(), any(), any(), any());
+
+        // verify that unwanted is ignored
+        clearInvocations(mIpClient);
+        clearInvocations(mNetworkAgent);
+        oldCbs.onNetworkUnwanted();
+        verify(mIpClient, never()).shutdown();
+        verify(mNetworkAgent, never()).unregister();
+    }
+
+    @Test
+    public void testTransportOverrideIsCorrectlySet() throws Exception {
+        initEthernetNetworkFactory();
+        // createProvisionedInterface() has verifications in place for transport override
+        // functionality which for EthernetNetworkFactory is network score and legacy type mappings.
+        createVerifyAndRemoveProvisionedInterface(NetworkCapabilities.TRANSPORT_ETHERNET,
+                ConnectivityManager.TYPE_ETHERNET);
+        createVerifyAndRemoveProvisionedInterface(NetworkCapabilities.TRANSPORT_BLUETOOTH,
+                ConnectivityManager.TYPE_BLUETOOTH);
+        createVerifyAndRemoveProvisionedInterface(NetworkCapabilities.TRANSPORT_WIFI,
+                ConnectivityManager.TYPE_WIFI);
+        createVerifyAndRemoveProvisionedInterface(NetworkCapabilities.TRANSPORT_CELLULAR,
+                ConnectivityManager.TYPE_MOBILE);
+        createVerifyAndRemoveProvisionedInterface(NetworkCapabilities.TRANSPORT_LOWPAN,
+                ConnectivityManager.TYPE_NONE);
+        createVerifyAndRemoveProvisionedInterface(NetworkCapabilities.TRANSPORT_WIFI_AWARE,
+                ConnectivityManager.TYPE_NONE);
+        createVerifyAndRemoveProvisionedInterface(NetworkCapabilities.TRANSPORT_TEST,
+                ConnectivityManager.TYPE_NONE);
+    }
+
+    @Test
+    public void testReachabilityLoss() throws Exception {
+        initEthernetNetworkFactory();
+        createAndVerifyProvisionedInterface(TEST_IFACE);
+
+        triggerOnReachabilityLost();
+
+        // Reachability loss should trigger a stop and start, since the interface is still there
+        verifyRestart(createDefaultIpConfig());
+    }
+
+    private IpClientCallbacks getStaleIpClientCallbacks() throws Exception {
+        createAndVerifyProvisionedInterface(TEST_IFACE);
+        final IpClientCallbacks staleIpClientCallbacks = mIpClientCallbacks;
+        mNetFactory.removeInterface(TEST_IFACE);
+        verifyStop();
+        assertNotSame(mIpClientCallbacks, staleIpClientCallbacks);
+        return staleIpClientCallbacks;
+    }
+
+    @Test
+    public void testIgnoreOnIpLayerStartedCallbackForStaleCallback() throws Exception {
+        initEthernetNetworkFactory();
+        final IpClientCallbacks staleIpClientCallbacks = getStaleIpClientCallbacks();
+
+        staleIpClientCallbacks.onProvisioningSuccess(new LinkProperties());
+        mLooper.dispatchAll();
+
+        verify(mIpClient, never()).startProvisioning(any());
+        verify(mNetworkAgent, never()).register();
+    }
+
+    @Test
+    public void testIgnoreOnIpLayerStoppedCallbackForStaleCallback() throws Exception {
+        initEthernetNetworkFactory();
+        when(mDeps.getNetworkInterfaceByName(TEST_IFACE)).thenReturn(mInterfaceParams);
+        final IpClientCallbacks staleIpClientCallbacks = getStaleIpClientCallbacks();
+
+        staleIpClientCallbacks.onProvisioningFailure(new LinkProperties());
+        mLooper.dispatchAll();
+
+        verify(mIpClient, never()).startProvisioning(any());
+    }
+
+    @Test
+    public void testIgnoreLinkPropertiesCallbackForStaleCallback() throws Exception {
+        initEthernetNetworkFactory();
+        final IpClientCallbacks staleIpClientCallbacks = getStaleIpClientCallbacks();
+        final LinkProperties lp = new LinkProperties();
+
+        staleIpClientCallbacks.onLinkPropertiesChange(lp);
+        mLooper.dispatchAll();
+
+        verify(mNetworkAgent, never()).sendLinkPropertiesImpl(eq(lp));
+    }
+
+    @Test
+    public void testIgnoreNeighborLossCallbackForStaleCallback() throws Exception {
+        initEthernetNetworkFactory();
+        final IpClientCallbacks staleIpClientCallbacks = getStaleIpClientCallbacks();
+
+        staleIpClientCallbacks.onReachabilityLost("Neighbor Lost");
+        mLooper.dispatchAll();
+
+        verify(mIpClient, never()).startProvisioning(any());
+        verify(mNetworkAgent, never()).register();
+    }
+
+    private void verifyRestart(@NonNull final IpConfiguration ipConfig) {
+        verifyStop();
+        verifyStart(ipConfig);
+    }
+
+    private void verifyStart(@NonNull final IpConfiguration ipConfig) {
+        verify(mDeps).makeIpClient(any(Context.class), anyString(), any());
+        verify(mIpClient).startProvisioning(
+                argThat(x -> Objects.equals(x.mStaticIpConfig, ipConfig.getStaticIpConfiguration()))
+        );
+    }
+
+    private void verifyStop() {
+        verify(mIpClient).shutdown();
+        verify(mNetworkAgent).unregister();
+    }
+
+    private void verifyNetworkAgentRegistersAndConnects() {
+        verify(mNetworkAgent).register();
+        verify(mNetworkAgent).markConnected();
+    }
+
+    private static final class TestNetworkManagementListener
+            implements INetworkInterfaceOutcomeReceiver {
+        private final CompletableFuture<String> mResult = new CompletableFuture<>();
+        private final CompletableFuture<EthernetNetworkManagementException> mError =
+                new CompletableFuture<>();
+
+        @Override
+        public void onResult(@NonNull String iface) {
+            mResult.complete(iface);
+        }
+
+        @Override
+        public void onError(@NonNull EthernetNetworkManagementException exception) {
+            mError.complete(exception);
+        }
+
+        String expectOnResult() throws Exception {
+            return mResult.get(TIMEOUT_MS, TimeUnit.MILLISECONDS);
+        }
+
+        EthernetNetworkManagementException expectOnError() throws Exception {
+            return mError.get(TIMEOUT_MS, TimeUnit.MILLISECONDS);
+        }
+
+        void expectOnErrorWithMessage(String msg) throws Exception {
+            assertTrue(expectOnError().getMessage().contains(msg));
+        }
+
+        @Override
+        public IBinder asBinder() {
+            return null;
+        }
+    }
+
+    @Test
+    public void testUpdateInterfaceCallsListenerCorrectlyOnSuccess() throws Exception {
+        initEthernetNetworkFactory();
+        createAndVerifyProvisionedInterface(TEST_IFACE);
+        final NetworkCapabilities capabilities = createDefaultFilterCaps();
+        final IpConfiguration ipConfiguration = createStaticIpConfig();
+        final TestNetworkManagementListener listener = new TestNetworkManagementListener();
+
+        mNetFactory.updateInterface(TEST_IFACE, ipConfiguration, capabilities, listener);
+        triggerOnProvisioningSuccess();
+
+        assertEquals(listener.expectOnResult(), TEST_IFACE);
+    }
+
+    @DevSdkIgnoreRule.IgnoreUpTo(SC_V2) // TODO: Use to Build.VERSION_CODES.SC_V2 when available
+    @Test
+    public void testUpdateInterfaceAbortsOnConcurrentRemoveInterface() throws Exception {
+        initEthernetNetworkFactory();
+        verifyNetworkManagementCallIsAbortedWhenInterrupted(
+                TEST_IFACE,
+                () -> mNetFactory.removeInterface(TEST_IFACE));
+    }
+
+    @DevSdkIgnoreRule.IgnoreUpTo(SC_V2) // TODO: Use to Build.VERSION_CODES.SC_V2 when available
+    @Test
+    public void testUpdateInterfaceAbortsOnConcurrentUpdateInterfaceLinkState() throws Exception {
+        initEthernetNetworkFactory();
+        verifyNetworkManagementCallIsAbortedWhenInterrupted(
+                TEST_IFACE,
+                () -> mNetFactory.updateInterfaceLinkState(TEST_IFACE, false, NULL_LISTENER));
+    }
+
+    @DevSdkIgnoreRule.IgnoreUpTo(SC_V2) // TODO: Use to Build.VERSION_CODES.SC_V2 when available
+    @Test
+    public void testUpdateInterfaceCallsListenerCorrectlyOnConcurrentRequests() throws Exception {
+        initEthernetNetworkFactory();
+        final NetworkCapabilities capabilities = createDefaultFilterCaps();
+        final IpConfiguration ipConfiguration = createStaticIpConfig();
+        final TestNetworkManagementListener successfulListener =
+                new TestNetworkManagementListener();
+
+        // If two calls come in before the first one completes, the first listener will be aborted
+        // and the second one will be successful.
+        verifyNetworkManagementCallIsAbortedWhenInterrupted(
+                TEST_IFACE,
+                () -> {
+                    mNetFactory.updateInterface(
+                            TEST_IFACE, ipConfiguration, capabilities, successfulListener);
+                    triggerOnProvisioningSuccess();
+                });
+
+        assertEquals(successfulListener.expectOnResult(), TEST_IFACE);
+    }
+
+    private void verifyNetworkManagementCallIsAbortedWhenInterrupted(
+            @NonNull final String iface,
+            @NonNull final Runnable interruptingRunnable) throws Exception {
+        createAndVerifyProvisionedInterface(iface);
+        final NetworkCapabilities capabilities = createDefaultFilterCaps();
+        final IpConfiguration ipConfiguration = createStaticIpConfig();
+        final TestNetworkManagementListener failedListener = new TestNetworkManagementListener();
+
+        // An active update request will be aborted on interrupt prior to provisioning completion.
+        mNetFactory.updateInterface(iface, ipConfiguration, capabilities, failedListener);
+        interruptingRunnable.run();
+
+        failedListener.expectOnErrorWithMessage("aborted");
+    }
+
+    @Test
+    public void testUpdateInterfaceRestartsAgentCorrectly() throws Exception {
+        initEthernetNetworkFactory();
+        createAndVerifyProvisionedInterface(TEST_IFACE);
+        final NetworkCapabilities capabilities = createDefaultFilterCaps();
+        final IpConfiguration ipConfiguration = createStaticIpConfig();
+        final TestNetworkManagementListener listener = new TestNetworkManagementListener();
+
+        mNetFactory.updateInterface(TEST_IFACE, ipConfiguration, capabilities, listener);
+        triggerOnProvisioningSuccess();
+
+        assertEquals(listener.expectOnResult(), TEST_IFACE);
+        verify(mDeps).makeEthernetNetworkAgent(any(), any(),
+                eq(capabilities), any(), any(), any(), any());
+        verifyRestart(ipConfiguration);
+    }
+
+    @Test
+    public void testUpdateInterfaceForNonExistingInterface() throws Exception {
+        initEthernetNetworkFactory();
+        // No interface exists due to not calling createAndVerifyProvisionedInterface(...).
+        final NetworkCapabilities capabilities = createDefaultFilterCaps();
+        final IpConfiguration ipConfiguration = createStaticIpConfig();
+        final TestNetworkManagementListener listener = new TestNetworkManagementListener();
+
+        mNetFactory.updateInterface(TEST_IFACE, ipConfiguration, capabilities, listener);
+
+        verifyNoStopOrStart();
+        listener.expectOnErrorWithMessage("can't be updated as it is not available");
+    }
+
+    @Test
+    public void testUpdateInterfaceWithNullIpConfiguration() throws Exception {
+        initEthernetNetworkFactory();
+        createAndVerifyProvisionedInterface(TEST_IFACE);
+
+        final IpConfiguration initialIpConfig = createStaticIpConfig();
+        mNetFactory.updateInterface(TEST_IFACE, initialIpConfig, null /*capabilities*/,
+                null /*listener*/);
+        triggerOnProvisioningSuccess();
+        verifyRestart(initialIpConfig);
+
+        // TODO: have verifyXyz functions clear invocations.
+        clearInvocations(mDeps);
+        clearInvocations(mIpClient);
+        clearInvocations(mNetworkAgent);
+
+
+        // verify that sending a null ipConfig does not update the current ipConfig.
+        mNetFactory.updateInterface(TEST_IFACE, null /*ipConfig*/, null /*capabilities*/,
+                null /*listener*/);
+        triggerOnProvisioningSuccess();
+        verifyRestart(initialIpConfig);
+    }
+}
diff --git a/tests/ethernet/java/com/android/server/ethernet/EthernetServiceImplTest.java b/tests/ethernet/java/com/android/server/ethernet/EthernetServiceImplTest.java
new file mode 100644
index 0000000..dd1f1ed
--- /dev/null
+++ b/tests/ethernet/java/com/android/server/ethernet/EthernetServiceImplTest.java
@@ -0,0 +1,372 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.ethernet;
+
+import static android.net.NetworkCapabilities.TRANSPORT_TEST;
+
+import static org.junit.Assert.assertThrows;
+import static org.junit.Assert.fail;
+
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.ArgumentMatchers.isNull;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.Manifest;
+import android.annotation.NonNull;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.net.INetworkInterfaceOutcomeReceiver;
+import android.net.EthernetNetworkUpdateRequest;
+import android.net.IpConfiguration;
+import android.net.NetworkCapabilities;
+import android.os.Handler;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class EthernetServiceImplTest {
+    private static final String TEST_IFACE = "test123";
+    private static final EthernetNetworkUpdateRequest UPDATE_REQUEST =
+            new EthernetNetworkUpdateRequest.Builder()
+                    .setIpConfiguration(new IpConfiguration())
+                    .setNetworkCapabilities(new NetworkCapabilities.Builder().build())
+                    .build();
+    private static final EthernetNetworkUpdateRequest UPDATE_REQUEST_WITHOUT_CAPABILITIES =
+            new EthernetNetworkUpdateRequest.Builder()
+                    .setIpConfiguration(new IpConfiguration())
+                    .build();
+    private static final EthernetNetworkUpdateRequest UPDATE_REQUEST_WITHOUT_IP_CONFIG =
+            new EthernetNetworkUpdateRequest.Builder()
+                    .setNetworkCapabilities(new NetworkCapabilities.Builder().build())
+                    .build();
+    private static final INetworkInterfaceOutcomeReceiver NULL_LISTENER = null;
+    private EthernetServiceImpl mEthernetServiceImpl;
+    @Mock private Context mContext;
+    @Mock private Handler mHandler;
+    @Mock private EthernetTracker mEthernetTracker;
+    @Mock private PackageManager mPackageManager;
+
+    @Before
+    public void setup() {
+        MockitoAnnotations.initMocks(this);
+        doReturn(mPackageManager).when(mContext).getPackageManager();
+        mEthernetServiceImpl = new EthernetServiceImpl(mContext, mHandler, mEthernetTracker);
+        mEthernetServiceImpl.mStarted.set(true);
+        toggleAutomotiveFeature(true);
+        shouldTrackIface(TEST_IFACE, true);
+    }
+
+    private void toggleAutomotiveFeature(final boolean isEnabled) {
+        doReturn(isEnabled)
+                .when(mPackageManager).hasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE);
+    }
+
+    private void shouldTrackIface(@NonNull final String iface, final boolean shouldTrack) {
+        doReturn(shouldTrack).when(mEthernetTracker).isTrackingInterface(iface);
+    }
+
+    @Test
+    public void testSetConfigurationRejectsWhenEthNotStarted() {
+        mEthernetServiceImpl.mStarted.set(false);
+        assertThrows(IllegalStateException.class, () -> {
+            mEthernetServiceImpl.setConfiguration("" /* iface */, new IpConfiguration());
+        });
+    }
+
+    @Test
+    public void testUpdateConfigurationRejectsWhenEthNotStarted() {
+        mEthernetServiceImpl.mStarted.set(false);
+        assertThrows(IllegalStateException.class, () -> {
+            mEthernetServiceImpl.updateConfiguration(
+                    "" /* iface */, UPDATE_REQUEST, null /* listener */);
+        });
+    }
+
+    @Test
+    public void testConnectNetworkRejectsWhenEthNotStarted() {
+        mEthernetServiceImpl.mStarted.set(false);
+        assertThrows(IllegalStateException.class, () -> {
+            mEthernetServiceImpl.connectNetwork("" /* iface */, null /* listener */);
+        });
+    }
+
+    @Test
+    public void testDisconnectNetworkRejectsWhenEthNotStarted() {
+        mEthernetServiceImpl.mStarted.set(false);
+        assertThrows(IllegalStateException.class, () -> {
+            mEthernetServiceImpl.disconnectNetwork("" /* iface */, null /* listener */);
+        });
+    }
+
+    @Test
+    public void testUpdateConfigurationRejectsNullIface() {
+        assertThrows(NullPointerException.class, () -> {
+            mEthernetServiceImpl.updateConfiguration(null, UPDATE_REQUEST, NULL_LISTENER);
+        });
+    }
+
+    @Test
+    public void testConnectNetworkRejectsNullIface() {
+        assertThrows(NullPointerException.class, () -> {
+            mEthernetServiceImpl.connectNetwork(null /* iface */, NULL_LISTENER);
+        });
+    }
+
+    @Test
+    public void testDisconnectNetworkRejectsNullIface() {
+        assertThrows(NullPointerException.class, () -> {
+            mEthernetServiceImpl.disconnectNetwork(null /* iface */, NULL_LISTENER);
+        });
+    }
+
+    @Test
+    public void testUpdateConfigurationWithCapabilitiesRejectsWithoutAutomotiveFeature() {
+        toggleAutomotiveFeature(false);
+        assertThrows(UnsupportedOperationException.class, () -> {
+            mEthernetServiceImpl.updateConfiguration(TEST_IFACE, UPDATE_REQUEST, NULL_LISTENER);
+        });
+    }
+
+    @Test
+    public void testUpdateConfigurationWithCapabilitiesWithAutomotiveFeature() {
+        toggleAutomotiveFeature(false);
+        mEthernetServiceImpl.updateConfiguration(TEST_IFACE, UPDATE_REQUEST_WITHOUT_CAPABILITIES,
+                NULL_LISTENER);
+        verify(mEthernetTracker).updateConfiguration(eq(TEST_IFACE),
+                eq(UPDATE_REQUEST_WITHOUT_CAPABILITIES.getIpConfiguration()),
+                eq(UPDATE_REQUEST_WITHOUT_CAPABILITIES.getNetworkCapabilities()), isNull());
+    }
+
+    @Test
+    public void testConnectNetworkRejectsWithoutAutomotiveFeature() {
+        toggleAutomotiveFeature(false);
+        assertThrows(UnsupportedOperationException.class, () -> {
+            mEthernetServiceImpl.connectNetwork("" /* iface */, NULL_LISTENER);
+        });
+    }
+
+    @Test
+    public void testDisconnectNetworkRejectsWithoutAutomotiveFeature() {
+        toggleAutomotiveFeature(false);
+        assertThrows(UnsupportedOperationException.class, () -> {
+            mEthernetServiceImpl.disconnectNetwork("" /* iface */, NULL_LISTENER);
+        });
+    }
+
+    private void denyManageEthPermission() {
+        doThrow(new SecurityException("")).when(mContext)
+                .enforceCallingOrSelfPermission(
+                        eq(Manifest.permission.MANAGE_ETHERNET_NETWORKS), anyString());
+    }
+
+    private void denyManageTestNetworksPermission() {
+        doThrow(new SecurityException("")).when(mContext)
+                .enforceCallingOrSelfPermission(
+                        eq(Manifest.permission.MANAGE_TEST_NETWORKS), anyString());
+    }
+
+    @Test
+    public void testUpdateConfigurationRejectsWithoutManageEthPermission() {
+        denyManageEthPermission();
+        assertThrows(SecurityException.class, () -> {
+            mEthernetServiceImpl.updateConfiguration(TEST_IFACE, UPDATE_REQUEST, NULL_LISTENER);
+        });
+    }
+
+    @Test
+    public void testConnectNetworkRejectsWithoutManageEthPermission() {
+        denyManageEthPermission();
+        assertThrows(SecurityException.class, () -> {
+            mEthernetServiceImpl.connectNetwork(TEST_IFACE, NULL_LISTENER);
+        });
+    }
+
+    @Test
+    public void testDisconnectNetworkRejectsWithoutManageEthPermission() {
+        denyManageEthPermission();
+        assertThrows(SecurityException.class, () -> {
+            mEthernetServiceImpl.disconnectNetwork(TEST_IFACE, NULL_LISTENER);
+        });
+    }
+
+    private void enableTestInterface() {
+        when(mEthernetTracker.isValidTestInterface(eq(TEST_IFACE))).thenReturn(true);
+    }
+
+    @Test
+    public void testUpdateConfigurationRejectsTestRequestWithoutTestPermission() {
+        enableTestInterface();
+        denyManageTestNetworksPermission();
+        assertThrows(SecurityException.class, () -> {
+            mEthernetServiceImpl.updateConfiguration(TEST_IFACE, UPDATE_REQUEST, NULL_LISTENER);
+        });
+    }
+
+    @Test
+    public void testConnectNetworkRejectsTestRequestWithoutTestPermission() {
+        enableTestInterface();
+        denyManageTestNetworksPermission();
+        assertThrows(SecurityException.class, () -> {
+            mEthernetServiceImpl.connectNetwork(TEST_IFACE, NULL_LISTENER);
+        });
+    }
+
+    @Test
+    public void testDisconnectNetworkRejectsTestRequestWithoutTestPermission() {
+        enableTestInterface();
+        denyManageTestNetworksPermission();
+        assertThrows(SecurityException.class, () -> {
+            mEthernetServiceImpl.disconnectNetwork(TEST_IFACE, NULL_LISTENER);
+        });
+    }
+
+    @Test
+    public void testUpdateConfiguration() {
+        mEthernetServiceImpl.updateConfiguration(TEST_IFACE, UPDATE_REQUEST, NULL_LISTENER);
+        verify(mEthernetTracker).updateConfiguration(
+                eq(TEST_IFACE),
+                eq(UPDATE_REQUEST.getIpConfiguration()),
+                eq(UPDATE_REQUEST.getNetworkCapabilities()), eq(NULL_LISTENER));
+    }
+
+    @Test
+    public void testConnectNetwork() {
+        mEthernetServiceImpl.connectNetwork(TEST_IFACE, NULL_LISTENER);
+        verify(mEthernetTracker).connectNetwork(eq(TEST_IFACE), eq(NULL_LISTENER));
+    }
+
+    @Test
+    public void testDisconnectNetwork() {
+        mEthernetServiceImpl.disconnectNetwork(TEST_IFACE, NULL_LISTENER);
+        verify(mEthernetTracker).disconnectNetwork(eq(TEST_IFACE), eq(NULL_LISTENER));
+    }
+
+    @Test
+    public void testUpdateConfigurationAcceptsTestRequestWithNullCapabilities() {
+        enableTestInterface();
+        final EthernetNetworkUpdateRequest request =
+                new EthernetNetworkUpdateRequest
+                        .Builder()
+                        .setIpConfiguration(new IpConfiguration()).build();
+        mEthernetServiceImpl.updateConfiguration(TEST_IFACE, request, NULL_LISTENER);
+        verify(mEthernetTracker).updateConfiguration(eq(TEST_IFACE),
+                eq(request.getIpConfiguration()),
+                eq(request.getNetworkCapabilities()), isNull());
+    }
+
+    @Test
+    public void testUpdateConfigurationAcceptsRequestWithNullIpConfiguration() {
+        mEthernetServiceImpl.updateConfiguration(TEST_IFACE, UPDATE_REQUEST_WITHOUT_IP_CONFIG,
+                NULL_LISTENER);
+        verify(mEthernetTracker).updateConfiguration(eq(TEST_IFACE),
+                eq(UPDATE_REQUEST_WITHOUT_IP_CONFIG.getIpConfiguration()),
+                eq(UPDATE_REQUEST_WITHOUT_IP_CONFIG.getNetworkCapabilities()), isNull());
+    }
+
+    @Test
+    public void testUpdateConfigurationRejectsInvalidTestRequest() {
+        enableTestInterface();
+        assertThrows(IllegalArgumentException.class, () -> {
+            mEthernetServiceImpl.updateConfiguration(TEST_IFACE, UPDATE_REQUEST, NULL_LISTENER);
+        });
+    }
+
+    private EthernetNetworkUpdateRequest createTestNetworkUpdateRequest() {
+        final NetworkCapabilities nc =  new NetworkCapabilities
+                .Builder(UPDATE_REQUEST.getNetworkCapabilities())
+                .addTransportType(TRANSPORT_TEST).build();
+
+        return new EthernetNetworkUpdateRequest
+                .Builder(UPDATE_REQUEST)
+                .setNetworkCapabilities(nc).build();
+    }
+
+    @Test
+    public void testUpdateConfigurationForTestRequestDoesNotRequireAutoOrEthernetPermission() {
+        enableTestInterface();
+        toggleAutomotiveFeature(false);
+        denyManageEthPermission();
+        final EthernetNetworkUpdateRequest request = createTestNetworkUpdateRequest();
+
+        mEthernetServiceImpl.updateConfiguration(TEST_IFACE, request, NULL_LISTENER);
+        verify(mEthernetTracker).updateConfiguration(
+                eq(TEST_IFACE),
+                eq(request.getIpConfiguration()),
+                eq(request.getNetworkCapabilities()), eq(NULL_LISTENER));
+    }
+
+    @Test
+    public void testConnectNetworkForTestRequestDoesNotRequireAutoOrNetPermission() {
+        enableTestInterface();
+        toggleAutomotiveFeature(false);
+        denyManageEthPermission();
+
+        mEthernetServiceImpl.connectNetwork(TEST_IFACE, NULL_LISTENER);
+        verify(mEthernetTracker).connectNetwork(eq(TEST_IFACE), eq(NULL_LISTENER));
+    }
+
+    @Test
+    public void testDisconnectNetworkForTestRequestDoesNotRequireAutoOrNetPermission() {
+        enableTestInterface();
+        toggleAutomotiveFeature(false);
+        denyManageEthPermission();
+
+        mEthernetServiceImpl.disconnectNetwork(TEST_IFACE, NULL_LISTENER);
+        verify(mEthernetTracker).disconnectNetwork(eq(TEST_IFACE), eq(NULL_LISTENER));
+    }
+
+    private void denyPermissions(String... permissions) {
+        for (String permission: permissions) {
+            doReturn(PackageManager.PERMISSION_DENIED).when(mContext)
+                    .checkCallingOrSelfPermission(eq(permission));
+        }
+    }
+
+    @Test
+    public void testSetEthernetEnabled() {
+        denyPermissions(android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK);
+        mEthernetServiceImpl.setEthernetEnabled(true);
+        verify(mEthernetTracker).setEthernetEnabled(true);
+        reset(mEthernetTracker);
+
+        denyPermissions(Manifest.permission.NETWORK_STACK);
+        mEthernetServiceImpl.setEthernetEnabled(false);
+        verify(mEthernetTracker).setEthernetEnabled(false);
+        reset(mEthernetTracker);
+
+        denyPermissions(Manifest.permission.NETWORK_SETTINGS);
+        try {
+            mEthernetServiceImpl.setEthernetEnabled(true);
+            fail("Should get SecurityException");
+        } catch (SecurityException e) { }
+        verify(mEthernetTracker, never()).setEthernetEnabled(false);
+    }
+}
diff --git a/tests/ethernet/java/com/android/server/ethernet/EthernetTrackerTest.java b/tests/ethernet/java/com/android/server/ethernet/EthernetTrackerTest.java
new file mode 100644
index 0000000..b1831c4
--- /dev/null
+++ b/tests/ethernet/java/com/android/server/ethernet/EthernetTrackerTest.java
@@ -0,0 +1,456 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.ethernet;
+
+import static android.net.TestNetworkManager.TEST_TAP_PREFIX;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertThrows;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.net.EthernetManager;
+import android.net.InetAddresses;
+import android.net.INetworkInterfaceOutcomeReceiver;
+import android.net.IEthernetServiceListener;
+import android.net.INetd;
+import android.net.IpConfiguration;
+import android.net.IpConfiguration.IpAssignment;
+import android.net.IpConfiguration.ProxySettings;
+import android.net.InterfaceConfigurationParcel;
+import android.net.LinkAddress;
+import android.net.NetworkCapabilities;
+import android.net.StaticIpConfiguration;
+import android.os.HandlerThread;
+import android.os.RemoteException;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.connectivity.resources.R;
+import com.android.testutils.HandlerUtils;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.net.InetAddress;
+import java.util.ArrayList;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class EthernetTrackerTest {
+    private static final String TEST_IFACE = "test123";
+    private static final int TIMEOUT_MS = 1_000;
+    private static final String THREAD_NAME = "EthernetServiceThread";
+    private static final INetworkInterfaceOutcomeReceiver NULL_LISTENER = null;
+    private EthernetTracker tracker;
+    private HandlerThread mHandlerThread;
+    @Mock private Context mContext;
+    @Mock private EthernetNetworkFactory mFactory;
+    @Mock private INetd mNetd;
+    @Mock private EthernetTracker.Dependencies mDeps;
+
+    @Before
+    public void setUp() throws RemoteException {
+        MockitoAnnotations.initMocks(this);
+        initMockResources();
+        when(mFactory.updateInterfaceLinkState(anyString(), anyBoolean(), any())).thenReturn(false);
+        when(mNetd.interfaceGetList()).thenReturn(new String[0]);
+        mHandlerThread = new HandlerThread(THREAD_NAME);
+        mHandlerThread.start();
+        tracker = new EthernetTracker(mContext, mHandlerThread.getThreadHandler(), mFactory, mNetd,
+                mDeps);
+    }
+
+    @After
+    public void cleanUp() {
+        mHandlerThread.quitSafely();
+    }
+
+    private void initMockResources() {
+        when(mDeps.getInterfaceRegexFromResource(eq(mContext))).thenReturn("");
+        when(mDeps.getInterfaceConfigFromResource(eq(mContext))).thenReturn(new String[0]);
+    }
+
+    private void waitForIdle() {
+        HandlerUtils.waitForIdle(mHandlerThread, TIMEOUT_MS);
+    }
+
+    /**
+     * Test: Creation of various valid static IP configurations
+     */
+    @Test
+    public void createStaticIpConfiguration() {
+        // Empty gives default StaticIPConfiguration object
+        assertStaticConfiguration(new StaticIpConfiguration(), "");
+
+        // Setting only the IP address properly cascades and assumes defaults
+        assertStaticConfiguration(new StaticIpConfiguration.Builder()
+                .setIpAddress(new LinkAddress("192.0.2.10/24")).build(), "ip=192.0.2.10/24");
+
+        final ArrayList<InetAddress> dnsAddresses = new ArrayList<>();
+        dnsAddresses.add(InetAddresses.parseNumericAddress("4.4.4.4"));
+        dnsAddresses.add(InetAddresses.parseNumericAddress("8.8.8.8"));
+        // Setting other fields properly cascades them
+        assertStaticConfiguration(new StaticIpConfiguration.Builder()
+                .setIpAddress(new LinkAddress("192.0.2.10/24"))
+                .setDnsServers(dnsAddresses)
+                .setGateway(InetAddresses.parseNumericAddress("192.0.2.1"))
+                .setDomains("android").build(),
+                "ip=192.0.2.10/24 dns=4.4.4.4,8.8.8.8 gateway=192.0.2.1 domains=android");
+
+        // Verify order doesn't matter
+        assertStaticConfiguration(new StaticIpConfiguration.Builder()
+                .setIpAddress(new LinkAddress("192.0.2.10/24"))
+                .setDnsServers(dnsAddresses)
+                .setGateway(InetAddresses.parseNumericAddress("192.0.2.1"))
+                .setDomains("android").build(),
+                "domains=android ip=192.0.2.10/24 gateway=192.0.2.1 dns=4.4.4.4,8.8.8.8 ");
+    }
+
+    /**
+     * Test: Attempt creation of various bad static IP configurations
+     */
+    @Test
+    public void createStaticIpConfiguration_Bad() {
+        assertStaticConfigurationFails("ip=192.0.2.1/24 gateway= blah=20.20.20.20");  // Unknown key
+        assertStaticConfigurationFails("ip=192.0.2.1");  // mask is missing
+        assertStaticConfigurationFails("ip=a.b.c");  // not a valid ip address
+        assertStaticConfigurationFails("dns=4.4.4.4,1.2.3.A");  // not valid ip address in dns
+        assertStaticConfigurationFails("=");  // Key and value is empty
+        assertStaticConfigurationFails("ip=");  // Value is empty
+        assertStaticConfigurationFails("ip=192.0.2.1/24 gateway=");  // Gateway is empty
+    }
+
+    private void assertStaticConfigurationFails(String config) {
+        try {
+            EthernetTracker.parseStaticIpConfiguration(config);
+            fail("Expected to fail: " + config);
+        } catch (IllegalArgumentException e) {
+            // expected
+        }
+    }
+
+    private void assertStaticConfiguration(StaticIpConfiguration expectedStaticIpConfig,
+                String configAsString) {
+        final IpConfiguration expectedIpConfiguration = new IpConfiguration();
+        expectedIpConfiguration.setIpAssignment(IpAssignment.STATIC);
+        expectedIpConfiguration.setProxySettings(ProxySettings.NONE);
+        expectedIpConfiguration.setStaticIpConfiguration(expectedStaticIpConfig);
+
+        assertEquals(expectedIpConfiguration,
+                EthernetTracker.parseStaticIpConfiguration(configAsString));
+    }
+
+    private NetworkCapabilities.Builder makeEthernetCapabilitiesBuilder(boolean clearAll) {
+        final NetworkCapabilities.Builder builder =
+                clearAll ? NetworkCapabilities.Builder.withoutDefaultCapabilities()
+                        : new NetworkCapabilities.Builder();
+        return builder.addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_ROAMING)
+                .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_CONGESTED)
+                .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_SUSPENDED);
+    }
+
+    /**
+     * Test: Attempt to create a capabilties with various valid sets of capabilities/transports
+     */
+    @Test
+    public void createNetworkCapabilities() {
+
+        // Particularly common expected results
+        NetworkCapabilities defaultEthernetCleared =
+                makeEthernetCapabilitiesBuilder(true /* clearAll */)
+                        .setLinkUpstreamBandwidthKbps(100000)
+                        .setLinkDownstreamBandwidthKbps(100000)
+                        .addTransportType(NetworkCapabilities.TRANSPORT_ETHERNET)
+                        .build();
+
+        NetworkCapabilities ethernetClearedWithCommonCaps =
+                makeEthernetCapabilitiesBuilder(true /* clearAll */)
+                        .setLinkUpstreamBandwidthKbps(100000)
+                        .setLinkDownstreamBandwidthKbps(100000)
+                        .addTransportType(NetworkCapabilities.TRANSPORT_ETHERNET)
+                        .addCapability(12)
+                        .addCapability(13)
+                        .addCapability(14)
+                        .addCapability(15)
+                        .build();
+
+        // Empty capabilities and transports lists with a "please clear defaults" should
+        // yield an empty capabilities set with TRANPORT_ETHERNET
+        assertParsedNetworkCapabilities(defaultEthernetCleared, true, "", "");
+
+        // Empty capabilities and transports without the clear defaults flag should return the
+        // default capabilities set with TRANSPORT_ETHERNET
+        assertParsedNetworkCapabilities(
+                makeEthernetCapabilitiesBuilder(false /* clearAll */)
+                        .setLinkUpstreamBandwidthKbps(100000)
+                        .setLinkDownstreamBandwidthKbps(100000)
+                        .addTransportType(NetworkCapabilities.TRANSPORT_ETHERNET)
+                        .build(),
+                false, "", "");
+
+        // A list of capabilities without the clear defaults flag should return the default
+        // capabilities, mixed with the desired capabilities, and TRANSPORT_ETHERNET
+        assertParsedNetworkCapabilities(
+                makeEthernetCapabilitiesBuilder(false /* clearAll */)
+                        .setLinkUpstreamBandwidthKbps(100000)
+                        .setLinkDownstreamBandwidthKbps(100000)
+                        .addTransportType(NetworkCapabilities.TRANSPORT_ETHERNET)
+                        .addCapability(11)
+                        .addCapability(12)
+                        .build(),
+                false, "11,12", "");
+
+        // Adding a list of capabilities with a clear defaults will leave exactly those capabilities
+        // with a default TRANSPORT_ETHERNET since no overrides are specified
+        assertParsedNetworkCapabilities(ethernetClearedWithCommonCaps, true, "12,13,14,15", "");
+
+        // Adding any invalid capabilities to the list will cause them to be ignored
+        assertParsedNetworkCapabilities(ethernetClearedWithCommonCaps, true, "12,13,14,15,65,73", "");
+        assertParsedNetworkCapabilities(ethernetClearedWithCommonCaps, true, "12,13,14,15,abcdefg", "");
+
+        // Adding a valid override transport will remove the default TRANSPORT_ETHERNET transport
+        // and apply only the override to the capabiltities object
+        assertParsedNetworkCapabilities(
+                makeEthernetCapabilitiesBuilder(true /* clearAll */)
+                        .setLinkUpstreamBandwidthKbps(100000)
+                        .setLinkDownstreamBandwidthKbps(100000)
+                        .addTransportType(0)
+                        .build(),
+                true, "", "0");
+        assertParsedNetworkCapabilities(
+                makeEthernetCapabilitiesBuilder(true /* clearAll */)
+                        .setLinkUpstreamBandwidthKbps(100000)
+                        .setLinkDownstreamBandwidthKbps(100000)
+                        .addTransportType(1)
+                        .build(),
+                true, "", "1");
+        assertParsedNetworkCapabilities(
+                makeEthernetCapabilitiesBuilder(true /* clearAll */)
+                        .setLinkUpstreamBandwidthKbps(100000)
+                        .setLinkDownstreamBandwidthKbps(100000)
+                        .addTransportType(2)
+                        .build(),
+                true, "", "2");
+        assertParsedNetworkCapabilities(
+                makeEthernetCapabilitiesBuilder(true /* clearAll */)
+                        .setLinkUpstreamBandwidthKbps(100000)
+                        .setLinkDownstreamBandwidthKbps(100000)
+                        .addTransportType(3)
+                        .build(),
+                true, "", "3");
+
+        // "4" is TRANSPORT_VPN, which is unsupported. Should default back to TRANPORT_ETHERNET
+        assertParsedNetworkCapabilities(defaultEthernetCleared, true, "", "4");
+
+        // "5" is TRANSPORT_WIFI_AWARE, which is currently supported due to no legacy TYPE_NONE
+        // conversion. When that becomes available, this test must be updated
+        assertParsedNetworkCapabilities(defaultEthernetCleared, true, "", "5");
+
+        // "6" is TRANSPORT_LOWPAN, which is currently supported due to no legacy TYPE_NONE
+        // conversion. When that becomes available, this test must be updated
+        assertParsedNetworkCapabilities(defaultEthernetCleared, true, "", "6");
+
+        // Adding an invalid override transport will leave the transport as TRANSPORT_ETHERNET
+        assertParsedNetworkCapabilities(defaultEthernetCleared,true, "", "100");
+        assertParsedNetworkCapabilities(defaultEthernetCleared, true, "", "abcdefg");
+
+        // Ensure the adding of both capabilities and transports work
+        assertParsedNetworkCapabilities(
+                makeEthernetCapabilitiesBuilder(true /* clearAll */)
+                        .setLinkUpstreamBandwidthKbps(100000)
+                        .setLinkDownstreamBandwidthKbps(100000)
+                        .addCapability(12)
+                        .addCapability(13)
+                        .addCapability(14)
+                        .addCapability(15)
+                        .addTransportType(3)
+                        .build(),
+                true, "12,13,14,15", "3");
+
+        // Ensure order does not matter for capability list
+        assertParsedNetworkCapabilities(ethernetClearedWithCommonCaps, true, "13,12,15,14", "");
+    }
+
+    private void assertParsedNetworkCapabilities(NetworkCapabilities expectedNetworkCapabilities,
+            boolean clearCapabilties, String configCapabiltiies,String configTransports) {
+        assertEquals(expectedNetworkCapabilities,
+                EthernetTracker.createNetworkCapabilities(clearCapabilties, configCapabiltiies,
+                        configTransports).build());
+    }
+
+    @Test
+    public void testCreateEthernetTrackerConfigReturnsCorrectValue() {
+        final String capabilities = "2";
+        final String ipConfig = "3";
+        final String transport = "4";
+        final String configString = String.join(";", TEST_IFACE, capabilities, ipConfig, transport);
+
+        final EthernetTracker.EthernetTrackerConfig config =
+                EthernetTracker.createEthernetTrackerConfig(configString);
+
+        assertEquals(TEST_IFACE, config.mIface);
+        assertEquals(capabilities, config.mCapabilities);
+        assertEquals(ipConfig, config.mIpConfig);
+        assertEquals(transport, config.mTransport);
+    }
+
+    @Test
+    public void testCreateEthernetTrackerConfigThrowsNpeWithNullInput() {
+        assertThrows(NullPointerException.class,
+                () -> EthernetTracker.createEthernetTrackerConfig(null));
+    }
+
+    @Test
+    public void testUpdateConfiguration() {
+        final NetworkCapabilities capabilities = new NetworkCapabilities.Builder().build();
+        final LinkAddress linkAddr = new LinkAddress("192.0.2.2/25");
+        final StaticIpConfiguration staticIpConfig =
+                new StaticIpConfiguration.Builder().setIpAddress(linkAddr).build();
+        final IpConfiguration ipConfig =
+                new IpConfiguration.Builder().setStaticIpConfiguration(staticIpConfig).build();
+        final INetworkInterfaceOutcomeReceiver listener = null;
+
+        tracker.updateConfiguration(TEST_IFACE, ipConfig, capabilities, listener);
+        waitForIdle();
+
+        verify(mFactory).updateInterface(
+                eq(TEST_IFACE), eq(ipConfig), eq(capabilities), eq(listener));
+    }
+
+    @Test
+    public void testConnectNetworkCorrectlyCallsFactory() {
+        tracker.connectNetwork(TEST_IFACE, NULL_LISTENER);
+        waitForIdle();
+
+        verify(mFactory).updateInterfaceLinkState(eq(TEST_IFACE), eq(true /* up */),
+                eq(NULL_LISTENER));
+    }
+
+    @Test
+    public void testDisconnectNetworkCorrectlyCallsFactory() {
+        tracker.disconnectNetwork(TEST_IFACE, NULL_LISTENER);
+        waitForIdle();
+
+        verify(mFactory).updateInterfaceLinkState(eq(TEST_IFACE), eq(false /* up */),
+                eq(NULL_LISTENER));
+    }
+
+    @Test
+    public void testIsValidTestInterfaceIsFalseWhenTestInterfacesAreNotIncluded() {
+        final String validIfaceName = TEST_TAP_PREFIX + "123";
+        tracker.setIncludeTestInterfaces(false);
+        waitForIdle();
+
+        final boolean isValidTestInterface = tracker.isValidTestInterface(validIfaceName);
+
+        assertFalse(isValidTestInterface);
+    }
+
+    @Test
+    public void testIsValidTestInterfaceIsFalseWhenTestInterfaceNameIsInvalid() {
+        final String invalidIfaceName = "123" + TEST_TAP_PREFIX;
+        tracker.setIncludeTestInterfaces(true);
+        waitForIdle();
+
+        final boolean isValidTestInterface = tracker.isValidTestInterface(invalidIfaceName);
+
+        assertFalse(isValidTestInterface);
+    }
+
+    @Test
+    public void testIsValidTestInterfaceIsTrueWhenTestInterfacesIncludedAndValidName() {
+        final String validIfaceName = TEST_TAP_PREFIX + "123";
+        tracker.setIncludeTestInterfaces(true);
+        waitForIdle();
+
+        final boolean isValidTestInterface = tracker.isValidTestInterface(validIfaceName);
+
+        assertTrue(isValidTestInterface);
+    }
+
+    public static class EthernetStateListener extends IEthernetServiceListener.Stub {
+        @Override
+        public void onEthernetStateChanged(int state) { }
+
+        @Override
+        public void onInterfaceStateChanged(String iface, int state, int role,
+                IpConfiguration configuration) { }
+    }
+
+    @Test
+    public void testListenEthernetStateChange() throws Exception {
+        final String testIface = "testtap123";
+        final String testHwAddr = "11:22:33:44:55:66";
+        final InterfaceConfigurationParcel ifaceParcel = new InterfaceConfigurationParcel();
+        ifaceParcel.ifName = testIface;
+        ifaceParcel.hwAddr = testHwAddr;
+        ifaceParcel.flags = new String[] {INetd.IF_STATE_UP};
+
+        tracker.setIncludeTestInterfaces(true);
+        waitForIdle();
+
+        when(mNetd.interfaceGetList()).thenReturn(new String[] {testIface});
+        when(mNetd.interfaceGetCfg(eq(testIface))).thenReturn(ifaceParcel);
+        doReturn(new String[] {testIface}).when(mFactory).getAvailableInterfaces(anyBoolean());
+        doReturn(EthernetManager.STATE_LINK_UP).when(mFactory).getInterfaceState(eq(testIface));
+
+        final EthernetStateListener listener = spy(new EthernetStateListener());
+        tracker.addListener(listener, true /* canUseRestrictedNetworks */);
+        // Check default state.
+        waitForIdle();
+        verify(listener).onInterfaceStateChanged(eq(testIface), eq(EthernetManager.STATE_LINK_UP),
+                anyInt(), any());
+        verify(listener).onEthernetStateChanged(eq(EthernetManager.ETHERNET_STATE_ENABLED));
+        reset(listener);
+
+        doReturn(EthernetManager.STATE_ABSENT).when(mFactory).getInterfaceState(eq(testIface));
+        tracker.setEthernetEnabled(false);
+        waitForIdle();
+        verify(mFactory).removeInterface(eq(testIface));
+        verify(listener).onEthernetStateChanged(eq(EthernetManager.ETHERNET_STATE_DISABLED));
+        verify(listener).onInterfaceStateChanged(eq(testIface), eq(EthernetManager.STATE_ABSENT),
+                anyInt(), any());
+        reset(listener);
+
+        doReturn(EthernetManager.STATE_LINK_UP).when(mFactory).getInterfaceState(eq(testIface));
+        tracker.setEthernetEnabled(true);
+        waitForIdle();
+        verify(mFactory).addInterface(eq(testIface), eq(testHwAddr), any(), any());
+        verify(listener).onEthernetStateChanged(eq(EthernetManager.ETHERNET_STATE_ENABLED));
+        verify(listener).onInterfaceStateChanged(eq(testIface), eq(EthernetManager.STATE_LINK_UP),
+                anyInt(), any());
+    }
+}
diff --git a/tests/unit/java/com/android/server/connectivity/CarrierPrivilegeAuthenticatorTest.java b/tests/unit/java/com/android/server/connectivity/CarrierPrivilegeAuthenticatorTest.java
index 553cb83..157507b 100644
--- a/tests/unit/java/com/android/server/connectivity/CarrierPrivilegeAuthenticatorTest.java
+++ b/tests/unit/java/com/android/server/connectivity/CarrierPrivilegeAuthenticatorTest.java
@@ -69,8 +69,6 @@
 @RunWith(DevSdkIgnoreRunner.class)
 @IgnoreUpTo(SC_V2) // TODO: Use to Build.VERSION_CODES.SC_V2 when available
 public class CarrierPrivilegeAuthenticatorTest {
-    // TODO : use ConstantsShim.RECEIVER_NOT_EXPORTED when it's available in tests.
-    private static final int RECEIVER_NOT_EXPORTED = 4;
     private static final int SUBSCRIPTION_COUNT = 2;
     private static final int TEST_SUBSCRIPTION_ID = 1;
 
@@ -117,7 +115,7 @@
 
     private IntentFilter getIntentFilter() {
         final ArgumentCaptor<IntentFilter> captor = ArgumentCaptor.forClass(IntentFilter.class);
-        verify(mContext).registerReceiver(any(), captor.capture(), any(), any(), anyInt());
+        verify(mContext).registerReceiver(any(), captor.capture(), any(), any());
         return captor.getValue();
     }
 
@@ -140,11 +138,10 @@
     @Test
     public void testConstructor() throws Exception {
         verify(mContext).registerReceiver(
-                eq(mCarrierPrivilegeAuthenticator),
-                any(IntentFilter.class),
-                any(),
-                any(),
-                eq(RECEIVER_NOT_EXPORTED));
+                        eq(mCarrierPrivilegeAuthenticator),
+                        any(IntentFilter.class),
+                        any(),
+                        any());
         final IntentFilter filter = getIntentFilter();
         assertEquals(1, filter.countActions());
         assertTrue(filter.hasAction(ACTION_MULTI_SIM_CONFIG_CHANGED));